mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
refactor(ui): give Settings and Admin one card rhythm and one page header
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 `<h1>`. 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 `<h1>`-free shells, and a sweep asserting `pl-7` stays gone.
This commit is contained in:
1 parent
00e8d05235
commit
5445bfc1f1
121 files changed
+832
-620
No files matched your search
@@ -27,6 +27,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
|
||||
@@ -303,14 +304,10 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
|
||||
if (markerError || listError) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="space-y-1.5">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{marker?.name ?? t("labs.detail.title")}
|
||||
</h1>
|
||||
<p className="text-foreground text-sm leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
</header>
|
||||
<PageHeader
|
||||
title={marker?.name ?? t("labs.detail.title")}
|
||||
description={description}
|
||||
/>
|
||||
<QueryErrorCard
|
||||
title={t("labs.loadError")}
|
||||
onRetry={() => {
|
||||
@@ -324,91 +321,88 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{marker?.name ?? <Skeleton className="h-7 w-32" />}
|
||||
</h1>
|
||||
{marker?.unit ? (
|
||||
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||
{marker.unit}
|
||||
{marker.panel ? ` · ${marker.panel}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{/* v1.25.3 — header control cluster, left → right:
|
||||
<PageHeader
|
||||
title={marker?.name ?? <Skeleton className="h-7 w-32" />}
|
||||
description={description}
|
||||
actions={
|
||||
<>
|
||||
{/* v1.25.3 — header control cluster, left → right:
|
||||
Delete · Edit · Show-all-values · Add. The icon controls share one
|
||||
ghost-icon treatment (the target-range editing lives inside the
|
||||
full marker editor behind the pencil, so the separate range
|
||||
control was dropped). */}
|
||||
<DeleteButton
|
||||
onConfirm={() => deleteMarker.mutate()}
|
||||
title={t("labs.biomarker.deleteConfirmTitle")}
|
||||
description={t("labs.biomarker.deleteConfirmDescription")}
|
||||
confirmLabel={t("labs.biomarker.delete")}
|
||||
triggerTitle={t("labs.biomarker.delete")}
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
iconClassName="h-4 w-4"
|
||||
/>
|
||||
{canManage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<DeleteButton
|
||||
onConfirm={() => deleteMarker.mutate()}
|
||||
title={t("labs.biomarker.deleteConfirmTitle")}
|
||||
description={t("labs.biomarker.deleteConfirmDescription")}
|
||||
confirmLabel={t("labs.biomarker.delete")}
|
||||
triggerTitle={t("labs.biomarker.delete")}
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
onClick={() => setEditOpen(true)}
|
||||
disabled={!marker}
|
||||
aria-label={t("labs.biomarker.edit")}
|
||||
title={t("labs.biomarker.edit")}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* "Show all readings" mirrors the metric sub-pages' `<SubPageShell>`
|
||||
iconClassName="h-4 w-4"
|
||||
/>
|
||||
{canManage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
onClick={() => setEditOpen(true)}
|
||||
disabled={!marker}
|
||||
aria-label={t("labs.biomarker.edit")}
|
||||
title={t("labs.biomarker.edit")}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* "Show all readings" mirrors the metric sub-pages' `<SubPageShell>`
|
||||
control. The full reading feed lives on
|
||||
`/labs/[biomarkerId]/values`; the detail page keeps the
|
||||
numbers-first spine. */}
|
||||
{readings.length > 0 ? (
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-slot="lab-show-all-values"
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
>
|
||||
<Link
|
||||
href={`/labs/${biomarkerId}/values`}
|
||||
aria-label={t("insights.subPage.showAllValues")}
|
||||
title={t("insights.subPage.showAllValues")}
|
||||
{readings.length > 0 ? (
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-slot="lab-show-all-values"
|
||||
className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{/* v1.36.x — entering a result is an admitted delegated write;
|
||||
<Link
|
||||
href={`/labs/${biomarkerId}/values`}
|
||||
aria-label={t("insights.subPage.showAllValues")}
|
||||
title={t("insights.subPage.showAllValues")}
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{/* v1.36.x — entering a result is an admitted delegated write;
|
||||
renaming or removing the marker is not. */}
|
||||
{canAdd && (
|
||||
<Button
|
||||
onClick={() => setAddOpen(true)}
|
||||
// v1.18.10 (W10) — on the narrowest phones the h1 + Edit + Delete +
|
||||
// text "Add" button crowd the row and truncate the title hard.
|
||||
// Drop the Add button to icon-only under `sm` (label kept for
|
||||
// screen readers via `aria-label`); the full text returns at `sm+`.
|
||||
className="min-h-11 min-w-11 shrink-0 sm:min-h-9 sm:min-w-0"
|
||||
aria-label={t("labs.addResult")}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t("labs.addResult")}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{canAdd && (
|
||||
<Button
|
||||
onClick={() => setAddOpen(true)}
|
||||
// v1.18.10 (W10) — on the narrowest phones the h1 + Edit + Delete +
|
||||
// text "Add" button crowd the row and truncate the title hard.
|
||||
// Drop the Add button to icon-only under `sm` (label kept for
|
||||
// screen readers via `aria-label`); the full text returns at `sm+`.
|
||||
className="min-h-11 min-w-11 shrink-0 sm:min-h-9 sm:min-w-0"
|
||||
aria-label={t("labs.addResult")}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t("labs.addResult")}</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* v1.25.1 — the static description leads the page, directly beneath the
|
||||
heading, mirroring the metric sub-pages' explainer caption. Computed
|
||||
once above (catalog desc → user `context` → generic fallback) so a
|
||||
catalog-less marker still carries a description. */}
|
||||
<p className="text-foreground text-sm leading-relaxed">{description}</p>
|
||||
{/* Unit + panel are meta about the marker, not its explainer, so they
|
||||
read below the header block rather than competing with it for the
|
||||
one description line every page header carries. */}
|
||||
{marker?.unit ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{marker.unit}
|
||||
{marker.panel ? ` · ${marker.panel}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/* Bridge to the source report PDFs. There is no per-value document
|
||||
relation in the data model yet, so this links to the vault filtered
|
||||
|
||||
@@ -79,7 +79,7 @@ export function LabsSettings() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* View — card vs compact list. */}
|
||||
<SettingsCard id="labs-view" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCard id="labs-view" className="scroll-mt-28">
|
||||
<SettingsCardHeader
|
||||
icon={LayoutGrid}
|
||||
title={t("moduleList.viewHeading")}
|
||||
@@ -89,7 +89,7 @@ export function LabsSettings() {
|
||||
</SettingsCard>
|
||||
|
||||
{/* Sort order — recency, alphabetical, or manual. */}
|
||||
<SettingsCard id="labs-sort-card" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCard id="labs-sort-card" className="scroll-mt-28">
|
||||
<SettingsCardHeader
|
||||
icon={ArrowDownUp}
|
||||
title={t("labs.sort.heading")}
|
||||
@@ -112,7 +112,7 @@ export function LabsSettings() {
|
||||
</SettingsCard>
|
||||
|
||||
{manual ? (
|
||||
<SettingsCard id="labs-order" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCard id="labs-order" className="scroll-mt-28">
|
||||
<SettingsCardHeader
|
||||
icon={ListOrdered}
|
||||
title={t("moduleList.reorder.heading")}
|
||||
@@ -130,7 +130,7 @@ export function LabsSettings() {
|
||||
) : null}
|
||||
|
||||
{/* v1.18.10 — local (in-browser) OCR opt-in for lab scans. */}
|
||||
<SettingsCard id="labs-local-ocr" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCard id="labs-local-ocr" className="scroll-mt-28">
|
||||
<SettingsCardHeader
|
||||
icon={ScanText}
|
||||
title={t("labs.localOcr.heading")}
|
||||
@@ -147,7 +147,7 @@ export function LabsSettings() {
|
||||
</SettingsCard>
|
||||
|
||||
{/* Biomarker catalog — define / edit / delete. */}
|
||||
<SettingsCard id="labs-biomarkers" className="scroll-mt-28 space-y-4">
|
||||
<SettingsCard id="labs-biomarkers" className="scroll-mt-28">
|
||||
<SettingsCardHeader
|
||||
icon={FlaskConical}
|
||||
title={t("labs.biomarker.manageTitle")}
|
||||
|
||||
Reference in new issue
Block a user