From d2be72d57a686317b3182742e7ca4666e1d52eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 19 Jun 2026 21:18:05 +0200 Subject: [PATCH 1/4] feat(mobile): add scroll-touch momentum utility for horizontal strips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `.scroll-touch` utility that restores iOS WebKit momentum scrolling (`-webkit-overflow-scrolling: touch`) and contains the overscroll chain so a flick at the end of a horizontal row does not rubber-band the whole page. Apply it to the insights tab strip and the cycle tab list — the two in-app horizontal scrollers that lost inertial flick under the default `overflow-x-auto`. --- src/app/globals.css | 15 +++++++++++++++ src/components/cycle/cycle-view.tsx | 2 +- src/components/insights/insights-tab-strip.tsx | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 4696df20e..20db09ff5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -448,6 +448,21 @@ display: none; } +/* + * `.scroll-touch` — momentum scrolling for horizontal scrollers on iOS + * WebKit. `-webkit-overflow-scrolling: touch` restores the inertial + * flick the default `overflow-x-auto` row loses on touch; `overscroll- + * behavior: contain` keeps a flick that reaches the end of the row from + * chaining into the document scroll (the "rubber-band drags the whole + * page" paper-cut). Applied to the in-app horizontal pill / tab strips + * (insights tab strip, cycle tab list) so the gesture matches the + * native feel the bottom-sheet scrollers already get for free. + */ +.scroll-touch { + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; +} + /* ── Insight card animations ─────────────────────── */ @keyframes insight-fade-in { from { diff --git a/src/components/cycle/cycle-view.tsx b/src/components/cycle/cycle-view.tsx index 74e6b9bea..37562ae52 100644 --- a/src/components/cycle/cycle-view.tsx +++ b/src/components/cycle/cycle-view.tsx @@ -255,7 +255,7 @@ export function CycleView() { triggers size to their content (`text-xs`, no `flex-1`) and the strip scrolls cleanly if needed; from `sm` up they reclaim the equal-width `flex-1` layout. */} - + {tabs.map((tab) => { if (tab.kind === "link") { 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 2/4] 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 }; From e3071d4f0a5e8e00d0bc406aa1e194f47a7ce601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 19 Jun 2026 21:18:19 +0200 Subject: [PATCH 3/4] fix(mobile): raise primary touch controls to the 44px floor Sweep the primary touch controls that sat below the 44px tap-target floor on phones onto the canonical `min-h-11 sm:min-h-9` (text) / `size-11 sm:size-9` (icon) pattern, condensing back at `sm:` upward: - Coach composer mic / stop / send icon buttons (were `size-9`). - Dashboard hero CTA, link and action variants (were `size="sm"`). - Vorsorge list-view "mark done" button (was `min-h-9`) and its kebab, which now uses `size="icon-lg"` so the rendered control is 44px rather than the icon variant's 40px. - Illness list-view "log day" button (was `min-h-9`). - Medication wizard Step 3 injection-sites disclosure (was `h-9`). The Vorsorge dashboard-card capture action was raised alongside the ListRow migration. --- src/components/dashboard/hero/dashboard-hero.tsx | 6 ++++-- src/components/illness/illness-view.tsx | 2 +- src/components/insights/coach-panel/coach-input.tsx | 10 +++++----- .../measurement-reminders/vorsorge-section.tsx | 6 +++--- src/components/medications/wizard/steps/Step3Dose.tsx | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/dashboard/hero/dashboard-hero.tsx b/src/components/dashboard/hero/dashboard-hero.tsx index 02154f6de..95a975e9e 100644 --- a/src/components/dashboard/hero/dashboard-hero.tsx +++ b/src/components/dashboard/hero/dashboard-hero.tsx @@ -258,15 +258,17 @@ export function DashboardHero({ cta.kind === "link" ? ( ) : ( @@ -432,7 +432,7 @@ export function CoachInput({ data-slot="coach-input-send" aria-label={t("insights.coach.send")} title={t("insights.coach.send")} - className="size-9 shrink-0 rounded-xl" + className="size-11 shrink-0 rounded-xl sm:size-9" > {isStreaming ? (