@motion-proto/live-tokens 0.67.1 → 0.69.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-build-page/SKILL.md +16 -5
- package/.claude/skills/live-tokens-create-component/SKILL.md +43 -8
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +131 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +2 -1
- package/CHANGELOG.md +239 -0
- package/README.md +11 -4
- package/bin/check-component.mjs +367 -63
- package/bin/check-page.mjs +409 -0
- package/bin/cli.mjs +57 -8
- package/bin/generate-theme.mjs +5 -4
- package/bin/lib/cssValues.mjs +50 -0
- package/bin/lib/findings.mjs +106 -0
- package/bin/lib/tokenVocabulary.mjs +213 -0
- package/dist-plugin/adjust/index.cjs +174 -23
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/adjust/index.js +68 -23
- package/dist-plugin/{chunk-J2JT4UEA.js → chunk-2YNERPXY.js} +26 -26
- package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
- package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.ts → dataPaths-bJTCEO4H.d.cts} +1 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.cts → dataPaths-bJTCEO4H.d.ts} +1 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.cjs +107 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +1 -1
- package/dist-plugin/index.cjs +173 -29
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +7 -7
- package/dist-plugin/migrateData/index.cjs +132 -26
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +6 -5
- package/src/app/site.css +4 -4
- package/src/editor/bootstrap.ts +6 -6
- package/src/editor/component-editor/ButtonEditor.svelte +71 -4
- package/src/editor/component-editor/CardEditor.svelte +6 -6
- package/src/editor/component-editor/DialogEditor.svelte +3 -3
- package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
- package/src/editor/component-editor/ImageEditor.svelte +8 -8
- package/src/editor/component-editor/ImageLightboxEditor.svelte +13 -2
- package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
- package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
- package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
- package/src/editor/component-editor/SliderEditor.svelte +186 -0
- package/src/editor/component-editor/TabBarEditor.svelte +63 -3
- package/src/editor/component-editor/registry.ts +10 -0
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +4 -4
- package/src/editor/core/components/aliasKinds.ts +51 -28
- package/src/editor/core/fonts/fontLoader.ts +2 -2
- package/src/editor/core/preview/{lookPreview.ts → themePreview.ts} +27 -27
- package/src/editor/core/productionPulse.ts +3 -3
- package/src/editor/core/sketch/index.ts +52 -44
- package/src/editor/core/sketch/maskField.ts +9 -9
- package/src/editor/core/sketch/sketchLayer.ts +25 -9
- package/src/editor/core/sketch/sketchRegistry.ts +34 -34
- package/src/editor/core/sketch/sketchStore.ts +98 -83
- package/src/editor/core/sketch/sketchStyleService.ts +4 -4
- package/src/editor/core/sketch/sketchStyles.ts +28 -28
- package/src/editor/core/store/editorPersistence.ts +44 -1
- package/src/editor/core/store/editorRenderer.ts +2 -2
- package/src/editor/core/store/editorStore.ts +18 -18
- package/src/editor/core/store/editorTypes.ts +7 -6
- package/src/editor/core/themes/colorsAndTypeService.ts +1 -1
- package/src/editor/core/themes/loadRows.ts +8 -8
- package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
- package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
- package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
- package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/slices/domainVars.ts +2 -2
- package/src/editor/core/themes/slices/washes.ts +107 -0
- package/src/editor/core/themes/themeDocumentSync.ts +2 -2
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeService.ts +17 -17
- package/src/editor/core/themes/{lookSummary.ts → themeSummary.ts} +9 -9
- package/src/editor/core/themes/themeTypes.ts +10 -6
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content/getting-started.md +1 -1
- package/src/editor/docs/content/sketch-mode.md +22 -17
- package/src/editor/docs/content/themes-workflow.md +11 -11
- package/src/editor/docs/content/where-themes-live.md +6 -6
- package/src/editor/docs/content.generated.ts +5 -5
- package/src/editor/index.ts +2 -2
- package/src/editor/pages/EditorShell.svelte +1 -1
- package/src/editor/ui/SurfacesTab.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +52 -52
- package/src/editor/ui/UITokenSelector.svelte +1 -0
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
- package/src/editor/ui/sketch/SketchPreview.svelte +2 -2
- package/src/editor/ui/sketch/SketchTab.svelte +64 -38
- package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
- package/src/live-tokens/data/colors-and-type/default.json +6 -6
- package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
- package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
- package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
- package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
- package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
- package/src/live-tokens/data/sketch-styles/dry.json +5 -5
- package/src/live-tokens/data/sketch-styles/hatched.json +9 -9
- package/src/live-tokens/data/themes/autumn.json +82 -14
- package/src/live-tokens/data/themes/halloween.json +82 -14
- package/src/live-tokens/data/themes/midnight-study.json +82 -14
- package/src/live-tokens/data/themes/ocean.json +82 -14
- package/src/live-tokens/data/themes/royal-velvet.json +82 -14
- package/src/live-tokens/data/themes/sketchy.json +82 -14
- package/src/live-tokens/data/themes/spring-meadow.json +82 -14
- package/src/live-tokens/data/themes/sunset.json +82 -14
- package/src/live-tokens/data/tokens.generated.css +6 -6
- package/src/system/backdrop/backdrop.ts +1 -1
- package/src/system/components/Button.svelte +28 -10
- package/src/system/components/Card.svelte +6 -6
- package/src/system/components/Dialog.svelte +3 -3
- package/src/system/components/IconButton.svelte +23 -7
- package/src/system/components/Image.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +17 -1
- package/src/system/components/SectionDivider.svelte +1 -1
- package/src/system/components/SegmentedControl.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +22 -2
- package/src/system/components/Slider.svelte +348 -0
- package/src/system/components/TabBar.svelte +20 -2
- package/src/system/styles/CONVENTIONS.md +2 -2
- package/src/system/styles/tokens.css +12 -4
- package/template/package.json +3 -2
- package/template/src/pages/Home.svelte +1 -1
- package/src/editor/core/themes/slices/overlays.ts +0 -101
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// Static validator for a live-tokens page.
|
|
2
|
+
//
|
|
3
|
+
// Asserts that a page satisfies the contract described in the
|
|
4
|
+
// live-tokens-build-page skill: it is assembled from catalogue components, and
|
|
5
|
+
// every value in its CSS is a theme token rather than a literal. The rules and
|
|
6
|
+
// their default severities are in PAGE_RULES; each is overridable per project
|
|
7
|
+
// (live-tokens.config.json) or per run (--off/--warn/--error/--strict), because
|
|
8
|
+
// the line between "wrong" and "deliberate" moves with the project.
|
|
9
|
+
//
|
|
10
|
+
// Returns { findings, checked } — findings carry a stable `rule` id so a skill
|
|
11
|
+
// can parse --json output, fix, and re-run until the exit code is 0.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, relative, resolve, basename } from 'node:path';
|
|
15
|
+
import { lineOf } from './lib/findings.mjs';
|
|
16
|
+
import { blankStrings, hasColorLiteral, hasDimensionLiteral, stripVarFallbacks } from './lib/cssValues.mjs';
|
|
17
|
+
import { isContractToken, loadVocabulary, walk } from './lib/tokenVocabulary.mjs';
|
|
18
|
+
import { resolveTokensCssPath } from './migrate.mjs';
|
|
19
|
+
|
|
20
|
+
export const PAGE_RULES = {
|
|
21
|
+
'unknown-component': 'error',
|
|
22
|
+
'unknown-prop': 'error',
|
|
23
|
+
'unknown-prop-value': 'error',
|
|
24
|
+
'deep-import': 'error',
|
|
25
|
+
'unknown-token': 'error',
|
|
26
|
+
'color-literal': 'error',
|
|
27
|
+
'reserved-route': 'error',
|
|
28
|
+
'site-css-in-main': 'error',
|
|
29
|
+
'dimension-literal': 'warn',
|
|
30
|
+
'hardcoded-columns': 'warn',
|
|
31
|
+
'raw-text-axis': 'warn',
|
|
32
|
+
'missing-source': 'warn',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Directories that hold the system, not pages built on it.
|
|
36
|
+
const NOT_PAGES = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
|
|
37
|
+
|
|
38
|
+
const COMPONENT_IMPORT =
|
|
39
|
+
/(?:@motion-proto\/live-tokens\/components|[./][^'"]*\/system\/components)\/([A-Za-z0-9]+)\.svelte$/;
|
|
40
|
+
|
|
41
|
+
const DEEP_IMPORT_PATTERNS = [
|
|
42
|
+
/^@motion-proto\/live-tokens\/src\//,
|
|
43
|
+
/node_modules\/@motion-proto\/live-tokens/,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const TEXT_AXES = ['font-size', 'font-family', 'font-weight', 'line-height', 'letter-spacing'];
|
|
47
|
+
|
|
48
|
+
// The geometry the theme owns: spacing, stroke, radius, and shadow all have a
|
|
49
|
+
// token scale, and `adjust` moves them. Sizing (a hero's height, a column's
|
|
50
|
+
// minimum width, a max content width) is layout, has no scale, and stays
|
|
51
|
+
// literal.
|
|
52
|
+
const THEMED_GEOMETRY = /^(padding|margin|gap|row-gap|column-gap|border|outline|inset|top|right|bottom|left|box-shadow|text-shadow)(-|$)|-radius$/;
|
|
53
|
+
|
|
54
|
+
// A local two-up or three-up is a layout. From four columns on, a hardcoded
|
|
55
|
+
// count reads as a claim about the page grid, which `--columns-count` owns.
|
|
56
|
+
const PAGE_GRID_COLUMNS = 4;
|
|
57
|
+
|
|
58
|
+
/** Blank out comments, url() payloads, and string contents so none of them can match a rule. */
|
|
59
|
+
function neutralise(css) {
|
|
60
|
+
return blankStrings(
|
|
61
|
+
css
|
|
62
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
63
|
+
.replace(/url\((?:[^()]|\([^()]*\))*\)/g, (m) => ' '.repeat(m.length)),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `<style>` blocks with their absolute offset in the file; whole file for .css. */
|
|
68
|
+
function styleRegions(text, file) {
|
|
69
|
+
if (file.endsWith('.css')) return [{ text, offset: 0 }];
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const m of text.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
72
|
+
out.push({ text: m[1], offset: m.index + m[0].indexOf(m[1]) });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Inline styles in markup, as declaration lists the value rules can read: a
|
|
79
|
+
* `style="..."` attribute verbatim, and a `style:prop="value"` directive
|
|
80
|
+
* rewritten as `prop: value;`. A `{...}` expression is dynamic and skipped.
|
|
81
|
+
*/
|
|
82
|
+
function inlineStyleRegions(code) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const m of code.matchAll(/\sstyle=(["'])([^"']*)\1/g)) {
|
|
85
|
+
out.push({ text: `${m[2]};`, offset: m.index + m[0].indexOf(m[2]) });
|
|
86
|
+
}
|
|
87
|
+
for (const m of code.matchAll(/\sstyle:([a-z-]+)=(["'])([^"']*)\2/g)) {
|
|
88
|
+
out.push({ text: `${m[1]}: ${m[3]};`, offset: m.index + 1 });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Everything outside `<style>`: script and markup. */
|
|
94
|
+
function codeRegion(text, file) {
|
|
95
|
+
if (file.endsWith('.css')) return null;
|
|
96
|
+
return text.replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => ' '.repeat(m.length));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Declarations in a stylesheet, with at-rule preludes excluded. A breakpoint in
|
|
101
|
+
* `@media (max-width: 768px)` is structural geometry, not a themeable value.
|
|
102
|
+
* A property name is read from its start, so `--heading-2xl` is one custom
|
|
103
|
+
* property and never the property `xl`.
|
|
104
|
+
*/
|
|
105
|
+
function declarations(css) {
|
|
106
|
+
const body = css.replace(/@[a-z-]+[^;{]*(?=\{)/gi, (m) => ' '.repeat(m.length));
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const m of body.matchAll(/(?<![\w-])((?:--)?[a-z][\w-]*)\s*:\s*([^;{}]+)[;}]/gi)) {
|
|
109
|
+
out.push({ prop: m[1].toLowerCase(), value: m[2].trim(), index: m.index });
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The attributes of one component tag starting at `start` (the `<`), read with
|
|
116
|
+
* `{}` depth and quotes tracked so an expression holding `>` does not end the
|
|
117
|
+
* tag early. Returns null when the tag spreads an object, which makes its prop
|
|
118
|
+
* set unknowable.
|
|
119
|
+
*/
|
|
120
|
+
function tagAttributes(code, start) {
|
|
121
|
+
let i = code.indexOf(' ', start);
|
|
122
|
+
const tagEnd = (() => {
|
|
123
|
+
let depth = 0;
|
|
124
|
+
let quote = null;
|
|
125
|
+
for (let j = start; j < code.length; j++) {
|
|
126
|
+
const c = code[j];
|
|
127
|
+
if (quote) {
|
|
128
|
+
if (c === quote) quote = null;
|
|
129
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
130
|
+
else if (c === '{') depth++;
|
|
131
|
+
else if (c === '}') depth--;
|
|
132
|
+
else if (c === '>' && depth === 0) return j;
|
|
133
|
+
}
|
|
134
|
+
return code.length;
|
|
135
|
+
})();
|
|
136
|
+
if (i === -1 || i > tagEnd) return { attrs: [], end: tagEnd };
|
|
137
|
+
const attrs = [];
|
|
138
|
+
while (i < tagEnd) {
|
|
139
|
+
const c = code[i];
|
|
140
|
+
if (/\s/.test(c) || c === '/') {
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (c === '{') {
|
|
145
|
+
let depth = 0;
|
|
146
|
+
let j = i;
|
|
147
|
+
for (; j < tagEnd; j++) {
|
|
148
|
+
if (code[j] === '{') depth++;
|
|
149
|
+
else if (code[j] === '}' && --depth === 0) break;
|
|
150
|
+
}
|
|
151
|
+
const inner = code.slice(i + 1, j).trim();
|
|
152
|
+
if (inner.startsWith('...')) return null;
|
|
153
|
+
if (/^\w+$/.test(inner)) attrs.push({ name: inner, value: null, index: i });
|
|
154
|
+
i = j + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const name = code.slice(i).match(/^[^\s=/>]+/)?.[0];
|
|
158
|
+
if (!name) break;
|
|
159
|
+
const at = i;
|
|
160
|
+
i += name.length;
|
|
161
|
+
let value = null;
|
|
162
|
+
if (code[i] === '=') {
|
|
163
|
+
i++;
|
|
164
|
+
const q = code[i];
|
|
165
|
+
if (q === '"' || q === "'") {
|
|
166
|
+
const close = code.indexOf(q, i + 1);
|
|
167
|
+
value = code.slice(i + 1, close === -1 ? tagEnd : close);
|
|
168
|
+
i = close === -1 ? tagEnd : close + 1;
|
|
169
|
+
} else if (q === '{') {
|
|
170
|
+
let depth = 0;
|
|
171
|
+
for (; i < tagEnd; i++) {
|
|
172
|
+
if (code[i] === '{') depth++;
|
|
173
|
+
else if (code[i] === '}' && --depth === 0) break;
|
|
174
|
+
}
|
|
175
|
+
i++;
|
|
176
|
+
} else {
|
|
177
|
+
const bare = code.slice(i).match(/^[^\s>]+/)?.[0] ?? '';
|
|
178
|
+
value = bare;
|
|
179
|
+
i += bare.length;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
attrs.push({ name, value, index: at });
|
|
183
|
+
}
|
|
184
|
+
return { attrs, end: tagEnd };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Props a page passes that the component does not declare, or values outside a prop's union. */
|
|
188
|
+
function checkComponentUsage(code, imports, add) {
|
|
189
|
+
for (const [local, entry] of imports) {
|
|
190
|
+
const props = entry.props;
|
|
191
|
+
if (!props) continue;
|
|
192
|
+
const re = new RegExp(`<${local}(?=[\\s/>])`, 'g');
|
|
193
|
+
for (const m of code.matchAll(re)) {
|
|
194
|
+
const tag = tagAttributes(code, m.index);
|
|
195
|
+
if (!tag) continue;
|
|
196
|
+
for (const { name, value, index } of tag.attrs) {
|
|
197
|
+
if (name.includes(':') || name.startsWith('@') || name === 'children') continue;
|
|
198
|
+
if (!props.props.has(name)) {
|
|
199
|
+
add('unknown-prop', index, `${entry.name} has no prop '${name}'; it accepts ${[...props.props].join(', ')}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const allowed = props.enums.get(name);
|
|
203
|
+
if (allowed && value !== null && !allowed.has(value)) {
|
|
204
|
+
add('unknown-prop-value', index, `${entry.name} ${name}="${value}" is not one of ${[...allowed].join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The object literal enclosing `index`, found by balancing braces outward. */
|
|
212
|
+
function enclosingObject(text, index) {
|
|
213
|
+
let depth = 0;
|
|
214
|
+
let start = -1;
|
|
215
|
+
for (let i = index; i >= 0; i--) {
|
|
216
|
+
const c = text[i];
|
|
217
|
+
if (c === '}') depth++;
|
|
218
|
+
else if (c === '{') {
|
|
219
|
+
if (depth === 0) {
|
|
220
|
+
start = i;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
depth--;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (start === -1) return null;
|
|
227
|
+
depth = 0;
|
|
228
|
+
for (let i = start; i < text.length; i++) {
|
|
229
|
+
const c = text[i];
|
|
230
|
+
if (c === '{') depth++;
|
|
231
|
+
else if (c === '}') {
|
|
232
|
+
depth--;
|
|
233
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function checkFile(file, text, vocab, root) {
|
|
240
|
+
const rel = relative(root, file);
|
|
241
|
+
const findings = [];
|
|
242
|
+
const add = (rule, index, message) =>
|
|
243
|
+
findings.push({ rule, file: rel, line: lineOf(text, index), message });
|
|
244
|
+
|
|
245
|
+
const code = codeRegion(text, file);
|
|
246
|
+
if (code !== null) {
|
|
247
|
+
const imports = new Map();
|
|
248
|
+
for (const m of code.matchAll(/import\s+(?:([^'"]*?)\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
249
|
+
const spec = m[2];
|
|
250
|
+
for (const pattern of DEEP_IMPORT_PATTERNS) {
|
|
251
|
+
if (pattern.test(spec)) {
|
|
252
|
+
add('deep-import', m.index, `deep import into package internals: ${spec}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const comp = spec.match(COMPONENT_IMPORT);
|
|
256
|
+
if (!comp) continue;
|
|
257
|
+
const entry = vocab.components.get(comp[1].toLowerCase());
|
|
258
|
+
if (!entry) {
|
|
259
|
+
add(
|
|
260
|
+
'unknown-component',
|
|
261
|
+
m.index,
|
|
262
|
+
`'${comp[1]}' is not in the component catalogue; author it with live-tokens-create-component or pick a shipped one`,
|
|
263
|
+
);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const local = m[1]?.trim().match(/^(\w+)$/)?.[1];
|
|
267
|
+
if (local) imports.set(local, entry);
|
|
268
|
+
}
|
|
269
|
+
checkComponentUsage(code, imports, add);
|
|
270
|
+
|
|
271
|
+
for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
|
|
272
|
+
add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const m of code.matchAll(/\blazy\s*:/g)) {
|
|
276
|
+
const entry = enclosingObject(code, m.index);
|
|
277
|
+
if (entry && !/\bsource\s*:/.test(entry)) {
|
|
278
|
+
add('missing-source', m.index, `route entry has no 'source', so Page Source cannot open it`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (/^main\.(ts|js)$/.test(basename(file))) {
|
|
283
|
+
for (const m of code.matchAll(/import\s+['"]([^'"]*site\.css)['"]/g)) {
|
|
284
|
+
add(
|
|
285
|
+
'site-css-in-main',
|
|
286
|
+
m.index,
|
|
287
|
+
`site.css imported from main; import it from each page's <script> so it cannot leak into editor routes`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// A page may also mint a custom property outside its <style> block — a
|
|
294
|
+
// `style:--x={...}` directive or an el.style.setProperty call — and those are
|
|
295
|
+
// just as declared as one written in CSS.
|
|
296
|
+
const declaredHere = new Set();
|
|
297
|
+
const regions = styleRegions(text, file);
|
|
298
|
+
for (const region of regions) {
|
|
299
|
+
for (const m of neutralise(region.text).matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gim)) {
|
|
300
|
+
declaredHere.add(m[1]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const m of text.matchAll(/(?:style:|setProperty\(\s*['"`]|['"`])(--[a-z0-9-]+)/g)) {
|
|
304
|
+
declaredHere.add(m[1]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const region of [...regions, ...(code === null ? [] : inlineStyleRegions(code))]) {
|
|
308
|
+
const css = neutralise(region.text);
|
|
309
|
+
const at = (i) => region.offset + i;
|
|
310
|
+
|
|
311
|
+
for (const m of css.matchAll(/var\(\s*(--[a-z0-9-]+)/g)) {
|
|
312
|
+
const name = m[1];
|
|
313
|
+
if (declaredHere.has(name) || vocab.knows(name)) continue;
|
|
314
|
+
add(
|
|
315
|
+
'unknown-token',
|
|
316
|
+
at(m.index),
|
|
317
|
+
isContractToken(name)
|
|
318
|
+
? `${name} looks like a theme token but no longer exists; check tokens.css for a rename`
|
|
319
|
+
: `${name} is not a theme token, a component token, or declared in this file`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const decl of declarations(css)) {
|
|
324
|
+
const { prop, value, index } = decl;
|
|
325
|
+
if (prop.startsWith('--')) continue;
|
|
326
|
+
|
|
327
|
+
// A `var()` fallback only renders when the token is missing, so a literal
|
|
328
|
+
// inside one is not the page's value.
|
|
329
|
+
const painted = stripVarFallbacks(value);
|
|
330
|
+
if (!TEXT_AXES.includes(prop) && hasColorLiteral(painted)) {
|
|
331
|
+
add('color-literal', at(index), `${prop}: ${value}. Use a theme token, not a colour literal.`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Only absolute type values are a finding. `em`, `%`, and a unitless
|
|
336
|
+
// line-height are relative to the inherited type, so they ride whatever
|
|
337
|
+
// the theme sets rather than overriding it.
|
|
338
|
+
if (
|
|
339
|
+
(TEXT_AXES.includes(prop) || prop === 'font') &&
|
|
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-*).`,
|
|
348
|
+
);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (THEMED_GEOMETRY.test(prop) && hasDimensionLiteral(painted)) {
|
|
353
|
+
add(
|
|
354
|
+
'dimension-literal',
|
|
355
|
+
at(index),
|
|
356
|
+
`${prop}: ${value}. Use a --space-*, --radius-*, --border-width-*, or --shadow-* token.`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const columns = value.match(/\brepeat\(\s*(\d+)\s*,\s*1fr\s*\)/);
|
|
361
|
+
if (columns && Number(columns[1]) >= PAGE_GRID_COLUMNS) {
|
|
362
|
+
add(
|
|
363
|
+
'hardcoded-columns',
|
|
364
|
+
at(index),
|
|
365
|
+
`${prop}: ${value}. Use repeat(var(--columns-count), 1fr) so the page grid stays in step.`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return findings;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Files that define the vocabulary rather than consume it.
|
|
375
|
+
const TOKEN_SOURCES = ['tokens.generated.css', 'fonts.css'];
|
|
376
|
+
|
|
377
|
+
/** Pages to check when the caller names none: every .svelte/.css under src/ that is not system code. */
|
|
378
|
+
export function discoverPages(root) {
|
|
379
|
+
const src = join(root, 'src');
|
|
380
|
+
if (!existsSync(src)) return [];
|
|
381
|
+
const tokensCss = resolveTokensCssPath(null, null, root);
|
|
382
|
+
return walk(src, ['.svelte', '.css', '.ts', '.js']).filter((f) => {
|
|
383
|
+
const rel = relative(root, f);
|
|
384
|
+
if (NOT_PAGES.some((d) => rel.startsWith(`${d}/`))) return false;
|
|
385
|
+
if (f === tokensCss || TOKEN_SOURCES.includes(basename(f))) return false;
|
|
386
|
+
if (/\.(test|spec)\.[tj]s$/.test(rel)) return false;
|
|
387
|
+
if (rel.endsWith('.ts') || rel.endsWith('.js')) return /main\.(ts|js)$/.test(rel);
|
|
388
|
+
return true;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function checkPages(targets, { root = process.cwd(), vocabulary } = {}) {
|
|
393
|
+
const vocab = vocabulary ?? loadVocabulary({ root });
|
|
394
|
+
const files = [];
|
|
395
|
+
for (const t of targets) {
|
|
396
|
+
const full = resolve(root, t);
|
|
397
|
+
if (!existsSync(full)) continue;
|
|
398
|
+
if (statSync(full).isDirectory()) {
|
|
399
|
+
files.push(...walk(full, ['.svelte', '.css']));
|
|
400
|
+
} else {
|
|
401
|
+
files.push(full);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const findings = [];
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
findings.push(...checkFile(file, readFileSync(file, 'utf8'), vocab, root));
|
|
407
|
+
}
|
|
408
|
+
return { findings, checked: files.length };
|
|
409
|
+
}
|
package/bin/cli.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Subcommands:
|
|
4
4
|
// create <dir> Scaffold a new app that depends on this package.
|
|
5
5
|
// setup-claude [--force] Copy bundled Claude Code skills into ./.claude/skills/.
|
|
6
|
-
// check-component
|
|
6
|
+
// check-component [id] Validate a component (or every authored one) against the create-component skill contract.
|
|
7
|
+
// check-page [paths...] Validate pages against the build-page skill contract.
|
|
7
8
|
// generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
|
|
8
9
|
// adjust <ops.json> Apply radius/padding/gap/border-width ops to the open buffer.
|
|
9
10
|
// set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
|
|
@@ -13,7 +14,16 @@ import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
|
13
14
|
import { dirname, join, resolve } from 'node:path';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import process from 'node:process';
|
|
16
|
-
import { checkComponent, formatReport } from './check-component.mjs';
|
|
17
|
+
import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
|
|
18
|
+
import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
|
|
19
|
+
import {
|
|
20
|
+
applySeverity,
|
|
21
|
+
countBySeverity,
|
|
22
|
+
formatFindings,
|
|
23
|
+
parseCheckFlags,
|
|
24
|
+
readChecksConfig,
|
|
25
|
+
toJson,
|
|
26
|
+
} from './lib/findings.mjs';
|
|
17
27
|
import {
|
|
18
28
|
runMigrate,
|
|
19
29
|
formatMigrateResult,
|
|
@@ -32,8 +42,20 @@ Commands:
|
|
|
32
42
|
create <dir> [--force] Scaffold a new Svelte + Vite app wired up with
|
|
33
43
|
live-tokens (editor, components, theme tokens)
|
|
34
44
|
setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
|
|
35
|
-
check-component
|
|
45
|
+
check-component [id] Validate <id>'s runtime, editor, and registration
|
|
36
46
|
against the live-tokens-create-component contract
|
|
47
|
+
check-page [paths...] Validate pages against the live-tokens-build-page
|
|
48
|
+
contract: catalogue components only, and every CSS
|
|
49
|
+
value a theme token. Checks every page under src/
|
|
50
|
+
when given no paths.
|
|
51
|
+
|
|
52
|
+
Both check commands accept:
|
|
53
|
+
--json Machine-readable findings, for a skill to iterate
|
|
54
|
+
against until the exit code is 0
|
|
55
|
+
--strict Treat warnings as errors
|
|
56
|
+
--off=<rule,...> Silence rules; --warn=/--error= change severity
|
|
57
|
+
(or set "checks": { "rules": {...} } in
|
|
58
|
+
live-tokens.config.json)
|
|
37
59
|
generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]
|
|
38
60
|
Build a full theme from a 10-seed OKLCH brief
|
|
39
61
|
(see the live-tokens-generate-theme skill),
|
|
@@ -109,12 +131,38 @@ if (command === 'create' || command === 'init') {
|
|
|
109
131
|
}
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
function reportChecks(label, findings, checked, rules, opts) {
|
|
135
|
+
const resolved = applySeverity(findings, rules, opts, readChecksConfig(process.cwd()));
|
|
136
|
+
console.log(
|
|
137
|
+
opts.json
|
|
138
|
+
? toJson(resolved, { label, checked })
|
|
139
|
+
: formatFindings(resolved, { label, checked }),
|
|
140
|
+
);
|
|
141
|
+
process.exit(countBySeverity(resolved).errors === 0 ? 0 : 1);
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
if (command === 'check-component') {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
const opts = parseCheckFlags(rest);
|
|
146
|
+
const ids = opts.rest.length > 0 ? [opts.rest[0]] : discoverComponents();
|
|
147
|
+
if (ids.length === 0) {
|
|
148
|
+
console.log('✓ check-component: no component authored under src/system/components yet.');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
const results = ids.map((id) => [id, checkComponent(id)]);
|
|
152
|
+
if (ids.length === 1 && !opts.json && !opts.strict && opts.off.length + opts.warn.length + opts.error.length === 0) {
|
|
153
|
+
const [id, result] = results[0];
|
|
154
|
+
console.log(formatReport(id, result));
|
|
155
|
+
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
156
|
+
}
|
|
157
|
+
const label = ids.length === 1 ? `check-component ${ids[0]}` : 'check-component';
|
|
158
|
+
reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (command === 'check-page') {
|
|
162
|
+
const opts = parseCheckFlags(rest);
|
|
163
|
+
const targets = opts.rest.length > 0 ? opts.rest : discoverPages(process.cwd());
|
|
164
|
+
const { findings, checked } = checkPages(targets, { root: process.cwd() });
|
|
165
|
+
reportChecks('check-page', findings, checked, PAGE_RULES, opts);
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
if (command === 'generate-theme') {
|
|
@@ -270,6 +318,7 @@ const SAMPLE_PROMPTS = {
|
|
|
270
318
|
'live-tokens-generate-theme': 'make me a bright and cheerful theme',
|
|
271
319
|
'live-tokens-adjust-geometry': 'make the buttons pill shaped',
|
|
272
320
|
'live-tokens-pair-fonts': 'pair some fonts for this theme',
|
|
321
|
+
'live-tokens-fix-findings': 'make check:design pass',
|
|
273
322
|
};
|
|
274
323
|
|
|
275
324
|
const installedSamples = skills
|
package/bin/generate-theme.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Reads a seed brief (JSON), builds a full validated colors-and-type layer via
|
|
4
4
|
// the compiled engine (dist-plugin/generateColorsAndType — the CLI never imports
|
|
5
5
|
// TS sources), enforces the AA contrast gate, and saves the result as a theme:
|
|
6
|
-
// <themesDir>/<slug>.json, the document that carries the whole
|
|
6
|
+
// <themesDir>/<slug>.json, the document that carries the whole theme by value.
|
|
7
7
|
// Unless --no-activate it then opens that theme the way the dev server's apply
|
|
8
8
|
// door does — existing `_working` buffers are cleared and
|
|
9
9
|
// `themes/_active.json` names it. Nothing else moves, so generating a theme
|
|
@@ -21,9 +21,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
22
|
const ENGINE = resolve(pkgRoot, 'dist-plugin/generateColorsAndType/index.js');
|
|
23
23
|
const packageDataDir = join(pkgRoot, 'src/live-tokens/data');
|
|
24
|
-
// Source of truth:
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
// Source of truth: src/editor/core/themes/themeTypes.ts, which
|
|
25
|
+
// normalizeTheme.ts re-exports. This copy cannot import TS, so
|
|
26
|
+
// `check:preset-themes` and generate-theme.test.ts are what catch a drift.
|
|
27
|
+
const THEME_SCHEMA_VERSION = 5;
|
|
27
28
|
|
|
28
29
|
async function loadEngine() {
|
|
29
30
|
if (!existsSync(ENGINE)) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Value-level parsing shared by check-page and check-component, so the two
|
|
2
|
+
// checkers can never disagree about what counts as a literal.
|
|
3
|
+
|
|
4
|
+
// `transparent` and `currentcolor` are left out: neither paints a colour of its own.
|
|
5
|
+
const NAMED_COLORS =
|
|
6
|
+
'aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen';
|
|
7
|
+
const NAMED_COLOR = new RegExp(`(?<![\\w-])(?:${NAMED_COLORS.split(' ').join('|')})(?![\\w-])`);
|
|
8
|
+
|
|
9
|
+
/** True when `value` paints a literal colour: hex, a colour function, or a named colour. */
|
|
10
|
+
export function hasColorLiteral(value) {
|
|
11
|
+
return /#[0-9a-f]{3,8}\b|\brgba?\(|\bhsla?\(|\boklch\(|\boklab\(/i.test(value) || NAMED_COLOR.test(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** True when `value` pins a non-zero px or rem length outside any token. */
|
|
15
|
+
export function hasDimensionLiteral(value) {
|
|
16
|
+
return [...value.matchAll(/(?<![\w.-])(\d*\.?\d+)(px|rem)\b/g)].some((d) => parseFloat(d[1]) !== 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Replace `var(--x, <fallback>)` with `var(--x)`. A fallback only renders when
|
|
21
|
+
* the token is missing, so its literals are not the value that paints.
|
|
22
|
+
*/
|
|
23
|
+
export function stripVarFallbacks(value) {
|
|
24
|
+
let out = '';
|
|
25
|
+
for (let i = 0; i < value.length; i++) {
|
|
26
|
+
if (!value.startsWith('var(', i)) {
|
|
27
|
+
out += value[i];
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let depth = 0;
|
|
31
|
+
let comma = -1;
|
|
32
|
+
let j = i;
|
|
33
|
+
for (; j < value.length; j++) {
|
|
34
|
+
const c = value[j];
|
|
35
|
+
if (c === '(') depth++;
|
|
36
|
+
else if (c === ')') {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth === 0) break;
|
|
39
|
+
} else if (c === ',' && depth === 1 && comma === -1) comma = j;
|
|
40
|
+
}
|
|
41
|
+
out += comma === -1 ? value.slice(i, j + 1) : `${value.slice(i, comma)})`;
|
|
42
|
+
i = j;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Blank string literal contents so a colour word inside `content: "..."` never matches a rule. */
|
|
48
|
+
export function blankStrings(css) {
|
|
49
|
+
return css.replace(/(["'])(?:\\.|(?!\1)[^\\])*\1/g, (m) => m[0] + ' '.repeat(m.length - 2) + m[0]);
|
|
50
|
+
}
|