mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
The design system site rendered black borders and dividers while the tokens said `#d4d4d4`. The border roles were declared in layer 2 and never registered with Tailwind, so `border-primary` silently resolved to `--color-primary` — the *text* colour — and every hairline drew at text contrast. Tailwind's `--color-*` is a single namespace and text/borders share the emphasis names while holding different values, so borders now register as `--border-color-*`. Add typography as the second foundation, same three layers as colour: a nine-step size ramp plus leading, tracking, and weight ramps in layer 1; ten public roles in layer 2, each carrying its whole setting via Tailwind v4's `--text-<name>--<property>` convention, so `text-body` alone produces correctly set text and two supposedly identical labels cannot drift apart. Inter Variable and JetBrains Mono, both already shipped in every current Buzz client. Every size derives from one virtual rem, so keyboard zoom and the font-size preference work by construction. Soften the documentation surface to match where the product is going: regions separated by soft fills rather than outlines, hairlines only for genuine boundaries and never above `border-secondary`, no page-wide gradient behind reading columns, and no all-caps labels anywhere. The `/design/typography` page renders from the registry, so it cannot drift from the system. Add `scripts/check-type.mjs` enforcing the four rules a token cannot express — no arbitrary sizes (px or rem), no `uppercase`, no hand-applied tracking, no size role paired with a weight or leading utility. Also collapse the nav to a wrapped row below `lg`; the fixed 256px column overflowed the viewport under 420px. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
126 lines
4.1 KiB
JavaScript
126 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Type-system guard.
|
|
*
|
|
* Three rules from DESIGN.md § Type, enforced rather than trusted. Each one has
|
|
* already cost the existing client real work:
|
|
*
|
|
* 1. No arbitrary text sizes — `text-[15px]`, `text-[0.9rem]`, `font-size:`.
|
|
* Fixed px froze against keyboard zoom and shipped a message-timeline
|
|
* regression; arbitrary rem re-fragments the scale we just consolidated.
|
|
* 2. No `uppercase` / `tracking-*` utilities. All-caps labels are less legible
|
|
* and read as enterprise chrome, and tracking is corrected per ramp step.
|
|
* 3. No size role paired with a weight or leading utility. A role carries its
|
|
* whole setting; pairing one with `font-medium` is how two supposedly
|
|
* identical labels drift apart.
|
|
*
|
|
* Run: pnpm check:type
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join, relative } from "node:path";
|
|
|
|
const SRC = new URL("../src", import.meta.url).pathname;
|
|
|
|
/** Size roles a component may use. Kept in sync with typography.css. */
|
|
const SIZE_ROLES = [
|
|
"display",
|
|
"title",
|
|
"heading",
|
|
"subheading",
|
|
"body-lg",
|
|
"body",
|
|
"label",
|
|
"caption",
|
|
"meta",
|
|
"code",
|
|
];
|
|
|
|
const RULES = [
|
|
{
|
|
id: "arbitrary-text-size",
|
|
// text-[...] with any unit, plus raw CSS font-size declarations.
|
|
pattern: /\btext-\[[^\]]*(?:px|rem|em|pt|%)[^\]]*\]|font-size\s*:/g,
|
|
message:
|
|
"arbitrary text size — use a named role (text-body, text-label, …). px freezes against zoom; arbitrary rem re-fragments the scale.",
|
|
},
|
|
{
|
|
id: "uppercase",
|
|
// Only inside a className/class string — otherwise prose describing the
|
|
// rule trips it, and a guard that cries wolf gets ignored.
|
|
pattern: /class(?:Name)?=(?:"|'|`)[^"'`]*\buppercase\b/g,
|
|
message:
|
|
"all-caps text — DESIGN.md § Type forbids it. A quiet label uses text-meta on text-tertiary instead.",
|
|
},
|
|
{
|
|
id: "manual-tracking",
|
|
pattern: /\btracking-(?:tighter|tight|normal|wide|wider|widest|\[)/g,
|
|
message:
|
|
"hand-applied tracking — the ramp already corrects tracking per step.",
|
|
},
|
|
{
|
|
id: "role-plus-weight",
|
|
// A size role sharing a *className string* with a weight or leading
|
|
// utility. Anchoring to the attribute keeps the ramp's own
|
|
// `--text-body--line-height` declarations out of it — those are the
|
|
// definitions, not a component overriding one.
|
|
pattern: new RegExp(
|
|
`class(?:Name)?=(?:"|'|\`)[^"'\`]*\\btext-(?:${SIZE_ROLES.join("|")})\\b[^"'\`]*\\b(?:font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)|leading-)`,
|
|
"g",
|
|
),
|
|
message:
|
|
"size role paired with a weight/leading utility — a role already carries its whole setting.",
|
|
},
|
|
];
|
|
|
|
function walk(dir) {
|
|
return readdirSync(dir).flatMap((entry) => {
|
|
const full = join(dir, entry);
|
|
if (statSync(full).isDirectory()) return walk(full);
|
|
return /\.(tsx?|css)$/.test(full) ? [full] : [];
|
|
});
|
|
}
|
|
|
|
const failures = [];
|
|
|
|
for (const file of walk(SRC)) {
|
|
const source = readFileSync(file, "utf8");
|
|
// The typography ramp itself is the one place literals are legal — it is
|
|
// layer 1, where values live by design.
|
|
const isRamp = file.endsWith("typography.css");
|
|
const lines = source.split("\n");
|
|
|
|
lines.forEach((line, index) => {
|
|
// Skip comment-only lines: the docs quote the very patterns they forbid.
|
|
const trimmed = line.trim();
|
|
if (
|
|
trimmed.startsWith("*") ||
|
|
trimmed.startsWith("//") ||
|
|
trimmed.startsWith("/*")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
for (const rule of RULES) {
|
|
if (isRamp && rule.id === "arbitrary-text-size") continue;
|
|
rule.pattern.lastIndex = 0;
|
|
const match = rule.pattern.exec(line);
|
|
if (match) {
|
|
failures.push(
|
|
`${relative(process.cwd(), file)}:${index + 1} ${rule.id}\n ${trimmed}\n → ${rule.message}`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(
|
|
`\n✗ Type system: ${failures.length} violation${failures.length === 1 ? "" : "s"}\n`,
|
|
);
|
|
console.error(`${failures.join("\n\n")}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("✓ Type system: no violations");
|