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:
2026-04-12 23:06:49 +02:00
parent 5eb2dd6904
commit 126522e606
17 changed files with 854 additions and 551 deletions
@@ -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))
@@ -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)
)
}
}
}
@@ -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,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
}
@@ -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 = ?;