From bf509f6b75301cae9e4cdcff5f3816ac8aac4c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 20 Jun 2026 12:53:41 +0200 Subject: [PATCH 1/4] fix(labs): give the OCR per-row confirm a 44px touch target --- src/components/labs/ocr-row-editor.tsx | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/labs/ocr-row-editor.tsx b/src/components/labs/ocr-row-editor.tsx index 54563333f..926a02f94 100644 --- a/src/components/labs/ocr-row-editor.tsx +++ b/src/components/labs/ocr-row-editor.tsx @@ -46,15 +46,25 @@ export function OcrRowEditor({ return (
- - onChange({ ...row, confirmed: checked === true }) - } - className="mt-1 min-h-5 min-w-5" - aria-label={t("labs.ocr.confirmRow")} - /> + {/* The checkbox is the PRIMARY per-row confirm on a touch-first OCR + review screen, so it needs a ≥44px hit area on coarse pointers. The + wrapping label supplies the touch target (and forwards the click to + the control) without enlarging the 20px glyph or bloating the row on + desktop, where the negative margins collapse the padding back. */} +
Date: Sat, 20 Jun 2026 12:53:41 +0200 Subject: [PATCH 2/4] fix(labs): raise lab-form result-type toggles to the touch floor --- src/components/labs/lab-form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/labs/lab-form.tsx b/src/components/labs/lab-form.tsx index 97dc2464f..c9f2a1e3f 100644 --- a/src/components/labs/lab-form.tsx +++ b/src/components/labs/lab-form.tsx @@ -245,7 +245,7 @@ export function LabForm({ type="button" size="sm" variant={resultType === "numeric" ? "secondary" : "ghost"} - className="h-8 flex-1" + className="min-h-11 flex-1 sm:min-h-9" onClick={() => setResultType("numeric")} aria-pressed={resultType === "numeric"} > @@ -255,7 +255,7 @@ export function LabForm({ type="button" size="sm" variant={resultType === "qualitative" ? "secondary" : "ghost"} - className="h-8 flex-1" + className="min-h-11 flex-1 sm:min-h-9" onClick={() => setResultType("qualitative")} aria-pressed={resultType === "qualitative"} > From a1f5ed625c941de70e8a4e23a2d8cf58740a5bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 20 Jun 2026 12:53:41 +0200 Subject: [PATCH 3/4] fix(coach): cap starter-question chips to a single-line pill --- src/components/insights/coach-panel/coach-hero.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/insights/coach-panel/coach-hero.tsx b/src/components/insights/coach-panel/coach-hero.tsx index b014288da..38a231aa3 100644 --- a/src/components/insights/coach-panel/coach-hero.tsx +++ b/src/components/insights/coach-panel/coach-hero.tsx @@ -110,16 +110,20 @@ export function CoachHero({ composer, prompts, onPickPrompt }: CoachHeroProps) { type="button" onClick={() => onPickPrompt(prompt)} data-slot="coach-hero-chip" + title={prompt} className={cn( "border-dracula-purple/20 hover:border-dracula-purple/45 hover:text-foreground", - "text-muted-foreground inline-flex min-h-11 items-center gap-1.5 sm:min-h-9", + "text-muted-foreground inline-flex min-h-11 max-w-full items-center gap-1.5 sm:min-h-9 sm:max-w-[18rem]", "rounded-full border bg-transparent px-3.5 py-1.5 text-xs sm:text-sm", "transition-colors focus-visible:ring-2 focus-visible:outline-none", "focus-visible:ring-dracula-purple/50", )} >
From 90a9407195b725ef9fc76361441f4e8582059f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 20 Jun 2026 12:53:41 +0200 Subject: [PATCH 4/4] fix(vorsorge): key due status on the local calendar day --- .../__tests__/vorsorge-section.test.tsx | 62 +++++++++++++++++++ .../vorsorge-section.tsx | 15 ++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx b/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx index d9ad228cd..eb608b13d 100644 --- a/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx +++ b/src/components/measurement-reminders/__tests__/vorsorge-section.test.tsx @@ -136,6 +136,68 @@ describe(" loading + empty", () => { expect(html).not.toContain("Log value"); }); + it("reads a same-day due time as 'today' (calendar-day delta, not rolling 24h)", () => { + // v1.18.9 recon — a reminder whose nextDue falls on the SAME local + // calendar day must read "Due today" with the green status hue, even when + // the wall-clock gap to now exceeds the 24h a rolling delta would key on. + // Anchor the due instant to local noon today so the floored calendar-day + // delta is 0 regardless of the hour the suite runs. + const todayNoon = new Date(); + todayNoon.setHours(12, 0, 0, 0); + const reminder: MeasurementReminder = { + id: "due-today", + label: "Blood pressure check", + measurementType: "BLOOD_PRESSURE_SYS", + intervalDays: 7, + rrule: null, + anchorDate: null, + endsOn: null, + origin: "VORSORGE", + nextDueAt: todayNoon.toISOString(), + notifyHour: 9, + location: null, + lastSatisfiedAt: null, + enabled: true, + createdAt: "2030-01-01T00:00:00.000Z", + updatedAt: "2030-01-01T00:00:00.000Z", + } as MeasurementReminder; + remindersMock.mockReturnValue({ data: [reminder], isLoading: false }); + const html = render(); + expect(html).toContain("Due today"); + // The status hue is the discreet success colour, never a card-bg tint. + expect(html).toContain("text-success"); + }); + + it("reads a prior-day due time as 'overdue' once the calendar day has passed", () => { + // A reminder whose due CALENDAR day is before today reads "Overdue", with + // the warning hue — a rolling-24h delta would mis-bucket a yesterday-evening + // due viewed this morning as "today". + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(20, 0, 0, 0); + const reminder: MeasurementReminder = { + id: "overdue", + label: "Annual physical", + measurementType: null, + intervalDays: 365, + rrule: null, + anchorDate: null, + endsOn: null, + origin: "VORSORGE", + nextDueAt: yesterday.toISOString(), + notifyHour: 9, + location: null, + lastSatisfiedAt: null, + enabled: true, + createdAt: "2030-01-01T00:00:00.000Z", + updatedAt: "2030-01-01T00:00:00.000Z", + } as MeasurementReminder; + remindersMock.mockReturnValue({ data: [reminder], isLoading: false }); + const html = render(); + expect(html).toContain("Overdue by 1 days"); + expect(html).toContain("text-warning"); + }); + it("translates a COACH-origin label i18n key and shows the neutral badge", () => { const reminder: MeasurementReminder = { id: "c1", diff --git a/src/components/measurement-reminders/vorsorge-section.tsx b/src/components/measurement-reminders/vorsorge-section.tsx index e3ea28f57..e1c1c59f8 100644 --- a/src/components/measurement-reminders/vorsorge-section.tsx +++ b/src/components/measurement-reminders/vorsorge-section.tsx @@ -40,6 +40,7 @@ import { import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { relativeCalendarDate } from "@/lib/i18n/relative-time"; +import { startOfLocalDayInTz } from "@/lib/tz/local-day"; import { cn } from "@/lib/utils"; import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs"; import { SettingsCardHeader } from "@/components/settings/_card-header"; @@ -124,9 +125,17 @@ function relativeDueKey( now: number, ): { key: string; days: number } { if (!nextDueAt) return { key: "nextDue.none", days: 0 }; - const due = new Date(nextDueAt).getTime(); - // Compare calendar-day deltas so "today" / "in 1 day" read cleanly. - const deltaDays = Math.round((due - now) / DAY_MS); + const due = new Date(nextDueAt); + if (Number.isNaN(due.getTime())) return { key: "nextDue.none", days: 0 }; + // v1.18.9 (recon) — compare CALENDAR-day deltas in the user's local zone, + // not a rolling-24h delta. A reminder due at 09:00 viewed the same evening + // at 20:00 must still read "heute", and one whose due calendar-day is before + // today must read "überfällig" — a raw `(due - now) / DAY_MS` round would + // mis-bucket both. Floor each instant to its local day start (host-local zone + // = the browser user's zone for this client component) before differencing. + const dueDay = startOfLocalDayInTz(due, undefined).getTime(); + const nowDay = startOfLocalDayInTz(new Date(now), undefined).getTime(); + const deltaDays = Math.round((dueDay - nowDay) / DAY_MS); if (deltaDays < 0) return { key: "overdueByDays", days: Math.abs(deltaDays) }; if (deltaDays === 0) return { key: "nextDue.today", days: 0 }; if (deltaDays === 1) return { key: "nextDue.tomorrow", days: 1 };