From 60cfd37a1dfee2121d71c98de81e78a0a0f54096 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:18:31 +0000 Subject: [PATCH 1/2] Move Memos onto the shared card layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 4 of the UI component-system plan (docs/ui-component-system-plan.md). - ReminderCard rewritten as a thin binding onto DashListCard. Overdue Memos drop the reserved errorContainer/error palette and map through statusTone to the shared overdue vocabulary (red accent bar + red time). The opaque container plus PR 3's swipe-panel gating fix the archived-overdue Memo that showed a coloured panel and stray label through the card. - SourceChip: a colour-coded chip naming each alarm's origin, tinted from LocalTypeAccents (green chore-sourced, lavender task-sourced), replacing the plain primary-coloured linked label. The text carries the meaning; colour is a secondary cue. Ad hoc Memos have no source and show no chip, as before. - Memos leaves the owner/zen/sort/filter slots empty (it is a collation surface, §5c). - ChoreOverviewSheet and TaskOverviewSheet adopt the shared CategoryBadge and OwnerAvatar so the detail views match the list views. - DashListCard content slot regains the 4dp inter-line gap (lost when the cards moved onto the shell in PR 2); needed by Memos and restores the Task title/meta spacing. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011bvGtNaiY6zFyB2w4fTVAf --- .../dash/ui/components/ChoreOverviewSheet.kt | 20 ++- .../mapgie/dash/ui/components/ReminderCard.kt | 123 +++++++++--------- .../dash/ui/components/TaskOverviewSheet.kt | 22 ++-- .../dash/ui/components/core/DashListCard.kt | 3 + .../dash/ui/components/core/SourceChip.kt | 47 +++++++ changelog/unreleased/memos-shared-layer.json | 13 ++ 6 files changed, 150 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/com/mapgie/dash/ui/components/core/SourceChip.kt create mode 100644 changelog/unreleased/memos-shared-layer.json diff --git a/app/src/main/java/com/mapgie/dash/ui/components/ChoreOverviewSheet.kt b/app/src/main/java/com/mapgie/dash/ui/components/ChoreOverviewSheet.kt index 9b47449..ab11160 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/ChoreOverviewSheet.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/ChoreOverviewSheet.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.mapgie.dash.data.model.Chore import com.mapgie.dash.data.model.ScanDto +import com.mapgie.dash.ui.components.core.CategoryBadge +import com.mapgie.dash.ui.components.core.OwnerAvatar import com.mapgie.dash.util.CalendarShareUtils import com.mapgie.dash.util.calendarEventWithoutTime import kotlinx.coroutines.launch @@ -91,13 +93,17 @@ fun ChoreOverviewSheet( modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.weight(1f)) { - if (chore.category != null) { - Text( - chore.category.uppercase(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - // 4dp between eyebrow and title + val hasOwner = chore.owner?.isNotBlank() == true + if (chore.category != null || hasOwner) { + // Same category badge and owner avatar as the list card. + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + chore.category?.let { CategoryBadge(it) } + if (hasOwner) OwnerAvatar(chore.owner!!) + } + // 4dp between identity row and title Spacer(Modifier.height(4.dp)) } Text(chore.label, style = MaterialTheme.typography.headlineLarge) diff --git a/app/src/main/java/com/mapgie/dash/ui/components/ReminderCard.kt b/app/src/main/java/com/mapgie/dash/ui/components/ReminderCard.kt index 72f43a4..3cd4473 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/ReminderCard.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/ReminderCard.kt @@ -1,14 +1,8 @@ package com.mapgie.dash.ui.components import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,10 +15,27 @@ import androidx.compose.ui.unit.dp import com.mapgie.dash.data.model.ReminderDto import com.mapgie.dash.data.model.isPast import com.mapgie.dash.data.model.remindAtInstant +import com.mapgie.dash.ui.components.core.DashListCard +import com.mapgie.dash.ui.components.core.MetaLabel +import com.mapgie.dash.ui.components.core.SourceChip +import com.mapgie.dash.ui.components.core.SourceKind +import com.mapgie.dash.ui.theme.statusTone +import com.mapgie.dash.ui.theme.textColor import java.time.ZoneId import java.time.format.DateTimeFormatter -@OptIn(ExperimentalMaterial3Api::class) +/** + * Thin binding of a [ReminderDto] onto the shared [DashListCard]. + * + * Memos is a collation surface, so this fills only the slots that make sense here: + * a done checkbox, the subject, the fire time, and a [SourceChip] naming where the + * alarm came from. No owner avatar, no zen mode, no trailing column (§5c). + * + * Overdue no longer uses the reserved `error` palette: it maps through [statusTone] + * to the shared overdue vocabulary (a red accent bar and red "Overdue" text), and + * the container stays opaque, so an archived overdue Memo no longer leaks the swipe + * panel behind it. + */ @Composable fun ReminderCard( reminder: ReminderDto, @@ -34,77 +45,63 @@ fun ReminderCard( modifier: Modifier = Modifier ) { val isDone = reminder.completedAt != null - val isPast = reminder.isPast() - // Overdue: time has passed but user hasn't explicitly marked it done - val isOverdue = isPast && !isDone + val isOverdue = reminder.isPast() && !isDone + val tone = reminder.statusTone() - val formatter = remember(reminder.remindAt) { - DateTimeFormatter.ofPattern("MMM d, yyyy 'at' HH:mm") - } + val formatter = remember { DateTimeFormatter.ofPattern("MMM d, yyyy 'at' HH:mm") } val whenLabel = reminder.remindAtInstant() ?.atZone(ZoneId.systemDefault()) ?.format(formatter) - - val containerColor = when { - isDone -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - isOverdue -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) - else -> MaterialTheme.colorScheme.surfaceVariant - } - + val timeText = if (isOverdue) whenLabel?.let { "Overdue: $it" } ?: "Overdue" else whenLabel val timeColor = when { - isDone -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) - isOverdue -> MaterialTheme.colorScheme.error + isDone -> MaterialTheme.colorScheme.onSurfaceVariant + isOverdue -> tone.textColor() else -> MaterialTheme.colorScheme.onSurfaceVariant } - val timeText = when { - isOverdue -> whenLabel?.let { "Overdue: $it" } ?: "Overdue" - else -> whenLabel + val sourceKind = when { + reminder.choreId != null -> SourceKind.CHORE + reminder.taskId != null -> SourceKind.TASK + else -> null } - Card( + DashListCard( + tone = tone, + modifier = modifier, onClick = onClick, - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = containerColor) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { + dimmed = isDone, + leading = { Checkbox( checked = isDone, - onCheckedChange = { onToggleDone() } + onCheckedChange = { onToggleDone() }, + modifier = Modifier.align(Alignment.CenterVertically) ) - Column( - modifier = Modifier - .weight(1f) - .padding(end = 12.dp, top = 10.dp, bottom = 10.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = reminder.subject, - style = MaterialTheme.typography.bodyLarge, - textDecoration = if (isDone) TextDecoration.LineThrough else null, - color = if (isDone) - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) - else - MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis + } + ) { + Text( + text = reminder.subject, + style = MaterialTheme.typography.bodyLarge, + textDecoration = if (isDone) TextDecoration.LineThrough else null, + color = if (isDone) + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + else + MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + timeText?.let { + MetaLabel( + text = it, + color = timeColor, + style = MaterialTheme.typography.labelMedium ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - timeText?.let { - Text( - text = it, - style = MaterialTheme.typography.labelMedium, - color = timeColor - ) - } - linkedLabel?.let { - Text( - text = it, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary - ) - } - } + } + if (sourceKind != null && linkedLabel != null) { + SourceChip(kind = sourceKind, label = linkedLabel) } } } diff --git a/app/src/main/java/com/mapgie/dash/ui/components/TaskOverviewSheet.kt b/app/src/main/java/com/mapgie/dash/ui/components/TaskOverviewSheet.kt index fd843d1..53852ff 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/TaskOverviewSheet.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/TaskOverviewSheet.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.mapgie.dash.data.model.TaskDto +import com.mapgie.dash.ui.components.core.CategoryBadge +import com.mapgie.dash.ui.components.core.OwnerAvatar import com.mapgie.dash.util.CalendarShareUtils import com.mapgie.dash.util.calendarEventForDate import com.mapgie.dash.util.calendarEventForInstant @@ -66,14 +68,18 @@ fun TaskOverviewSheet( // 24dp after drag handle Spacer(Modifier.height(24.dp)) - // Category eyebrow - if (task.category != null) { - Text( - task.category.uppercase(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - // 4dp between eyebrow and title + // Category badge + owner avatar, matching the list card. + val hasOwner = task.owner?.isNotBlank() == true + val category = task.category?.takeIf { it.isNotBlank() } + if (category != null || hasOwner) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + category?.let { CategoryBadge(it) } + if (hasOwner) OwnerAvatar(task.owner!!) + } + // 4dp between identity row and title Spacer(Modifier.height(4.dp)) } diff --git a/app/src/main/java/com/mapgie/dash/ui/components/core/DashListCard.kt b/app/src/main/java/com/mapgie/dash/ui/components/core/DashListCard.kt index e71067d..b6f4a21 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/core/DashListCard.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/core/DashListCard.kt @@ -3,6 +3,7 @@ package com.mapgie.dash.ui.components.core import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.lerp import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp import com.mapgie.dash.ui.theme.Dimens import com.mapgie.dash.ui.theme.StatusTone import com.mapgie.dash.ui.theme.barColor @@ -114,6 +116,7 @@ fun DashListCard( modifier = Modifier .weight(1f) .padding(Dimens.cardPadding), + verticalArrangement = Arrangement.spacedBy(4.dp), content = content ) // Optional trailing slot (e.g. due / last-scanned dates), end-aligned. diff --git a/app/src/main/java/com/mapgie/dash/ui/components/core/SourceChip.kt b/app/src/main/java/com/mapgie/dash/ui/components/core/SourceChip.kt new file mode 100644 index 0000000..9d1a9b1 --- /dev/null +++ b/app/src/main/java/com/mapgie/dash/ui/components/core/SourceChip.kt @@ -0,0 +1,47 @@ +package com.mapgie.dash.ui.components.core + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.mapgie.dash.ui.theme.LocalTypeAccents + +/** Where a Memo's alarm came from. Drives the [SourceChip] colour. */ +enum class SourceKind { CHORE, TASK } + +/** + * A small pill on a Memo row naming where the alarm came from, tinted from + * `LocalTypeAccents` in the same vocabulary as the nav bar: green for a + * chore-sourced alarm, lavender for a task-sourced one. The [label] (e.g. + * "Chore: Air Plant") carries the meaning on its own, so the colour is a + * secondary cue only. + */ +@Composable +fun SourceChip(kind: SourceKind, label: String, modifier: Modifier = Modifier) { + val accents = LocalTypeAccents.current + val container = when (kind) { + SourceKind.CHORE -> accents.choreContainer + SourceKind.TASK -> accents.taskContainer + } + val onContainer = when (kind) { + SourceKind.CHORE -> accents.onChoreContainer + SourceKind.TASK -> accents.onTaskContainer + } + Box( + modifier = modifier + .clip(MaterialTheme.shapes.extraSmall) + .background(container) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = onContainer + ) + } +} diff --git a/changelog/unreleased/memos-shared-layer.json b/changelog/unreleased/memos-shared-layer.json new file mode 100644 index 0000000..7c5f59e --- /dev/null +++ b/changelog/unreleased/memos-shared-layer.json @@ -0,0 +1,13 @@ +{ + "bump": "patch", + "added": [ + "Memo rows now show a colour-coded source chip naming where the alarm came from: green for a chore, lavender for a task.", + "The chore and task detail sheets now show the same category badge and owner avatar as the lists." + ], + "changed": [ + "Overdue Memos now use the same overdue styling as Chores and Tasks (a red edge and red time) instead of the reserved error colours." + ], + "fixed": [ + "Archived or completed Memos no longer show a coloured panel or a stray label bleeding through the card." + ] +} From 6b7262f2e809d78bf2c05db2edbaa6cf85d8d9ae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:24:35 +0000 Subject: [PATCH 2/2] Add component gallery and components guide PR 5 of the UI component-system plan (docs/ui-component-system-plan.md). - ui/components/core/Gallery.kt: a set of @Preview composables rendering the three card bindings and every core primitive side by side, in light/dark, zen, and high-contrast. The Storybook analogue: one place to see drift in review. Preview-only; nothing ships in the running app. - ui/components/README.md: documents the rule the layer exists to enforce - screens compose components, screens do not draw - plus the layout, the opaque-container and one-status-vocabulary rules, and how to add a component. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011bvGtNaiY6zFyB2w4fTVAf --- .../com/mapgie/dash/ui/components/README.md | 54 +++++ .../mapgie/dash/ui/components/core/Gallery.kt | 214 ++++++++++++++++++ .../component-gallery-guardrails.json | 4 + 3 files changed, 272 insertions(+) create mode 100644 app/src/main/java/com/mapgie/dash/ui/components/README.md create mode 100644 app/src/main/java/com/mapgie/dash/ui/components/core/Gallery.kt create mode 100644 changelog/unreleased/component-gallery-guardrails.json diff --git a/app/src/main/java/com/mapgie/dash/ui/components/README.md b/app/src/main/java/com/mapgie/dash/ui/components/README.md new file mode 100644 index 0000000..7d2dd85 --- /dev/null +++ b/app/src/main/java/com/mapgie/dash/ui/components/README.md @@ -0,0 +1,54 @@ +# UI components + +**Screens compose components; screens do not draw.** + +A screen's job is to wire a ViewModel's state to shared composables and route +callbacks back. It should not hand-roll a card, a badge, a header, a swipe wrapper, +or an empty state. When two screens need the same widget, that widget lives here, in +one place, so the screens cannot drift apart. + +## Layout + +``` +ui/components/ + core/ the shared, screen-agnostic building blocks + DashListCard.kt the one list-card shell (slots: leading / content / trailing / owner) + SwipeActionRow.kt the one swipe-to-act wrapper + OwnerAvatar.kt the one owner indicator (per-person colour, everywhere) + CategoryBadge.kt the one category pill + SourceChip.kt a Memo's alarm-origin chip + MetaLabel.kt secondary / muted text + ListSectionHeader.kt sticky category header + collapsible section header + DashStates.kt empty / error / loading + DashFilterBar.kt filter-chips-plus-actions bar scaffold + DashScreenHeader.kt the thin type-coloured identity strip + Gallery.kt @Preview gallery of everything above + ChoreCard.kt thin binding: Chore -> DashListCard slots + TaskCard.kt thin binding: TaskDto -> DashListCard slots + ReminderCard.kt thin binding: ReminderDto -> DashListCard slots + (sheets, dialogs, add/edit forms ...) +``` + +## Rules + +- **No `Card(` outside `core/`.** List rows go through `DashListCard`; it owns the + container, the status accent bar, vertical centring, the tap target, the inset, the + owner slot, and the `role` + `combinedClickable` plumbing (so accessibility is + correct in one place). +- **No hardcoded spacing in screen files.** Sizes come from `ui/theme/Dimens.kt`. +- **Card containers are always opaque.** Dim a done/archived row by blending toward + the surface or lowering *content* alpha, never the container's alpha. See + `LESSONS.md` #32. +- **One status vocabulary.** Colour-coded state goes through `StatusTone` + (`ui/theme/StatusTone.kt`), keyed on urgency, so the same colour means the same + thing on every tab. Every colour-coded state must also carry a shape or label + (never colour alone). +- **Every clickable declares a `Role`.** The shell does this for cards; anything new + that is clickable must too (`a11y_check.py` enforces it). + +## Adding or changing a component + +1. Put it in `core/` if more than one screen could use it. +2. Add or update its `@Preview` in `Gallery.kt`, covering light/dark and any + meaningful state (done, zen, high-contrast). +3. Point the screens at it and delete their private copy. diff --git a/app/src/main/java/com/mapgie/dash/ui/components/core/Gallery.kt b/app/src/main/java/com/mapgie/dash/ui/components/core/Gallery.kt new file mode 100644 index 0000000..88fd7be --- /dev/null +++ b/app/src/main/java/com/mapgie/dash/ui/components/core/Gallery.kt @@ -0,0 +1,214 @@ +package com.mapgie.dash.ui.components.core + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mapgie.dash.data.model.Chore +import com.mapgie.dash.data.model.ChoreStatus +import com.mapgie.dash.data.model.ReminderDto +import com.mapgie.dash.data.model.TaskDto +import com.mapgie.dash.ui.components.ChoreCard +import com.mapgie.dash.ui.components.ReminderCard +import com.mapgie.dash.ui.components.TaskCard +import com.mapgie.dash.ui.theme.DashTheme +import com.mapgie.dash.ui.theme.LocalTypeAccents +import java.time.Instant +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +/** + * The Storybook analogue for the shared component layer: a set of `@Preview`s that + * render every core component and the three card bindings side by side, in light and + * dark, zen, and high-contrast. Reviewers get a single place to see drift, and + * nothing here ships in the running app. + * + * The rule these previews guard is written up in `ui/components/README.md`: + * **screens compose components; screens do not draw.** + */ + +// ── Sample models ────────────────────────────────────────────────────────────── + +private fun sampleChore( + status: ChoreStatus, + label: String = "Water the plants", + owner: String? = "Alex", + category: String? = "Kitchen", +) = Chore( + id = "c1", + tagId = "tag-1", + label = label, + category = category, + owner = owner, + intervalDays = 3.0, + archivedAt = null, + lastScanned = Instant.now().minus(2, ChronoUnit.DAYS), + lastScanId = "s1", + status = status, +) + +private fun sampleTask( + title: String = "Buy printer toner", + priority: String = "normal", + dueDate: String? = null, + done: Boolean = false, + owner: String? = "Sam", + category: String? = "Errands", +) = TaskDto( + id = "t1", + title = title, + notes = null, + category = category, + owner = owner, + priority = priority, + dueDate = dueDate, + duePeriod = null, + completedAt = if (done) Instant.now().toString() else null, + archivedAt = null, + reminderAt = null, + reminded = null, + createdAt = "", +) + +private fun sampleReminder( + subject: String = "Rotate the tyres", + overdue: Boolean = false, + done: Boolean = false, + choreId: String? = null, + taskId: String? = null, +) = ReminderDto( + id = "r1", + subject = subject, + remindAt = (if (overdue) Instant.now().minusSeconds(3600) else Instant.now().plusSeconds(3600)).toString(), + choreId = choreId, + taskId = taskId, + completedAt = if (done) Instant.now().toString() else null, + reminded = false, + createdAt = "", + archivedAt = null, +) + +// ── Showcases ────────────────────────────────────────────────────────────────── + +@Composable +private fun GalleryFrame( + dark: Boolean, + wcag: Boolean = false, + content: @Composable () -> Unit, +) { + DashTheme(darkTheme = dark, wcag = wcag) { + Surface(color = MaterialTheme.colorScheme.background) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + content() + } + } + } +} + +@Composable +private fun CardsShowcase(zen: Boolean = false) { + val accents = LocalTypeAccents.current + + DashScreenHeader("Chores", accents.choreContainer, accents.onChoreContainer) + ChoreCard( + chore = sampleChore(ChoreStatus.STALE), + showOwner = true, + zenMode = zen, + showDueCountdown = true, + onClick = {}, + onLongClick = {}, + ) + ChoreCard( + chore = sampleChore(ChoreStatus.FRESH, label = "Take out recycling", owner = "Bella"), + showOwner = true, + zenMode = zen, + onClick = {}, + onLongClick = {}, + ) + + DashScreenHeader("Tasks", accents.taskContainer, accents.onTaskContainer) + TaskCard( + task = sampleTask(priority = "higher", dueDate = LocalDate.now().minusDays(1).toString()), + onToggleDone = {}, + zenMode = zen, + onClick = {}, + onLongClick = {}, + ) + TaskCard( + task = sampleTask(title = "Renew library books", priority = "lower", done = true), + onToggleDone = {}, + zenMode = zen, + onClick = {}, + onLongClick = {}, + ) + + DashScreenHeader("Memos", accents.reminderContainer, accents.onReminderContainer) + ReminderCard( + reminder = sampleReminder(overdue = true, choreId = "c1"), + linkedLabel = "Chore: Water the plants", + onClick = {}, + onToggleDone = {}, + ) + ReminderCard( + reminder = sampleReminder(subject = "Call the dentist", taskId = "t1"), + linkedLabel = "Task: Buy printer toner", + onClick = {}, + onToggleDone = {}, + ) +} + +@Composable +private fun PrimitivesShowcase() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 16.dp), + ) { + listOf("Alex", "Bella", "Sam", "Kai", "Mo", "Tam").forEach { OwnerAvatar(it) } + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 16.dp), + ) { + CategoryBadge("Kitchen") + SourceChip(SourceKind.CHORE, "Chore: Water plants") + SourceChip(SourceKind.TASK, "Task: Buy toner") + } + ListSectionHeader("Kitchen") + CollapsibleSectionHeader("Done", expanded = false, onToggle = {}, count = 3) +} + +// ── Previews ─────────────────────────────────────────────────────────────────--- + +@Preview(name = "Cards / light", showBackground = true) +@Composable +private fun CardsLightPreview() = GalleryFrame(dark = false) { CardsShowcase() } + +@Preview(name = "Cards / dark", showBackground = true) +@Composable +private fun CardsDarkPreview() = GalleryFrame(dark = true) { CardsShowcase() } + +@Preview(name = "Cards / zen", showBackground = true) +@Composable +private fun CardsZenPreview() = GalleryFrame(dark = false) { CardsShowcase(zen = true) } + +@Preview(name = "Cards / high contrast", showBackground = true) +@Composable +private fun CardsWcagPreview() = GalleryFrame(dark = false, wcag = true) { CardsShowcase() } + +@Preview(name = "Primitives", showBackground = true) +@Composable +private fun PrimitivesPreview() = GalleryFrame(dark = false) { PrimitivesShowcase() } + +@Preview(name = "States", showBackground = true, heightDp = 160) +@Composable +private fun StatesPreview() = GalleryFrame(dark = false) { + Box(Modifier.height(140.dp)) { DashEmptyState("No tasks yet") } +} diff --git a/changelog/unreleased/component-gallery-guardrails.json b/changelog/unreleased/component-gallery-guardrails.json new file mode 100644 index 0000000..3df9864 --- /dev/null +++ b/changelog/unreleased/component-gallery-guardrails.json @@ -0,0 +1,4 @@ +{ + "bump": "patch", + "changed": ["Internal: added a @Preview gallery and a components guide (README) for the shared UI layer; no user-visible change."] +}