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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-25 14:00:25 +02:00
1 parent 405bebf1a0
commit 933dfcfdfb
9 files changed
+90 -16

No files matched your search

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { TagChip } from "../tag-chip";
describe("<TagChip>", () => {
it("renders the neutral chip shape on the text-2xs token", () => {
const html = renderToStaticMarkup(<TagChip>Glucose</TagChip>);
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(
<TagChip className="ml-2" title="source label">
Steps
</TagChip>,
);
expect(html).toContain("ml-2");
expect(html).toContain('title="source label"');
// The base shape survives the merge.
expect(html).toContain("rounded-full");
});
});
+41
View File
@@ -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<React.ComponentPropsWithoutRef<"span">, "children">) {
return (
<span
data-slot="tag-chip"
className={cn(
"bg-muted text-foreground text-2xs rounded-full px-2 py-0.5 font-medium",
className,
)}
{...props}
>
{children}
</span>
);
}