From 5445bfc1f19e844131b6ff4e1fdca901ded1854e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 7 Aug 2026 19:05:15 +0200 Subject: [PATCH] refactor(ui): give Settings and Admin one card rhythm and one page header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two consoles used the same primitives and still read as two systems, because everything without a primitive had drifted: the header-to-body step existed in three competing techniques at once (`mt-4` on the first body child, `space-y-4` on the card, `mb-4` on the header), the body started either at the card edge or 28 px in, and both shells hand-rolled the page title instead of calling `PageHeader`. Move the decisions into the primitives so a call site cannot re-open them: - `SettingsCard` is gap-based like `Card` (`flex flex-col gap-4`). The header-to-body step is the container's, not a margin somebody remembers. A dense card still overrides the gap on the card itself. - The body starts at the card edge. `pl-7` is gone — off the spacing scale, and it mis-aligned action rows against their own forms wherever two neighbouring cards disagreed about it. - The card header's description slot loses its `space-y-1`. That was the licence call sites used to stack explainer paragraphs into a muted `text-xs` slot; the slot holds one sentence. - New `SettingsCardActions`: one action row, trailing edge, last thing in the card. `align="start"` spells the one exception, a card whose only actions are links out. - New `StatTile` replaces four hand-rolled label/value boxes in Admin. - `SectionHeading` moves from `insights/` to `ui/`. It is the heading above a group of cards, which is what Settings needs; Settings had been using a card header with no card, which reads as a card that failed to paint. Both shells now render their heading through `PageHeader`, which gained `headingAs="div"` for the second copy each shell paints (mobile above the nav strip, desktop inside the grid) so only one is a real `

`. The page explainer is muted everywhere: the primitive said foreground, the standards and every hand-rolled copy said muted, so pages disagreed with each other. Medications, Vorsorge, the labs / custom-metric / illness detail pages and the clinician view drop their hand-rolled title blocks too, including the mid-word truncation on the medications subtitle. Tests pin the parts that can be checked exactly: the card's gap, the description slot's missing rhythm, the action row's two alignments, the `

