chore(misc): auth padding, daily-briefing links, tab fade, trends min-h, list metadata, passkey badge

v1.4.27 MB7 — residual surface polish bundled into a single
commit:

- CF-61: the auth login + register cards drop `p-8` to `p-6 sm:p-8`
  so Galaxy Fold / Pixel 5 keep the card breathable instead of
  surrendering half the visible viewport to padding.
- CF-68: each daily-briefing key-finding row wraps in a `<Link>` to
  the matching routed sub-page so the whole card is tappable on
  mobile. Metrics without a dedicated sub-page (HRV, resting HR,
  steps, …) render the legacy static row.
- CF-70: the sleep-stage window-toggle row bumps from `gap-1` to
  `gap-1.5` so the active pill border doesn't fuse with the
  inactive neighbour on Pixel 5.
- CF-71: the trends-row card `min-h-[300px]` floor moves behind a
  `md:` gate so mobile single-column doesn't eat dead space below
  a short chart.
- CF-72: the insights tab-strip paints a right-edge fade gradient on
  `<sm` so the user reads "there's more to scroll" when the pill
  row overflows.
- CF-46: the measurement-list filter row stacks vertically on `<sm`
  so the `w-48` SelectTrigger doesn't crowd the count caption to a
  2-line wrap.
- CF-76: the measurement-list mobile-row badges bump from
  `text-[10px]` to `text-[11px]` so the metadata holds the
  legibility floor.
- CF-75: the passkey mobile card promotes the device-type from
  plain text to an outline Badge so the chip row reads consistent
  with the desktop table column.
- MA2-F8: the medikamente page card header wraps the title in
  `min-w-0` + `truncate` so long medication names don't push the
  streak badge off the card on Galaxy Fold.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 19:23:34 +02:00
1 parent ab4529fcb3
commit 0b3f9e3e2c
9 files changed
+148 -26

No files matched your search

