fix(design): the button and chip hover colours were never generated — an act: variant instead of a runtime prefix (E28)
buttonStyles/chipStyles glued the hover guard on at runtime (`${ACT}hover:text-fg`). Tailwind emits CSS
only for a class it finds whole in the source, so all 22 guarded hovers shipped with no rule: no text or
edge colour on hover for ghost/outline/danger buttons, no white layer on the player's control row, no
underline on a link button, no accent hover on token/suggest/ghost chips. The stylesheet had 0 rules
for `not-aria-disabled`.
- `@custom-variant act` (index.css): the same guard as the state layers; `:disabled` never matches an
<a>, so one variant serves buttons and links and the per-element variant maps go.
- ui/tailwindScan.contract.test.ts: every class the primitives can emit stands whole in their source.
- a lint rule refuses a variant right after an interpolation; the `enabled:hover:` rule now points at
`act:hover:`.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonClass,
|
||||
type IconButtonSize,
|
||||
type IconButtonVariant,
|
||||
iconButtonClass,
|
||||
} from "./buttonStyles";
|
||||
import { type ChipSize, type ChipVariant, chipClass } from "./chipStyles";
|
||||
|
||||
// Tailwind generates CSS only for the class names it FINDS in the source, token by token. A class
|
||||
// assembled at runtime (`${ACT}hover:text-fg`) is never found whole, so it ships with no rule at all:
|
||||
// every hover colour of Button, IconButton and Chip was dead that way, found in the E28 audit by
|
||||
// reading the stylesheet (0 rules for `not-aria-disabled`). So: every class a primitive can emit has
|
||||
// to stand in its source file whole, between the delimiters the scanner splits on.
|
||||
const tokens = (file: string) =>
|
||||
new Set(readFileSync(new URL(file, import.meta.url), "utf8").split(/[\s"'`{}$]+/));
|
||||
|
||||
// Records, so a new variant or size that is not listed here fails to compile.
|
||||
const keys = <K extends string>(r: Record<K, true>) => Object.keys(r) as K[];
|
||||
const BV = keys<ButtonVariant>({
|
||||
primary: true, danger: true, warn: true, outline: true, "outline-accent": true,
|
||||
"outline-danger": true, "outline-warn": true, ghost: true, glass: true, overlay: true,
|
||||
media: true, "danger-soft": true, link: true, "link-muted": true,
|
||||
}); // prettier-ignore
|
||||
const BS = keys<ButtonSize>({ xs: true, sm: true, md: true, lg: true, none: true });
|
||||
const IV = keys<IconButtonVariant>({
|
||||
ghost: true, "ghost-accent": true, primary: true, outline: true, "outline-accent": true,
|
||||
"outline-danger": true, "outline-warn": true, danger: true, "danger-soft": true, glass: true,
|
||||
media: true, overlay: true,
|
||||
}); // prettier-ignore
|
||||
const IS = keys<IconButtonSize>({ sm: true, md: true, lg: true, player: true });
|
||||
const CV = keys<ChipVariant>({ filter: true, token: true, tag: true, suggest: true, ghost: true });
|
||||
const CS = keys<ChipSize>({ md: true, sm: true, xs: true });
|
||||
|
||||
const missing = (emitted: string[], file: string) => {
|
||||
const found = tokens(file);
|
||||
return [...new Set(emitted.flatMap((c) => c.split(/\s+/)).filter(Boolean))].filter(
|
||||
(c) => !found.has(c),
|
||||
);
|
||||
};
|
||||
|
||||
describe("every class a primitive emits is visible to the Tailwind scanner", () => {
|
||||
it("Button and IconButton", () => {
|
||||
const emitted: string[] = [];
|
||||
for (const pressed of [undefined, true]) {
|
||||
for (const variant of BV)
|
||||
for (const size of BS) emitted.push(buttonClass({ variant, size, pressed, round: true }));
|
||||
for (const variant of IV)
|
||||
for (const size of IS) emitted.push(iconButtonClass({ variant, size, pressed }));
|
||||
}
|
||||
expect(missing(emitted, "./buttonStyles.ts")).toEqual([]);
|
||||
});
|
||||
|
||||
it("Chip", () => {
|
||||
const emitted = CV.flatMap((v) => CS.map((s) => chipClass(v, s)));
|
||||
expect(missing(emitted, "./chipStyles.ts")).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user