@motion-proto/live-tokens 0.78.0 → 0.80.0
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/skills/live-tokens-check-compliance/SKILL.md +49 -38
- package/.claude/skills/live-tokens-create-component/SKILL.md +23 -28
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +61 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -15
- package/.claude/skills/live-tokens-create-page/SKILL.md +6 -4
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +1 -1
- package/CHANGELOG.md +271 -0
- package/README.md +10 -16
- package/bin/check-component.mjs +154 -551
- package/bin/check-page.mjs +57 -541
- package/bin/cli.mjs +93 -16
- package/bin/contractRunner.mjs +37 -91
- package/bin/lib/buildChecks.mjs +32 -0
- package/bin/lib/catalogue.mjs +161 -34
- package/bin/lib/componentSource.mjs +152 -0
- package/bin/lib/cssValues.mjs +9 -0
- package/bin/lib/dataDir.mjs +126 -0
- package/bin/lib/findings.mjs +103 -12
- package/bin/lib/fixers.mjs +64 -0
- package/bin/lib/geometry.mjs +92 -0
- package/bin/lib/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- package/bin/migrate-build-script.mjs +66 -0
- package/bin/migrate.mjs +5 -0
- package/bin/rules/componentStructure.mjs +344 -0
- package/bin/rules/componentUse.mjs +313 -0
- package/bin/rules/importsAndRoutes.mjs +136 -0
- package/bin/rules/testRuns.mjs +122 -0
- package/bin/rules/tokens.mjs +426 -0
- package/bin/setup-claude.mjs +1 -2
- package/dist-plugin/{chunk-PDNL4NC5.js → chunk-D4WRIKEZ.js} +7 -2
- package/dist-plugin/{chunk-SWXRVZKT.js → chunk-REBHE3ZM.js} +414 -1
- package/dist-plugin/index.cjs +470 -19
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +87 -55
- package/dist-plugin/migrateData/index.cjs +422 -4
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +414 -1
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +423 -13
- package/dist-plugin/setGeometry/index.d.cts +3 -3
- package/dist-plugin/setGeometry/index.d.ts +3 -3
- package/dist-plugin/setGeometry/index.js +10 -13
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-BxRtuN5V.d.cts → themeTypes-B8_Idrp4.d.cts} +2 -2
- package/dist-plugin/{themeTypes-BxRtuN5V.d.ts → themeTypes-B8_Idrp4.d.ts} +2 -2
- package/package.json +2 -2
- package/src/editor/component-editor/CalloutEditor.svelte +2 -2
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +14 -15
- package/src/editor/component-editor/CornerBadgeEditor.svelte +14 -14
- package/src/editor/component-editor/DialogEditor.svelte +5 -5
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +2 -2
- package/src/editor/component-editor/RadioButtonEditor.svelte +21 -21
- package/src/editor/component-editor/SectionDividerEditor.svelte +7 -7
- package/src/editor/component-editor/SegmentedControlEditor.svelte +5 -5
- package/src/editor/component-editor/SideNavigationEditor.svelte +25 -25
- package/src/editor/component-editor/TabBarEditor.svelte +6 -6
- package/src/editor/component-editor/TableEditor.svelte +6 -6
- package/src/editor/component-editor/ToggleEditor.svelte +2 -2
- package/src/editor/component-editor/index.ts +3 -0
- package/src/editor/component-editor/registry.ts +57 -1
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +14 -14
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +3 -0
- package/src/editor/component-editor/scaffolding/types.ts +15 -0
- package/src/editor/core/components/adjustAliases.ts +4 -4
- package/src/editor/core/components/aliasKinds.ts +20 -19
- package/src/editor/core/sketch/sketchLayer.ts +3 -3
- package/src/editor/core/themes/migrateComponentConfig.ts +14 -6
- package/src/editor/core/themes/migrations/2026-09-13-badge-brand.ts +42 -0
- package/src/editor/core/themes/migrations/2026-09-13-collapsiblesection-open.ts +33 -0
- package/src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts +70 -0
- package/src/editor/core/themes/migrations/2026-09-13-hairline.ts +92 -0
- package/src/editor/core/themes/migrations/2026-09-13-indicator.ts +54 -0
- package/src/editor/core/themes/migrations/2026-09-13-sectiondivider-surface.ts +36 -0
- package/src/editor/core/themes/migrations/2026-09-13-selected-state.ts +124 -0
- package/src/editor/core/themes/migrations/2026-09-13-toggle-label.ts +31 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/themeService.ts +3 -3
- package/src/editor/core/themes/themeTypes.ts +13 -15
- package/src/editor/docs/Docs.svelte +1 -1
- package/src/editor/docs/content/light-and-dark.md +3 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/index.ts +1 -0
- package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +4 -4
- package/src/editor/skill-atlas/TreeNodeCard.svelte +4 -4
- package/src/editor/skill-atlas/skillSources.generated.ts +11 -14
- package/src/editor/skill-atlas/skillTrees.ts +1 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +37 -149
- package/src/editor/skill-atlas/trees/create-component.ts +62 -64
- package/src/editor/skill-atlas/trees/create-page.ts +38 -17
- package/src/editor/skill-atlas/trees/pick-component.ts +3 -3
- package/src/editor/skill-atlas/trees/set-geometry.ts +1 -1
- package/src/editor/ui/UIPaletteSelector.svelte +11 -15
- package/src/editor/ui/variantScales.ts +8 -8
- package/src/live-tokens/data/themes/autumn.json +188 -188
- package/src/live-tokens/data/themes/halloween.json +188 -188
- package/src/live-tokens/data/themes/midnight-study.json +245 -245
- package/src/live-tokens/data/themes/ocean.json +188 -188
- package/src/live-tokens/data/themes/royal-velvet.json +188 -188
- package/src/live-tokens/data/themes/sketchy.json +188 -188
- package/src/live-tokens/data/themes/spring-meadow.json +188 -188
- package/src/live-tokens/data/themes/sunset.json +188 -188
- package/src/system/components/Badge.svelte +27 -23
- package/src/system/components/Button.svelte +13 -7
- package/src/system/components/Callout.svelte +16 -11
- package/src/system/components/Card.svelte +22 -15
- package/src/system/components/CodeSnippet.svelte +10 -6
- package/src/system/components/CollapsibleSection.svelte +81 -76
- package/src/system/components/CornerBadge.svelte +78 -72
- package/src/system/components/Dialog.svelte +21 -18
- package/src/system/components/IconButton.svelte +13 -9
- package/src/system/components/Image.svelte +14 -8
- package/src/system/components/ImageLightbox.svelte +10 -6
- package/src/system/components/InlineEditActions.svelte +17 -14
- package/src/system/components/Input.svelte +13 -7
- package/src/system/components/MenuSelect.svelte +13 -7
- package/src/system/components/Notification.svelte +17 -11
- package/src/system/components/Panel.svelte +13 -6
- package/src/system/components/ProgressBar.svelte +10 -5
- package/src/system/components/RadioButton.svelte +33 -30
- package/src/system/components/SectionDivider.svelte +28 -21
- package/src/system/components/SegmentedControl.svelte +27 -23
- package/src/system/components/SideNavigation.svelte +175 -171
- package/src/system/components/Slider.svelte +11 -7
- package/src/system/components/TabBar.svelte +53 -49
- package/src/system/components/Table.svelte +20 -15
- package/src/system/components/Toggle.svelte +14 -10
- package/src/system/components/Tooltip.svelte +10 -6
- package/src/system/styles/CONVENTIONS.md +3 -4
- package/src/testing-js/chunk-3UKGXCDL.js +48 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +1 -0
- package/src/testing-js/{chunk-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
- package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
- package/src/testing-js/{chunk-GNIUPIU2.js → chunk-U7OJE5DU.js} +18 -18
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- package/src/testing-js/{chunk-4JQX6WWL.js → chunk-ZMZQZ33J.js} +474 -166
- package/src/testing-js/chunk-ZMZQZ33J.js.map +1 -0
- package/src/testing-js/component-behavior.contract.js +155 -0
- package/src/testing-js/component-behavior.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +6 -4
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +14 -10
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -4
- package/src/testing-js/index.js +13 -7
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +48 -10
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-C-wNWcoA.d.ts → vitest-BMLIbDs2.d.ts} +8 -2
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +2 -1
- package/template/package.json +1 -2
- package/template/vite.config.ts +3 -2
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -105
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -504
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-GNIUPIU2.js.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { basename, relative } from 'node:path';
|
|
2
|
+
import { deepImportRepair } from '../lib/catalogue.mjs';
|
|
3
|
+
import { lineOf } from '../lib/findings.mjs';
|
|
4
|
+
|
|
5
|
+
const shared = {
|
|
6
|
+
'deep-import': {
|
|
7
|
+
severity: 'error',
|
|
8
|
+
repair: 'auto',
|
|
9
|
+
guidance:
|
|
10
|
+
'A run without --no-fix rewrites a /src/system/components/<Name>.svelte specifier to the public /components/<Name>.svelte. Import any other deep specifier from a public subpath, which `details.exports` lists.',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const pageRules = {
|
|
15
|
+
'reserved-route': {
|
|
16
|
+
severity: 'error',
|
|
17
|
+
repair: 'authored',
|
|
18
|
+
guidance: 'Move the route out of /live-tokens/*.',
|
|
19
|
+
},
|
|
20
|
+
'site-css-in-main': {
|
|
21
|
+
severity: 'error',
|
|
22
|
+
repair: 'authored',
|
|
23
|
+
guidance:
|
|
24
|
+
"Delete the import from main.ts, and add it to each page's <script>. Page CSS then stays off the editor routes.",
|
|
25
|
+
},
|
|
26
|
+
'missing-source': {
|
|
27
|
+
severity: 'warn',
|
|
28
|
+
repair: 'authored',
|
|
29
|
+
guidance: "Add source: 'src/...' to the route entry.",
|
|
30
|
+
},
|
|
31
|
+
...shared,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const componentRules = {
|
|
35
|
+
...shared,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Deep imports into the package internals are not a supported API.
|
|
39
|
+
const DEEP_IMPORT_PATTERNS = [
|
|
40
|
+
/^@motion-proto\/live-tokens\/src\//,
|
|
41
|
+
/node_modules\/@motion-proto\/live-tokens/,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** `deep-import` for one import statement in a page. */
|
|
45
|
+
export function checkPageImport({ index, specifier }, add) {
|
|
46
|
+
for (const pattern of DEEP_IMPORT_PATTERNS) {
|
|
47
|
+
if (pattern.test(specifier)) {
|
|
48
|
+
add('deep-import', index, `deep import into package internals: ${specifier}`, deepImportRepair(specifier));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `reserved-route`, then `missing-source`, then `site-css-in-main`, over a page's script and markup. */
|
|
54
|
+
export function checkRoutes({ file, code }, add) {
|
|
55
|
+
for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
|
|
56
|
+
add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const m of code.matchAll(/\blazy\s*:/g)) {
|
|
60
|
+
const entry = enclosingObject(code, m.index);
|
|
61
|
+
if (entry && !/\bsource\s*:/.test(entry)) {
|
|
62
|
+
add('missing-source', m.index, `route entry has no 'source', so Page Source cannot open it`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (/^main\.(ts|js)$/.test(basename(file))) {
|
|
67
|
+
for (const m of code.matchAll(/import\s+['"]([^'"]*site\.css)['"]/g)) {
|
|
68
|
+
add(
|
|
69
|
+
'site-css-in-main',
|
|
70
|
+
m.index,
|
|
71
|
+
`site.css imported from main; import it from each page's <script> so it cannot leak into editor routes`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The object literal enclosing `index`, found by balancing braces outward. */
|
|
78
|
+
function enclosingObject(text, index) {
|
|
79
|
+
let depth = 0;
|
|
80
|
+
let start = -1;
|
|
81
|
+
for (let i = index; i >= 0; i--) {
|
|
82
|
+
const c = text[i];
|
|
83
|
+
if (c === '}') depth++;
|
|
84
|
+
else if (c === '{') {
|
|
85
|
+
if (depth === 0) {
|
|
86
|
+
start = i;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
depth--;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (start === -1) return null;
|
|
93
|
+
depth = 0;
|
|
94
|
+
for (let i = start; i < text.length; i++) {
|
|
95
|
+
const c = text[i];
|
|
96
|
+
if (c === '{') depth++;
|
|
97
|
+
else if (c === '}') {
|
|
98
|
+
depth--;
|
|
99
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractImports(source) {
|
|
106
|
+
const out = [];
|
|
107
|
+
const re = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = re.exec(source)) !== null) {
|
|
110
|
+
out.push({ specifier: m[1], index: m.index + m[0].length - 1 - m[1].length });
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `deep-import` for each `[path, text]` pair a component owns: its runtime,
|
|
117
|
+
* its editor, or the file that registers it. Each line resolves against that
|
|
118
|
+
* file's own text.
|
|
119
|
+
*/
|
|
120
|
+
export function checkComponentImports(root, files, recordAt) {
|
|
121
|
+
for (const [path, text] of files) {
|
|
122
|
+
for (const { specifier, index } of extractImports(text)) {
|
|
123
|
+
for (const pattern of DEEP_IMPORT_PATTERNS) {
|
|
124
|
+
if (pattern.test(specifier)) {
|
|
125
|
+
recordAt(
|
|
126
|
+
'deep-import',
|
|
127
|
+
`${relative(root, path)}: deep import not supported: ${specifier}`,
|
|
128
|
+
relative(root, path),
|
|
129
|
+
lineOf(text, index),
|
|
130
|
+
deepImportRepair(specifier),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { COLOR_BY_ROLE } from './tokens.mjs';
|
|
2
|
+
|
|
3
|
+
// Detection lives in bin/contractRunner.mjs: `runContractTests` for the
|
|
4
|
+
// component rules, `runPageTests` for the page rules. The ids are fixed by
|
|
5
|
+
// design decisions 8 and 9. Every one is an error, including the setup rules,
|
|
6
|
+
// which `--tests` treats as never-silenceable (see cli.mjs). A failed
|
|
7
|
+
// obligation is always authored: the component has to start behaving.
|
|
8
|
+
|
|
9
|
+
const RERUN =
|
|
10
|
+
'Rerun the same command with --tests until every applicable rule passes with no rule left --off.';
|
|
11
|
+
|
|
12
|
+
const CREATE_COMPONENT = 'live-tokens-create-component';
|
|
13
|
+
|
|
14
|
+
const tooling = {
|
|
15
|
+
'tests-not-installed': {
|
|
16
|
+
severity: 'error',
|
|
17
|
+
repair: 'authored',
|
|
18
|
+
guidance: `The run itself failed because a tool is missing. Install the named package, @playwright/test, vitest, or happy-dom, as a devDependency, then run \`npx playwright install chromium\` for a missing browser. ${RERUN}`,
|
|
19
|
+
},
|
|
20
|
+
'tests-setup': {
|
|
21
|
+
severity: 'error',
|
|
22
|
+
repair: 'authored',
|
|
23
|
+
guidance: `The run itself failed before it reported. Fix the tool, the path, or the config the message names. ${RERUN}`,
|
|
24
|
+
},
|
|
25
|
+
'tests-incomplete': {
|
|
26
|
+
severity: 'error',
|
|
27
|
+
repair: 'authored',
|
|
28
|
+
guidance: `An obligation never ran to a result. Add the missing contract, or find from the message why the suite skipped it. ${RERUN}`,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const componentRules = {
|
|
33
|
+
'contract-registry': {
|
|
34
|
+
severity: 'error',
|
|
35
|
+
repair: 'authored',
|
|
36
|
+
guidance: `The registry contract failed. Register the component in the shared module the Registration section of ${CREATE_COMPONENT} wires up, importable by the app and by check-component --tests.`,
|
|
37
|
+
},
|
|
38
|
+
'contract-behavior': {
|
|
39
|
+
severity: 'error',
|
|
40
|
+
repair: 'authored',
|
|
41
|
+
guidance: `A declared case in the component's contract failed under Vitest: a callback that never fired, fired with the wrong argument, or fired when the case said it should stay silent. Wire the runtime as the recipe in ${CREATE_COMPONENT} wires it.`,
|
|
42
|
+
},
|
|
43
|
+
'contract-render': {
|
|
44
|
+
severity: 'error',
|
|
45
|
+
repair: 'authored',
|
|
46
|
+
guidance: `A shipped alias never wrote through its editor control, or the editor targets the wrong part. Fix the editor schema, states, or preview props by the Component editor section of ${CREATE_COMPONENT}.`,
|
|
47
|
+
},
|
|
48
|
+
'contract-alias': {
|
|
49
|
+
severity: 'error',
|
|
50
|
+
repair: 'authored',
|
|
51
|
+
guidance: `The editor has to declare every part and every shipped alias, and each alias it paints with has to resolve at the root. Fix the editor schema by the Component editor section of ${CREATE_COMPONENT}, and point a broken alias at a design token.`,
|
|
52
|
+
},
|
|
53
|
+
'contract-persist': {
|
|
54
|
+
severity: 'error',
|
|
55
|
+
repair: 'authored',
|
|
56
|
+
guidance: `An edit failed to persist, or reset failed to restore the saved config. Make the :global(:root) default the value Reset should restore, per the Runtime component section of ${CREATE_COMPONENT}.`,
|
|
57
|
+
},
|
|
58
|
+
'contract-theme': {
|
|
59
|
+
severity: 'error',
|
|
60
|
+
repair: 'authored',
|
|
61
|
+
guidance: `The component failed to take the theme's values and give them back. Make each :global(:root) default read a design token, composed when needed, and declare a structural keyword, such as start, in the editor's \`intrinsics\`.`,
|
|
62
|
+
},
|
|
63
|
+
'contract-states': {
|
|
64
|
+
severity: 'error',
|
|
65
|
+
repair: 'authored',
|
|
66
|
+
guidance: `The editor preview failed to show the state being edited. Fix the editor states or preview props by the Component editor section of ${CREATE_COMPONENT}.`,
|
|
67
|
+
},
|
|
68
|
+
'contract-interaction': {
|
|
69
|
+
severity: 'error',
|
|
70
|
+
repair: 'authored',
|
|
71
|
+
guidance: `The component failed to answer the pointer or the keyboard in the editor. Fix the editor schema, states, or preview props by the Component editor section of ${CREATE_COMPONENT}.`,
|
|
72
|
+
},
|
|
73
|
+
'contract-listed': {
|
|
74
|
+
severity: 'error',
|
|
75
|
+
repair: 'authored',
|
|
76
|
+
guidance: `The component is missing from its registry group. Register it in the shared module the Registration section of ${CREATE_COMPONENT} wires up, importable by the app and by check-component --tests.`,
|
|
77
|
+
},
|
|
78
|
+
'contract-sketch': {
|
|
79
|
+
severity: 'error',
|
|
80
|
+
repair: 'authored',
|
|
81
|
+
guidance: `Add the missing Sketch part or marker, per the Sketch mode and overlays section of ${CREATE_COMPONENT}.`,
|
|
82
|
+
},
|
|
83
|
+
'contract-missing': {
|
|
84
|
+
severity: 'error',
|
|
85
|
+
repair: 'authored',
|
|
86
|
+
guidance: `Add a ComponentContract for the component to the module contractsModule names, as references/contract-tests.md in ${CREATE_COMPONENT} shows. ${RERUN}`,
|
|
87
|
+
},
|
|
88
|
+
...tooling,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const pageRules = {
|
|
92
|
+
'page-component-paint': {
|
|
93
|
+
severity: 'error',
|
|
94
|
+
repair: 'authored',
|
|
95
|
+
guidance:
|
|
96
|
+
"The finding names the page file and the line of the instance whose part painted a value its semantic property never resolves to. Remove the global rule that reaches past the component, from site.css or the page's own CSS, and retune the component's semantic property for the whole project at /live-tokens/components.",
|
|
97
|
+
},
|
|
98
|
+
'page-text-style': {
|
|
99
|
+
severity: 'error',
|
|
100
|
+
repair: 'authored',
|
|
101
|
+
guidance:
|
|
102
|
+
'The finding names the page file, the line of the text element, and the nearest bundle it missed. Set the text style on the text element itself, from one shipped bundle: heading, body, editorial, or code, each printed by `npx live-tokens tokens --scale <name>`. An ancestor typed for a different role hands down the wrong style.',
|
|
103
|
+
},
|
|
104
|
+
'page-contrast': {
|
|
105
|
+
severity: 'error',
|
|
106
|
+
repair: 'authored',
|
|
107
|
+
guidance: `The finding names the page file, the line of the text element, the surface ancestor, and both computed colors. Pick the text token the surface pairs with. ${COLOR_BY_ROLE}`,
|
|
108
|
+
},
|
|
109
|
+
'page-grid': {
|
|
110
|
+
severity: 'error',
|
|
111
|
+
repair: 'authored',
|
|
112
|
+
guidance:
|
|
113
|
+
'The finding names the page file and the line of the section whose edge sits off a column line. Move the edge onto the line: place it by page-column numbers per the Grid section of live-tokens-create-page, and center a section with symmetric insets. Keep its margin, width, and transform out of the centering.',
|
|
114
|
+
},
|
|
115
|
+
'page-overflow': {
|
|
116
|
+
severity: 'error',
|
|
117
|
+
repair: 'authored',
|
|
118
|
+
guidance:
|
|
119
|
+
'The finding names the page file and the line of the element or the instance that overflows. Give the control its shipped width, remove a fixed width wider than its column at the viewport, and set overflow-x: auto only on an element meant to scroll, such as a code block. When the page grid itself overflows at a phone width, collapse it to grid-template-columns: 1fr and column-gap: 0, per the Grid section of live-tokens-create-page.',
|
|
120
|
+
},
|
|
121
|
+
...tooling,
|
|
122
|
+
};
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import { loadBuiltEngine, resolveTokensCssPath } from '../migrate.mjs';
|
|
4
|
+
import { scaleTokens } from '../lib/catalogue.mjs';
|
|
5
|
+
import { COLOR_SCALES, GEOMETRY_SCALES, STATE_TOKENS, readKindRules, tokenScale, unreadTokens } from '../lib/componentSource.mjs';
|
|
6
|
+
import { colorScaleOfProperty, hasColorLiteral, hasDimensionLiteral, stripVarFallbacks } from '../lib/cssValues.mjs';
|
|
7
|
+
import { readComponentConfig } from '../lib/dataDir.mjs';
|
|
8
|
+
import { findJsonKeyLine, isExcluded } from '../lib/findings.mjs';
|
|
9
|
+
import { geometryScaleOfProperty, resolveGeometryLiteral } from '../lib/geometry.mjs';
|
|
10
|
+
import { declarationPatch, declarations, neutralise, pageDeclaredNames } from '../lib/pageSource.mjs';
|
|
11
|
+
import { isContractToken } from '../lib/tokenVocabulary.mjs';
|
|
12
|
+
|
|
13
|
+
export const COLOR_BY_ROLE =
|
|
14
|
+
'Pick the color token by the role the color plays, and the theme moves every role together. ' +
|
|
15
|
+
'Text on a surface takes --text-primary through --text-disabled, the neutral text scale, or --text-<family> for a family color. ' +
|
|
16
|
+
'Light text on a dark chip takes --text-inverted, which carries no AA guarantee. ' +
|
|
17
|
+
'A surface fill takes --surface-<family>-<level>, where the role names the family: neutral for chrome, brand for emphasis, danger for status. ' +
|
|
18
|
+
'A stroke takes --border-<family>-<level>, with levels from faint to strong. ' +
|
|
19
|
+
'A translucent layer that dims what is behind it, such as the layer behind a modal, takes --scrim-low, --scrim, or --scrim-high. ' +
|
|
20
|
+
'A translucent wash on a surface, such as a hover state, takes --tint-low, --tint, or --tint-high. ' +
|
|
21
|
+
"Any other translucent color takes the role's token at an opacity, color-mix(in srgb, var(--surface-brand) 80%, transparent), the form the editor reads. " +
|
|
22
|
+
'A fully transparent color takes --color-transparent. ' +
|
|
23
|
+
'A gradient takes a --gradient-* token, or one composed from surface tokens. ' +
|
|
24
|
+
"`npx live-tokens tokens --scale <name>` prints a scale's names and values, with --json for data.";
|
|
25
|
+
|
|
26
|
+
export const GEOMETRY_BY_SCALE =
|
|
27
|
+
'Pick the geometry token from its scale. ' +
|
|
28
|
+
'Spacing takes the nearest --space-<px> step, and `npx live-tokens tokens --scale space` prints the steps. ' +
|
|
29
|
+
'A stroke width, an outline included, takes --border-width-1, --border-width-2, or --border-width-4. ' +
|
|
30
|
+
'A corner takes --radius-sm through --radius-4xl, or --radius-full. ' +
|
|
31
|
+
'A shadow takes --shadow-sm through --shadow-xl in place of the whole value. ' +
|
|
32
|
+
'Part of a calc() takes the token inside the calc, such as calc(var(--space-64) * -2 + var(--space-8)). ' +
|
|
33
|
+
'A duration or easing takes --duration-* or --ease-*, and a blur() takes --blur-*. No rule reports those three, so fix them while in the file.';
|
|
34
|
+
|
|
35
|
+
const COMPONENT_DEFAULT = 'In a component, make the :global(:root) default read the token, composed when needed.';
|
|
36
|
+
|
|
37
|
+
const INTRINSIC = "Declare a structural keyword, such as start, in the editor's `intrinsics`.";
|
|
38
|
+
|
|
39
|
+
const shared = {
|
|
40
|
+
'tokens-migration': {
|
|
41
|
+
severity: 'error',
|
|
42
|
+
repair: 'auto',
|
|
43
|
+
guidance:
|
|
44
|
+
'A run without --no-fix applies these migrations to tokens.css. Each one adds design tokens the installed package reads and changes no existing token. `details.migrations` names them.',
|
|
45
|
+
},
|
|
46
|
+
'tokens-breaking-migration': {
|
|
47
|
+
severity: 'error',
|
|
48
|
+
repair: 'choice',
|
|
49
|
+
guidance:
|
|
50
|
+
'Each migration in `details.migrations` renames, removes, or rewrites design tokens in tokens.css. Read the plan with `npx live-tokens migrate --check`, apply it with `npx live-tokens migrate`, and move any page or component that reads an old name to the new one. To keep tokens.css as it is, record the `exception`.',
|
|
51
|
+
},
|
|
52
|
+
'color-literal': {
|
|
53
|
+
severity: 'error',
|
|
54
|
+
repair: 'choice',
|
|
55
|
+
guidance: `Replace the literal with a design token. \`details.candidates\` lists the tokens on the scale the property reads. ${COLOR_BY_ROLE} ${COMPONENT_DEFAULT}`,
|
|
56
|
+
},
|
|
57
|
+
'dimension-literal': {
|
|
58
|
+
severity: 'warn',
|
|
59
|
+
repair: 'auto',
|
|
60
|
+
guidance:
|
|
61
|
+
'A run without --no-fix replaces each literal that has one nearest step. ' +
|
|
62
|
+
'A literal left over has no single nearest step, so pick by the visual weight the candidates in `details` show. ' +
|
|
63
|
+
`A size, such as a hero's height, is layout. Leave it. ${GEOMETRY_BY_SCALE} ${COMPONENT_DEFAULT}`,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const pageRules = {
|
|
68
|
+
'unknown-token': {
|
|
69
|
+
severity: 'error',
|
|
70
|
+
repair: 'choice',
|
|
71
|
+
guidance:
|
|
72
|
+
'Search tokens.css for the stem. When the name has the shape of a design token and no longer exists, `npx live-tokens migrate --check` lists the migration that adds the current name.',
|
|
73
|
+
},
|
|
74
|
+
'raw-text-axis': {
|
|
75
|
+
severity: 'error',
|
|
76
|
+
repair: 'choice',
|
|
77
|
+
guidance:
|
|
78
|
+
'Set every axis from one text style bundle, -font-family through -letter-spacing. The text styles are heading, body, editorial, and code, and `npx live-tokens tokens --scale heading` prints one of them. Rewrite a font: shorthand the same way.',
|
|
79
|
+
},
|
|
80
|
+
'hardcoded-columns': {
|
|
81
|
+
severity: 'warn',
|
|
82
|
+
repair: 'choice',
|
|
83
|
+
guidance:
|
|
84
|
+
'Use repeat(var(--columns-count), 1fr) for the page grid, and repeat(calc(var(--columns-count) - 2), 1fr) for a sub-grid that spans fewer columns. `details.candidates` holds both forms.',
|
|
85
|
+
},
|
|
86
|
+
...shared,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const componentRules = {
|
|
90
|
+
'unread-token': {
|
|
91
|
+
severity: 'warn',
|
|
92
|
+
repair: 'choice',
|
|
93
|
+
guidance:
|
|
94
|
+
"The message names the property the runtime declares in :global(:root) and never reads. Wire it into the runtime's CSS where it paints, as the Runtime component section of live-tokens-create-component wires a property, or delete the declaration when nothing should paint with it.",
|
|
95
|
+
},
|
|
96
|
+
'unknown-token-ref': {
|
|
97
|
+
severity: 'error',
|
|
98
|
+
repair: 'choice',
|
|
99
|
+
guidance:
|
|
100
|
+
"Point the default at a design token or one of the component's own semantic properties. A state is a segment of a property name, so replace a reference that starts with a state by the token that state should paint. When the name has the shape of a design token and no longer exists, search tokens.css for the rename, and `npx live-tokens migrate --check` lists the migration that adds it. " +
|
|
101
|
+
COMPONENT_DEFAULT,
|
|
102
|
+
},
|
|
103
|
+
'default-not-token': {
|
|
104
|
+
severity: 'error',
|
|
105
|
+
repair: 'choice',
|
|
106
|
+
guidance: `Make the :global(:root) default read a design token, composed when needed. ${INTRINSIC}`,
|
|
107
|
+
},
|
|
108
|
+
'config-token': {
|
|
109
|
+
severity: 'error',
|
|
110
|
+
repair: 'choice',
|
|
111
|
+
guidance:
|
|
112
|
+
"The message names the alias in component-configs/<id>/default.json that names something outside the vocabulary, or a bare literal on a property the editor declares no intrinsic for. Point the alias at a design token or one of the component's own semantic properties, or declare the intrinsic in the editor. " +
|
|
113
|
+
`${COLOR_BY_ROLE} ${GEOMETRY_BY_SCALE}`,
|
|
114
|
+
},
|
|
115
|
+
...shared,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The semantic half of the contract: a component token is a *property name*, and
|
|
120
|
+
* its default is the theme token that property reads. So every default must
|
|
121
|
+
* resolve to a real token — otherwise the component stops repainting when the
|
|
122
|
+
* theme changes, which is the whole point of declaring it.
|
|
123
|
+
*/
|
|
124
|
+
export function checkDefaultsAreSemantic({ blocks, runtime, root, runtimePath, vocab, intrinsic }, record) {
|
|
125
|
+
const rel = relative(root, runtimePath);
|
|
126
|
+
const kindRules = readKindRules(root);
|
|
127
|
+
|
|
128
|
+
const own = new Set();
|
|
129
|
+
for (const block of blocks) {
|
|
130
|
+
for (const m of block.matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gm)) own.add(m[1]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const block of blocks) {
|
|
134
|
+
for (const m of block.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
135
|
+
const [decl, name, raw] = m;
|
|
136
|
+
const value = raw.trim();
|
|
137
|
+
const at = runtime.indexOf(decl);
|
|
138
|
+
|
|
139
|
+
const painted = stripVarFallbacks(value);
|
|
140
|
+
const refs = [...painted.matchAll(/var\(\s*(--[a-z0-9-]+)/g)].map((x) => x[1]);
|
|
141
|
+
for (const ref of refs) {
|
|
142
|
+
if (own.has(ref) || vocab.knows(ref)) continue;
|
|
143
|
+
record(
|
|
144
|
+
'unknown-token-ref',
|
|
145
|
+
STATE_TOKENS.includes(ref.replace(/^--/, '').split('-')[0])
|
|
146
|
+
? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name. Read the token the state should paint`
|
|
147
|
+
: isContractToken(ref)
|
|
148
|
+
? `${rel}: ${name} reads ${ref}, which has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
149
|
+
: `${rel}: ${name} reads ${ref}, which is not a design token or a semantic property`,
|
|
150
|
+
at,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (hasColorLiteral(painted)) {
|
|
155
|
+
const scale = tokenScale(name, kindRules, COLOR_SCALES);
|
|
156
|
+
record(
|
|
157
|
+
'color-literal',
|
|
158
|
+
`${rel}: ${name}: ${value} is a colour literal; defaults must reference design tokens (e.g. var(--surface-primary))`,
|
|
159
|
+
at,
|
|
160
|
+
{ details: { scale, candidates: scaleTokens(vocab, scale).map((t) => t.name) } },
|
|
161
|
+
);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (refs.length === 0 && !intrinsic.some((re) => re.test(name))) {
|
|
166
|
+
record(
|
|
167
|
+
'default-not-token',
|
|
168
|
+
`${rel}: ${name}: ${value} has no design token behind it. Back it with a token, or declare it in the editor's \`intrinsics\` when it is a structural keyword`,
|
|
169
|
+
at,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (hasDimensionLiteral(painted)) {
|
|
174
|
+
const scale = tokenScale(name, kindRules, GEOMETRY_SCALES);
|
|
175
|
+
const resolved = resolveGeometryLiteral(value, scale, scaleTokens(vocab, scale));
|
|
176
|
+
// Anchored at the whole declaration: a value-only patch applied at the
|
|
177
|
+
// first namesake literal at or after the line, whatever property held it.
|
|
178
|
+
const patch = resolved.patch
|
|
179
|
+
? { from: decl, to: `${decl.slice(0, decl.length - raw.length - 1)}${raw.replace(value, resolved.patch.to)};` }
|
|
180
|
+
: null;
|
|
181
|
+
record(
|
|
182
|
+
'dimension-literal',
|
|
183
|
+
`${rel}: ${name}: ${value} pins a raw dimension; use a --space-*, --radius-*, or --border-width-* token`,
|
|
184
|
+
at,
|
|
185
|
+
{
|
|
186
|
+
details: { scale: resolved.scale, literals: resolved.literals, ...(patch ? { patch } : {}) },
|
|
187
|
+
...(patch ? {} : { repair: 'choice' }),
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Every `--name` inside a JSON string value, in the order it appears. Simple
|
|
196
|
+
* greedy extraction: `--card-default-body-padding` is one match, never two,
|
|
197
|
+
* since `[a-z0-9-]+` already consumes the longer run first. */
|
|
198
|
+
const TOKEN_NAME_RE = /--[a-z0-9-]+/g;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Saved assignments, validated as data. Every `--name` an alias string in
|
|
202
|
+
* `component-configs/<id>/default.json` carries, whatever wraps it (`var()`,
|
|
203
|
+
* the color-mix opacity form the editor writes, or nothing), must resolve —
|
|
204
|
+
* a design token, or one of the component's own properties. A string with no
|
|
205
|
+
* `--name` at all is a bare literal, allowed only on a property the editor
|
|
206
|
+
* declares in `intrinsics`. A structured (non-string) alias value, such as a
|
|
207
|
+
* gradient, is out of scope here.
|
|
208
|
+
*/
|
|
209
|
+
export function checkConfigTokens({ id, root, intrinsic, vocab }, recordAt) {
|
|
210
|
+
const config = readComponentConfig(root, id);
|
|
211
|
+
if (!config) return;
|
|
212
|
+
const { text, data } = config;
|
|
213
|
+
const rel = relative(root, config.path);
|
|
214
|
+
// Only this component's own tokens, not the union across every component:
|
|
215
|
+
// an alias naming a sibling component's property is exactly the case this
|
|
216
|
+
// rule exists to catch.
|
|
217
|
+
const ownTokens = vocab.components.get(id)?.tokens;
|
|
218
|
+
const knows = (name) => vocab.themeTokens.has(name) || (ownTokens?.has(name) ?? false);
|
|
219
|
+
for (const [prop, value] of Object.entries(data.aliases ?? {})) {
|
|
220
|
+
if (typeof value !== 'string') continue;
|
|
221
|
+
const line = findJsonKeyLine(text, prop);
|
|
222
|
+
const names = value.match(TOKEN_NAME_RE);
|
|
223
|
+
if (!names) {
|
|
224
|
+
if (!intrinsic.some((re) => re.test(prop))) {
|
|
225
|
+
recordAt(
|
|
226
|
+
'config-token',
|
|
227
|
+
`${rel}: ${prop}: "${value}" has no design token behind it, and ${prop} is not a declared intrinsic`,
|
|
228
|
+
rel,
|
|
229
|
+
line,
|
|
230
|
+
{ details: { property: prop, value } },
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
if (knows(name)) continue;
|
|
237
|
+
recordAt(
|
|
238
|
+
'config-token',
|
|
239
|
+
`${rel}: ${prop} names ${name}, which is not a design token or a semantic property`,
|
|
240
|
+
rel,
|
|
241
|
+
line,
|
|
242
|
+
{ details: { property: prop, value } },
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A property nothing in the file reads paints nothing, so the editor offers a
|
|
249
|
+
* control that moves nothing. */
|
|
250
|
+
export function checkUnreadTokens({ root, runtimePath, runtime, declared }, record) {
|
|
251
|
+
for (const name of unreadTokens(runtime, declared)) {
|
|
252
|
+
record(
|
|
253
|
+
'unread-token',
|
|
254
|
+
`${relative(root, runtimePath)}: ${name} is declared and never read in this file's own CSS. Read it where it paints, or drop it`,
|
|
255
|
+
runtime.indexOf(name),
|
|
256
|
+
{ details: { property: name } },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const TEXT_AXES = ['font-size', 'font-family', 'font-weight', 'line-height', 'letter-spacing'];
|
|
262
|
+
|
|
263
|
+
// The single-axis scales in tokens.css. A text style bundle carries its axis
|
|
264
|
+
// as a suffix (--body-md-font-size, --code-font-family), so no bundle name
|
|
265
|
+
// matches, and neither does a custom property the page declares itself. A
|
|
266
|
+
// weight alone cannot move the scale or the fonts, so --font-weight-* is not one.
|
|
267
|
+
const SINGLE_AXIS_TOKEN =
|
|
268
|
+
/^--(?:font-size|line-height|letter-spacing)-|^--font-(?:sans|serif|mono|display|editorial)$/;
|
|
269
|
+
|
|
270
|
+
// The geometry the theme owns: spacing, stroke, radius, and shadow all have a
|
|
271
|
+
// token scale, and `set-geometry` moves them. Sizing (a hero's height, a
|
|
272
|
+
// column's minimum width, a max content width) is layout, has no scale, and
|
|
273
|
+
// stays literal.
|
|
274
|
+
const THEMED_GEOMETRY = /^(padding|margin|gap|row-gap|column-gap|border|outline|inset|top|right|bottom|left|box-shadow|text-shadow)(-|$)|-radius$/;
|
|
275
|
+
|
|
276
|
+
// A local two-up or three-up is a layout. From four columns on, a hardcoded
|
|
277
|
+
// count reads as a claim about the page grid, which `--columns-count` owns.
|
|
278
|
+
const PAGE_GRID_COLUMNS = 4;
|
|
279
|
+
|
|
280
|
+
// The two forms that keep a grid in step: the page grid itself, and a sub-grid
|
|
281
|
+
// spanning fewer than every column.
|
|
282
|
+
const COLUMN_FORMS = ['repeat(var(--columns-count), 1fr)', 'repeat(calc(var(--columns-count) - N), 1fr)'];
|
|
283
|
+
|
|
284
|
+
/** `unknown-token` over each style region, then the value rules over each of its declarations. */
|
|
285
|
+
export function checkPageValues({ text, regions, inlineRegions, vocab }, add) {
|
|
286
|
+
const declared = pageDeclaredNames(text, regions);
|
|
287
|
+
for (const region of [...regions, ...inlineRegions]) {
|
|
288
|
+
const css = neutralise(region.text);
|
|
289
|
+
const at = (i) => region.offset + i;
|
|
290
|
+
|
|
291
|
+
for (const m of css.matchAll(/var\(\s*(--[a-z0-9-]+)/g)) {
|
|
292
|
+
const name = m[1];
|
|
293
|
+
if (declared.has(name) || vocab.knows(name)) continue;
|
|
294
|
+
add(
|
|
295
|
+
'unknown-token',
|
|
296
|
+
at(m.index),
|
|
297
|
+
isContractToken(name)
|
|
298
|
+
? `${name} has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
299
|
+
: `${name} is not a design token, a semantic property, or declared in this file`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const decl of declarations(css)) checkDeclaration(decl, { text, region, vocab, at }, add);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** A colour literal or a raw text axis ends the checks for its declaration. */
|
|
308
|
+
function checkDeclaration(decl, { text, region, vocab, at }, add) {
|
|
309
|
+
const { prop, value, index } = decl;
|
|
310
|
+
if (prop.startsWith('--')) return;
|
|
311
|
+
|
|
312
|
+
// A `var()` fallback only renders when the token is missing, so a literal
|
|
313
|
+
// inside one is not the page's value.
|
|
314
|
+
const painted = stripVarFallbacks(value);
|
|
315
|
+
if (!TEXT_AXES.includes(prop) && hasColorLiteral(painted)) {
|
|
316
|
+
const scale = colorScaleOfProperty(prop);
|
|
317
|
+
add('color-literal', at(index), `${prop}: ${value}. Use a design token.`, {
|
|
318
|
+
details: { scale, candidates: scaleTokens(vocab, scale).map((t) => t.name) },
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (TEXT_AXES.includes(prop) || prop === 'font') {
|
|
324
|
+
const axis = [...painted.matchAll(/var\(\s*(--[a-z0-9-]+)/g)]
|
|
325
|
+
.map((m) => m[1])
|
|
326
|
+
.find((name) => SINGLE_AXIS_TOKEN.test(name));
|
|
327
|
+
if (axis) {
|
|
328
|
+
add(
|
|
329
|
+
'raw-text-axis',
|
|
330
|
+
at(index),
|
|
331
|
+
`${prop}: ${value}. ${axis} is one axis. Set every axis from one text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Of the literals only absolute type values are a finding. `em`, `%`,
|
|
337
|
+
// and a unitless line-height are relative to the inherited type, so
|
|
338
|
+
// they ride whatever the theme sets rather than overriding it.
|
|
339
|
+
if (
|
|
340
|
+
!value.includes('var(') &&
|
|
341
|
+
!/^(inherit|initial|unset|normal)$/.test(value) &&
|
|
342
|
+
/\d(px|rem|pt)\b|^[a-z"']/i.test(value)
|
|
343
|
+
) {
|
|
344
|
+
add(
|
|
345
|
+
'raw-text-axis',
|
|
346
|
+
at(index),
|
|
347
|
+
`${prop}: ${value}. Set type from a text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (THEMED_GEOMETRY.test(prop) && hasDimensionLiteral(painted)) {
|
|
354
|
+
const scale = geometryScaleOfProperty(prop);
|
|
355
|
+
const resolved = resolveGeometryLiteral(value, scale, scaleTokens(vocab, scale));
|
|
356
|
+
const patch = resolved.patch ? declarationPatch(text, region, decl, resolved.patch.to) : null;
|
|
357
|
+
add(
|
|
358
|
+
'dimension-literal',
|
|
359
|
+
at(index),
|
|
360
|
+
`${prop}: ${value}. Use a --space-*, --radius-*, --border-width-*, or --shadow-* token.`,
|
|
361
|
+
{
|
|
362
|
+
details: { scale: resolved.scale, literals: resolved.literals, ...(patch ? { patch } : {}) },
|
|
363
|
+
...(patch ? {} : { repair: 'choice' }),
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const columns = value.match(/\brepeat\(\s*(\d+)\s*,\s*1fr\s*\)/);
|
|
369
|
+
if (columns && Number(columns[1]) >= PAGE_GRID_COLUMNS) {
|
|
370
|
+
add(
|
|
371
|
+
'hardcoded-columns',
|
|
372
|
+
at(index),
|
|
373
|
+
`${prop}: ${value}. Use repeat(var(--columns-count), 1fr) so the page grid stays in step.`,
|
|
374
|
+
{ details: { columns: Number(columns[1]), candidates: COLUMN_FORMS } },
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Runs before either checker reads the vocabulary, so a tokens.css behind the
|
|
381
|
+
* installed package never surfaces as unknown tokens. `engine` is a test seam.
|
|
382
|
+
*/
|
|
383
|
+
export async function checkTokensCssMigrations({ root = process.cwd(), apply, engine } = {}) {
|
|
384
|
+
const result = { applied: [], findings: [] };
|
|
385
|
+
const loaded = engine ?? (await loadBuiltEngine());
|
|
386
|
+
if (!loaded) return result;
|
|
387
|
+
const { TOKENS_CSS_MIGRATIONS, readLiveTokensConfig, runAdditiveTokensCssMigrations, runTokensCssMigrations } = loaded;
|
|
388
|
+
const tokensPath = resolveTokensCssPath(null, readLiveTokensConfig().tokensCssPath, root);
|
|
389
|
+
if (!tokensPath || !existsSync(tokensPath)) return result;
|
|
390
|
+
const file = relative(root, tokensPath);
|
|
391
|
+
if (isExcluded(file, root)) return result;
|
|
392
|
+
|
|
393
|
+
const migration = (id) => TOKENS_CSS_MIGRATIONS.find((m) => m.id === id);
|
|
394
|
+
const listed = (ids) => ids.map((id) => ({ id, description: migration(id).description }));
|
|
395
|
+
|
|
396
|
+
let css = readFileSync(tokensPath, 'utf8');
|
|
397
|
+
const additive = runAdditiveTokensCssMigrations(css);
|
|
398
|
+
if (additive.changed) {
|
|
399
|
+
const finding = {
|
|
400
|
+
rule: 'tokens-migration',
|
|
401
|
+
file,
|
|
402
|
+
line: 1,
|
|
403
|
+
message: `${file} lacks design tokens the installed package reads: ${additive.applied.join(', ')}`,
|
|
404
|
+
details: { migrations: listed(additive.applied) },
|
|
405
|
+
};
|
|
406
|
+
if (apply) {
|
|
407
|
+
writeFileSync(tokensPath, additive.css);
|
|
408
|
+
css = additive.css;
|
|
409
|
+
result.applied.push(finding);
|
|
410
|
+
} else {
|
|
411
|
+
result.findings.push(finding);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const breaking = runTokensCssMigrations(css).applied.filter((id) => migration(id).kind === 'breaking');
|
|
416
|
+
if (breaking.length > 0) {
|
|
417
|
+
result.findings.push({
|
|
418
|
+
rule: 'tokens-breaking-migration',
|
|
419
|
+
file,
|
|
420
|
+
line: 1,
|
|
421
|
+
message: `${file} has breaking migrations pending: ${breaking.join(', ')}`,
|
|
422
|
+
details: { migrations: listed(breaking) },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|