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:
+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