@motion-proto/live-tokens 0.77.0 → 0.79.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 +60 -29
- 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 +9 -6
- 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 +284 -0
- package/README.md +9 -15
- package/bin/check-component.mjs +152 -551
- package/bin/check-page.mjs +56 -420
- package/bin/cli.mjs +83 -15
- package/bin/contractRunner.mjs +380 -120
- 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/pageRoutes.mjs +309 -0
- package/bin/lib/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- 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 +363 -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 +432 -15
- package/dist-plugin/index.js +8 -9
- 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 +6 -4
- 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/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +16 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -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 +258 -93
- package/src/editor/skill-atlas/trees/create-component.ts +73 -62
- package/src/editor/skill-atlas/trees/create-page.ts +54 -33
- 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 +68 -63
- 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-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.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-7TI7Z6Y6.js → chunk-U7OJE5DU.js} +19 -19
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- 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-alias.contract.js +2 -2
- 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 +7 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +15 -11
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +50 -4
- package/src/testing-js/index.js +32 -8
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +867 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-BMLIbDs2.d.ts} +29 -3
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +3 -2
- package/template/README.md +16 -9
- package/template/package.json +2 -2
- package/template/src/pages/Home.svelte +13 -0
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -98
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -469
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-7TI7Z6Y6.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
package/bin/lib/catalogue.mjs
CHANGED
|
@@ -4,25 +4,121 @@
|
|
|
4
4
|
// use, so a skill or a script sees exactly what the checkers will hold it to.
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { relative } from 'node:path';
|
|
8
|
-
import { CONTRACT_SCALES } from './tokenVocabulary.mjs';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import { CONTRACT_SCALES, PKG_ROOT } from './tokenVocabulary.mjs';
|
|
9
|
+
|
|
10
|
+
const PKG = '@motion-proto/live-tokens';
|
|
11
|
+
|
|
12
|
+
// The one deep import with a public equivalent: a component by file.
|
|
13
|
+
const DEEP_COMPONENT = new RegExp(`^${PKG}/src/system/components/([A-Za-z0-9]+\\.svelte)$`);
|
|
14
|
+
|
|
15
|
+
let subpaths = null;
|
|
16
|
+
|
|
17
|
+
/** Everything the package's `exports` map makes importable, as specifiers. */
|
|
18
|
+
export function publicSubpaths() {
|
|
19
|
+
if (subpaths) return subpaths;
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
22
|
+
subpaths = Object.keys(pkg.exports ?? {}).map((key) => (key === '.' ? PKG : `${PKG}/${key.slice(2)}`));
|
|
23
|
+
} catch {
|
|
24
|
+
subpaths = [];
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
return subpaths;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a `deep-import` finding's repair needs. A component file has one public
|
|
31
|
+
* specifier, so the rewrite is mechanical; anything else reaches for internals
|
|
32
|
+
* that may have no public equivalent at all, which leaves the choice open.
|
|
33
|
+
*/
|
|
34
|
+
export function deepImportRepair(specifier) {
|
|
35
|
+
const m = DEEP_COMPONENT.exec(specifier);
|
|
36
|
+
if (!m) return { details: { specifier, exports: publicSubpaths() }, repair: 'choice' };
|
|
37
|
+
const publicSpecifier = `${PKG}/components/${m[1]}`;
|
|
38
|
+
return { details: { specifier, public: publicSpecifier, patch: { from: specifier, to: publicSpecifier } } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STRING_LITERAL = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/;
|
|
42
|
+
|
|
43
|
+
function literalValue(raw) {
|
|
44
|
+
const quote = raw[0];
|
|
45
|
+
const inner = raw.slice(1, -1);
|
|
46
|
+
if (quote === '`' && inner.includes('${')) return undefined;
|
|
47
|
+
return inner.replace(/\s+/g, ' ').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Brace matching skips over quoted literals so a `}` inside a description
|
|
51
|
+
// (or a description containing a stray brace) never closes the object early.
|
|
52
|
+
function findBalanced(text, openIndex) {
|
|
53
|
+
let depth = 0;
|
|
54
|
+
for (let i = openIndex; i < text.length; i++) {
|
|
55
|
+
const ch = text[i];
|
|
56
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
57
|
+
const quote = ch;
|
|
58
|
+
i++;
|
|
59
|
+
while (i < text.length && text[i] !== quote) i += text[i] === '\\' ? 2 : 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === '{') depth++;
|
|
63
|
+
else if (ch === '}') {
|
|
64
|
+
depth--;
|
|
65
|
+
if (depth === 0) return { content: text.slice(openIndex + 1, i), end: i + 1 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseFieldObject(body) {
|
|
72
|
+
const fields = {};
|
|
73
|
+
const re = new RegExp(`([A-Za-z_$][A-Za-z0-9_$]*)\\s*:\\s*(${STRING_LITERAL.source})`, 'g');
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = re.exec(body))) {
|
|
76
|
+
if (m[1] in fields) continue;
|
|
77
|
+
const value = literalValue(m[2]);
|
|
78
|
+
if (value !== undefined) fields[m[1]] = value;
|
|
79
|
+
}
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bounded, non-evaluating parse of the runtime file's `catalogue` export:
|
|
85
|
+
* `key: <string literal>` pairs inside the `<script module>` block's
|
|
86
|
+
* `export const catalogue = { ... }`, the same way `builtInIds` and
|
|
87
|
+
* `componentProps` read the rest of the vocabulary without importing the
|
|
88
|
+
* module. A field given as an identifier, a template with `${}`, or a
|
|
89
|
+
* concatenation is not a string literal, so it is silently absent rather
|
|
90
|
+
* than evaluated. Returns `null` when the file has no such export.
|
|
91
|
+
*/
|
|
92
|
+
export function catalogueOf(source) {
|
|
93
|
+
const moduleBlock = source.match(/<script\s+module[^>]*>([\s\S]*?)<\/script>/);
|
|
94
|
+
if (!moduleBlock) return null;
|
|
95
|
+
const exportMatch = /export\s+const\s+catalogue\s*=\s*\{/.exec(moduleBlock[1]);
|
|
96
|
+
if (!exportMatch) return null;
|
|
97
|
+
const openIndex = exportMatch.index + exportMatch[0].length - 1;
|
|
98
|
+
const balanced = findBalanced(moduleBlock[1], openIndex);
|
|
99
|
+
if (!balanced) return null;
|
|
100
|
+
|
|
101
|
+
let body = balanced.content;
|
|
102
|
+
let props;
|
|
103
|
+
const propsMatch = /\bprops\s*:\s*\{/.exec(body);
|
|
104
|
+
if (propsMatch) {
|
|
105
|
+
const propsOpen = propsMatch.index + propsMatch[0].length - 1;
|
|
106
|
+
const propsBalanced = findBalanced(body, propsOpen);
|
|
107
|
+
if (propsBalanced) {
|
|
108
|
+
const parsedProps = parseFieldObject(propsBalanced.content);
|
|
109
|
+
if (Object.keys(parsedProps).length) props = parsedProps;
|
|
110
|
+
body = body.slice(0, propsMatch.index) + body.slice(propsBalanced.end);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fields = parseFieldObject(body);
|
|
115
|
+
const catalogue = {
|
|
116
|
+
description: fields.description,
|
|
117
|
+
useFor: fields.useFor,
|
|
118
|
+
notFor: fields.notFor,
|
|
119
|
+
};
|
|
120
|
+
if (props) catalogue.props = props;
|
|
121
|
+
return catalogue;
|
|
26
122
|
}
|
|
27
123
|
|
|
28
124
|
function scaleOf(name) {
|
|
@@ -33,6 +129,41 @@ function scaleOf(name) {
|
|
|
33
129
|
return hit ?? stem.split('-')[0];
|
|
34
130
|
}
|
|
35
131
|
|
|
132
|
+
// Cached per vocabulary: a checker asks for a scale once per finding, and the
|
|
133
|
+
// values come from re-reading tokens.css.
|
|
134
|
+
const scalesCache = new WeakMap();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every design token grouped by its scale, each with the value tokens.css
|
|
138
|
+
* declares for it. The candidates a `color-literal` or `dimension-literal`
|
|
139
|
+
* repair picks from, and what `describeTokens` prints.
|
|
140
|
+
*/
|
|
141
|
+
export function tokenScales(vocab) {
|
|
142
|
+
const cached = scalesCache.get(vocab);
|
|
143
|
+
if (cached) return cached;
|
|
144
|
+
|
|
145
|
+
const values = new Map();
|
|
146
|
+
if (vocab.tokensCssPath) {
|
|
147
|
+
const css = readFileSync(vocab.tokensCssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
148
|
+
for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
149
|
+
if (!values.has(m[1])) values.set(m[1], m[2].trim());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const byScale = new Map();
|
|
153
|
+
for (const name of vocab.themeTokens) {
|
|
154
|
+
const scale = scaleOf(name);
|
|
155
|
+
if (!byScale.has(scale)) byScale.set(scale, []);
|
|
156
|
+
byScale.get(scale).push({ name, value: values.get(name) ?? '' });
|
|
157
|
+
}
|
|
158
|
+
scalesCache.set(vocab, byScale);
|
|
159
|
+
return byScale;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** One scale's tokens, empty when the project has no such scale. */
|
|
163
|
+
export function scaleTokens(vocab, scale) {
|
|
164
|
+
return (scale && tokenScales(vocab).get(scale)) || [];
|
|
165
|
+
}
|
|
166
|
+
|
|
36
167
|
export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
37
168
|
const out = [];
|
|
38
169
|
for (const entry of vocab.components.values()) {
|
|
@@ -50,7 +181,7 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
|
50
181
|
origin: entry.origin,
|
|
51
182
|
file: relative(root, entry.file),
|
|
52
183
|
registered: vocab.builtIn.has(entry.id) || vocab.registered.has(entry.id),
|
|
53
|
-
|
|
184
|
+
catalogue: catalogueOf(source),
|
|
54
185
|
variants: entry.props?.enums.get('variant') ? [...entry.props.enums.get('variant')] : [],
|
|
55
186
|
props,
|
|
56
187
|
tokens: [...entry.tokens].map(([name, value]) => ({ name, default: value })),
|
|
@@ -60,22 +191,9 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
|
60
191
|
}
|
|
61
192
|
|
|
62
193
|
export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
63
|
-
const values = new Map();
|
|
64
|
-
if (vocab.tokensCssPath) {
|
|
65
|
-
const css = readFileSync(vocab.tokensCssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
66
|
-
for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
67
|
-
if (!values.has(m[1])) values.set(m[1], m[2].trim());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const byScale = new Map();
|
|
71
|
-
for (const name of vocab.themeTokens) {
|
|
72
|
-
const scale = scaleOf(name);
|
|
73
|
-
if (!byScale.has(scale)) byScale.set(scale, []);
|
|
74
|
-
byScale.get(scale).push({ name, value: values.get(name) ?? '' });
|
|
75
|
-
}
|
|
76
194
|
return {
|
|
77
195
|
tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
|
|
78
|
-
scales: [...
|
|
196
|
+
scales: [...tokenScales(vocab)].map(([scale, tokens]) => ({ scale, tokens })),
|
|
79
197
|
components: [...vocab.components.values()].map((c) => ({
|
|
80
198
|
id: c.id,
|
|
81
199
|
tokens: [...c.tokens].map(([name, value]) => ({ name, default: value })),
|
|
@@ -83,7 +201,16 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
|
83
201
|
};
|
|
84
202
|
}
|
|
85
203
|
|
|
86
|
-
|
|
204
|
+
function describeLines(c) {
|
|
205
|
+
if (!c.catalogue) return [];
|
|
206
|
+
const { description, useFor, notFor, props } = c.catalogue;
|
|
207
|
+
const lines = [];
|
|
208
|
+
if (description) lines.push(description);
|
|
209
|
+
if (useFor) lines.push(`Use for: ${useFor}`);
|
|
210
|
+
if (notFor) lines.push(`Not for: ${notFor}`);
|
|
211
|
+
for (const [prop, text] of Object.entries(props ?? {})) lines.push(`${prop}: ${text}`);
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
87
214
|
|
|
88
215
|
export function formatComponents(list, { id } = {}) {
|
|
89
216
|
const lines = [];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PKG_ROOT, declaredCustomProperties, extractGlobalRootBlocks } from './tokenVocabulary.mjs';
|
|
4
|
+
|
|
5
|
+
// Property suffixes come from the editor's own kind table, so the checker and
|
|
6
|
+
// the picker can never disagree about what a name means. Read as text rather
|
|
7
|
+
// than imported: this module must load without the compiled engine (CI runs the
|
|
8
|
+
// suite before the plugin is built).
|
|
9
|
+
const ALIAS_KINDS = 'src/editor/core/components/aliasKinds.ts';
|
|
10
|
+
|
|
11
|
+
/** Each kind with the suffixes that name it, in the file's own order, since
|
|
12
|
+
* the first match wins there too. */
|
|
13
|
+
export function readKindRules(root) {
|
|
14
|
+
for (const base of [root, PKG_ROOT]) {
|
|
15
|
+
const path = join(base, ALIAS_KINDS);
|
|
16
|
+
if (!existsSync(path)) continue;
|
|
17
|
+
const src = readFileSync(path, 'utf8');
|
|
18
|
+
const block = src.match(/KIND_RULES[^=]*=\s*\[([\s\S]*?)\n\];/);
|
|
19
|
+
if (!block) continue;
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const m of block[1].matchAll(/\{\s*kind:\s*'([a-z-]+)',\s*suffix:\s*\[([\s\S]*?)\]/g)) {
|
|
22
|
+
out.push({ kind: m[1], suffixes: [...m[2].matchAll(/'-([a-z0-9-]+)'/g)].map((n) => n[1]) });
|
|
23
|
+
}
|
|
24
|
+
if (out.length > 0) return out;
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readKnownSuffixes(root) {
|
|
30
|
+
return [...new Set(readKindRules(root).flatMap((r) => r.suffixes))];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The token scale a property draws its value from, by way of the editor's own
|
|
35
|
+
* kind for it. A kind with no scale behind it — a length, a duration, a font
|
|
36
|
+
* axis — has none, so a literal there has no candidate to offer.
|
|
37
|
+
*/
|
|
38
|
+
const KIND_SCALE = {
|
|
39
|
+
'text-color': 'text',
|
|
40
|
+
surface: 'surface',
|
|
41
|
+
border: 'border',
|
|
42
|
+
radius: 'radius',
|
|
43
|
+
padding: 'space',
|
|
44
|
+
gap: 'space',
|
|
45
|
+
'hairline-inset': 'space',
|
|
46
|
+
'border-width': 'border-width',
|
|
47
|
+
'hairline-width': 'border-width',
|
|
48
|
+
'indicator-width': 'border-width',
|
|
49
|
+
shadow: 'shadow',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const COLOR_SCALES = ['text', 'surface', 'border'];
|
|
53
|
+
export const GEOMETRY_SCALES = ['space', 'radius', 'border-width', 'shadow'];
|
|
54
|
+
|
|
55
|
+
export function tokenScale(token, kindRules, wanted) {
|
|
56
|
+
const bare = SIDE_SUFFIXES.find((x) => token.endsWith(x)) ? token.slice(0, token.lastIndexOf('-')) : token;
|
|
57
|
+
const rule = kindRules.find((r) => r.suffixes.some((s) => bare.endsWith(`-${s}`)));
|
|
58
|
+
const scale = rule ? (KIND_SCALE[rule.kind] ?? null) : null;
|
|
59
|
+
return wanted.includes(scale) ? scale : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Per-side padding names (`--card-body-padding-top`) are written by the padding
|
|
63
|
+
// selector, never declared by hand, and belong with their parent.
|
|
64
|
+
export const SIDE_SUFFIXES = ['-top', '-right', '-bottom', '-left'];
|
|
65
|
+
|
|
66
|
+
// State tokens that must come *before* the property, never after.
|
|
67
|
+
export const STATE_TOKENS = ['hover', 'disabled', 'selected', 'focus', 'active', 'focused'];
|
|
68
|
+
|
|
69
|
+
export function tokenSuffix(token, known) {
|
|
70
|
+
const bare = SIDE_SUFFIXES.find((x) => token.endsWith(x)) ? token.slice(0, token.lastIndexOf('-')) : token;
|
|
71
|
+
for (const suffix of known) {
|
|
72
|
+
if (bare.endsWith(`-${suffix}`)) return suffix;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Token patterns the editor declares in `intrinsics` — the only tokens allowed a
|
|
79
|
+
* bare keyword instead of a theme token.
|
|
80
|
+
*
|
|
81
|
+
* Matched on each spec's `variable`, not its `key`: the two need not agree
|
|
82
|
+
* (Image's `zoom` key declares `--image-zoom-enabled`), and `variable` is what
|
|
83
|
+
* actually names the token. A `${...}` hole stands for a variant segment.
|
|
84
|
+
*/
|
|
85
|
+
export function intrinsicMatchers(editor) {
|
|
86
|
+
const block = editor.match(/export\s+const\s+intrinsics[^=]*=\s*\[([\s\S]*?)\];/);
|
|
87
|
+
if (!block) return [];
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const m of block[1].matchAll(/\bvariable\s*:[^`'"]*[`'"]([^`'"]+)[`'"]/g)) {
|
|
90
|
+
const pattern = m[1]
|
|
91
|
+
.replace(/[.*+?^${}()|[\]\\]/g, (c) => (c === '$' ? '$' : `\\${c}`))
|
|
92
|
+
.replace(/\$\\\{[^}]*\\\}/g, '[a-z0-9-]+')
|
|
93
|
+
.replace(/\$\{[^}]*\}/g, '[a-z0-9-]+');
|
|
94
|
+
out.push(new RegExp(`^${pattern}$`));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Every custom property the `:global(:root)` blocks declare, comments ignored. */
|
|
100
|
+
export function declaredTokens(blocks) {
|
|
101
|
+
const declared = new Set();
|
|
102
|
+
for (const block of blocks) {
|
|
103
|
+
for (const n of declaredCustomProperties(block.replace(/\/\*[\s\S]*?\*\//g, ' '))) declared.add(n);
|
|
104
|
+
}
|
|
105
|
+
return declared;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Properties a component declares that nothing in its own file reads. A read is
|
|
110
|
+
* the name appearing outside the `:global(:root)` block: in a `var()`, in a
|
|
111
|
+
* `style:` directive, or as the string a padding mixin takes. SCSS interpolation
|
|
112
|
+
* (`--badge-#{$v}-surface`) reads every property the pattern covers. A per-side
|
|
113
|
+
* padding is read through its parent.
|
|
114
|
+
*/
|
|
115
|
+
export function unreadTokens(source, tokens) {
|
|
116
|
+
let body = source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/<!--[\s\S]*?-->/g, ' ');
|
|
117
|
+
for (const block of extractGlobalRootBlocks(body)) body = body.replace(block, ' ');
|
|
118
|
+
const patterns = [...body.matchAll(/--[a-z0-9-]*(?:#\{[^}]*\}[a-z0-9-]*)+/g)].map(
|
|
119
|
+
(m) => new RegExp(`^${m[0].replace(/[.*+?^()|[\]\\]/g, '\\$&').replace(/#\{[^}]*\}/g, '[a-z0-9-]+')}$`),
|
|
120
|
+
);
|
|
121
|
+
const isRead = (name) => body.includes(name) || patterns.some((re) => re.test(name));
|
|
122
|
+
return [...tokens].filter((name) => {
|
|
123
|
+
const side = SIDE_SUFFIXES.find((s) => name.endsWith(s));
|
|
124
|
+
return !isRead(name) && !(side && isRead(name.slice(0, -side.length)));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Every token the editor names in a row: `variable: '--x'`, the type-group
|
|
130
|
+
* `colorVariable` / `familyVariable` / ... keys, and the arrow form intrinsics
|
|
131
|
+
* use. A `${...}` hole stands for a variant segment. Per-side padding names are
|
|
132
|
+
* written by the padding selector rather than declared, so they resolve to
|
|
133
|
+
* their parent.
|
|
134
|
+
*/
|
|
135
|
+
export function editorTokenRefs(editor) {
|
|
136
|
+
const literals = new Set();
|
|
137
|
+
const patterns = [];
|
|
138
|
+
for (const m of editor.matchAll(/\b(?:variable|[a-zA-Z]+Variable)\s*:\s*(?:\([^)]*\)\s*=>\s*)?[`'"](--(?:\$\{[^}]*\}|[^`'"])+)[`'"]/g)) {
|
|
139
|
+
const name = stripSide(m[1]);
|
|
140
|
+
if (name.includes('${')) {
|
|
141
|
+
patterns.push([name, new RegExp(`^${name.replace(/[.*+?^()|[\]\\]/g, '\\$&').replace(/\$\{[^}]*\}/g, '[a-z0-9-]+')}$`)]);
|
|
142
|
+
} else {
|
|
143
|
+
literals.add(name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { literals, patterns };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripSide(token) {
|
|
150
|
+
const side = SIDE_SUFFIXES.find((x) => token.endsWith(x));
|
|
151
|
+
return side ? token.slice(0, -side.length) : token;
|
|
152
|
+
}
|
package/bin/lib/cssValues.mjs
CHANGED
|
@@ -48,3 +48,12 @@ export function stripVarFallbacks(value) {
|
|
|
48
48
|
export function blankStrings(css) {
|
|
49
49
|
return css.replace(/(["'])(?:\\.|(?!\1)[^\\])*\1/g, (m) => m[0] + ' '.repeat(m.length - 2) + m[0]);
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/** The token scale a CSS property draws its colour from. The role inside that
|
|
53
|
+
* scale stays the user's choice. */
|
|
54
|
+
export function colorScaleOfProperty(prop) {
|
|
55
|
+
if (prop === 'color') return 'text';
|
|
56
|
+
if (/^background/.test(prop)) return 'surface';
|
|
57
|
+
if (/^(border|outline)(-|$)/.test(prop)) return 'border';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Resolves the project's live-tokens data directory and its optional
|
|
2
|
+
// `live-tokens.testing.ts` settings file, statically — no engine import, no
|
|
3
|
+
// dynamic import of the consumer's own config. Shared by `contractRunner.mjs`
|
|
4
|
+
// (which isolates a copy of this directory before spawning the test tools)
|
|
5
|
+
// and the `config-token` rule in `bin/rules/tokens.mjs` (which reads
|
|
6
|
+
// `component-configs/<id>/default.json` under it directly), so the two agree
|
|
7
|
+
// on which tree a project's saved assignments live in.
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export function settingsFilePath(root) {
|
|
13
|
+
for (const name of ['live-tokens.testing.ts', 'live-tokens.testing.mts', 'live-tokens.testing.js', 'live-tokens.testing.mjs']) {
|
|
14
|
+
const path = join(root, name);
|
|
15
|
+
if (existsSync(path)) return path;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A generated config's own extensionless relative imports (Vite/Playwright's
|
|
22
|
+
* loaders resolve those, matching how this repo's own `vite.config.ts` and
|
|
23
|
+
* `live-tokens.testing.ts` are written) only get that treatment for a
|
|
24
|
+
* *static* import. A dynamic `import()` of the same path, called after a Vite
|
|
25
|
+
* config finishes loading, runs through plain Node resolution instead and
|
|
26
|
+
* fails on the same files: verified against `vitest.contract.config.ts` with
|
|
27
|
+
* both a `file://` URL and a plain absolute path as the dynamic specifier,
|
|
28
|
+
* identical `ERR_MODULE_NOT_FOUND` both times. So neither `settings.viteConfig`
|
|
29
|
+
* nor `settings.dataDir` (the resolved values `resolveTestingConfig` computes)
|
|
30
|
+
* has a reader here on purpose — reading either would mean importing the
|
|
31
|
+
* settings file dynamically first, which reintroduces exactly this failure.
|
|
32
|
+
* Regexing the settings file's *source text* for a field, imprecise as that
|
|
33
|
+
* is, is what stays inside the static-import constraint: a wrong guess still
|
|
34
|
+
* fails loudly (a bad `viteConfig` throws on its own static import in the
|
|
35
|
+
* generated file; a bad `dataDir` throws "no data directory at ..." below),
|
|
36
|
+
* never as a silent pass. A settings-level `dataDir` this cannot see at all
|
|
37
|
+
* is worse than one resolved this imprecisely, since a project that names its
|
|
38
|
+
* data directory only in `live-tokens.testing.ts` would otherwise have its
|
|
39
|
+
* contracts checked against whatever happens to sit at the default path.
|
|
40
|
+
*/
|
|
41
|
+
function stripComments(text) {
|
|
42
|
+
// Block comments first: a `//` inside one (`/* // note */`) must not seed a
|
|
43
|
+
// second, overlapping strip.
|
|
44
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Measured across eleven settings-file shapes: a commented-out `dataDir:`
|
|
48
|
+
* above the real one won the regex, because the regex only sees text
|
|
49
|
+
* position, never comment syntax, and a comment naming the field with
|
|
50
|
+
* nothing else present invented a setting out of prose. Comments are
|
|
51
|
+
* stripped before the field ever gets a chance to match. A field present in
|
|
52
|
+
* live code as a template literal, a computed value, or an import throws
|
|
53
|
+
* instead of falling through to a default silently: the two guesses this
|
|
54
|
+
* feeds, `viteConfig` and `dataDir`, can each resolve to a real path that
|
|
55
|
+
* simply names the wrong tree, which then reads as a clean run. */
|
|
56
|
+
export function scrapeSettingsField(settingsPath, fieldName) {
|
|
57
|
+
if (!settingsPath) return null;
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = readFileSync(settingsPath, 'utf8');
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const live = stripComments(text);
|
|
65
|
+
const literal = new RegExp(`\\b${fieldName}\\s*:\\s*['"]([^'"]+)['"]`).exec(live);
|
|
66
|
+
if (literal) return literal[1];
|
|
67
|
+
if (new RegExp(`\\b${fieldName}\\s*:`).test(live)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`"${fieldName}" in ${settingsPath} is set to something other than a plain string literal, ` +
|
|
70
|
+
`so it cannot be read statically. Use a literal string, or remove the key to fall back to the default.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `dataDir` as the plugin resolves it: a `dataDir` field scraped from the
|
|
78
|
+
* settings file (see above), else `live-tokens.config.json`'s own key, else
|
|
79
|
+
* the default. Mirrors `resolveTestingConfig`'s own `configuredDataDir`
|
|
80
|
+
* fallback (`src/testing/config.ts`) rather than importing it: that module
|
|
81
|
+
* compiles into `src/testing-js`, which does not exist until `build:testing`
|
|
82
|
+
* runs, and importing the *source* `.ts` module to reach it hits the same
|
|
83
|
+
* extensionless-import failure documented above — measured with
|
|
84
|
+
* `src/testing-js` moved aside, `resolveTestingEntry`'s `.ts` fallback threw
|
|
85
|
+
* exactly that trying to load `vitest.ts`. Duplicating this small a resolver
|
|
86
|
+
* has precedent in this module already, in `settingsFilePath`.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveSourceDataDir(root, settingsPath) {
|
|
89
|
+
const scraped = scrapeSettingsField(settingsPath, 'dataDir');
|
|
90
|
+
if (scraped) return resolve(root, scraped);
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(readFileSync(join(root, 'live-tokens.config.json'), 'utf8'));
|
|
93
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.dataDir === 'string') {
|
|
94
|
+
return resolve(root, parsed.dataDir);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Missing or unparseable reads as absent, matching the plugin's own resolver.
|
|
98
|
+
}
|
|
99
|
+
return resolve(root, 'src/live-tokens/data');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `component-configs/<id>/default.json` under the source data directory, as
|
|
104
|
+
* `{ path, text, data }`, or null when there is none to read. A non-literal
|
|
105
|
+
* `dataDir` in `live-tokens.testing.ts` (a template literal, a computed value)
|
|
106
|
+
* is only resolvable at `--tests` time, when the settings module itself runs;
|
|
107
|
+
* `resolveSourceDataDir` throws rather than guess. A plain-Node rule has no
|
|
108
|
+
* settings module to run, so it reads that as no config instead of taking the
|
|
109
|
+
* whole static lint down with it. Unparseable JSON reads as no config too.
|
|
110
|
+
*/
|
|
111
|
+
export function readComponentConfig(root, id) {
|
|
112
|
+
let dataDir;
|
|
113
|
+
try {
|
|
114
|
+
dataDir = resolveSourceDataDir(root, settingsFilePath(root));
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const path = join(dataDir, 'component-configs', id, 'default.json');
|
|
119
|
+
if (!existsSync(path)) return null;
|
|
120
|
+
const text = readFileSync(path, 'utf8');
|
|
121
|
+
try {
|
|
122
|
+
return { path, text, data: JSON.parse(text) };
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|