mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
1 parent
405bebf1a0
commit
933dfcfdfb
9 files changed
+90
-16
No files matched your search
@@ -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);
|
||||
|
||||
@@ -354,7 +354,7 @@ function TargetReferencePanel({
|
||||
}
|
||||
/>
|
||||
{heading ? (
|
||||
<p className="text-muted-foreground text-[0.6875rem] font-medium tracking-[0.06em] uppercase">
|
||||
<p className="text-muted-foreground text-xs font-medium tracking-[0.06em] uppercase">
|
||||
{heading}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -623,7 +623,7 @@ function LedgerRowItem({
|
||||
{t(`medications.detail.verlauf.status.${row.status}`)}
|
||||
</span>
|
||||
{row.kind === "ad_hoc" && (
|
||||
<Badge variant="outline" className="text-[0.65rem]">
|
||||
<Badge variant="outline" className="text-2xs">
|
||||
{t("medications.detail.verlauf.adHocTag")}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -634,7 +634,7 @@ function LedgerRowItem({
|
||||
{row.kind === "slot" && row.pinned && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.65rem]"
|
||||
className="text-2xs"
|
||||
data-slot="ledger-pinned-badge"
|
||||
>
|
||||
{t("medications.detail.verlauf.pin.pinnedTag")}
|
||||
@@ -862,7 +862,7 @@ function LedgerSummaryChart({
|
||||
style={{ width: `${pct(missed)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 text-[0.7rem]">
|
||||
<div className="text-muted-foreground flex flex-wrap gap-x-3 gap-y-0.5 text-xs">
|
||||
<span className="text-success">
|
||||
{t("medications.detail.verlauf.status.taken_on_time")}: {takenOnTime}
|
||||
</span>
|
||||
|
||||
@@ -162,7 +162,7 @@ export function WizardStepper({
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"max-w-[6rem] truncate text-[0.6875rem] leading-none",
|
||||
"text-2xs max-w-[6rem] truncate leading-none",
|
||||
isActive
|
||||
? "text-foreground font-medium"
|
||||
: "text-muted-foreground",
|
||||
|
||||
@@ -36,6 +36,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { TagChip } from "@/components/ui/tag-chip";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
|
||||
@@ -204,9 +205,7 @@ export function FitbitCard({
|
||||
title={t("settings.fitbit")}
|
||||
titleAccessory={
|
||||
<>
|
||||
<span className="bg-muted text-foreground rounded-full px-2 py-0.5 text-[0.6875rem] font-medium">
|
||||
{t("settings.fitbitTag")}
|
||||
</span>
|
||||
<TagChip>{t("settings.fitbitTag")}</TagChip>
|
||||
<Badge
|
||||
variant="outline"
|
||||
data-testid="fitbit-experimental-badge"
|
||||
|
||||
@@ -44,6 +44,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { TagChip } from "@/components/ui/tag-chip";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
|
||||
@@ -212,9 +213,7 @@ export function GoogleHealthCard({
|
||||
title={t("settings.googleHealth")}
|
||||
titleAccessory={
|
||||
<>
|
||||
<span className="bg-muted text-foreground rounded-full px-2 py-0.5 text-[0.6875rem] font-medium">
|
||||
{t("settings.googleHealthTag")}
|
||||
</span>
|
||||
<TagChip>{t("settings.googleHealthTag")}</TagChip>
|
||||
<Badge
|
||||
variant="outline"
|
||||
data-testid="google-health-beta-badge"
|
||||
|
||||
@@ -37,6 +37,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { TagChip } from "@/components/ui/tag-chip";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { IntegrationStatusPill } from "@/components/settings/integration-status-pill";
|
||||
@@ -186,11 +187,7 @@ export function NightscoutCard({
|
||||
<SettingsCardHeader
|
||||
icon={Droplet}
|
||||
title={t("settings.nightscout")}
|
||||
titleAccessory={
|
||||
<span className="bg-muted text-foreground rounded-full px-2 py-0.5 text-[0.6875rem] font-medium">
|
||||
{t("settings.nightscoutTag")}
|
||||
</span>
|
||||
}
|
||||
titleAccessory={<TagChip>{t("settings.nightscoutTag")}</TagChip>}
|
||||
description={
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix="settings.nightscout"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user