Refine MsTextField component: introduce compact mode, enhance visual styling and error handling, and improve placeholder and keyboard interaction logic. Add Dimens and Colors updates, implement navigation rail and header layout for the desktop shell, and update ROADMAP documentation with planned phases.
This commit is contained in:
+8
-13
@@ -41,20 +41,16 @@ fun <T> MsSearchableSelect(
|
||||
|
||||
Column(modifier = modifier) {
|
||||
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
||||
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
|
||||
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { showDialog = true },
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = Icons.Default.Search,
|
||||
modifier = modifier.clickable(enabled = enabled) { showDialog = true },
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
||||
@@ -75,17 +71,16 @@ fun <T> MsSearchableSelect(
|
||||
)
|
||||
|
||||
// Internes Suchfeld im Dialog
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
onSearchQueryChange(it)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Suchbegriff eingeben...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
placeholder = "Suchbegriff eingeben...",
|
||||
leadingIcon = Icons.Default.Search,
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
+46
-28
@@ -1,12 +1,11 @@
|
||||
package at.mocode.frontend.core.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -14,6 +13,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
@Composable
|
||||
fun MsTextField(
|
||||
@@ -31,28 +31,41 @@ fun MsTextField(
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
compact: Boolean = true // Desktop-optimiert (kompakter)
|
||||
) {
|
||||
val height = if (compact) Dimens.TextFieldHeight else Dimens.TextFieldHeightL
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 4.dp, start = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = label?.let { { Text(it) } },
|
||||
placeholder = placeholder?.let { { Text(it) } },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = height),
|
||||
placeholder = placeholder?.let { { Text(it, style = MaterialTheme.typography.bodyMedium) } },
|
||||
leadingIcon = leadingIcon?.let { icon ->
|
||||
{ Icon(imageVector = icon, contentDescription = null) }
|
||||
{ Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM)) }
|
||||
},
|
||||
trailingIcon = if (trailingIcon != null) {
|
||||
{
|
||||
IconButton(
|
||||
onClick = onTrailingIconClick ?: {}
|
||||
) {
|
||||
Icon(imageVector = trailingIcon, contentDescription = null)
|
||||
Icon(imageVector = trailingIcon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM))
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
@@ -61,6 +74,15 @@ fun MsTextField(
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction
|
||||
@@ -70,24 +92,20 @@ fun MsTextField(
|
||||
)
|
||||
|
||||
// Error or helper text
|
||||
when {
|
||||
isError && errorMessage != null -> {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
helperText != null -> {
|
||||
Text(
|
||||
text = helperText,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
if (isError && errorMessage != null) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||
)
|
||||
} else if (helperText != null) {
|
||||
Text(
|
||||
text = helperText,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -14,12 +14,16 @@ object AppColors {
|
||||
val PrimaryContainer = Color(0xFFDEEBFF)
|
||||
val OnPrimaryContainer = Color(0xFF0052CC)
|
||||
|
||||
// Subtiles Sidebar-Grau / Navigation
|
||||
val NavigationSurface = Color(0xFFF4F5F7)
|
||||
val NavigationContent = Color(0xFF42526E)
|
||||
|
||||
// Helleres Blau für sekundäre Akzente
|
||||
val Secondary = Color(0xFF2684FF)
|
||||
val OnSecondary = Color.White
|
||||
|
||||
// Neutral- & Hintergrund (Light Mode)
|
||||
val BackgroundLight = Color(0xFFF4F5F7) // Helles Grau (nicht hartes Weiß)
|
||||
val BackgroundLight = Color(0xFFF9FAFB) // Sehr helles Grau für den Content Bereich
|
||||
val SurfaceLight = Color.White
|
||||
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
|
||||
|
||||
|
||||
+13
@@ -13,10 +13,17 @@ object Dimens {
|
||||
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
||||
val SpacingM = 16.dp // Abstand für Sektionen
|
||||
val SpacingL = 24.dp // Außenabstand für Screens
|
||||
val SpacingXL = 32.dp
|
||||
|
||||
// Navigations-Maße
|
||||
val NavRailWidth = 72.dp
|
||||
val NavRailExpandedWidth = 240.dp
|
||||
val TopBarHeight = 56.dp
|
||||
|
||||
// Sizes (Größen)
|
||||
val IconSizeS = 16.dp
|
||||
val IconSizeM = 24.dp
|
||||
val IconSizeL = 32.dp
|
||||
|
||||
// Borders
|
||||
val BorderThin = 1.dp
|
||||
@@ -24,4 +31,10 @@ object Dimens {
|
||||
// Corner Radius (Ecken)
|
||||
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
||||
val CornerRadiusM = 8.dp
|
||||
val CornerRadiusL = 12.dp
|
||||
|
||||
// Form-Elemente (Eingabefelder, Buttons)
|
||||
val TextFieldHeight = 44.dp // Kompakte Höhe für Desktop-Enterprise-Apps
|
||||
val TextFieldHeightL = 56.dp // Standard Material Höhe (für prominente Felder)
|
||||
val ButtonHeight = 40.dp
|
||||
}
|
||||
|
||||
+31
@@ -3,8 +3,39 @@ CREATE TABLE LocalSettings (
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- SyncEvents Tabelle für Offline-First/Event-Sourcing (ADR-0022/Concept)
|
||||
CREATE TABLE SyncEvents (
|
||||
sequence_number INTEGER NOT NULL,
|
||||
origin_node_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
turnier_id TEXT,
|
||||
aggregate_type TEXT NOT NULL,
|
||||
aggregate_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
checksum TEXT,
|
||||
synced_at INTEGER, -- Null if not yet synced to backend
|
||||
PRIMARY KEY (origin_node_id, sequence_number)
|
||||
);
|
||||
|
||||
insertOrReplace:
|
||||
INSERT OR REPLACE INTO LocalSettings(key, value) VALUES (?, ?);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM LocalSettings;
|
||||
|
||||
-- SyncEvents Queries
|
||||
insertSyncEvent:
|
||||
INSERT INTO SyncEvents(sequence_number, origin_node_id, event_id, turnier_id, aggregate_type, aggregate_id, event_type, payload, created_at, schema_version, checksum)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
selectUnsyncedEvents:
|
||||
SELECT * FROM SyncEvents WHERE synced_at IS NULL ORDER BY sequence_number ASC;
|
||||
|
||||
markSynced:
|
||||
UPDATE SyncEvents SET synced_at = ? WHERE origin_node_id = ? AND sequence_number = ?;
|
||||
|
||||
getLastSequenceNumber:
|
||||
SELECT MAX(sequence_number) FROM SyncEvents WHERE origin_node_id = ?;
|
||||
|
||||
Reference in New Issue
Block a user