Files
HealthLog/src/components/settings/_card-actions.tsx
T
Marc-André Bombeck 5445bfc1f1 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.
2026-08-07 19:05:15 +02:00

47 lines
1.7 KiB
TypeScript

import * as React from "react";
import { cn } from "@/lib/utils";
/**
* `<SettingsCardActions>` — the action row of a Settings or Admin card.
*
* The audit found four positions (bottom-right, bottom-left, the header's
* status slot, inline mid-card) and five variants carrying the SAME role —
* "the primary action of this card" — sometimes two of them in one file. The
* two sections that already read right (`score-section.tsx`,
* `coach-prefs-section.tsx`) agreed on one shape, and this is it.
*
* The contract (design standards §12):
* - The row is the LAST thing in the card. Status lines and results sit
* above it; a result set big enough to need a frame becomes its own card.
* - Primary action last (rightmost), `variant` default, `size="sm"` with
* the `min-h-11` mobile tap floor. One per card.
* - Secondary action `outline`, before the primary in the same row.
* - Destructive card-wide action: same row, `variant="destructive"` —
* never an `outline` hand-tinted with `text-destructive`.
*
* `align="start"` is the one documented exception: a card whose only actions
* are deep links out of it (privacy, about, export) has no primary action, so
* its links read with the text on the left rather than against it.
*/
export function SettingsCardActions({
align = "end",
className,
children,
...props
}: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
return (
<div
data-slot="settings-card-actions"
className={cn(
"flex flex-wrap items-center gap-2",
align === "end" ? "justify-end" : "justify-start",
className,
)}
{...props}
>
{children}
</div>
);
}