mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(lint): extend spacing-scale to the full off-scale step family on shells
The shell check caught only the bare `5` step on p/px/py/space-y/gap, so off-scale directional and half-step spacing (`ml-5`, `pl-5`, `gap-3.5`, `py-2.5`, the `7` step) slipped through on hand-rolled card shells. Widen it to the whole spacing prefix family (padding, margin, gap, space) and every banned step — `5`, `7`, and half-steps other than the sanctioned `1.5`. Height stays out of scope: it is a dimension, not a scale step. Verified the rule bites by injecting `gap-3.5` onto a real bg-card+border shell and watching lint flag it, then reverting; the sweep this rule guards left the tree clean apart from one tooltip shell, now on-scale.
This commit is contained in:
1 parent
a959db8679
commit
b24a6eac2c
2 files changed
+101
-34
No files matched your search
@@ -60,22 +60,38 @@ ruleTester.run("spacing-scale", rule, {
|
||||
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
|
||||
// The card-shell 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.
|
||||
// Off-scale spacing without a shell surface is fine — a form-body
|
||||
// `space-y-5` rhythm and a half-step gap outside a shell both pass.
|
||||
{
|
||||
code: '<form className="space-y-5" />',
|
||||
filename: APP_FILE,
|
||||
},
|
||||
{
|
||||
code: '<div className="flex flex-col gap-3.5" />',
|
||||
filename: APP_FILE,
|
||||
},
|
||||
// The sanctioned `1.5` half-step is on-scale, even on a shell.
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border gap-1.5" />',
|
||||
filename: APP_FILE,
|
||||
},
|
||||
// A deliberate negative margin is never matched (boundary anchored).
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-full border -mt-5" />',
|
||||
filename: APP_FILE,
|
||||
},
|
||||
// Height is a dimension, not a scale step — `h-15` on a shell is out of
|
||||
// this rule's scope by design.
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border h-15" />',
|
||||
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" />',
|
||||
@@ -138,30 +154,64 @@ ruleTester.run("spacing-scale", rule, {
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border p-5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellStep5" }],
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// …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" }],
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border gap-5 flex" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellStep5" }],
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// Split across cn() args — the join still sees the shell + the step-5.
|
||||
// The `7` step is banned with the `5` step.
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border p-7" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// Half-steps other than 1.5 on a shell: gap-3.5, space-y-3.5, py-2.5.
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border flex flex-col gap-3.5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
{
|
||||
code: '<div className="bg-card border rounded-xl space-y-3.5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-lg border py-2.5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// The directional `pl-`/`ml-` spellings that slipped the `5`-only check
|
||||
// now trip it on a shell.
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border pl-5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
{
|
||||
code: '<div className="bg-card border-border rounded-xl border ml-5" />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// Split across cn() args — the join still sees the shell + the step.
|
||||
{
|
||||
code: '<div className={cn("bg-card border rounded-xl", "p-5")} />',
|
||||
filename: APP_FILE,
|
||||
errors: [{ messageId: "cardShellStep5" }],
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
// 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" }],
|
||||
errors: [{ messageId: "cardShellOffScale" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -17,16 +17,25 @@
|
||||
* modifier-prefixed (`md:pb-2`) and arbitrary-value (`pb-[10px]`) forms.
|
||||
* Non-numeric suffixes (`pb-safe`) never match.
|
||||
*
|
||||
* 2. `cardShellStep5` — no off-scale `5` step (20 px, banned by §2) on a
|
||||
* hand-rolled card SHELL. The shell heuristic (§7.2) keys off the
|
||||
* `bg-card` + `border` pair on ONE element: a `p-5` / `px-5` / `py-5` /
|
||||
* `space-y-5` / `gap-5` there sits denser than every sibling `<Card>`
|
||||
* (which is `p-4 md:p-6`), and the 4 px step reads next to a conformant
|
||||
* card on the same viewport. Deliberately NARROW: it fires only when the
|
||||
* same element paints a card surface, so justified `pl-5`/`pl-7`
|
||||
* list-marker insets (no `bg-card`) and form-body `space-y-5` rhythm (no
|
||||
* shell) never trip it. Hand-rolled shells sweep to `p-4 md:p-6`
|
||||
* (or `p-3` for a dense inner tile); new surfaces compose `<Card>`.
|
||||
* 2. `cardShellOffScale` — no off-scale spacing step on a hand-rolled card
|
||||
* SHELL. The scale (§2) is `1 / 1.5 / 2 / 3 / 4 / 6 / 8 / 10`; the `5`
|
||||
* (20 px) and `7` (28 px) steps and every half-step other than `1.5`
|
||||
* (`2.5`, `3.5`, …) are banned. The shell heuristic (§7.2) keys off the
|
||||
* `bg-card` + `border` pair on ONE element: a `p-5` / `py-2.5` /
|
||||
* `space-y-3.5` / `gap-3.5` / `ml-5` there sits off the scale next to
|
||||
* every sibling `<Card>` (which is `p-4 md:p-6`), and reads on the same
|
||||
* viewport. The prefix set is the full spacing family — padding
|
||||
* (`p/px/py/pt/pb/pl/pr`), margin (`m/mx/my/mt/mb/ml/mr`), gap
|
||||
* (`gap/gap-x/gap-y`), and `space-x/space-y` — so the `pl-`/`ml-`/`gap-`
|
||||
* spellings that slipped the earlier `5`-only check are covered.
|
||||
* Deliberately NARROW: it fires only when the same element paints a card
|
||||
* surface, so a justified `pl-5` list-marker inset (no `bg-card`), a
|
||||
* form-body `space-y-5` rhythm (no shell), and a deliberate negative
|
||||
* margin (`-mt-5`, never matched) never trip it. Height (`h-`) is a
|
||||
* dimension, not a scale step (`h-11` tap floor, `h-14`, `h-64` are all
|
||||
* legal), so it is out of scope by design. Hand-rolled shells sweep to
|
||||
* `p-4 md:p-6` (or `p-3` for a dense inner tile); new surfaces compose
|
||||
* `<Card>`.
|
||||
*
|
||||
* Both checks collect strings from the whole `className` subtree, so
|
||||
* `cn("…", cond && "…")`, ternaries, and template literals are all seen.
|
||||
@@ -63,12 +72,20 @@ const SLOT_NAMES = new Set(["CardHeader", "CardContent"]);
|
||||
const SHELL_BG_RE = /\bbg-card\b/;
|
||||
const SHELL_BORDER_RE = /\bborder(?:-|\b)/;
|
||||
|
||||
// The banned `5` step (20 px) on a shell — the subset §7.2 scopes to an
|
||||
// error-clean rule: p-5 / px-5 / py-5 / space-y-5 / gap-5, with optional
|
||||
// modifier prefixes (`sm:p-5`). Directional insets (`pl-5`/`pr-5`) and
|
||||
// half-steps are intentionally OUT of scope (see the header).
|
||||
const SHELL_STEP5_RE =
|
||||
/(?:^|\s)(?:[^\s"']*:)*((?:p|px|py|space-y|gap)-5)(?=\s|$)/;
|
||||
// An off-scale spacing step on a shell — the banned `5` / `7` integer steps
|
||||
// and any half-step other than `1.5` (`2.5`, `3.5`, …), across the full
|
||||
// spacing prefix family (padding, margin, gap, space), with optional modifier
|
||||
// prefixes (`sm:p-5`). A leading `-` (negative margin) is never matched
|
||||
// because the prefix has to sit at a `(?:^|\s)` / modifier boundary. Height
|
||||
// (`h-`) is a dimension, not a scale step, and is intentionally OUT of scope.
|
||||
const SPACING_PREFIX =
|
||||
"space-x|space-y|gap-x|gap-y|gap|px|py|pt|pb|pl|pr|p|mx|my|mt|mb|ml|mr|m";
|
||||
// `5` or `7`; or a half-step whose integer part is anything but `1`
|
||||
// (`0.5`, `2.5`…`9.5`, `10.5`+), so the sanctioned `1.5` is left alone.
|
||||
const OFF_SCALE_STEP = "5|7|(?:[02-9]|\\d\\d+)\\.5";
|
||||
const SHELL_OFFSCALE_RE = new RegExp(
|
||||
`(?:^|\\s)(?:[^\\s"']*:)*((?:${SPACING_PREFIX})-(?:${OFF_SCALE_STEP}))(?=\\s|$)`,
|
||||
);
|
||||
|
||||
function toPosix(filename) {
|
||||
return filename.replace(/\\/g, "/");
|
||||
@@ -148,14 +165,14 @@ const spacingScaleRule = {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Spacing-scale discipline — no pt-*/pb-* overrides on gap-based Card slots, and no off-scale `5` step on a bg-card+border shell.",
|
||||
"Spacing-scale discipline — no pt-*/pb-* overrides on gap-based Card slots, and no off-scale spacing step (`5`/`7`/half-steps) on a bg-card+border shell.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
slotPadding:
|
||||
'"{{match}}" on <{{element}}> fights the gap-based Card contract — the header→body distance comes from the parent flex gap, so slot padding stacks on top of it (or is a no-op). Tune density on the Card itself instead: <Card className="gap-2 py-3 md:py-4">.',
|
||||
cardShellStep5:
|
||||
'Off-scale "{{match}}" (20 px) on a bg-card+border shell — the `5` step is banned (UI-STANDARDS §2) and reads denser than every sibling <Card> (p-4 md:p-6). Sweep the shell to `p-4 md:p-6` (or `p-3` for a dense inner tile); new surfaces should compose <Card>.',
|
||||
cardShellOffScale:
|
||||
'Off-scale "{{match}}" on a bg-card+border shell — the scale is 1/1.5/2/3/4/6/8/10, so the `5`/`7` steps and half-steps other than 1.5 are banned (UI-STANDARDS §2) and read off next to every sibling <Card> (p-4 md:p-6). Sweep the shell to an on-scale step (`p-4 md:p-6`, or `p-3` for a dense inner tile); new surfaces should compose <Card>.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
@@ -197,11 +214,11 @@ const spacingScaleRule = {
|
||||
// seen together even when split across cn() args, then report once.
|
||||
const joined = strings.map((s) => s.text).join(" ");
|
||||
if (SHELL_BG_RE.test(joined) && SHELL_BORDER_RE.test(joined)) {
|
||||
const m = SHELL_STEP5_RE.exec(joined);
|
||||
const m = SHELL_OFFSCALE_RE.exec(joined);
|
||||
if (m) {
|
||||
context.report({
|
||||
node: classNameAttr,
|
||||
messageId: "cardShellStep5",
|
||||
messageId: "cardShellOffScale",
|
||||
data: { match: m[1] },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user