Merge v189/recon-b into release/v1.18.9

Enlarge the OCR confirm and lab result-type tap targets to 44px; clamp the coach
hero chips; switch the Vorsorge due-status to a calendar-day delta so an item due
earlier today still reads as due today and a prior day reads overdue.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 12:54:22 +02:00
commit f9d3e83bab
5 files changed
+101 -16

No files matched your search

@@ -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",
)}
>
<Quote className="size-3 shrink-0" aria-hidden="true" />
<span>{prompt}</span>
{/* 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. */}
<span className="truncate">{prompt}</span>
</button>
))}
</div>
+2 -2
View File
@@ -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"}
>
+19 -9
View File
@@ -46,15 +46,25 @@ export function OcrRowEditor({
return (
<div className="space-y-3 rounded-lg border p-3">
<div className="flex items-start gap-3">
<Checkbox
id={`${fieldId}-confirm`}
checked={row.confirmed}
onCheckedChange={(checked) =>
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. */}
<label
htmlFor={`${fieldId}-confirm`}
className="-m-3 flex min-h-11 min-w-11 cursor-pointer items-start justify-center p-3 sm:m-0 sm:min-h-0 sm:min-w-0 sm:p-0"
>
<Checkbox
id={`${fieldId}-confirm`}
checked={row.confirmed}
onCheckedChange={(checked) =>
onChange({ ...row, confirmed: checked === true })
}
className="mt-0.5 min-h-5 min-w-5 sm:mt-1"
aria-label={t("labs.ocr.confirmRow")}
/>
</label>
<div className="min-w-0 flex-1 space-y-1">
<Input
value={row.analyte}
@@ -136,6 +136,68 @@ describe("<VorsorgeSection> 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(<VorsorgeSection />);
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(<VorsorgeSection />);
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",
@@ -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 };