+5 -1
View File
@@ -124,7 +124,11 @@ export default function LoginPage() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<div className="border-border bg-card rounded-xl border p-8 shadow-lg shadow-black/20">
{/* v1.4.27 MB7 / CF-61 — drop `p-8` to `p-6 sm:p-8` so the
auth card breathes on Galaxy Fold / Pixel 5 without stealing
half the visible viewport with padding. Tablet+ keeps the
generous 32 px padding. */}
<div className="border-border bg-card rounded-xl border p-6 shadow-lg shadow-black/20 sm:p-8">
<div className="flex flex-col items-center gap-3 text-center">
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-lg">
<Logo className="text-primary" size={28} />
+5 -1
View File
@@ -65,7 +65,11 @@ export default function RegisterPage() {
return (
<div className="w-full max-w-sm">
<div className="border-border bg-card rounded-xl border p-8 shadow-lg shadow-black/20">
{/* v1.4.27 MB7 / CF-61 — drop `p-8` to `p-6 sm:p-8` so the
auth card breathes on Galaxy Fold / Pixel 5 without stealing
half the visible viewport with padding. Tablet+ keeps the
generous 32 px padding. */}
<div className="border-border bg-card rounded-xl border p-6 shadow-lg shadow-black/20 sm:p-8">
<div className="flex flex-col items-center gap-3 text-center">
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-lg">
<Logo className="text-primary" size={28} />
+12 -5
View File
@@ -154,15 +154,22 @@ export default function InsightsMedikamentePage() {
return (
<Card key={med.id}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Pill className="text-dracula-orange h-4 w-4" />
<CardTitle className="text-sm font-medium">
{/* v1.4.27 MB7 / MA2-F8 — `min-w-0` on the title
wrapper + `truncate` on the CardTitle keeps long
medication names (e.g. "Semaglutid Wegovy 0.5 mg
weekly") from pushing the streak badge off the
card on Galaxy Fold / Pixel 5. The Pill icon
+ CardTitle pair shrinks to fit; the badge stays
on the right at its natural width. */}
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<Pill className="text-dracula-orange h-4 w-4 shrink-0" />
<CardTitle className="truncate text-sm font-medium">
{med.name}
</CardTitle>
</div>
{med.streak > 0 && (
<Badge variant="outline" className="text-xs">
<Badge variant="outline" className="shrink-0 text-xs">
{t("insights.dayStreak", { count: med.streak })}
</Badge>
)}
+62 -5
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import {
Activity,
FileText,
@@ -30,6 +31,33 @@ import type {
DailyBriefingKeyFinding,
} from "@/lib/ai/schema";
/**
* v1.4.27 MB7 / CF-68 — per-metric routing target for the briefing
* key findings. Maps the schema's `sourceMetric` discriminator to the
* sub-page slug that owns the deeper view. Metrics that don't have a
* dedicated routed sub-page yet (hrv, resting_hr, steps, …) fall
* through to `null` and render as a plain row (no link wrap).
*/
const METRIC_HREF: Record<DailyBriefingKeyFinding["sourceMetric"], string | null> = {
bp: "/insights/blutdruck",
weight: "/insights/gewicht",
pulse: "/insights/puls",
mood: "/insights/stimmung",
compliance: "/insights/medikamente",
// Apple Health / GLP-1 additive metrics — no dedicated sub-page yet.
// They render as static rows; v1.4.28 can wire each as they ship.
hrv: null,
sleep: "/insights/schlaf",
resting_hr: null,
steps: null,
active_energy: null,
flights: null,
distance: null,
vo2_max: "/insights/puls",
body_temp: null,
glp1_plateau: "/insights/medikamente",
};
/**
* v1.4.20 phase B1 — full-width Daily Briefing card.
*
@@ -128,11 +156,15 @@ function DeltaBadge({
function KeyFindingRow({ finding }: { finding: DailyBriefingKeyFinding }) {
const Icon = METRIC_ICON[finding.sourceMetric];
return (
<div
data-slot="daily-briefing-finding"
className="border-border/60 bg-card/40 relative flex items-start gap-3 rounded-md border p-3"
>
const href = METRIC_HREF[finding.sourceMetric];
// v1.4.27 MB7 / CF-68 — when the briefing's `sourceMetric` maps to
// a routed sub-page, wrap the entire row in a `<Link>` so the
// whole card is tappable on mobile instead of the user having to
// hit the small headline text. Hover + focus paint a subtle bg
// shift; metrics that don't have a sub-page yet render the
// original static row.
const rowContent = (
<>
<span
aria-hidden="true"
className={cn(
@@ -158,6 +190,31 @@ function KeyFindingRow({ finding }: { finding: DailyBriefingKeyFinding }) {
{stripChartTokens(finding.detail)}
</p>
</div>
</>
);
if (href) {
return (
<Link
href={href}
data-slot="daily-briefing-finding"
data-metric={finding.sourceMetric}
className={cn(
"border-border/60 bg-card/40 relative flex items-start gap-3 rounded-md border p-3",
"hover:bg-accent/40 transition-colors",
"focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none",
)}
>
{rowContent}
</Link>
);
}
return (
<div
data-slot="daily-briefing-finding"
data-metric={finding.sourceMetric}
className="border-border/60 bg-card/40 relative flex items-start gap-3 rounded-md border p-3"
>
{rowContent}
</div>
);
}
@@ -137,6 +137,13 @@ export function InsightsTabStrip({
data-slot="insights-tab-strip"
aria-label={t("insights.navAriaLabel")}
className={cn(
// v1.4.27 MB7 / CF-72 — relative wrapper hosts the right-edge
// fade overlay below so the user reads "there's more to
// scroll" when the pill row overflows. The fade is a tiny
// pointer-events-none pseudo-strip painted onto the inner
// strip via a sibling div; it sits only on `<sm` because the
// wider viewport above already shows every pill.
"relative",
"bg-background/95 sticky top-0 z-30 overflow-x-auto border-b py-2 backdrop-blur",
"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
)}
@@ -175,6 +182,20 @@ export function InsightsTabStrip({
);
})}
</div>
{/* v1.4.27 MB7 / CF-72 — right-edge fade. The gradient
absolute-positions over the rightmost ~24 px of the strip
so the last visible pill softly fades into the background
colour, signalling "scroll for more" without a scrollbar.
Only paints on `<sm` because the wider viewports above
already fit every pill. The fade is a sibling of the
scrollable inner row + the regenerate button so it
visually overlays both without trapping pointer events. */}
<div
aria-hidden="true"
className={cn(
"from-background/95 pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l to-transparent sm:hidden",
)}
/>
{onRegenerate && (
<button
type="button"
@@ -216,7 +216,11 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) {
</span>
</div>
<div
className="flex items-center gap-1 self-end sm:self-auto"
// v1.4.27 MB7 / CF-70 — bump the gap from `gap-1` to
// `gap-1.5` so the three window-toggle buttons (7d /
// 14d / 30d) breathe enough on Pixel 5 that the active
// pill's border doesn't fuse with the inactive neighbour.
className="flex items-center gap-1.5 self-end sm:self-auto"
data-slot="sleep-stage-window-toggle"
>
{WINDOW_DAYS.map((w) => (
+9 -5
View File
@@ -105,13 +105,17 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
v1.4.25 W3 — strengthen the equal-height contract with
`md:auto-rows-fr` + `md:items-stretch` on the grid and
`h-full` on each card so the tallest annotation still pins
every row member to a single baseline. The `min-h-[300px]`
floor stays in place for the single-column mobile view. */}
every row member to a single baseline.
v1.4.27 MB7 / CF-71 — drop the unconditional `min-h-[300px]`
floor to `md:min-h-[300px]` so mobile single-column doesn't
eat dead space below a short chart; the floor stays on `md+`
to preserve the equal-height baseline across the row. */}
<div className="grid grid-cols-1 gap-4 md:auto-rows-fr md:grid-cols-3 md:items-stretch">
<div
data-slot="trends-row-card"
data-metric="bp"
className="flex h-full min-h-[300px] flex-col gap-2"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<HealthChart
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
@@ -131,7 +135,7 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
<div
data-slot="trends-row-card"
data-metric="weight"
className="flex h-full min-h-[300px] flex-col gap-2"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<HealthChart
types={["WEIGHT"]}
@@ -150,7 +154,7 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
<div
data-slot="trends-row-card"
data-metric="mood"
className="flex h-full min-h-[300px] flex-col gap-2"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<MoodChart
title={t("charts.mood")}
@@ -318,10 +318,16 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
return (
<>
<div className="space-y-4">
<div className="flex items-center justify-between">
{/* v1.4.27 MB7 / CF-46 — the filter row stacks the SelectTrigger
and the count caption vertically on `<sm` so the trigger
fills the column and the count drops to a separate line.
Pre-fix the trigger had a fixed `w-48` (192 px) which on
Pixel 5 (375 px content width) left only ~120 px for the
count, which wrapped to 2 lines. */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger
className="w-48"
className="w-full sm:w-48"
aria-label={t("measurements.filterByType")}
>
<SelectValue placeholder={t("measurements.allTypes")} />
@@ -501,11 +507,20 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
</div>
)}
<div className="min-w-0">
{/* v1.4.27 MB7 / CF-76 — bump the metadata
badges from `text-[10px]` to `text-[11px]`
so the legibility floor (12 px is the
mobile baseline; 11 px is the lowest
tolerated value for non-primary chrome)
holds across the row. The badge heights
stay at `h-5` / `h-4` since the type-
preface badge owns vertical real estate
inside the headline line. */}
{(m.type === "BLOOD_PRESSURE_SYS" ||
m.type === "BLOOD_PRESSURE_DIA") && (
<Badge
variant="outline"
className="mr-1.5 h-5 px-1 text-[10px]"
className="mr-1.5 h-5 px-1 text-[11px]"
>
{t(TYPE_LABEL_KEYS[m.type])}
</Badge>
@@ -519,7 +534,7 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
<Badge
variant="outline"
data-testid="measurement-source-badge"
className={`ml-1.5 h-4 px-1 text-[10px] ${sourceBadgeClass(m.source)}`.trim()}
className={`ml-1.5 h-4 px-1 text-[11px] ${sourceBadgeClass(m.source)}`.trim()}
>
{formatMeasurementSource(m.source, t)}
</Badge>
+10 -4
View File
@@ -833,11 +833,17 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{pk.name}</p>
<p className="text-muted-foreground text-xs">
{DEVICE_TYPE_LABELS[pk.credentialDeviceType] ??
pk.credentialDeviceType}
</p>
{/* v1.4.27 MB7 / CF-75 — promote the device-type from
plain text to an outline Badge so the mobile card
list reads consistent with the desktop table's
"Single-device / Multi-device" column. Sits on the
same chip row as the backup status and date so all
metadata reads as a single horizontal stride. */}
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
<Badge variant="outline" className="text-[11px]">
{DEVICE_TYPE_LABELS[pk.credentialDeviceType] ??
pk.credentialDeviceType}
</Badge>
<Badge
variant={pk.credentialBackedUp ? "secondary" : "outline"}
className="text-[11px]"