From 933dfcfdfbd3adbfcb08c95c4e60d76df8f57433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 25 Jul 2026 14:00:25 +0200 Subject: [PATCH] refactor(ui): mint the text-2xs token and the neutral TagChip Three integration cards carried the same hand-copied chip literal in the card-header accessory slot, each free to drift on the next touch; the size itself was an arbitrary value repeated at three slightly different spellings across the tree. - Mint `--text-2xs` (0.6875rem with a matched line-height) as the one sanctioned step below `text-xs`. - Mint `TagChip` and adopt it on the Fitbit, Google Health, and Nightscout cards. It stays distinct from `Badge`, which carries status meaning. - Sweep the arbitrary micro sizes: the stepper label and the two dose-ledger outline badges move onto the token; the muted target caption and the muted ledger meta row move up to the `text-xs` muted floor. No visible change beyond the two muted rows gaining a fraction of a pixel. --- src/app/globals.css | 8 ++++ .../insights/metric-target-summary.tsx | 2 +- .../medications/dose-history-ledger.tsx | 6 +-- .../medications/wizard/wizard-stepper.tsx | 2 +- .../settings/integrations/fitbit-card.tsx | 5 +-- .../integrations/google-health-card.tsx | 5 +-- .../settings/integrations/nightscout-card.tsx | 7 +--- src/components/ui/__tests__/tag-chip.test.tsx | 30 ++++++++++++++ src/components/ui/tag-chip.tsx | 41 +++++++++++++++++++ 9 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 src/components/ui/__tests__/tag-chip.test.tsx create mode 100644 src/components/ui/tag-chip.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 441b14be6..3e1e61a6d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -71,6 +71,14 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); + /* The one sanctioned step below `text-xs` (12px). Minted so badges, + tag chips, and tabular-nums micro-labels stop hand-rolling + `text-[0.6875rem]` / `text-[0.65rem]` arbitrary values that drift by a + third of a pixel per site. UI-STANDARDS ยง5 pins the usage: never for + muted body or meta text (the muted floor stays `text-xs`) โ€” only for + chips, badges, and tabular micro-labels with verified contrast. */ + --text-2xs: 0.6875rem; + --text-2xs--line-height: 1rem; --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx index 2718c3b1b..199711035 100644 --- a/src/components/insights/metric-target-summary.tsx +++ b/src/components/insights/metric-target-summary.tsx @@ -354,7 +354,7 @@ function TargetReferencePanel({ } /> {heading ? ( -

+

{heading}

) : null} diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx index 9b3724a91..ef6567479 100644 --- a/src/components/medications/dose-history-ledger.tsx +++ b/src/components/medications/dose-history-ledger.tsx @@ -623,7 +623,7 @@ function LedgerRowItem({ {t(`medications.detail.verlauf.status.${row.status}`)} {row.kind === "ad_hoc" && ( - + {t("medications.detail.verlauf.adHocTag")} )} @@ -634,7 +634,7 @@ function LedgerRowItem({ {row.kind === "slot" && row.pinned && ( {t("medications.detail.verlauf.pin.pinnedTag")} @@ -862,7 +862,7 @@ function LedgerSummaryChart({ style={{ width: `${pct(missed)}%` }} /> -
+
{t("medications.detail.verlauf.status.taken_on_time")}: {takenOnTime} diff --git a/src/components/medications/wizard/wizard-stepper.tsx b/src/components/medications/wizard/wizard-stepper.tsx index 765453fbc..b2477320e 100644 --- a/src/components/medications/wizard/wizard-stepper.tsx +++ b/src/components/medications/wizard/wizard-stepper.tsx @@ -162,7 +162,7 @@ export function WizardStepper({ /> - - {t("settings.fitbitTag")} - + {t("settings.fitbitTag")} - - {t("settings.googleHealthTag")} - + {t("settings.googleHealthTag")} - {t("settings.nightscoutTag")} - - } + titleAccessory={{t("settings.nightscoutTag")}} description={ ", () => { + it("renders the neutral chip shape on the text-2xs token", () => { + const html = renderToStaticMarkup(Glucose); + expect(html).toContain("Glucose"); + expect(html).toContain('data-slot="tag-chip"'); + expect(html).toContain("bg-muted"); + expect(html).toContain("text-foreground"); + expect(html).toContain("rounded-full"); + expect(html).toContain("text-2xs"); + // The arbitrary value the three hand-copied sites carried is gone. + expect(html).not.toContain("text-[0.6875rem]"); + }); + + it("merges caller classes and forwards span props", () => { + const html = renderToStaticMarkup( + + Steps + , + ); + expect(html).toContain("ml-2"); + expect(html).toContain('title="source label"'); + // The base shape survives the merge. + expect(html).toContain("rounded-full"); + }); +}); diff --git a/src/components/ui/tag-chip.tsx b/src/components/ui/tag-chip.tsx new file mode 100644 index 000000000..cd8676330 --- /dev/null +++ b/src/components/ui/tag-chip.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +/** + * The small neutral tag chip that labels a card's subject line. + * + * Three Settings integration cards carried the same hand-copied literal + * (`bg-muted text-foreground rounded-full px-2 py-0.5 text-[0.6875rem] + * font-medium`) in `SettingsCardHeader`'s `titleAccessory` slot, each free to + * drift on the next touch. This is that literal, named โ€” on the `text-2xs` + * token rather than an arbitrary value. + * + * It is deliberately NOT a `Badge`: `Badge` carries status meaning (variant โ†’ + * semantic colour), while a tag chip is a neutral noun ("Glucose", "Steps"). + * Keeping them apart is what stops a neutral label from creeping into the + * status vocabulary. A chip that means a state is a `Badge`. + * + * RSC-safe: no hooks, no browser API. + */ +export function TagChip({ + children, + className, + ...props +}: { + children: ReactNode; + className?: string; +} & Omit, "children">) { + return ( + + {children} + + ); +}