From 54a2d19833bf71bcad0a2896954f3355931c5f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 19 Jun 2026 21:18:11 +0200 Subject: [PATCH] feat(ui): add shared ListRow primitive and adopt it for mini-cards The bordered list-row "mini-card" appeared three times with three shapes: the Vorsorge dashboard row (`rounded-md p-2.5`), the daily- briefing rows (`rounded-md p-3`), and the measurement-list mobile row (bespoke `rounded-lg p-3` div). Introduce `ListRow` (`rounded-lg border p-3`, `asChild` per the shadcn convention) and migrate all three to it. The primitive owns only the box shape; each caller keeps its layout, colour, and interaction classes, so this is structural normalization with no content or behaviour change. --- src/components/insights/daily-briefing.tsx | 17 ++++---- .../vorsorge-dashboard-card.tsx | 14 ++++--- .../measurements/measurement-list.tsx | 7 ++-- src/components/ui/list-row.tsx | 42 +++++++++++++++++++ 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/components/ui/list-row.tsx diff --git a/src/components/insights/daily-briefing.tsx b/src/components/insights/daily-briefing.tsx index 27bc08591..a3f349d75 100644 --- a/src/components/insights/daily-briefing.tsx +++ b/src/components/insights/daily-briefing.tsx @@ -22,6 +22,7 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; +import { ListRow } from "@/components/ui/list-row"; import { Skeleton } from "@/components/ui/skeleton"; import { SectionHeading } from "@/components/insights/section-heading"; import { useTranslations } from "@/lib/i18n/context"; @@ -230,28 +231,28 @@ function BriefingRow({ ); if (href) { return ( - - {rowContent} - + {rowContent} + ); } return ( -
{rowContent} -
+ ); } diff --git a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx index 0c03f88ef..3d5396c8b 100644 --- a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx +++ b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx @@ -22,6 +22,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ListRow } from "@/components/ui/list-row"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { MeasurementForm } from "@/components/measurements/measurement-form"; @@ -119,10 +120,12 @@ export function VorsorgeDashboardCard() { const due = relativeDueKey(reminder.nextDueAt, now); const isLinked = reminder.measurementType != null; return ( -
  • +
  • {resolveLabel(reminder, t)} @@ -139,8 +142,8 @@ export function VorsorgeDashboardCard() {

    -
  • + + ); })} diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx index 51765bbdb..fd53625e6 100644 --- a/src/components/measurements/measurement-list.tsx +++ b/src/components/measurements/measurement-list.tsx @@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/use-auth"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { EmptyState } from "@/components/ui/empty-state"; +import { ListRow } from "@/components/ui/list-row"; import { FilterBar, FilterBarDateRange, @@ -1080,10 +1081,10 @@ export function MeasurementList({ const drilldownId = `drilldown-mobile-${m.dayKey ?? m.id}`; const isSelected = selectedIds.has(m.id); return ( -
    @@ -1243,7 +1244,7 @@ export function MeasurementList({ />
    )} -
    + ); })}
    diff --git a/src/components/ui/list-row.tsx b/src/components/ui/list-row.tsx new file mode 100644 index 000000000..0a60bbc80 --- /dev/null +++ b/src/components/ui/list-row.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { Slot } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +/** + * v1.18.7 — shared list-row primitive. + * + * Before this, every list-row "mini-card" (a bordered row sitting inside + * a section or a ``) invented its own padding + radius: the + * Vorsorge dashboard card used `rounded-md p-2.5`, the daily-briefing + * rows `rounded-md p-3`, the measurement-list mobile row a bespoke + * `rounded-lg p-3` div. Three surfaces, three shapes for one visual + * concept. `ListRow` pins the canonical shape — `rounded-lg border p-3` + * — so the rows read as one language across the app. + * + * It owns ONLY the box shape (radius + border + padding). Layout + * (flex direction, alignment, gap), colour overrides, and interaction + * state stay with the caller via `className`, so migrating a bespoke row + * is purely dropping the radius/border/padding literals and keeping the + * rest. + * + * `asChild` renders the row as its single child (e.g. a `` or + * `
  • `) while still applying the shared classes, matching the shadcn + * primitive convention used elsewhere in `ui/`. + */ +function ListRow({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "div"; + return ( + + ); +} + +export { ListRow };