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",
)}
>
- {prompt}
+ {/* Cap the chip to a tidy single line — long localized prompts
+ (notably DE) otherwise wrap to ragged multi-line pills on a
+ narrow viewport. The full prompt stays available via title. */}
+ {prompt}
))}
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"}
>
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. */}
+
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 };