Files
HealthLog/eslint-plugins/healthlog/__tests__/spacing-scale.test.mjs
T
Marc-André Bombeck 6d83115d16 chore(release): v1.28.43 — visual consistency polish
Verified UI-consistency findings from the campaign's design audit, each
pixel-faithful except the intended correction:

- insights hero band: off-scale py-5/gap-5 → the standard scale; H1
  font-semibold → font-bold; sm:text-[28px] → sm:text-3xl.
- medication-compliance-chart + recent-achievements-card: hand-rolled
  muted h-4 icon + text-sm headers → the standard TileHeader primitive so
  they match sibling tiles.
- achievements stat tiles + clinician-view shells: p-5 → p-4 md:p-6.
- two text-primary header glyphs (research-mode dialog, enroll-mfa) →
  text-foreground (never accent on a header glyph); the deliberate
  badge-boxed icons left untouched.
- SheetHeader now reserves the trailing close-X gutter like DialogHeader,
  closing the latent overlap class for future raw-Sheet consumers.
- five raw Dialogs on phone-reachable surfaces → ResponsiveSheet.
- new bg-card-scoped healthlog/spacing-scale lint (flags p-5/gap-5 etc.
  only on card shells; list-marker insets + half-steps excluded) at error,
  with the five offenders it flagged swept.
- dropped the stale CLAUDE.md '18 PascalCase outliers' line (0 remain).

Every data-slot preserved. No migration.
2026-07-16 03:44:00 +02:00

168 lines
5.3 KiB
JavaScript

/**
* Unit tests for the `healthlog/spacing-scale` rule — pt-* / pb-* slot
* padding on CardHeader/CardContent is banned (the Card is gap-based;
* density lives on the Card itself). Covers modifier prefixes,
* cn()/ternary/template collection, the safe-area non-match, and the
* scope gating shared with the sibling rules.
*/
import { describe, it } from "vitest";
import { RuleTester } from "eslint";
import rule from "../spacing-scale.js";
RuleTester.describe = describe;
RuleTester.it = it;
RuleTester.itOnly = it.only;
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2023,
sourceType: "module",
parserOptions: { ecmaFeatures: { jsx: true } },
},
});
const APP_FILE = "/repo/src/components/insights/example.tsx";
ruleTester.run("spacing-scale", rule, {
valid: [
// Density on the Card itself is the sanctioned pattern.
{
code: '<Card className="gap-2 py-3 md:py-4"><CardContent>x</CardContent></Card>',
filename: APP_FILE,
},
// Slots without className, or with unrelated classes, are fine.
{ code: "<CardHeader />", filename: APP_FILE },
{
code: '<CardContent className="px-0 space-y-4" />',
filename: APP_FILE,
},
// Other paddings on slots are not this rule's business.
{
code: '<CardContent className="pl-2 pr-2" />',
filename: APP_FILE,
},
// Safe-area helpers are not scale steps and never match.
{
code: '<CardContent className="pb-safe" />',
filename: APP_FILE,
},
// pt-/pb- on arbitrary elements stays legal — the contract is about
// the gap-based Card slots only.
{ code: '<div className="pb-2" />', filename: APP_FILE },
// Outside the enforced roots nothing matches.
{
code: '<CardHeader className="pb-2" />',
filename: "/repo/scripts/tool.tsx",
},
// Test files are exempt.
{
code: '<CardHeader className="pb-2" />',
filename: "/repo/src/components/__tests__/example.test.tsx",
},
// The card-shell `5` check is scoped to the bg-card+border pair, so the
// justified list-marker inset never trips it (no shell).
{
code: '<ul className="list-disc pl-5 space-y-1" />',
filename: APP_FILE,
},
// A directional inset ON a shell is out of scope (pl-5/pr-5 not banned).
{
code: '<div className="bg-card border-border rounded-xl border pl-5" />',
filename: APP_FILE,
},
// Form-body rhythm without a shell surface is fine.
{
code: '<form className="space-y-5" />',
filename: APP_FILE,
},
// A shell already on-scale is fine.
{
code: '<div className="bg-card border-border rounded-xl border p-4 md:p-6" />',
filename: APP_FILE,
},
// bg-card without a border is not treated as a shell.
{
code: '<div className="bg-card p-5" />',
filename: APP_FILE,
},
],
invalid: [
{
code: '<CardHeader className="pb-2" />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
{
code: '<CardContent className="pt-0" />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// Modifier-prefixed and fractional steps.
{
code: '<CardHeader className="md:pb-1.5" />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// Arbitrary-value padding.
{
code: '<CardContent className="pt-[10px]" />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// Collected through cn() args…
{
code: '<CardHeader className={cn("pb-3", extra)} />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// …ternaries…
{
code: '<CardContent className={dense ? "pt-2" : "pt-4"} />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }, { messageId: "slotPadding" }],
},
// …and template literals.
{
code: "<CardHeader className={`pb-2 ${extra}`} />",
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// Member-expression component tails match too.
{
code: '<UI.CardHeader className="pb-2" />',
filename: APP_FILE,
errors: [{ messageId: "slotPadding" }],
},
// Card-shell `5` step: bg-card + border + p-5 on one element.
{
code: '<div className="bg-card border-border rounded-xl border p-5" />',
filename: APP_FILE,
errors: [{ messageId: "cardShellStep5" }],
},
// …py-5, …space-y-5, …gap-5 on a shell all trip it.
{
code: '<section className="bg-card rounded-lg border py-5" />',
filename: APP_FILE,
errors: [{ messageId: "cardShellStep5" }],
},
{
code: '<div className="bg-card border-border rounded-xl border gap-5 flex" />',
filename: APP_FILE,
errors: [{ messageId: "cardShellStep5" }],
},
// Split across cn() args — the join still sees the shell + the step-5.
{
code: '<div className={cn("bg-card border rounded-xl", "p-5")} />',
filename: APP_FILE,
errors: [{ messageId: "cardShellStep5" }],
},
// Modifier-prefixed step on a shell.
{
code: '<div className="bg-card border-border rounded-xl border sm:p-5" />',
filename: APP_FILE,
errors: [{ messageId: "cardShellStep5" }],
},
],
});