mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(ui): ScorePill for every grade chip
Confidence levels, the weekly score gain, target bands and lab range flags were drawn by hand in two looks: a /15 wash without border in the mood boards, a /10 wash with a /40 border on the correlation and trend cards, and medium confidence was blue on one card and amber on the next. They now render through one ScorePill (rounded, xs, soft wash, tone success / info / warning / destructive / neutral, optional tabular figures) with one confidence scale. The target band keeps its own AA-tuned tokens in the shared anatomy. A structural test refuses a new hand-rolled grade chip.
This commit is contained in:
1 parent
ab3f21c58c
commit
ee2bc8019c
14 files changed
+278
-131
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { CONFIDENCE_TONE, ScorePill } from "../score-pill";
|
||||
|
||||
describe("<ScorePill>", () => {
|
||||
it("paints the pill anatomy with the tone's soft wash and no border", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ScorePill tone="info">Medium</ScorePill>,
|
||||
);
|
||||
expect(html).toContain("rounded-full");
|
||||
expect(html).toContain("px-2 py-0.5 text-xs font-medium");
|
||||
expect(html).toContain("bg-info/15 text-info");
|
||||
expect(html).toContain('data-tone="info"');
|
||||
expect(html).not.toMatch(/\bborder\b/);
|
||||
});
|
||||
|
||||
it("defaults to the neutral tone", () => {
|
||||
const html = renderToStaticMarkup(<ScorePill>Low</ScorePill>);
|
||||
expect(html).toContain("bg-secondary text-muted-foreground");
|
||||
expect(html).toContain('data-tone="neutral"');
|
||||
});
|
||||
|
||||
it("sets figures in tabular numerals when numeric", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ScorePill tone="success" numeric>
|
||||
+4
|
||||
</ScorePill>,
|
||||
);
|
||||
expect(html).toContain("tabular-nums");
|
||||
expect(html).toContain("font-semibold");
|
||||
});
|
||||
|
||||
it("lets a call site keep its own slot and override a tone token", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ScorePill
|
||||
data-slot="target-status-pill"
|
||||
className="bg-(--status-pill-in)/10 text-(--status-pill-in)"
|
||||
>
|
||||
Optimal
|
||||
</ScorePill>,
|
||||
);
|
||||
expect(html).toContain('data-slot="target-status-pill"');
|
||||
expect(html).toContain("bg-(--status-pill-in)/10");
|
||||
expect(html).not.toContain("bg-secondary");
|
||||
});
|
||||
|
||||
it("reads both confidence scales as the same three steps", () => {
|
||||
expect(CONFIDENCE_TONE.medium).toBe(CONFIDENCE_TONE.moderate);
|
||||
expect(CONFIDENCE_TONE.high).toBe("success");
|
||||
expect(CONFIDENCE_TONE.low).toBe("neutral");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* The small rounded chip that states a grade beside a reading: a confidence
|
||||
* level, a score gain, a band ("Optimal", "Hypertension grade 1"), a lab
|
||||
* value below or above its range.
|
||||
*
|
||||
* One anatomy (`rounded-full px-2 py-0.5 text-xs font-medium`, a soft wash of
|
||||
* the tone, no border) and one tone vocabulary, where there used to be two
|
||||
* looks for the same meaning: the crosstab chips washed at /15 without a
|
||||
* border, the correlation and trend badges washed at /10 with a /40 border,
|
||||
* and "medium confidence" was blue on one card and amber on the next.
|
||||
*
|
||||
* The tone carries meaning, never decoration (UI standards §3):
|
||||
* `success` / `info` / `warning` / `destructive` for a verdict, `neutral`
|
||||
* for a grade that says nothing good or bad (low confidence).
|
||||
*
|
||||
* Not for a neutral label (that is `TagChip`) and not for the state of a
|
||||
* connection or an account (that is `Badge`).
|
||||
*/
|
||||
const scorePillVariants = cva(
|
||||
"inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap [&>svg]:size-3 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
tone: {
|
||||
success: "bg-success/15 text-success",
|
||||
info: "bg-info/15 text-info",
|
||||
warning: "bg-warning/15 text-warning",
|
||||
destructive: "bg-destructive/15 text-destructive",
|
||||
neutral: "bg-secondary text-muted-foreground",
|
||||
},
|
||||
numeric: {
|
||||
true: "font-semibold tabular-nums",
|
||||
false: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: { tone: "neutral", numeric: false },
|
||||
},
|
||||
);
|
||||
|
||||
export type ScorePillTone = NonNullable<
|
||||
VariantProps<typeof scorePillVariants>["tone"]
|
||||
>;
|
||||
|
||||
/**
|
||||
* The tone of a confidence level. Two scales reach the UI — the crosstabs say
|
||||
* `medium`, the correlation and trend payloads say `moderate` — and both mean
|
||||
* the same middle step.
|
||||
*/
|
||||
export const CONFIDENCE_TONE = {
|
||||
low: "neutral",
|
||||
medium: "info",
|
||||
moderate: "info",
|
||||
high: "success",
|
||||
} as const satisfies Record<string, ScorePillTone>;
|
||||
|
||||
function ScorePill({
|
||||
className,
|
||||
tone,
|
||||
numeric,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> & VariantProps<typeof scorePillVariants>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="score-pill"
|
||||
data-tone={tone ?? "neutral"}
|
||||
className={cn(scorePillVariants({ tone, numeric }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScorePill, scorePillVariants };
|
||||
Reference in new issue
Block a user