mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +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,98 @@
|
||||
/**
|
||||
* A grade chip goes through `ScorePill` (`src/components/ui/score-pill.tsx`).
|
||||
*
|
||||
* Eleven chips that state a grade beside a reading (confidence, score gain,
|
||||
* band, lab range) were hand-rolled in two looks: a /15 wash without border
|
||||
* in the crosstabs, a /10 wash with a /40 border on the correlation and trend
|
||||
* cards, and the middle confidence step was blue on one and amber on the
|
||||
* other. This guard keeps a new copy of the chip from appearing beside the
|
||||
* primitive.
|
||||
*
|
||||
* Matched: one string literal that combines the pill shape (`rounded-full`,
|
||||
* a small padding `px-1.5|2|2.5`, `text-xs|2xs`) with a semantic tone wash
|
||||
* (`bg-success|info|warning|destructive/10|15`). A neutral `bg-muted` chip is
|
||||
* `TagChip`'s business, not this guard's.
|
||||
*
|
||||
* Not matched: a tone assembled at runtime from a map whose entries live in
|
||||
* another literal. The guard catches the idiom, not every way to paint one.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(__dirname, "..");
|
||||
|
||||
const ALLOWED: Record<string, string> = {
|
||||
// A link to the release notes styled as a call to action, not a grade.
|
||||
"components/admin/version-tile-section.tsx":
|
||||
"update-available link, not a grade",
|
||||
};
|
||||
|
||||
const LITERAL_RE = /["'`]([^"'`\n]*\brounded-full\b[^"'`\n]*)["'`]/g;
|
||||
|
||||
function isGradeChip(cls: string): boolean {
|
||||
return (
|
||||
/(?:^|\s)px-(?:1\.5|2|2\.5)(?:\s|$)/.test(cls) &&
|
||||
/(?:^|\s)text-(?:xs|2xs)(?:\s|$)/.test(cls) &&
|
||||
/(?:^|\s)bg-(?:success|info|warning|destructive)\/1[05](?:\s|$)/.test(cls)
|
||||
);
|
||||
}
|
||||
|
||||
function findGradeChips(files: { path: string; source: string }[]): string[] {
|
||||
const hits = new Set<string>();
|
||||
for (const { path, source } of files) {
|
||||
for (const match of source.matchAll(LITERAL_RE)) {
|
||||
if (isGradeChip(match[1])) hits.add(path);
|
||||
}
|
||||
}
|
||||
return [...hits].sort();
|
||||
}
|
||||
|
||||
describe("grade chips go through ScorePill", () => {
|
||||
const files = walkSourceFiles(SRC, { floor: 2500 })
|
||||
.filter(
|
||||
(p) =>
|
||||
(p.startsWith("components/") || p.startsWith("app/")) &&
|
||||
!p.startsWith("components/ui/") &&
|
||||
!p.includes("__tests__/"),
|
||||
)
|
||||
.map((path) => ({ path, source: readFileSync(join(SRC, path), "utf8") }));
|
||||
|
||||
it("walks the UI tree", () => {
|
||||
expect(files.length).toBeGreaterThan(1000);
|
||||
});
|
||||
|
||||
it("has no hand-rolled grade chip outside the allowlist", () => {
|
||||
expect(findGradeChips(files).filter((p) => !(p in ALLOWED))).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps every allowlist entry live", () => {
|
||||
const hits = new Set(findGradeChips(files));
|
||||
for (const key of Object.keys(ALLOWED))
|
||||
expect(hits.has(key), key).toBe(true);
|
||||
});
|
||||
|
||||
it("recognises the idiom it exists to refuse", () => {
|
||||
expect(
|
||||
findGradeChips([
|
||||
{
|
||||
path: "components/a.tsx",
|
||||
source:
|
||||
'const C = { high: "bg-success/15 text-success" }; <span className={cn("shrink-0 rounded-full px-2 py-0.5 text-xs font-medium bg-info/15 text-info")} />',
|
||||
},
|
||||
{
|
||||
path: "components/b.tsx",
|
||||
source:
|
||||
'<span className="bg-muted rounded-full px-2 py-0.5 text-xs" />',
|
||||
},
|
||||
]),
|
||||
).toEqual(["components/a.tsx"]);
|
||||
});
|
||||
|
||||
it("is used where the copies were", () => {
|
||||
const users = files.filter((f) => f.source.includes("<ScorePill"));
|
||||
expect(users.length).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
});
|
||||
@@ -164,7 +164,6 @@ const PINNED_AFFORDANCES: Record<
|
||||
// behind the delete toast's Undo action.
|
||||
"toast.success": 3,
|
||||
},
|
||||
"src/components/cycle/cycle-phase-crosstab.tsx": { "text-success": 1 },
|
||||
// v1.36.x — two call sites, one decision: the intake toast routes through
|
||||
// `intakeToastOptions()`, which returns `undefined` in the caller's own
|
||||
// record, and sonner's one-argument form is what the unit tests assert. So
|
||||
@@ -214,25 +213,10 @@ const PINNED_AFFORDANCES: Record<
|
||||
"src/components/insights/coach-panel/suggested-action-card.tsx": {
|
||||
"text-success": 1,
|
||||
},
|
||||
"src/components/insights/confidence-badge.ts": { "text-success": 1 },
|
||||
"src/components/insights/daily-briefing.tsx": { "text-success": 1 },
|
||||
"src/components/insights/derived/band-tokens.ts": { "text-success": 1 },
|
||||
// The green `+N` chip beside the Health Score label. It reports a week that
|
||||
// went up, not an action that succeeded, and it wears the same band token
|
||||
// the score's own number does — there is no outcome here to route through
|
||||
// the outcome module.
|
||||
"src/components/insights/health-score-card.tsx": { "text-success": 1 },
|
||||
"src/components/insights/insights-edit-mode.tsx": { "toast.success": 2 },
|
||||
"src/components/insights/insights-tab-strip.tsx": { "toast.success": 1 },
|
||||
"src/components/insights/mood/mood-better-days.tsx": { "text-success": 1 },
|
||||
"src/components/insights/mood/mood-factor-metric-crosstab.tsx": {
|
||||
"text-success": 1,
|
||||
},
|
||||
"src/components/insights/mood/mood-tag-influence.tsx": { "text-success": 1 },
|
||||
"src/components/insights/mood/mood-tag-metric-crosstab.tsx": {
|
||||
"text-success": 1,
|
||||
},
|
||||
"src/components/insights/personal-record-badge.tsx": { "text-success": 1 },
|
||||
"src/components/labs/biomarker-form.tsx": { "toast.success": 1 },
|
||||
"src/components/labs/biomarker-manager.tsx": { "toast.success": 2 },
|
||||
"src/components/labs/lab-biomarker-detail.tsx": { "toast.success": 1 },
|
||||
@@ -430,6 +414,11 @@ const PINNED_AFFORDANCES: Record<
|
||||
"src/components/settings/webhook-card.tsx": { "text-success": 1 },
|
||||
"src/components/targets/target-edit-sheet.tsx": { "toast.success": 2 },
|
||||
"src/components/ui/password-strength.tsx": { "text-success": 2 },
|
||||
// The success tone of the grade chip (a high confidence, a score that went
|
||||
// up). It states a grade beside a reading, never that a write succeeded;
|
||||
// the crosstab, confidence and score chips that each carried their own copy
|
||||
// of this class now render through it.
|
||||
"src/components/ui/score-pill.tsx": { "text-success": 1 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user