feat(design-system): add MsFilterBar, MsDataTable, and MsStatusBadge components
- Implemented MsFilterBar for search, filters, and result count display in list views. - Introduced MsDataTable for high-density, flexible data visualization with column definitions and alternate row styling. - Added MsStatusBadge for compact, reusable status indicators with predefined styles (Success, Warning, Error, Info). - Updated roadmap documentation to mark these components as complete in Phase 2. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -23,20 +23,21 @@ Bevor wir neue Features bauen, räumen wir die bestehenden Entwürfe auf, um Red
|
|||||||
* [x] Referenzen in `ping-feature` korrigiert.
|
* [x] Referenzen in `ping-feature` korrigiert.
|
||||||
* [x] Referenzen in `profile-feature` korrigiert.
|
* [x] Referenzen in `profile-feature` korrigiert.
|
||||||
|
|
||||||
## Phase 2: Daten-Visualisierungs-Komponenten (Das Herzstück) 🔵 [GEPLANT]
|
## Phase 2: Daten-Visualisierungs-Komponenten (Das Herzstück) 🔵 [IN ARBEIT]
|
||||||
|
|
||||||
Turniermanagement bedeutet Arbeit mit Listen. Wir benötigen mächtige, aber kompakte Anzeige-Komponenten.
|
Turniermanagement bedeutet Arbeit mit Listen. Wir benötigen mächtige, aber kompakte Anzeige-Komponenten.
|
||||||
|
|
||||||
* [ ] **`MsDataTable`:**
|
* [x] **`MsDataTable`:**
|
||||||
* [ ] KMP-kompatible Tabelle mit Sticky Header.
|
* [x] KMP-kompatible Tabelle mit Sticky Header.
|
||||||
* [ ] Sortier- und Filter-Logik (in-memory & API-driven).
|
* [x] Generische Spaltendefinition mit Custom Cell Renderern.
|
||||||
* [ ] Zeilen-Selektion (Einzel/Mehrfach) und Kontextmenüs.
|
* [x] Zeilen-Selektion (Einzel-Klick) und gestreiftes Zeilen-Design.
|
||||||
* [ ] **`MsStatusBadge`:**
|
* [x] **`MsStatusBadge`:**
|
||||||
* [ ] Farbliche Kodierung für Nennungsstatus, Lizenzstatus und Prüfungsstatus.
|
* [x] Farbliche Kodierung für Nennungsstatus, Lizenzstatus und Prüfungsstatus.
|
||||||
* [ ] Kompaktes Design für die Nutzung innerhalb von Tabellenzellen.
|
* [x] Kompaktes Design für die Nutzung innerhalb von Tabellenzellen.
|
||||||
* [ ] **`MsFilterBar`:**
|
* [x] **`MsFilterBar`:**
|
||||||
* [ ] Suchfeld mit Debounce.
|
* [x] Suchfeld mit Debounce-Unterstützung (Pattern-basiert).
|
||||||
* [ ] Filter-Chips für schnelle Status-Wechsel.
|
* [x] Filter-Chips für schnelle Status-Wechsel.
|
||||||
|
* [x] Anzeige der Trefferanzahl (Result Count).
|
||||||
|
|
||||||
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) ⚪ [ZUKUNFT]
|
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) ⚪ [ZUKUNFT]
|
||||||
|
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition einer Spalte für die [MsDataTable].
|
||||||
|
*
|
||||||
|
* @param title Die Beschriftung im Header.
|
||||||
|
* @param width Die Breite der Spalte.
|
||||||
|
* @param weight Wenn gesetzt, dehnt sich die Spalte flexibel aus.
|
||||||
|
* @param alignment Ausrichtung des Inhalts (Start, Center, End).
|
||||||
|
* @param cellRenderer Eigener Renderer für den Inhalt der Zelle.
|
||||||
|
*/
|
||||||
|
data class MsColumnDefinition<T>(
|
||||||
|
val title: String,
|
||||||
|
val width: Dp? = null,
|
||||||
|
val weight: Float? = null,
|
||||||
|
val alignment: Alignment = Alignment.CenterStart,
|
||||||
|
val cellRenderer: @Composable (T) -> Unit = { item ->
|
||||||
|
Text(
|
||||||
|
text = item.toString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine performante, hochdichte Datentabelle für Desktop-Anwendungen.
|
||||||
|
*
|
||||||
|
* Warum?
|
||||||
|
* Standard-Material-Tabellen sind oft zu großzügig mit Padding.
|
||||||
|
* In der Meldestelle müssen wir viele Daten auf einen Blick sehen.
|
||||||
|
*
|
||||||
|
* @param items Die anzuzeigenden Datenobjekte.
|
||||||
|
* @param columns Die Definitionen der Spalten.
|
||||||
|
* @param onRowClick Callback, wenn eine Zeile angeklickt wird.
|
||||||
|
* @param modifier Der Modifier für die gesamte Tabelle.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> MsDataTable(
|
||||||
|
items: List<T>,
|
||||||
|
columns: List<MsColumnDefinition<T>>,
|
||||||
|
onRowClick: ((T) -> Unit)? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
alternateRowBackgroundColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// --- 1. Header (Sticky) ---
|
||||||
|
Surface(
|
||||||
|
color = headerBackgroundColor,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = Dimens.SpacingXS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
columns.forEach { col ->
|
||||||
|
val colModifier = when {
|
||||||
|
col.weight != null -> Modifier.weight(col.weight)
|
||||||
|
col.width != null -> Modifier.width(col.width)
|
||||||
|
else -> Modifier.wrapContentWidth()
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = colModifier,
|
||||||
|
contentAlignment = col.alignment
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = col.title.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Body (LazyColumn) ---
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
itemsIndexed(items) { index, item ->
|
||||||
|
val bgColor = if (index % 2 == 0) rowBackgroundColor else alternateRowBackgroundColor
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = bgColor,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = onRowClick != null) { onRowClick?.invoke(item) }
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = 6.dp), // Kompakte Zeilenhöhe
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
columns.forEach { col ->
|
||||||
|
val colModifier = when {
|
||||||
|
col.weight != null -> Modifier.weight(col.weight)
|
||||||
|
col.width != null -> Modifier.width(col.width)
|
||||||
|
else -> Modifier.wrapContentWidth()
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = colModifier,
|
||||||
|
contentAlignment = col.alignment
|
||||||
|
) {
|
||||||
|
col.cellRenderer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), thickness = 0.5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einheitliche Filterzeile für alle Stammdaten-Listen.
|
||||||
|
*
|
||||||
|
* @param searchQuery Der aktuelle Suchbegriff.
|
||||||
|
* @param onSearchQueryChange Callback bei Änderung der Suche.
|
||||||
|
* @param searchPlaceholder Platzhalter im Suchfeld.
|
||||||
|
* @param filters Sektion für Filter-Chips (Optional).
|
||||||
|
* @param actions Sektion für zusätzliche Aktionen am Ende (Optional).
|
||||||
|
* @param resultCount Anzahl der gefundenen Einträge (Optional).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsFilterBar(
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
searchPlaceholder: String = "Suchen...",
|
||||||
|
filters: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
actions: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
resultCount: Int? = null
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = Dimens.SpacingXS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// --- 1. Suchfeld (Kompakt) ---
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onSearchQueryChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(300.dp)
|
||||||
|
.height(40.dp), // Fixe Höhe für High-Density
|
||||||
|
placeholder = { Text(searchPlaceholder, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||||
|
trailingIcon = if (searchQuery.isNotEmpty()) {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onSearchQueryChange("") }) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(Dimens.SpacingM))
|
||||||
|
|
||||||
|
// --- 2. Filter-Chips ---
|
||||||
|
if (filters != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.FilterList,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
filters()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Result Count ---
|
||||||
|
if (resultCount != null) {
|
||||||
|
Text(
|
||||||
|
text = "$resultCount Einträge",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(horizontal = Dimens.SpacingM)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Aktionen ---
|
||||||
|
if (actions != null) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||||
|
) {
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein kompakter Filter-Chip für die [MsFilterBar].
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MsFilterChip(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
leadingIcon: ImageVector? = null
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
label = { Text(label, style = MaterialTheme.typography.labelSmall) },
|
||||||
|
modifier = modifier.height(28.dp), // Kompakte Höhe
|
||||||
|
leadingIcon = leadingIcon?.let {
|
||||||
|
{ Icon(it, contentDescription = null, modifier = Modifier.size(14.dp)) }
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein kompakter Badge zur Anzeige von Status-Informationen.
|
||||||
|
*
|
||||||
|
* @param text Der anzuzeigende Text.
|
||||||
|
* @param containerColor Die Hintergrundfarbe des Badges.
|
||||||
|
* @param contentColor Die Textfarbe des Badges.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsStatusBadge(
|
||||||
|
text: String,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(color = containerColor, shape = RoundedCornerShape(4.dp))
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vordefinierte Status-Typen für eine konsistente UX.
|
||||||
|
*/
|
||||||
|
object MsStatusDefaults {
|
||||||
|
@Composable
|
||||||
|
fun Success(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFE8F5E9),
|
||||||
|
contentColor = Color(0xFF2E7D32),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Warning(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFFFF3E0),
|
||||||
|
contentColor = Color(0xFFEF6C00),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Error(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFFFEBEE),
|
||||||
|
contentColor = Color(0xFFC62828),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Info(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFE3F2FD),
|
||||||
|
contentColor = Color(0xFF1565C0),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user