trykeyline 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/LICENSE +0 -16
- package/bin/keyline.mjs +2 -1
- package/package.json +1 -1
- package/plugins/keyline/.claude-plugin/plugin.json +1 -1
- package/plugins/keyline/plugin.json +22 -0
- package/plugins/keyline/skills/keyline/SKILL.md +22 -32
- package/plugins/keyline/skills/keyline/commands/audit.md +8 -3
- package/plugins/keyline/skills/keyline/commands/critique.md +3 -26
- package/plugins/keyline/skills/keyline/commands/init.md +5 -4
- package/plugins/keyline/skills/keyline/commands/polish.md +2 -2
- package/plugins/keyline/skills/keyline/commands/refine.md +6 -16
- package/plugins/keyline/skills/keyline/commands/sync.md +1 -3
- package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +1 -1
- package/plugins/keyline/skills/keyline/domains/color/guide.md +10 -46
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +2 -2
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +2 -8
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +12 -41
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +31 -56
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +16 -16
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +2 -2
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +12 -47
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +1 -1
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +1 -1
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +20 -62
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +1 -9
- package/plugins/keyline/skills/keyline/domains/type/guide.md +13 -37
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +5 -8
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -165
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
- package/plugins/keyline/skills/keyline/glass/color/guide.md +3 -30
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
- package/plugins/keyline/skills/keyline/glass/color/theming.md +5 -72
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +14 -121
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +29 -188
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -16
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +4 -73
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +3 -11
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +5 -91
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +3 -6
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +11 -138
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +10 -15
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +4 -6
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +3 -2
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +4 -6
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +10 -184
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +21 -105
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +2 -31
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +10 -41
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -7
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +3 -32
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +1 -1
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +10 -80
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +49 -86
- package/plugins/keyline/skills/keyline/glass/type/guide.md +5 -7
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +9 -74
- package/plugins/keyline/skills/keyline/glass/type/scale.md +2 -6
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +4 -20
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +5 -14
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +3 -1
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +70 -26
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +3 -3
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +9 -1
- package/plugins/keyline/skills/keyline/scripts/check.mjs +6 -3
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +2 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +29 -45
- package/plugins/keyline/skills/keyline/system/design-md.md +4 -4
- package/plugins/keyline/skills/keyline/system/guide.md +4 -8
- package/plugins/keyline/skills/keyline/system/presets.md +16 -16
- package/plugins/keyline/skills/keyline/system/roles.md +8 -8
|
@@ -17,7 +17,7 @@ A modular scale gives too few sizes in the 11–16px band, where Glass needs 11,
|
|
|
17
17
|
- 12.5px code. Monospace runs wide, so it steps down to occupy the same measure as the prose around it — the one size that compensates for a *family* rather than a role.
|
|
18
18
|
- 12px. Tags, prev/next labels, hover-card titles; small enough to read as annotation.
|
|
19
19
|
- 11px. Tooltips and avatar initials. The floor for anything a user is expected to read.
|
|
20
|
-
- 8.5px lettermarks, 7.5px card lettering. Below the floor on purpose — recognised rather than read
|
|
20
|
+
- 8.5px lettermarks, 7.5px card lettering. Below the floor on purpose — recognised rather than read (the 600 exception: §2 of the guide).
|
|
21
21
|
|
|
22
22
|
**What was deleted**
|
|
23
23
|
|
|
@@ -44,11 +44,7 @@ In the 11–16px band the steps are 1px, and that is enough (`domains/type/ladde
|
|
|
44
44
|
|
|
45
45
|
## Fixed sizes, not `rem`, and why that's a choice
|
|
46
46
|
|
|
47
|
-
The reference implementation uses `px` throughout (`text-[14px]`, not `text-sm` or `0.875rem`).
|
|
48
|
-
|
|
49
|
-
The trade: `rem` respects a user's browser font-size setting and `px` doesn't, a real accessibility cost. But this hierarchy depends on precise 1px relationships in the 11–16 band, a root scaled to 20px turns them into 15px steps that break the ladder, and browser *zoom* — which everyone uses, and which scales `px` fine — already covers the need.
|
|
50
|
-
|
|
51
|
-
If your audience skews toward users who set a root font size — documentation, government, healthcare — use `rem` and accept a looser ladder. Convert at a fixed 16px root: 18px → 1.125rem, 14px → 0.875rem, 13px → 0.8125rem. Never mix the two (the domain's unit dial).
|
|
47
|
+
The reference implementation uses `px` throughout (`text-[14px]`, not `text-sm` or `0.875rem`), to keep the 1px steps of the 11–16 band exact under a scaled root; the trade is the domain's unit dial. Moving to `rem`, convert at a fixed 16px root: 18px → 1.125rem, 14px → 0.875rem, 13px → 0.8125rem.
|
|
52
48
|
|
|
53
49
|
## Keeping every element on it
|
|
54
50
|
|
|
@@ -16,13 +16,11 @@ All body copy. All prose. Deks. Meta lines. Tags. Tooltips. Timestamps. Quiet na
|
|
|
16
16
|
**500 — structure and state.**
|
|
17
17
|
Headings h1–h4. `<strong>`. The active row in a table of contents. Names and wordmarks. Captions that label an image rather than describe it. Lettermarks. Company/role titles in a list, where the role is structural and the date supporting.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
The lists are defaults; the choice is made per spot (domain law 5): the same 13px line is 400 as a date under a title and 500 as the name heading a group.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### 300
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Rarely, and never for text read at length: at a very large display size (48px+), where 400 starts to look heavy, or a decorative numeral. If your interface tops out at 30px — most do — you don't need it. The reference implementation loaded 300 for a while and never used it; dropping it from the font request was a straight performance win.
|
|
23
|
+
Not loaded. The reference implementation loaded it for a while and never used it; dropping it from the font request was a straight performance win.
|
|
26
24
|
|
|
27
25
|
### Weight as an accessibility signal
|
|
28
26
|
|
|
@@ -32,23 +30,9 @@ The active table-of-contents row takes the primary text colour *and* weight 500
|
|
|
32
30
|
className={`… ${tone} ${state === "active" ? "font-medium" : ""}`}
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
Weight is cheaper here than a background pill, border or icon, each of which adds visual weight to a list meant to be quiet.
|
|
36
|
-
|
|
37
33
|
## Tracking
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| Size | Tracking | Reasoning |
|
|
42
|
-
|---|---|---|
|
|
43
|
-
| 7.5px | −0.01em | Card lettering, set solid. The exception below the floor — see the note. |
|
|
44
|
-
| 8.5–11px | +0.02em | Tiny glyphs and near-caps need air or the counters close up. |
|
|
45
|
-
| 12–14px | 0 | Default metrics are designed for this range. Leave them. |
|
|
46
|
-
| 16px | −0.015em | Enough to look set; small enough to be invisible as a decision. |
|
|
47
|
-
| 18px | −0.02em | A page title is a tighter, more deliberate object than a section heading two pixels below. |
|
|
48
|
-
|
|
49
|
-
Only two rows are the negative-tracking-at-size rule, because the ladder tops out at 18px. An earlier table ran to 30px at −0.025em, with separate values for a 19px heading and a 19px quote; all three sizes left the ladder and took their tracking with them.
|
|
50
|
-
|
|
51
|
-
The 7.5px row doesn't contradict the 8.5–11px one: that lettering is set solid at near-caps size on a card, so the tightening compensates for the *setting*, not the size. It's also the one place a 600 weight is allowed. Two exceptions in one place hint you're outside the text system — which you are, since nobody reads it.
|
|
35
|
+
The values are §4 of `glass/type/guide.md`. Only 16px and 18px are the negative-tracking-at-size rule, because the ladder tops out at 18px. The 7.5px card lettering's −0.01em doesn't contradict the lettermarks' +0.02em: it is set solid at near-caps size, so the tightening compensates for the *setting*, not the size.
|
|
52
36
|
|
|
53
37
|
### The 8.5px lettermark
|
|
54
38
|
|
|
@@ -13,12 +13,6 @@ export function blankComments(text) {
|
|
|
13
13
|
return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function lineAt(text, offset) {
|
|
17
|
-
let line = 1;
|
|
18
|
-
for (let i = 0; i < offset && i < text.length; i++) if (text.charCodeAt(i) === 10) line++;
|
|
19
|
-
return line;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
/** Lines start offsets, for fast offset → line lookups on big files. */
|
|
23
17
|
export function lineIndex(text) {
|
|
24
18
|
const starts = [0];
|
|
@@ -44,8 +38,7 @@ export function parseCss(source, baseLine = 1) {
|
|
|
44
38
|
const at = lineIndex(text);
|
|
45
39
|
const decls = [];
|
|
46
40
|
const atRules = [];
|
|
47
|
-
const
|
|
48
|
-
const stack = []; // open blocks: { kind: 'rule'|'at', prelude, block }
|
|
41
|
+
const stack = []; // open blocks: { kind: 'rule'|'at', prelude, block: { decls } }
|
|
49
42
|
let start = 0;
|
|
50
43
|
let quote = null;
|
|
51
44
|
let paren = 0;
|
|
@@ -65,7 +58,7 @@ export function parseCss(source, baseLine = 1) {
|
|
|
65
58
|
const value = raw.slice(colon + 1).trim();
|
|
66
59
|
const lead = from + raw.search(/\S/);
|
|
67
60
|
const ctx = context();
|
|
68
|
-
const d = { prop, value, line: at(lead) + baseLine - 1,
|
|
61
|
+
const d = { prop, value, line: at(lead) + baseLine - 1, selector: ctx.selector, atRules: ctx.atRules, block: ctx.block };
|
|
69
62
|
decls.push(d);
|
|
70
63
|
ctx.block?.decls.push(d);
|
|
71
64
|
};
|
|
@@ -85,10 +78,8 @@ export function parseCss(source, baseLine = 1) {
|
|
|
85
78
|
const prelude = text.slice(start, i).trim().replace(/\s+/g, " ");
|
|
86
79
|
const kind = prelude.startsWith("@") ? "at" : "rule";
|
|
87
80
|
const lead = start + text.slice(start, i).search(/\S/);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
blocks.push(block);
|
|
91
|
-
stack.push({ kind, prelude, block });
|
|
81
|
+
if (kind === "at") atRules.push({ prelude, line: at(Math.max(lead, 0)) + baseLine - 1 });
|
|
82
|
+
stack.push({ kind, prelude, block: { decls: [] } });
|
|
92
83
|
start = i + 1;
|
|
93
84
|
} else if (c === ";") {
|
|
94
85
|
if (stack.length) pushDecl(start, i);
|
|
@@ -103,7 +94,7 @@ export function parseCss(source, baseLine = 1) {
|
|
|
103
94
|
start = i + 1;
|
|
104
95
|
}
|
|
105
96
|
}
|
|
106
|
-
return { decls, atRules
|
|
97
|
+
return { decls, atRules };
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
/** Parse a `style="..."` attribute's body. */
|
|
@@ -50,7 +50,9 @@ export function readDesign(root) {
|
|
|
50
50
|
// or mentions one — their values are the ladder.
|
|
51
51
|
const layout = section(md, "Layout");
|
|
52
52
|
const zLadder = new Set();
|
|
53
|
-
|
|
53
|
+
// A project rung (`--z-header`, `$z-header`, a `z-header` utility) counts when it opens the token cell, the row's second.
|
|
54
|
+
const zCell = (c) => /^`(--|\$)?z-[\w-]+`/.test(c);
|
|
55
|
+
for (const row of layout.split("\n").filter((l) => /^\|\s*`?z\./.test(l) || (l.startsWith("|") && (/`-?z-\[?-?\d|`z\./.test(l) || zCell((l.split("|")[2] ?? "").trim()))))) {
|
|
54
56
|
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
55
57
|
for (const c of cells.slice(1)) for (const m of c.matchAll(/(?<![\w.#-])(-?\d+)(?![\w.%])/g)) zLadder.add(Number(m[1]));
|
|
56
58
|
}
|
|
@@ -1,44 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which files
|
|
3
|
-
*
|
|
2
|
+
* Which files Keyline reads. One walker for the detector (check.mjs) and the
|
|
3
|
+
* inventory (scan.mjs), so the two skip the same folders and match ignores the
|
|
4
|
+
* same way; each passes only the extensions and filters its job needs.
|
|
4
5
|
*/
|
|
5
6
|
import { readdirSync, statSync } from "node:fs";
|
|
6
|
-
import { extname, join, relative, sep } from "node:path";
|
|
7
|
+
import { basename, extname, join, relative, sep } from "node:path";
|
|
7
8
|
|
|
9
|
+
/** Dependencies, build output, served assets and reports. Dot-folders are skipped wholesale, bar `.storybook`. */
|
|
8
10
|
export const SKIP_DIRS = new Set([
|
|
9
|
-
"node_modules", "
|
|
10
|
-
"
|
|
11
|
-
"storybook-static", ".claude", ".agents", "public", "static",
|
|
11
|
+
"node_modules", "dist", "build", "out", "coverage", "vendor", "storybook-static", "public", "static",
|
|
12
|
+
"keyline-report", "smoke-report",
|
|
12
13
|
]);
|
|
13
14
|
export const STYLE = new Set([".css", ".scss", ".sass", ".less", ".pcss"]);
|
|
14
15
|
export const MARKUP = new Set([".tsx", ".jsx", ".ts", ".js", ".mjs", ".vue", ".svelte", ".astro", ".html", ".mdx"]);
|
|
15
|
-
|
|
16
|
+
/** Minified, declaration, test and story files: generated, or not the interface. */
|
|
17
|
+
export const GENERATED = /\.(min|test|spec|stories)\.|\.d\.[a-z]+$/;
|
|
18
|
+
/** Tool configs. Not the interface, though a Tailwind config holds theme values. */
|
|
19
|
+
export const CONFIG = /\.config\.[a-z]+$|^(eslint|postcss|tailwind|next|vite|vitest|jest|playwright)\.config\./;
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const clean = (ig) => ig.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
22
|
+
|
|
23
|
+
/** A test for paths relative to the root: `src/content` drops `src/content/a.tsx`, never `src/contentful`. */
|
|
24
|
+
export function ignoredBy(ignores = []) {
|
|
25
|
+
const list = ignores.map(clean).filter(Boolean);
|
|
26
|
+
return (rel) => list.some((ig) => rel === ig || rel.startsWith(ig + "/"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every file under `paths` whose extension is in `accept` and whose name `skip`
|
|
31
|
+
* doesn't match, up to `maxSize` bytes (bigger is a bundle, not source). Folders
|
|
32
|
+
* in SKIP_DIRS and dot-folders are passed over unless named in `paths`;
|
|
33
|
+
* symlinked folders aren't followed.
|
|
34
|
+
* @returns {{ abs: string, rel: string, ext: string }[]} rel is relative to root, with `/`
|
|
35
|
+
*/
|
|
36
|
+
export function walk(root, { paths = [root], accept, skip = GENERATED, maxSize = 400_000, ignores = [] }) {
|
|
19
37
|
const out = [];
|
|
20
|
-
const ignored = (
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
const ignored = ignoredBy(ignores);
|
|
39
|
+
const rel = (abs) => relative(root, abs).split(sep).join("/");
|
|
40
|
+
const file = (abs, name, size) => {
|
|
41
|
+
const ext = extname(name);
|
|
42
|
+
if (!accept.has(ext) || skip?.test(name)) return;
|
|
43
|
+
if ((size ?? statSync(abs).size) > maxSize) return;
|
|
44
|
+
out.push({ abs, rel: rel(abs) || name, ext });
|
|
45
|
+
};
|
|
46
|
+
const dir = (abs) => {
|
|
47
|
+
let entries;
|
|
25
48
|
try {
|
|
26
|
-
|
|
49
|
+
entries = readdirSync(abs, { withFileTypes: true });
|
|
27
50
|
} catch {
|
|
28
51
|
return;
|
|
29
52
|
}
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
if (rel
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
const child = join(abs, e.name);
|
|
55
|
+
if (ignored(rel(child))) continue;
|
|
56
|
+
if (e.isDirectory()) {
|
|
57
|
+
if (!SKIP_DIRS.has(e.name) && (!e.name.startsWith(".") || e.name === ".storybook")) dir(child);
|
|
58
|
+
} else {
|
|
59
|
+
try {
|
|
60
|
+
file(child, e.name);
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
35
63
|
}
|
|
36
|
-
const ext = extname(abs);
|
|
37
|
-
if (!(STYLE.has(ext) || MARKUP.has(ext))) return;
|
|
38
|
-
if (SKIP_FILES.test(abs.split(sep).pop())) return;
|
|
39
|
-
if (st.size > 400_000) return; // generated bundles, not source
|
|
40
|
-
out.push({ abs, rel: rel || abs.split(sep).pop(), ext, kind: STYLE.has(ext) ? "style" : "markup" });
|
|
41
64
|
};
|
|
42
|
-
for (const p of paths)
|
|
65
|
+
for (const p of paths) {
|
|
66
|
+
const r = rel(p);
|
|
67
|
+
if (r && ignored(r)) continue;
|
|
68
|
+
let st;
|
|
69
|
+
try {
|
|
70
|
+
st = statSync(p);
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (st.isDirectory()) dir(p);
|
|
75
|
+
else file(p, basename(p), st.size);
|
|
76
|
+
}
|
|
43
77
|
return out;
|
|
44
78
|
}
|
|
79
|
+
|
|
80
|
+
const DETECTOR_SKIP = new RegExp(`${GENERATED.source}|${CONFIG.source}`);
|
|
81
|
+
|
|
82
|
+
/** The detector's files: style and markup, minus generated files and tool configs. */
|
|
83
|
+
export function collect(root, paths, ignores) {
|
|
84
|
+
return walk(root, { paths, ignores, accept: new Set([...STYLE, ...MARKUP]), skip: DETECTOR_SKIP }).map((f) => ({
|
|
85
|
+
...f,
|
|
86
|
+
kind: STYLE.has(f.ext) ? "style" : "markup",
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
@@ -57,7 +57,7 @@ export function readMarkup(source, ext) {
|
|
|
57
57
|
text = text.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " "));
|
|
58
58
|
const at = lineIndex(text);
|
|
59
59
|
|
|
60
|
-
const classLists = []; // {
|
|
60
|
+
const classLists = []; // { line, tokens: [{...splitClass, line}], tag }
|
|
61
61
|
const seen = new Set();
|
|
62
62
|
const addList = (value, offset, origin) => {
|
|
63
63
|
if (seen.has(offset)) return;
|
|
@@ -66,7 +66,7 @@ export function readMarkup(source, ext) {
|
|
|
66
66
|
const re = /\S+/g;
|
|
67
67
|
let m;
|
|
68
68
|
while ((m = re.exec(value))) tokens.push({ ...splitClass(m[0]), line: at(offset + m.index) });
|
|
69
|
-
if (tokens.length) classLists.push({
|
|
69
|
+
if (tokens.length) classLists.push({ line: at(offset), tokens, tag: origin === "attribute" ? tagAround(offset, value.length) : "" });
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/** The opening tag a class attribute sits in, for rules that need its element or attributes. */
|
|
@@ -93,7 +93,7 @@ export function readMarkup(source, ext) {
|
|
|
93
93
|
addList(s[1] === "`" ? staticParts(s[2]) : s[2], open + 1 + s.index + 1, "helper");
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
if (
|
|
96
|
+
if (ext !== ".html") {
|
|
97
97
|
for (const m of text.matchAll(EXPR_STRING)) {
|
|
98
98
|
const value = m[2];
|
|
99
99
|
if (value.length < 3 || !CLASSY.test(value) || !TAILWINDISH.test(value)) continue;
|
|
@@ -226,8 +226,16 @@ const colorRules = [
|
|
|
226
226
|
// recipe, not a palette colour; the recipe itself is what depth law 4 wants
|
|
227
227
|
// kept in one definition. A tinted shadow (a blue glow) still counts.
|
|
228
228
|
if (/^(box-shadow|text-shadow)$/.test(d.prop) && neutralOnly(d.value)) continue;
|
|
229
|
+
// Gradient stops and filter lights only exist inside SVG artwork, which colour law 1 exempts.
|
|
230
|
+
if (/^(stop-color|flood-color|lighting-color)$/.test(d.prop)) continue;
|
|
229
231
|
const c = literalColor(d.value);
|
|
230
|
-
if (c)
|
|
232
|
+
if (!c) continue;
|
|
233
|
+
// fill and stroke paint SVG: artwork (exempt) or an icon (should be currentColor). A regex can't tell which.
|
|
234
|
+
if (/^(fill|stroke)$/.test(d.prop)) {
|
|
235
|
+
out.push({ ...where(d), severity: "info", message: `\`${c}\` painted into \`${d.prop}\`. Inside artwork marked \`role="img"\` that's fine (colour law 1); on an icon, use \`currentColor\` so it follows its label.` });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
out.push({ ...where(d), severity: "warning", message: `\`${c}\` typed into \`${d.prop}\`${d.inline ? " in an inline style" : ""}. Give it a token named for its job (a text tier, a border, a status), so a theme or a contrast fix changes it once.` });
|
|
231
239
|
}
|
|
232
240
|
for (const c of tw(facts, (b) => TAILWIND_ARBITRARY_COLOR.test(b))) {
|
|
233
241
|
out.push({ ...where(c), severity: "warning", message: `\`${c.base}\` puts a raw colour in a class. Map it to a token named for its job.` });
|
|
@@ -92,8 +92,8 @@ export function gather(root, files) {
|
|
|
92
92
|
const VIEWPORT_VALUE = /<meta\b[^>]*\bviewport\b[^>]*>|["'`][^"'`\n<>]*width\s*=\s*device-width[^"'`\n<>]*["'`]/gi;
|
|
93
93
|
for (const v of m.text.matchAll(VIEWPORT_VALUE)) for (const z of v[0].matchAll(LOCK)) facts.zoomLocks.push({ file: f.rel, line: at(v.index + z.index) });
|
|
94
94
|
for (const z of m.text.matchAll(/\bmaximumScale\s*:\s*1\b|\buserScalable\s*:\s*false\b/g)) facts.zoomLocks.push({ file: f.rel, line: at(z.index) });
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const imp = m.text.match(MOTION_IMPORT);
|
|
96
|
+
if (imp) {
|
|
97
97
|
noteAnimation({ file: f.rel, line: at(imp.index) });
|
|
98
98
|
for (const d of m.text.matchAll(/\bduration\s*:\s*(\d*\.?\d+)\b/g)) {
|
|
99
99
|
// A spring's duration is a settling time set by its character, not a tier.
|
|
@@ -277,6 +277,9 @@ export function run(argv) {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const result = check({ root: opts.root, paths: opts.paths.length ? opts.paths : ["."], ignores: opts.ignores, rules: opts.rules });
|
|
280
|
+
// The verdict counts every finding, so --quiet trims the output, never the exit code.
|
|
281
|
+
const all = summarise(result.findings);
|
|
282
|
+
const failed = all.error > 0 || (opts.strict && all.warning > 0);
|
|
280
283
|
if (opts.quiet) result.findings = result.findings.filter((f) => f.severity === "error");
|
|
281
284
|
const s = summarise(result.findings);
|
|
282
285
|
|
|
@@ -286,7 +289,7 @@ export function run(argv) {
|
|
|
286
289
|
} else {
|
|
287
290
|
console.log(human(result, { all: opts.all, color: process.stdout.isTTY && !process.env.NO_COLOR }));
|
|
288
291
|
}
|
|
289
|
-
return
|
|
292
|
+
return failed ? 1 : 0;
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -150,4 +150,6 @@ if (ladder) {
|
|
|
150
150
|
if (rest.length !== 2) die("give a foreground and a background (or --ladder)");
|
|
151
151
|
const r = ratio(rest[0], rest[1]);
|
|
152
152
|
console.log(`${r >= floor ? "✓" : "✗"} ${fmt(r)} (floor ${floor}:1)`);
|
|
153
|
+
// 1:1 means the two colours came out identical — nearly always the arguments, not the design.
|
|
154
|
+
if (r < 1.01) console.error(`contrast: ${rest[0]} and ${rest[1]} come out as the same colour. Check the order: <fg> <bg>, or --over <backdrop> <fg> <bg>, or --ladder <bg> <fg>…`);
|
|
153
155
|
process.exit(r >= floor ? 0 : 2);
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* at runtime or a class built by string concatenation. When the summary and the
|
|
17
17
|
* code disagree, the code wins — open the file the summary points at.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import { join,
|
|
19
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
20
|
+
import { join, basename, resolve } from "node:path";
|
|
21
|
+
import { walk, STYLE as CHECKED_STYLE, MARKUP as CHECKED_MARKUP } from "./check/files.mjs";
|
|
21
22
|
|
|
22
23
|
const argv = process.argv.slice(2);
|
|
23
24
|
const json = argv.includes("--json");
|
|
@@ -33,37 +34,14 @@ if (!existsSync(root)) {
|
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const STYLE = new Set([
|
|
42
|
-
const MARKUP = new Set([
|
|
43
|
-
|
|
44
|
-
/* ── Walk ─────────────────────────────────────────────────────────────────── */
|
|
45
|
-
|
|
46
|
-
const files = [];
|
|
47
|
-
(function walk(dir) {
|
|
48
|
-
let entries;
|
|
49
|
-
try {
|
|
50
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
51
|
-
} catch {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
for (const e of entries) {
|
|
55
|
-
if (e.name.startsWith(".") && e.isDirectory() && e.name !== ".storybook") continue;
|
|
56
|
-
const path = join(dir, e.name);
|
|
57
|
-
if (ignores.some((ig) => ig && relative(root, path).startsWith(ig))) continue;
|
|
58
|
-
if (e.isDirectory()) {
|
|
59
|
-
if (!SKIP_DIRS.has(e.name)) walk(path);
|
|
60
|
-
} else if (STYLE.has(extname(e.name)) || MARKUP.has(extname(e.name))) {
|
|
61
|
-
if (/\.min\.|\.d\.ts$|\.test\.|\.spec\.|\.stories\./.test(e.name)) continue;
|
|
62
|
-
if (statSync(path).size > 1_000_000) continue;
|
|
63
|
-
files.push(path);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
})(root);
|
|
37
|
+
/*
|
|
38
|
+
* The detector's walker and folder skips, read wider: an inventory wants every
|
|
39
|
+
* hand-written value, so it also reads Stylus and CommonJS, keeps tool configs
|
|
40
|
+
* (a Tailwind config is where a theme is declared) and allows files up to 1 MB.
|
|
41
|
+
*/
|
|
42
|
+
const STYLE = new Set([...CHECKED_STYLE, ".styl"]);
|
|
43
|
+
const MARKUP = new Set([...CHECKED_MARKUP, ".cjs"]);
|
|
44
|
+
const files = walk(resolve(root), { accept: new Set([...STYLE, ...MARKUP]), maxSize: 1_000_000, ignores });
|
|
67
45
|
|
|
68
46
|
/* ── Tallies ──────────────────────────────────────────────────────────────── */
|
|
69
47
|
|
|
@@ -74,7 +52,7 @@ function count(bucket, value, file) {
|
|
|
74
52
|
tallies[bucket] ??= new Map();
|
|
75
53
|
const entry = tallies[bucket].get(v) ?? { n: 0, files: new Set() };
|
|
76
54
|
entry.n++;
|
|
77
|
-
entry.files.add(
|
|
55
|
+
entry.files.add(file);
|
|
78
56
|
tallies[bucket].set(v, entry);
|
|
79
57
|
}
|
|
80
58
|
|
|
@@ -91,7 +69,7 @@ const classWords = new Map();
|
|
|
91
69
|
const signals = {};
|
|
92
70
|
function signal(name, file) {
|
|
93
71
|
signals[name] ??= new Set();
|
|
94
|
-
signals[name].add(
|
|
72
|
+
signals[name].add(file);
|
|
95
73
|
}
|
|
96
74
|
|
|
97
75
|
/** Tailwind's scale names, so `text-sm` counts as a size and `text-red-500` doesn't. */
|
|
@@ -156,9 +134,9 @@ const SIGNALS = [
|
|
|
156
134
|
["numerals: tabular-nums", /tabular-nums|font-variant-numeric/],
|
|
157
135
|
];
|
|
158
136
|
|
|
159
|
-
for (const file of files) {
|
|
160
|
-
const text = readFileSync(
|
|
161
|
-
const kind = STYLE.has(
|
|
137
|
+
for (const { abs, rel: file, ext } of files) {
|
|
138
|
+
const text = readFileSync(abs, "utf8");
|
|
139
|
+
const kind = STYLE.has(ext) ? "style" : "markup";
|
|
162
140
|
|
|
163
141
|
for (const [bucket, re, group, only] of RULES) {
|
|
164
142
|
if (only !== "all" && only !== kind) continue;
|
|
@@ -175,7 +153,7 @@ for (const file of files) {
|
|
|
175
153
|
if (m[1].startsWith("--tw-")) continue;
|
|
176
154
|
const entry = props.get(m[1]) ?? { values: [], files: new Set() };
|
|
177
155
|
if (entry.values.length < 4 && !entry.values.includes(m[2].trim())) entry.values.push(m[2].trim());
|
|
178
|
-
entry.files.add(
|
|
156
|
+
entry.files.add(file);
|
|
179
157
|
props.set(m[1], entry);
|
|
180
158
|
}
|
|
181
159
|
if (kind === "markup") {
|
|
@@ -196,14 +174,20 @@ for (const file of files) {
|
|
|
196
174
|
* but it's the difference between "unused" and "used in 40 files".
|
|
197
175
|
*/
|
|
198
176
|
const NAMESPACES = ["color", "font", "text", "radius", "shadow", "ease", "animate", "container", "spacing", "tracking", "leading", "breakpoint", "blur", "inset-shadow", "drop-shadow"];
|
|
199
|
-
|
|
177
|
+
/** Class-word uses by every tail after a hyphen: `hover:text-ink-soft` counts for `ink-soft` and `soft`. */
|
|
178
|
+
const bySuffix = new Map();
|
|
179
|
+
for (const [word, c] of classWords) {
|
|
180
|
+
for (let i = word.indexOf("-"); i >= 0; i = word.indexOf("-", i + 1)) {
|
|
181
|
+
const tail = word.slice(i + 1);
|
|
182
|
+
bySuffix.set(tail, (bySuffix.get(tail) ?? 0) + c);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const utilityCount = new Map();
|
|
186
|
+
for (const prop of props.keys()) {
|
|
200
187
|
const ns = NAMESPACES.find((n) => prop.startsWith(`--${n}-`));
|
|
201
|
-
|
|
202
|
-
const name = prop.slice(ns.length + 3);
|
|
203
|
-
let n = 0;
|
|
204
|
-
for (const [word, c] of classWords) if (word.endsWith(`-${name}`) && word !== name) n += c;
|
|
205
|
-
return n;
|
|
188
|
+
utilityCount.set(prop, ns ? (bySuffix.get(prop.slice(ns.length + 3)) ?? 0) : 0);
|
|
206
189
|
}
|
|
190
|
+
const utilityUses = (prop) => utilityCount.get(prop) ?? 0;
|
|
207
191
|
const uses = (prop) => (propUses.get(prop) ?? 0) + utilityUses(prop);
|
|
208
192
|
|
|
209
193
|
/* ── Stack ────────────────────────────────────────────────────────────────── */
|
|
@@ -80,9 +80,9 @@ Copy this shape. Drop any table or row that doesn't apply; add project roles und
|
|
|
80
80
|
|
|
81
81
|
| Role | Name here | Value |
|
|
82
82
|
|---|---|---|
|
|
83
|
-
| `z.popover` | `z-popover` |
|
|
84
|
-
| `z.overlay` | `z-overlay` |
|
|
85
|
-
| `z.toast` | `z-toast` |
|
|
83
|
+
| `z.popover` | `z-popover` | 20 |
|
|
84
|
+
| `z.overlay` | `z-overlay` | 60 |
|
|
85
|
+
| `z.toast` | `z-toast` | 70 |
|
|
86
86
|
|
|
87
87
|
**Breakpoints**
|
|
88
88
|
|
|
@@ -123,7 +123,7 @@ Plus springs by character, and the reduced-motion policy in one sentence.
|
|
|
123
123
|
## Decisions
|
|
124
124
|
|
|
125
125
|
- YYYY-MM-DD — Card titles at 500, not 600: they read heavy beside the pane title. (User decision; the type guide dial.)
|
|
126
|
-
- YYYY-MM-DD — Three weights, not two: the billing
|
|
126
|
+
- YYYY-MM-DD — Three weights, not two: the billing screens need a 600 heading weight above the 500 that lifts lines inside rows. (Client feedback; the type guide dial override.)
|
|
127
127
|
- YYYY-MM-DD — Page on white `surface.page`, not the grey `bg-neutral-50`: the text tiers failed against the grey. (Law; the colour guide.)
|
|
128
128
|
|
|
129
129
|
## Open issues
|
|
@@ -33,7 +33,7 @@ Five places projects have quietly broken a law:
|
|
|
33
33
|
- **One token doing two jobs.** A grey used for borders and text: a border needs 3:1, text 4.5:1, and one value rarely serves both. Record the coupling as an open issue; don't split it yet.
|
|
34
34
|
- **A job with no token.** Captions at six greys, durations typed inline. Propose a name in the project's style (`design-md.md` says how to detect it) and mark it `proposed`. Where a whole domain has no tokens — no type tokens, no motion tokens — record that as one open issue, not one per role; that domain's refining pass proposes a full set in the project's style, marked `proposed`.
|
|
35
35
|
- **Roles the list doesn't have.** A `keyline`, a `pull-quote`, a `kicker`: record each as a project role under its nearest canonical parent, with what separates it — size, weight, leading, colour or a rule. If nothing does, that's a finding.
|
|
36
|
-
- **Boxes as the default.** Every section, stat and list in a card, cards inside cards, a bento grid of unlike sections, tinted strips behind labels, uppercase eyebrows, several chip shapes, a second hue that calls for attention (a hue family answering what, where or who under `product` is not one), success colour on done steps where completion isn't the news (a sequence the reader passes through, not a board or queue). Count the boxes per main screen and list each that holds more than one object (depth law 2, layout law 3). If the user has a reference interface they've already approved, screenshot each main screen beside it; the gaps are open issues.
|
|
36
|
+
- **Boxes as the default.** Every section, stat and list in a card, cards inside cards, a bento grid of unlike sections, tinted strips behind labels, uppercase eyebrows (outside an `expressive` eyebrow role), several chip shapes, a second hue that calls for attention (a hue family answering what, where or who under `product` is not one), success colour on done steps where completion isn't the news (a sequence the reader passes through, not a board or queue). Count the boxes per main screen and list each that holds more than one object (depth law 2, layout law 3); a panel that layout law 3's table gives a settings or form group is not one. If the user has a reference interface they've already approved, screenshot each main screen beside it; the gaps are open issues.
|
|
37
37
|
- **Values on no ladder.** Twenty-three font sizes, fourteen radii. Cluster them by usage, propose the ladder, list the strays that could snap to a neighbour unseen.
|
|
38
38
|
|
|
39
39
|
Compute contrast; don't estimate it:
|
|
@@ -46,7 +46,7 @@ node <keyline>/scripts/contrast.mjs --ladder "#ffffff" "#16161a" "#4b4b52" "#6b6
|
|
|
46
46
|
|
|
47
47
|
## 3. Choose the preset
|
|
48
48
|
|
|
49
|
-
From what the reader is mostly doing — reading, working, or being persuaded — propose `editorial`, `product` or `expressive` (`presets.md`). Offer `glass` only when the user wants modimihir.com's exact system rather than their own; it fills the dials from `glass
|
|
49
|
+
From what the reader is mostly doing — reading, working, or being persuaded — propose `editorial`, `product` or `expressive` (`presets.md`). Offer `glass` only when the user wants modimihir.com's exact system rather than their own; it fills the dials from the tables in each `glass/<domain>/guide.md`, under the project's existing names wherever it has them.
|
|
50
50
|
|
|
51
51
|
State the evidence: *"Mostly long-form pages under `app/blog`, two weights in use, one accent — this reads as `editorial`."* Mixed projects get a base preset plus areas: `product`, with `app/(marketing)/** → expressive`.
|
|
52
52
|
|
|
@@ -70,20 +70,16 @@ Don't ask what you can find out, and don't ask for approval of the whole mapping
|
|
|
70
70
|
Follow `system/design-md.md`: the project's names in `Name here`, canonical roles in `Role`, values as the source states them, measured contrast for every text tier, per-theme columns only where themes exist, nothing for roles the project lacks. Then:
|
|
71
71
|
|
|
72
72
|
- **`## Decisions`** — every dial set differently from the preset, with its reason, or a question mark if unknown.
|
|
73
|
-
- **`## Open issues`** — every law the code breaks, one line each with evidence (a ratio, a count, a file).
|
|
73
|
+
- **`## Open issues`** — every law the code breaks, one line each with evidence (a ratio, a count, a file). Cite each law by the right number (`commands/init.md`). **Don't fix anything** unless asked; a setup run that also rewrites forty components can't be reviewed.
|
|
74
74
|
|
|
75
75
|
Tell the user briefly: the preset and why, how many roles mapped cleanly, what you proposed names for, and the three most important open issues.
|
|
76
76
|
|
|
77
77
|
## 6. Generating tokens, only when asked
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Apart from the `glass` preset's drop-in files under `glass/<domain>/assets/`, the set ships no token files, because the right file depends on the stack. Asked for tokens — for a new project, or to fill `proposed` rows — generate them **in the project's existing mechanism**: a Tailwind v4 `@theme` block, a `tailwind.config` extension, CSS custom properties, a JS theme object, a design-tokens JSON. Match the existing layout and naming; never add a second token system beside the first.
|
|
80
80
|
|
|
81
81
|
With no tokens yet, `presets.md` and each domain guide's `## Dials` section give defaults. Say they're defaults, choose names with the user, and record the choices in `DESIGN.md`.
|
|
82
82
|
|
|
83
|
-
## How the other commands use this file
|
|
84
|
-
|
|
85
|
-
Each domain command reads `DESIGN.md`, finds its section, and works in the project's names. If the file is missing, it runs steps 1–2 for its own domain, does the task and recommends `/keyline init` — it doesn't refuse. A decision that changes the system — a new role, a moved dial, a law knowingly broken, a change a law forced that a later session might undo — goes back under `Decisions` — dated, marked user decision, client feedback or law, newest first, replacing what it supersedes — which is what keeps the file true after the first day.
|
|
86
|
-
|
|
87
83
|
## Checklist
|
|
88
84
|
|
|
89
85
|
- [ ] An existing `DESIGN.md` was reconciled, not overwritten; every user-chosen name and decision survived.
|
|
@@ -25,34 +25,34 @@ When the evidence doesn't settle it, propose one, say what evidence you used, an
|
|
|
25
25
|
|
|
26
26
|
*The interface should disappear so the reading doesn't.* Restraint is the point, and a single loud moment reads as craft because nothing else competes with it.
|
|
27
27
|
|
|
28
|
-
- **Type** — a dense ladder that keeps the reading loudest
|
|
29
|
-
- **Colour** — near-monochrome
|
|
30
|
-
- **Layout** — one reading measure every page shares
|
|
31
|
-
- **Surfaces** — mostly flat
|
|
32
|
-
- **Motion** — short, and only where it makes a change legible.
|
|
28
|
+
- **Type** — a dense ladder that keeps the reading loudest.
|
|
29
|
+
- **Colour** — near-monochrome, the accent spent on focus.
|
|
30
|
+
- **Layout** — one reading measure every page shares.
|
|
31
|
+
- **Surfaces** — mostly flat; translucency, if any, only on chrome.
|
|
32
|
+
- **Motion** — short, and only where it makes a change legible.
|
|
33
33
|
|
|
34
34
|
## `product`
|
|
35
35
|
|
|
36
36
|
*The interface is a tool, and the reader is fluent in it.* Density, predictability and scannability beat atmosphere. Signal has to survive a screen full of data.
|
|
37
37
|
|
|
38
|
-
- **Type** — tighter, smaller steps; UI and data roles dominate.
|
|
39
|
-
- **Colour** — near-monochrome
|
|
40
|
-
- **Layout** — an app shell rather than a single measure
|
|
41
|
-
- **Surfaces** — content on the page, rows parted by hairlines; a
|
|
42
|
-
- **Motion** — shortest of the three
|
|
38
|
+
- **Type** — tighter, smaller steps; UI and data roles dominate.
|
|
39
|
+
- **Colour** — near-monochrome with a real status palette, and one accent hue that acts.
|
|
40
|
+
- **Layout** — an app shell rather than a single measure; tighter spacing, still grouped.
|
|
41
|
+
- **Surfaces** — content on the page, rows parted by hairlines; a card only for one object.
|
|
42
|
+
- **Motion** — shortest of the three; nothing delays a repeated action.
|
|
43
43
|
|
|
44
44
|
## `expressive`
|
|
45
45
|
|
|
46
46
|
*The interface is making an impression, usually once.* Scale, colour and motion are allowed to carry brand — but still by rule, or it turns into a template.
|
|
47
47
|
|
|
48
|
-
- **Type** — a real display tier far above body, possibly a second family
|
|
49
|
-
- **Colour** — a brand palette used broadly,
|
|
50
|
-
- **Layout** — sections may
|
|
51
|
-
- **Surfaces** — depth, gradients and translucency
|
|
52
|
-
- **Motion** — longer and more characterful on entrances; exits still get out of the way.
|
|
48
|
+
- **Type** — a real display tier far above body, possibly a second family.
|
|
49
|
+
- **Colour** — a brand palette used broadly, each colour placed by rule.
|
|
50
|
+
- **Layout** — sections may go full-bleed; paragraphs keep the measure.
|
|
51
|
+
- **Surfaces** — depth, gradients and translucency as brand material, each with its fallback.
|
|
52
|
+
- **Motion** — longer and more characterful on entrances; exits still get out of the way.
|
|
53
53
|
|
|
54
54
|
## `glass`
|
|
55
55
|
|
|
56
|
-
The Glass preset's exact values: `editorial` in character, with every dial already decided on [modimihir.com](https://modimihir.com). Offer it only when the user wants that system itself rather than their own — `/keyline init` copies its values from `glass
|
|
56
|
+
The Glass preset's exact values: `editorial` in character, with every dial already decided on [modimihir.com](https://modimihir.com). Offer it only when the user wants that system itself rather than their own — `/keyline init` copies its values from the tables in each `glass/<domain>/guide.md` into `DESIGN.md`, in the project's own names wherever the project already has names, and each guide supplies the exact recipes, with drop-in token files and components under its `assets/`.
|
|
57
57
|
|
|
58
58
|
A `glass` project can still override a dial. It should expect to: the Glass values were chosen for one text-heavy portfolio, and a project that isn't one will find the places they don't fit.
|