diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-mobile.png index 93e1295ce..e1d4b25d1 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-tablet.png index fdad8f9bb..6263413ac 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-desktop.png index fbd2260de..cfda51f8f 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-desktop.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-mobile.png index 33c11fc3a..47751d4eb 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-tablet.png index 6f98209f2..4dc61e9c7 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement.png index 2d2f42872..c3b67571a 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-overlay-popover--auto-placement.png differ diff --git a/packages/webkit/src/components/overlay/popover/popover-header/popover-header.vue b/packages/webkit/src/components/overlay/popover/popover-header/popover-header.vue index 9566bdbc0..1f65938c1 100644 --- a/packages/webkit/src/components/overlay/popover/popover-header/popover-header.vue +++ b/packages/webkit/src/components/overlay/popover/popover-header/popover-header.vue @@ -27,12 +27,19 @@ class="relative border-b border-(--border-default) py-(--spacing-sm) px-(--spacing-md)" >
diff --git a/packages/webkit/src/eslint-plugin/token-checks.js b/packages/webkit/src/eslint-plugin/token-checks.js index 066d5b4a0..4be5f6cb9 100644 --- a/packages/webkit/src/eslint-plugin/token-checks.js +++ b/packages/webkit/src/eslint-plugin/token-checks.js @@ -98,6 +98,33 @@ export const TOKEN_CHECKS = [ message: 'Zero with the wrong unit inside a math function. `calc()`/`min()`/`max()`/`clamp()` require a unit on the zero — write `0rem`, not `0px` / `0em` (.claude/rules/styling.md).' }, + { + id: 'dead-token-shorthand', + // A MALFORMED token shorthand never reaches the element, silently. Two spellings, + // both found in the wild, failing at two different stages: + // `bg-(--token )` a stray space TERMINATES the Tailwind candidate, so it sees + // the unterminated `bg-(--token` and emits NOTHING at all. + // `min-h-[--(token)]` bracket/paren inverted. This one DOES emit a rule — with the + // declaration `min-height: --(token)`, an invalid value the + // browser discards at parse time. Same outcome, later stage. + // Neither is a lint, type, or build error anywhere else, and the class reads as correct + // forever. chip's `filled` kind was transparent in BOTH themes from the day it shipped + // because of the first, and its own visual baselines were generated from the broken + // render — so they encoded the bug as correct and nothing ever failed. + // + // The scan is over raw file text, so a COMMENT quoting a malformed class trips this + // too. Describe such a class ("an inverted bracket/paren shorthand") rather than + // spelling it out. + // + // Read as: `-(`, whose contents mention a custom property, contain whitespace ANYWHERE + // (before the `--`, inside it, or trailing), and then close. Requiring the `--` via + // lookahead is what keeps this off ordinary subtraction — JS `x -(y + z)` has no `--` + // — and `var(--a, var(--b))` never matches because the character before that paren is + // `r`, not `-`, so a nested var with a fallback is untouched. + regex: /-\((?=[^)]*--)[^)]*\s[^)]*\)|\[--\(/g, + message: + 'Malformed token shorthand — the style NEVER applies (Tailwind emits nothing for a space inside the parens; an inverted `[--(token)]` emits an invalid value the browser discards). Write `prop-(--token)` / `prop-(type:--token)` with no whitespace inside the parens, and `[--token:value]` to DECLARE a custom property (.claude/rules/styling.md).' + }, { id: 'class-in-defineprops', regex: /defineProps\s*[<(][^>)]*['"]?class['"]?\s*:/s, diff --git a/packages/webkit/test/eslint-plugin/token-checks.test.mjs b/packages/webkit/test/eslint-plugin/token-checks.test.mjs index f08e3c1d6..5a08c684b 100644 --- a/packages/webkit/test/eslint-plugin/token-checks.test.mjs +++ b/packages/webkit/test/eslint-plugin/token-checks.test.mjs @@ -138,6 +138,48 @@ test('the widened guardrails do not fire on the canonical whole-value paren toke } }) +test('dead-token-shorthand fires on a paren shorthand that emits no CSS', () => { + for (const content of [ + // The one that shipped: a stray space terminates the candidate. + 'class="data-[kind=filled]:bg-(--bg-surface-raised )"', + 'class="bg-( --bg-surface)"', + 'class="text-(length:--text-body-md )"', + // A token broken across lines is the same defect. + 'class="bg-(--bg-\n surface)"', + // Bracket and paren forms inverted. + 'class="min-h-[--(size-4)]"' + ]) { + assert.ok( + ids(content).includes('dead-token-shorthand'), + `expected dead-token-shorthand for: ${content}` + ) + } +}) + +test('dead-token-shorthand stays silent on every VALID token spelling', () => { + for (const content of [ + 'class="bg-(--bg-surface) text-(--text-default) rounded-(--shape-card)"', + 'class="text-(length:--text-body-md) font-(family-name:--font-sora)"', + 'class="data-[disabled]:bg-(--bg-disabled)"', + // Declaring a custom property keeps its brackets — the sanctioned form. + 'class="[--table-row-bg:var(--bg-surface)]"', + 'class="data-[state=selected]:[--table-row-bg:var(--bg-selected)]"', + // Expression values legitimately carry spaces INSIDE brackets, not parens. + 'class="w-[calc(100%_-_var(--x))]"', + 'style="width: calc(var(--a) - (var(--b) * 2))"', + // A var with a fallback: `-(` never precedes the `--`, so this is not a match. + 'style="background: var(--x, var(--y))"', + // Ordinary subtraction in script, with and without a space. + 'const gap = width -(padding + border)', + 'const gap = width - (padding + border)' + ]) { + assert.ok( + !ids(content).includes('dead-token-shorthand'), + `expected dead-token-shorthand silent for: ${content}` + ) + } +}) + test('tokenChecksApply scopes enforcement to component sources', () => { assert.ok(tokenChecksApply('packages/webkit/src/components/actions/button/button.vue')) assert.ok(tokenChecksApply('packages/webkit/src/components/data/table/injection-key.ts'))