`-free shells, and a sweep asserting `pl-7` stays gone. --- src/app/medications/page-client.tsx | 142 +++++++------- src/components/admin/_stat-tile.tsx | 43 +++++ src/components/admin/admin-shell.tsx | 23 +-- src/components/admin/ai-quality-section.tsx | 2 +- .../admin/ai-server-key-section.tsx | 2 +- .../admin/api-token-overview-section.tsx | 2 +- .../admin/app-log-preview-section.tsx | 4 +- src/components/admin/assistant-section.tsx | 2 +- src/components/admin/backups-section.tsx | 19 +- .../admin/central-codex-section.tsx | 2 +- .../admin/coach-feedback-section.tsx | 2 +- src/components/admin/danger-zone-section.tsx | 2 +- src/components/admin/encryption-section.tsx | 6 +- .../admin/general-settings-section.tsx | 2 +- src/components/admin/glitchtip-section.tsx | 4 +- src/components/admin/host-metrics-chart.tsx | 2 +- .../admin/invite-tokens-section.tsx | 4 +- .../admin/login-overview-section.tsx | 2 +- .../admin/module-availability-section.tsx | 2 +- src/components/admin/recent-audit-preview.tsx | 2 +- src/components/admin/reminders-section.tsx | 10 +- src/components/admin/services-section.tsx | 2 +- .../admin/system-status-section.tsx | 12 +- .../admin/system-status-summary.tsx | 6 +- src/components/admin/umami-section.tsx | 4 +- .../admin/user-management-section.tsx | 12 +- src/components/admin/version-tile-section.tsx | 2 +- .../admin/web-push-vapid-section.tsx | 4 +- src/components/clinician/clinician-view.tsx | 10 +- .../custom-metrics/custom-metric-detail.tsx | 126 ++++++------- .../cycle/cycle-insight-summary-card.tsx | 2 +- .../illness/illness-episode-detail.tsx | 67 +++---- src/components/illness/illness-settings.tsx | 4 +- .../insights/breathing-screening-card.tsx | 2 +- .../insights/coincident-deviation-card.tsx | 2 +- src/components/insights/daily-briefing.tsx | 2 +- .../insights/derived/vitals-dashboard.tsx | 2 +- .../insights/derived/wellness-scores.tsx | 2 +- src/components/insights/device-score-tile.tsx | 2 +- src/components/insights/ecg-section.tsx | 2 +- .../insights/health-status-card.tsx | 2 +- src/components/insights/labs-changes-card.tsx | 2 +- .../insights/period-narrative-card.tsx | 2 +- .../insights/rhythm-events-card.tsx | 2 +- .../insights/sleep/sleep-quality-section.tsx | 2 +- src/components/insights/trends-row.tsx | 2 +- .../insights/workout-detail/day-context.tsx | 2 +- src/components/labs/lab-biomarker-detail.tsx | 158 ++++++++-------- src/components/labs/labs-settings.tsx | 10 +- .../vorsorge-section.tsx | 32 ++-- .../vorsorge-settings.tsx | 4 +- .../__tests__/one-piece-anatomy.test.tsx | 173 ++++++++++++++++++ src/components/settings/_card-actions.tsx | 46 +++++ src/components/settings/_card-header.tsx | 17 +- src/components/settings/about-me-section.tsx | 11 +- src/components/settings/about-section.tsx | 8 +- .../settings/access/grant-invite-card.tsx | 2 +- .../settings/access/grants-given-card.tsx | 2 +- .../settings/access/grants-received-card.tsx | 2 +- .../settings/access/managed-profile-card.tsx | 2 +- .../settings/access/record-activity-card.tsx | 2 +- .../account-section/avatar-section.tsx | 10 +- .../settings/account-section/index.tsx | 8 +- src/components/settings/advanced-section.tsx | 6 +- .../settings/ai/ai-insights-card.tsx | 6 +- .../settings/ai/disable-coach-card.tsx | 4 +- src/components/settings/anamnesis-section.tsx | 10 +- src/components/settings/api-section.tsx | 6 +- .../settings/coach-memory-section.tsx | 1 - src/components/settings/coach-nudge-card.tsx | 8 +- .../settings/coach-prefs-section.tsx | 9 +- .../settings/coach-reminders-section.tsx | 1 - .../settings/cycle-tracking-card.tsx | 2 +- .../settings/dashboard-layout-section.tsx | 2 +- src/components/settings/email-card.tsx | 2 +- .../settings/environment-section.tsx | 22 +-- src/components/settings/export-section.tsx | 4 +- .../settings/glucose-reference-card.tsx | 2 +- .../import-panel/import-card-shell.tsx | 2 +- .../settings/injection-sites-card.tsx | 2 +- .../settings/insights-pill-order-section.tsx | 1 - .../integrations/apple-health-card.tsx | 7 +- .../settings/integrations/fitbit-card.tsx | 7 +- .../integrations/google-health-card.tsx | 7 +- .../integrations/integration-fallback-row.tsx | 2 +- .../settings/integrations/nightscout-card.tsx | 7 +- .../integrations/oauth-provider-card.tsx | 7 +- .../settings/integrations/whoop-card.tsx | 7 +- .../settings/integrations/withings-card.tsx | 7 +- src/components/settings/low-stock-card.tsx | 10 +- .../settings/managed-integration-status.tsx | 2 +- .../managed-record-settings-section.tsx | 2 +- src/components/settings/mcp-section.tsx | 6 +- .../settings/medications-section.tsx | 4 +- src/components/settings/modules-section.tsx | 2 +- .../settings/mood-reminder-card.tsx | 6 +- src/components/settings/mood-section.tsx | 6 +- .../settings/notification-status-card.tsx | 2 +- src/components/settings/ntfy-card.tsx | 2 +- .../settings/nutrient-intake-card.tsx | 5 +- src/components/settings/privacy-section.tsx | 22 +-- .../settings/record-anamnesis-section.tsx | 4 +- .../report-selection/selection-panel.tsx | 1 - .../settings/score-change-notice.tsx | 1 - src/components/settings/score-section.tsx | 17 +- .../settings/security-activity-card.tsx | 2 +- .../security-section/passkey-list-section.tsx | 2 +- .../security-section/security-keys-card.tsx | 2 +- .../settings/security-section/totp-card.tsx | 2 +- .../settings/security-sessions-card.tsx | 2 +- src/components/settings/settings-card.tsx | 23 ++- src/components/settings/settings-shell.tsx | 50 ++--- src/components/settings/sharing-section.tsx | 2 +- src/components/settings/sources-section.tsx | 2 +- src/components/settings/telegram-card.tsx | 2 +- .../settings/thresholds-editor-section.tsx | 2 +- .../settings/trusted-devices-card.tsx | 3 +- src/components/settings/web-push-card.tsx | 2 +- src/components/settings/webhook-card.tsx | 2 +- src/components/ui/page-header.tsx | 53 +++++- .../{insights => ui}/section-heading.tsx | 23 ++- 121 files changed, 832 insertions(+), 620 deletions(-) create mode 100644 src/components/admin/_stat-tile.tsx create mode 100644 src/components/settings/__tests__/one-piece-anatomy.test.tsx create mode 100644 src/components/settings/_card-actions.tsx rename src/components/{insights => ui}/section-heading.tsx (78%) diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx index d7dc51dd4..0cc5afb05 100644 --- a/src/app/medications/page-client.tsx +++ b/src/app/medications/page-client.tsx @@ -33,6 +33,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { EmptyState } from "@/components/ui/empty-state"; +import { PageHeader } from "@/components/ui/page-header"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { Skeleton } from "@/components/ui/skeleton"; import { @@ -359,14 +360,10 @@ export default function MedicationsPageClient() { if (!isAuthenticated) { return (
-
-

- {t("medications.title")} -

-

- {t("medications.loginRequired")} -

-
+
); } @@ -407,90 +404,83 @@ export default function MedicationsPageClient() { return (
-
-
-

- {t("medications.title")} -

- {/* v1.4.34 IW-G — subtitle stays visible on mobile so the - H1 isn't an unframed label. */} -

- {t("medications.subtitle")} -

-
-
- {/* v1.16.11 (#316) — "Alle fälligen einnehmen". Contextual, not a + {t("medications.title")} + } + description={t("medications.subtitle")} + actions={ + <> + {/* v1.16.11 (#316) — "Alle fälligen einnehmen". Contextual, not a permanent fixture: it renders only while at least TWO medications are currently due (a single due dose is the card's own one-tap job). Calm outline variant so the primary Add button keeps the visual lead; same responsive 44-px mobile tap floor as its neighbours. */} - {canAdd && dueMeds.length >= 2 && ( - - )} - {/* v1.16.11 — the wrench is the one customize entry point: + {canAdd && dueMeds.length >= 2 && ( + + )} + {/* v1.16.11 — the wrench is the one customize entry point: it links to /settings/medications, which owns the view preference (cards ⇄ table) and the manual-order editor. Same glyph, slot (left of the add button) and responsive 44-px mobile tap floor as the dashboard and insights headers. */} - {canManage && ( - - )} - {/* v1.14.0 — the "Add" button is now a choice: log an intake + +
-
+ {canAdd && ( + + + + + + setLogIntakeOpen(true)} + disabled={activeMeds.length === 0} + > + + {t("medications.addChoice.logIntake")} + + + + {t("medications.addChoice.newMedication")} + + + + )} + + } + /> {isLoading || isLayoutLoading ? ( // v1.11.3 C4 — skeletons instead of a bare centred spinner so the diff --git a/src/components/admin/_stat-tile.tsx b/src/components/admin/_stat-tile.tsx new file mode 100644 index 000000000..de52cf4ed --- /dev/null +++ b/src/components/admin/_stat-tile.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +/** + * `` — the one label/value mini tile in the Admin console. + * + * Four hand-rolled variants of this box existed side by side: `StatusItem` + * (`bg-muted/50 p-3`), the encryption section's local `Stat` + * (`bg-muted/40 px-3 py-2`), the user-management inline panels + * (`bg-muted/80 p-4`), and the backups upload box (`bg-muted/30 p-3`). Four + * fills, three paddings, one role. This is the role. + * + * `icon` is optional — a tile with a glyph and one without still share the + * frame, so a grid can mix them without a step in tile height. + */ +export function StatTile({ + icon: Icon, + label, + value, + valueClassName, + className, +}: { + icon?: React.ComponentType<{ className?: string }>; + label: React.ReactNode; + value: React.ReactNode; + /** Extra classes on the value line — a status colour, or `font-mono`. */ + valueClassName?: string; + className?: string; +}) { + return ( +
+
+ {Icon ? : null} + {label} +
+

{value}

+
+ ); +} diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 9ba3cebad..2961dbb7a 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -37,6 +37,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; +import { PageHeader } from "@/components/ui/page-header"; import { SUB_SHELL_GRID_FLOOR } from "@/components/layout/shell-metrics"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; @@ -253,21 +254,15 @@ export function AdminShell({ active, children }: AdminShellProps) { ? t("admin.subtitle") : t(`admin.section.${activeSlug}.subtitle`); + // Same two-copy heading as the Settings shell (mobile above the chip strip, + // desktop inside the grid row) and the same primitive, so the two consoles + // cannot drift a step apart again. const headingBlock = (mobile = false) => ( -
- {mobile ? ( -
- {headingTitle} -
- ) : ( -

{headingTitle}

- )} -

{headingSubtitle}

-
+ ); // v1.4.25 W8 — AuthShell wraps the page in `px-4 py-6 md:px-6` diff --git a/src/components/admin/ai-quality-section.tsx b/src/components/admin/ai-quality-section.tsx index 98e31a7a7..57cb8adf4 100644 --- a/src/components/admin/ai-quality-section.tsx +++ b/src/components/admin/ai-quality-section.tsx @@ -76,7 +76,7 @@ export function AiQualitySection() { // resolves. Mirrors (header outside the // fetch-state branch). return ( - + -
+

-

+

{t("admin.invites.description")}

-

+

diff --git a/src/components/admin/login-overview-section.tsx b/src/components/admin/login-overview-section.tsx index 1af12d217..d601f396d 100644 --- a/src/components/admin/login-overview-section.tsx +++ b/src/components/admin/login-overview-section.tsx @@ -227,7 +227,7 @@ export function LoginOverviewSection() { description={t("admin.loginOverviewDescription")} /> -
+
{/* Quick-filter pills. Failed-only is the most common admin "is something wrong" question — keep the one-tap shortcut. */}
diff --git a/src/components/admin/module-availability-section.tsx b/src/components/admin/module-availability-section.tsx index e578d451f..59e5f37d3 100644 --- a/src/components/admin/module-availability-section.tsx +++ b/src/components/admin/module-availability-section.tsx @@ -94,7 +94,7 @@ export function ModuleAvailabilitySection() { title={t("admin.modules.title")} description={t("admin.modules.description")} /> -
+
{MODULE_KEYS // A module switched off in code (pending a rebuild) is hard-off // server-wide; drop its row so the operator toggle can't mislead. diff --git a/src/components/admin/recent-audit-preview.tsx b/src/components/admin/recent-audit-preview.tsx index e330f56f1..63235bcef 100644 --- a/src/components/admin/recent-audit-preview.tsx +++ b/src/components/admin/recent-audit-preview.tsx @@ -70,7 +70,7 @@ export function RecentAuditPreview() { } /> -
+
{isLoading ? (
-
+
{isLoading && ( )} @@ -205,7 +205,7 @@ export function RemindersSection() { description={t("admin.medicationRemindersDescription")} /> -
+
-
+
) : ( -
+
{t("admin.loadingStatus")} diff --git a/src/components/admin/system-status-summary.tsx b/src/components/admin/system-status-summary.tsx index 85ab28048..5551cef03 100644 --- a/src/components/admin/system-status-summary.tsx +++ b/src/components/admin/system-status-summary.tsx @@ -45,7 +45,7 @@ export function SystemStatusSummary() { /> {status ? ( -
+
{t("admin.overview.snapshotLoadError")}
) : ( -
+