@motion-proto/live-tokens 0.74.0 → 0.76.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 +33 -38
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
- package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
- package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
- package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
- package/.claude/skills/live-tokens-create-theme/SKILL.md +52 -48
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
- package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
- package/.claude/skills/live-tokens-set-colors/SKILL.md +47 -38
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +58 -35
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +5 -3
- package/.claude/skills/live-tokens-set-type/SKILL.md +30 -30
- package/CHANGELOG.md +138 -0
- package/README.md +17 -7
- package/bin/check-component.mjs +91 -19
- package/bin/check-page.mjs +102 -21
- package/bin/cli.mjs +87 -113
- package/bin/contractRunner.mjs +945 -0
- package/bin/create.mjs +1 -1
- package/bin/lib/catalogue.mjs +37 -30
- package/bin/lib/findings.mjs +46 -15
- package/bin/lib/report.mjs +3 -3
- package/bin/lib/tokenVocabulary.mjs +4 -4
- package/bin/migrate-routes.mjs +5 -5
- package/bin/migrate.mjs +4 -4
- package/bin/save-theme.mjs +8 -9
- package/bin/set-colors.mjs +9 -11
- package/bin/set-geometry.mjs +7 -7
- package/bin/set-type.mjs +5 -7
- package/bin/setup-claude.mjs +110 -0
- package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
- package/dist-plugin/{chunk-7VRTBGJT.js → chunk-PDNL4NC5.js} +9 -2
- package/dist-plugin/{chunk-V3YF6CGT.js → chunk-SWXRVZKT.js} +43 -1
- package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.cts} +1 -0
- package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.ts} +1 -0
- package/dist-plugin/index.cjs +310 -147
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +218 -122
- package/dist-plugin/migrateData/index.cjs +66 -2
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +3 -3
- package/dist-plugin/setColors/index.cjs +61 -4
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +5 -5
- package/dist-plugin/setGeometry/index.cjs +107 -44
- package/dist-plugin/setGeometry/index.d.cts +5 -5
- package/dist-plugin/setGeometry/index.d.ts +5 -5
- package/dist-plugin/setGeometry/index.js +51 -45
- package/dist-plugin/setType/index.cjs +15 -0
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/setType/index.js +1 -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 +35 -8
- package/src/app/site.css +19 -9
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
- package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
- package/src/editor/component-editor/TabBarEditor.svelte +1 -1
- package/src/editor/component-editor/TableEditor.svelte +2 -2
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
- package/src/editor/core/components/adjustAliases.ts +59 -45
- package/src/editor/core/components/aliasKinds.ts +9 -5
- package/src/editor/core/preview/themePreview.ts +9 -2
- package/src/editor/core/sketch/sketchLayer.ts +22 -0
- package/src/editor/core/store/editorStore.ts +10 -1
- package/src/editor/core/themes/buildColors.ts +3 -3
- package/src/editor/core/themes/liveStateStream.ts +26 -0
- package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/themeDocumentSync.ts +22 -11
- package/src/editor/core/themes/themeService.ts +9 -2
- package/src/editor/pages/ComponentEditorPage.svelte +17 -1
- package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +75 -573
- package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
- package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
- package/src/editor/skill-atlas/edges.ts +31 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +20 -19
- package/src/editor/skill-atlas/skillTrees.ts +19 -3840
- package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
- package/src/editor/skill-atlas/trees/create-component.ts +275 -0
- package/src/editor/skill-atlas/trees/create-page.ts +320 -0
- package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
- package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
- package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
- package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
- package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
- package/src/editor/skill-atlas/trees/set-type.ts +142 -0
- package/src/editor/skill-atlas/types.ts +3 -4
- package/src/editor/skill-atlas/wireLayout.ts +287 -0
- package/src/live-tokens/data/themes/autumn.json +15 -15
- package/src/live-tokens/data/themes/halloween.json +15 -15
- package/src/live-tokens/data/themes/midnight-study.json +15 -15
- package/src/live-tokens/data/themes/ocean.json +15 -15
- package/src/live-tokens/data/themes/royal-velvet.json +15 -15
- package/src/live-tokens/data/themes/sketchy.json +15 -15
- package/src/live-tokens/data/themes/spring-meadow.json +15 -15
- package/src/live-tokens/data/themes/sunset.json +15 -15
- package/src/system/components/Badge.svelte +7 -0
- package/src/system/components/Button.svelte +7 -0
- package/src/system/components/Callout.svelte +10 -6
- package/src/system/components/Card.svelte +23 -4
- package/src/system/components/CodeSnippet.svelte +4 -3
- package/src/system/components/CollapsibleSection.svelte +23 -8
- package/src/system/components/CornerBadge.svelte +6 -0
- package/src/system/components/Dialog.svelte +13 -6
- package/src/system/components/IconButton.svelte +9 -0
- package/src/system/components/Image.svelte +8 -0
- package/src/system/components/ImageLightbox.svelte +6 -0
- package/src/system/components/InlineEditActions.svelte +7 -0
- package/src/system/components/Input.svelte +7 -0
- package/src/system/components/MenuSelect.svelte +7 -0
- package/src/system/components/Notification.svelte +7 -0
- package/src/system/components/Panel.svelte +6 -0
- package/src/system/components/ProgressBar.svelte +5 -0
- package/src/system/components/RadioButton.svelte +11 -5
- package/src/system/components/SectionDivider.svelte +8 -0
- package/src/system/components/SegmentedControl.svelte +6 -0
- package/src/system/components/SideNavigation.svelte +6 -0
- package/src/system/components/Slider.svelte +7 -4
- package/src/system/components/TabBar.svelte +15 -9
- package/src/system/components/Table.svelte +8 -3
- package/src/system/components/Toggle.svelte +4 -4
- package/src/system/components/Tooltip.svelte +6 -0
- package/src/testing-js/chunk-AO7EZHYV.js +776 -0
- package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
- package/src/testing-js/chunk-FAFOAWYL.js +39 -0
- package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
- package/src/testing-js/chunk-L73N4NSO.js +23 -0
- package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
- package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
- package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
- package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
- package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +81 -0
- package/src/testing-js/component-alias.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +62 -0
- package/src/testing-js/component-editor.contract.js.map +1 -0
- package/src/testing-js/component-render.contract.js +568 -0
- package/src/testing-js/component-render.contract.js.map +1 -0
- package/src/testing-js/index.d.ts +293 -0
- package/src/testing-js/index.js +222 -0
- package/src/testing-js/index.js.map +1 -0
- package/src/testing-js/registry.contract.js +39 -0
- package/src/testing-js/registry.contract.js.map +1 -0
- package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
- package/src/testing-js/vitest.d.ts +3 -0
- package/src/testing-js/vitest.js +13 -0
- package/src/testing-js/vitest.js.map +1 -0
- package/template/README.md +13 -0
- package/template/_gitignore +6 -0
- package/template/package.json +3 -1
- package/template/src/pages/Home.svelte +4 -18
- package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
- package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
package/bin/check-component.mjs
CHANGED
|
@@ -43,6 +43,60 @@ export const COMPONENT_RULES = {
|
|
|
43
43
|
'default-not-token': 'error',
|
|
44
44
|
'phantom-link': 'warn',
|
|
45
45
|
'dimension-literal': 'warn',
|
|
46
|
+
// `--tests` (bin/contractRunner.mjs). Fixed by design decision 8 so
|
|
47
|
+
// `fix-findings` can map them; every one is an error, including the setup
|
|
48
|
+
// rules, which `--tests` treats as never-silenceable (see cli.mjs).
|
|
49
|
+
'contract-registry': 'error',
|
|
50
|
+
'contract-render': 'error',
|
|
51
|
+
'contract-alias': 'error',
|
|
52
|
+
'contract-persist': 'error',
|
|
53
|
+
'contract-theme': 'error',
|
|
54
|
+
'contract-states': 'error',
|
|
55
|
+
'contract-interaction': 'error',
|
|
56
|
+
'contract-listed': 'error',
|
|
57
|
+
'contract-sketch': 'error',
|
|
58
|
+
'tests-not-installed': 'error',
|
|
59
|
+
'tests-setup': 'error',
|
|
60
|
+
'tests-incomplete': 'error',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where a rule is fixed, as a stable slug carried on the finding. The skills
|
|
65
|
+
* resolve the slug to one of their own headings, so adding a rule here costs
|
|
66
|
+
* no skill edit as long as it reuses a slug. Two skills used to enumerate every
|
|
67
|
+
* rule id in their own tables, with nothing holding the two in agreement or
|
|
68
|
+
* checking either against this list.
|
|
69
|
+
*/
|
|
70
|
+
export const COMPONENT_RULE_FIX = {
|
|
71
|
+
'unknown-suffix': 'property-name',
|
|
72
|
+
'state-after-property': 'property-name',
|
|
73
|
+
'disabled-is-terminal': 'property-name',
|
|
74
|
+
'default-not-token': 'property-token',
|
|
75
|
+
'color-literal': 'property-token',
|
|
76
|
+
'dimension-literal': 'property-token',
|
|
77
|
+
'unknown-token-ref': 'property-token',
|
|
78
|
+
'contract-theme': 'property-token',
|
|
79
|
+
'invalid-id': 'runtime',
|
|
80
|
+
'missing-file': 'runtime',
|
|
81
|
+
'missing-root-block': 'runtime',
|
|
82
|
+
'no-tokens': 'runtime',
|
|
83
|
+
'contract-persist': 'runtime-defaults',
|
|
84
|
+
'missing-component-const': 'editor',
|
|
85
|
+
'missing-all-tokens': 'editor',
|
|
86
|
+
'phantom-editor-token': 'editor',
|
|
87
|
+
'phantom-link': 'editor',
|
|
88
|
+
'deep-import': 'editor',
|
|
89
|
+
'contract-render': 'editor',
|
|
90
|
+
'contract-alias': 'editor',
|
|
91
|
+
'contract-states': 'editor',
|
|
92
|
+
'contract-interaction': 'editor',
|
|
93
|
+
'missing-registration': 'registration',
|
|
94
|
+
'contract-registry': 'registration',
|
|
95
|
+
'contract-listed': 'registration',
|
|
96
|
+
'contract-sketch': 'sketch',
|
|
97
|
+
'tests-not-installed': 'tooling',
|
|
98
|
+
'tests-setup': 'tooling',
|
|
99
|
+
'tests-incomplete': 'coverage',
|
|
46
100
|
};
|
|
47
101
|
|
|
48
102
|
// Shipped components keep their editor beside the other editors; a
|
|
@@ -97,6 +151,35 @@ function capitalize(id) {
|
|
|
97
151
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
98
152
|
}
|
|
99
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Where `id`'s runtime and editor files live, resolved the same way
|
|
156
|
+
* `checkComponent` does: the real on-disk filename rather than a capitalised
|
|
157
|
+
* guess, since `cornerbadge` ships as `CornerBadge.svelte`. Exported so a
|
|
158
|
+
* caller outside the lint (the `--tests` runner, attributing a contract
|
|
159
|
+
* finding to a consumer artifact) doesn't re-derive it differently.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveComponentPaths(id, root = process.cwd()) {
|
|
162
|
+
// A consumer names a shipped id and owns no copy of its files. Falling back
|
|
163
|
+
// to the package keeps the lint reading the same source the contract run
|
|
164
|
+
// resolves, instead of reporting the component's own files missing. In this
|
|
165
|
+
// repo the consumer paths always exist, so the fallback never fires here.
|
|
166
|
+
const roots = builtInIds(root).has(id) ? [root, PKG_ROOT] : [root];
|
|
167
|
+
for (const base of roots) {
|
|
168
|
+
const dir = join(base, 'src/system/components');
|
|
169
|
+
const Id =
|
|
170
|
+
(existsSync(dir) ? readdirSync(dir) : [])
|
|
171
|
+
.find((f) => f.toLowerCase() === `${id}.svelte`)
|
|
172
|
+
?.replace('.svelte', '') ?? capitalize(id);
|
|
173
|
+
const runtimePath = join(dir, `${Id}.svelte`);
|
|
174
|
+
const editorPath =
|
|
175
|
+
EDITOR_DIRS.map((d) => join(base, d, `${Id}Editor.svelte`)).find(existsSync) ??
|
|
176
|
+
join(base, EDITOR_DIRS[0], `${Id}Editor.svelte`);
|
|
177
|
+
if (base === roots.at(-1) || (existsSync(runtimePath) && existsSync(editorPath))) {
|
|
178
|
+
return { Id, runtimePath, editorPath };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
100
183
|
function extractImports(source) {
|
|
101
184
|
const out = [];
|
|
102
185
|
const re = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
@@ -267,10 +350,10 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
|
|
|
267
350
|
record(
|
|
268
351
|
'unknown-token-ref',
|
|
269
352
|
STATE_TOKENS.includes(ref.replace(/^--/, '').split('-')[0])
|
|
270
|
-
? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name
|
|
353
|
+
? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name. Read the token the state should paint`
|
|
271
354
|
: isContractToken(ref)
|
|
272
|
-
? `${rel}: ${name} reads ${ref}, which
|
|
273
|
-
: `${rel}: ${name} reads ${ref}, which is not a
|
|
355
|
+
? `${rel}: ${name} reads ${ref}, which has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
356
|
+
: `${rel}: ${name} reads ${ref}, which is not a design token or a semantic property`,
|
|
274
357
|
at,
|
|
275
358
|
);
|
|
276
359
|
}
|
|
@@ -278,7 +361,7 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
|
|
|
278
361
|
if (hasColorLiteral(painted)) {
|
|
279
362
|
record(
|
|
280
363
|
'color-literal',
|
|
281
|
-
`${rel}: ${name}: ${value} is a colour literal; defaults must reference
|
|
364
|
+
`${rel}: ${name}: ${value} is a colour literal; defaults must reference design tokens (e.g. var(--surface-primary))`,
|
|
282
365
|
at,
|
|
283
366
|
);
|
|
284
367
|
continue;
|
|
@@ -287,7 +370,7 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
|
|
|
287
370
|
if (refs.length === 0 && !matchers.some((re) => re.test(name))) {
|
|
288
371
|
record(
|
|
289
372
|
'default-not-token',
|
|
290
|
-
`${rel}: ${name}: ${value} has no
|
|
373
|
+
`${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`,
|
|
291
374
|
at,
|
|
292
375
|
);
|
|
293
376
|
}
|
|
@@ -326,18 +409,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
|
|
|
326
409
|
return done();
|
|
327
410
|
}
|
|
328
411
|
|
|
329
|
-
|
|
330
|
-
// ships as `CornerBadge.svelte`, and the casing is what gives the kebab form
|
|
331
|
-
// of its token prefix.
|
|
332
|
-
const dir = join(root, 'src/system/components');
|
|
333
|
-
const Id =
|
|
334
|
-
(existsSync(dir) ? readdirSync(dir) : [])
|
|
335
|
-
.find((f) => f.toLowerCase() === `${id}.svelte`)
|
|
336
|
-
?.replace('.svelte', '') ?? capitalize(id);
|
|
337
|
-
const runtimePath = join(dir, `${Id}.svelte`);
|
|
338
|
-
const editorPath =
|
|
339
|
-
EDITOR_DIRS.map((d) => join(root, d, `${Id}Editor.svelte`)).find(existsSync) ??
|
|
340
|
-
join(root, EDITOR_DIRS[0], `${Id}Editor.svelte`);
|
|
412
|
+
const { Id, runtimePath, editorPath } = resolveComponentPaths(id, root);
|
|
341
413
|
|
|
342
414
|
file = relative(root, runtimePath);
|
|
343
415
|
if (!existsSync(runtimePath)) {
|
|
@@ -381,7 +453,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
|
|
|
381
453
|
record(
|
|
382
454
|
'state-after-property',
|
|
383
455
|
`${relative(root, runtimePath)}: ${token} has '${trailingState}' after the property; ` +
|
|
384
|
-
`state must come before property (e.g. -${trailingState}-surface
|
|
456
|
+
`state must come before property (e.g. -${trailingState}-surface)`,
|
|
385
457
|
runtime.indexOf(token),
|
|
386
458
|
);
|
|
387
459
|
}
|
|
@@ -512,7 +584,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
|
|
|
512
584
|
if (!registrationFile) {
|
|
513
585
|
record(
|
|
514
586
|
'missing-registration',
|
|
515
|
-
`no registration for '${id}' under src
|
|
587
|
+
`no registration for '${id}' under src/. Expected registerComponent({ id: '${id}', ... }) or bootLiveTokens({ components: [{ id: '${id}', ... }] })`,
|
|
516
588
|
);
|
|
517
589
|
} else {
|
|
518
590
|
// Check the registration file's imports too.
|
package/bin/check-page.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Static validator for a live-tokens page.
|
|
2
2
|
//
|
|
3
3
|
// Asserts that a page satisfies the contract described in the
|
|
4
|
-
// live-tokens-
|
|
4
|
+
// live-tokens-create-page skill: it is assembled from catalogue components, and
|
|
5
5
|
// every value in its CSS is a theme token rather than a literal. The rules and
|
|
6
6
|
// their default severities are in PAGE_RULES; each is overridable per project
|
|
7
7
|
// (live-tokens.config.json) or per run (--off/--warn/--error/--strict), because
|
|
@@ -26,10 +26,13 @@ export const PAGE_RULES = {
|
|
|
26
26
|
'color-literal': 'error',
|
|
27
27
|
'reserved-route': 'error',
|
|
28
28
|
'site-css-in-main': 'error',
|
|
29
|
+
'raw-text-axis': 'error',
|
|
29
30
|
'dimension-literal': 'warn',
|
|
30
31
|
'hardcoded-columns': 'warn',
|
|
31
|
-
'raw-text-axis': 'warn',
|
|
32
32
|
'missing-source': 'warn',
|
|
33
|
+
'control-size': 'warn',
|
|
34
|
+
'multiple-primary': 'warn',
|
|
35
|
+
'danger-without-dialog': 'warn',
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
// Directories that hold the system, not pages built on it.
|
|
@@ -45,6 +48,13 @@ const DEEP_IMPORT_PATTERNS = [
|
|
|
45
48
|
|
|
46
49
|
const TEXT_AXES = ['font-size', 'font-family', 'font-weight', 'line-height', 'letter-spacing'];
|
|
47
50
|
|
|
51
|
+
// The single-axis scales in tokens.css. A text style bundle carries its axis
|
|
52
|
+
// as a suffix (--body-md-font-size, --code-font-family), so no bundle name
|
|
53
|
+
// matches, and neither does a custom property the page declares itself. A
|
|
54
|
+
// weight alone cannot move the scale or the fonts, so --font-weight-* is not one.
|
|
55
|
+
const SINGLE_AXIS_TOKEN =
|
|
56
|
+
/^--(?:font-size|line-height|letter-spacing)-|^--font-(?:sans|serif|mono|display|editorial)$/;
|
|
57
|
+
|
|
48
58
|
// The geometry the theme owns: spacing, stroke, radius, and shadow all have a
|
|
49
59
|
// token scale, and `set-geometry` moves them. Sizing (a hero's height, a
|
|
50
60
|
// column's minimum width, a max content width) is layout, has no scale, and
|
|
@@ -184,7 +194,10 @@ function tagAttributes(code, start) {
|
|
|
184
194
|
return { attrs, end: tagEnd };
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* Props a page passes that the component does not declare, values outside a
|
|
199
|
+
* prop's union, and a shipped component the page sizes itself.
|
|
200
|
+
*/
|
|
188
201
|
function checkComponentUsage(code, imports, add) {
|
|
189
202
|
for (const [local, entry] of imports) {
|
|
190
203
|
const props = entry.props;
|
|
@@ -203,11 +216,64 @@ function checkComponentUsage(code, imports, add) {
|
|
|
203
216
|
if (allowed && value !== null && !allowed.has(value)) {
|
|
204
217
|
add('unknown-prop-value', index, `${entry.name} ${name}="${value}" is not one of ${[...allowed].join(', ')}`);
|
|
205
218
|
}
|
|
219
|
+
if (name === 'size' && entry.origin === 'shipped') {
|
|
220
|
+
add(
|
|
221
|
+
'control-size',
|
|
222
|
+
index,
|
|
223
|
+
`${entry.name} ${value === null ? 'is sized here' : `size="${value}"`}. Drop it for the shipped default, or retune ${entry.name} for the whole project in /live-tokens/components.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
206
226
|
}
|
|
207
227
|
}
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
/**
|
|
232
|
+
* One finding when a page holds more than one primary Button, at the second of
|
|
233
|
+
* them. Emphasis is what the variant carries, so a second primary leaves the
|
|
234
|
+
* page with no single most important action. A Button with no variant is
|
|
235
|
+
* primary, the component's default.
|
|
236
|
+
*/
|
|
237
|
+
function checkPrimaryActions(code, imports, add) {
|
|
238
|
+
const primaries = [];
|
|
239
|
+
for (const [local, entry] of imports) {
|
|
240
|
+
if (entry.id !== 'button') continue;
|
|
241
|
+
for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
|
|
242
|
+
const tag = tagAttributes(code, m.index);
|
|
243
|
+
const variant = tag?.attrs.find((a) => a.name === 'variant');
|
|
244
|
+
if (tag && (variant === undefined || variant.value === 'primary')) primaries.push(m.index);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (primaries.length < 2) return;
|
|
248
|
+
primaries.sort((a, b) => a - b);
|
|
249
|
+
add(
|
|
250
|
+
'multiple-primary',
|
|
251
|
+
primaries[1],
|
|
252
|
+
`${primaries.length} primary Buttons in this page. Keep the most important action primary and make the other ${primaries.length - 1} secondary.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* One finding when a page holds a danger Button and imports no Dialog. A
|
|
258
|
+
* danger action destroys saved work, and the page has nothing to confirm it.
|
|
259
|
+
*/
|
|
260
|
+
function checkDestructiveActions(code, imports, add) {
|
|
261
|
+
if ([...imports.values()].some((entry) => entry.id === 'dialog')) return;
|
|
262
|
+
for (const [local, entry] of imports) {
|
|
263
|
+
if (entry.id !== 'button' && entry.id !== 'iconbutton') continue;
|
|
264
|
+
for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
|
|
265
|
+
const tag = tagAttributes(code, m.index);
|
|
266
|
+
if (!tag?.attrs.some((a) => a.name === 'variant' && a.value === 'danger')) continue;
|
|
267
|
+
add(
|
|
268
|
+
'danger-without-dialog',
|
|
269
|
+
m.index,
|
|
270
|
+
`${entry.name} variant="danger" with no Dialog in this page. A destructive action confirms in a Dialog before it runs.`,
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
/** The object literal enclosing `index`, found by balancing braces outward. */
|
|
212
278
|
function enclosingObject(text, index) {
|
|
213
279
|
let depth = 0;
|
|
@@ -267,6 +333,8 @@ function checkFile(file, text, vocab, root) {
|
|
|
267
333
|
if (local) imports.set(local, entry);
|
|
268
334
|
}
|
|
269
335
|
checkComponentUsage(code, imports, add);
|
|
336
|
+
checkPrimaryActions(code, imports, add);
|
|
337
|
+
checkDestructiveActions(code, imports, add);
|
|
270
338
|
|
|
271
339
|
for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
|
|
272
340
|
add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
|
|
@@ -315,8 +383,8 @@ function checkFile(file, text, vocab, root) {
|
|
|
315
383
|
'unknown-token',
|
|
316
384
|
at(m.index),
|
|
317
385
|
isContractToken(name)
|
|
318
|
-
? `${name}
|
|
319
|
-
: `${name} is not a
|
|
386
|
+
? `${name} has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
387
|
+
: `${name} is not a design token, a semantic property, or declared in this file`,
|
|
320
388
|
);
|
|
321
389
|
}
|
|
322
390
|
|
|
@@ -328,25 +396,38 @@ function checkFile(file, text, vocab, root) {
|
|
|
328
396
|
// inside one is not the page's value.
|
|
329
397
|
const painted = stripVarFallbacks(value);
|
|
330
398
|
if (!TEXT_AXES.includes(prop) && hasColorLiteral(painted)) {
|
|
331
|
-
add('color-literal', at(index), `${prop}: ${value}. Use a
|
|
399
|
+
add('color-literal', at(index), `${prop}: ${value}. Use a design token.`);
|
|
332
400
|
continue;
|
|
333
401
|
}
|
|
334
402
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
403
|
+
if (TEXT_AXES.includes(prop) || prop === 'font') {
|
|
404
|
+
const axis = [...painted.matchAll(/var\(\s*(--[a-z0-9-]+)/g)]
|
|
405
|
+
.map((m) => m[1])
|
|
406
|
+
.find((name) => SINGLE_AXIS_TOKEN.test(name));
|
|
407
|
+
if (axis) {
|
|
408
|
+
add(
|
|
409
|
+
'raw-text-axis',
|
|
410
|
+
at(index),
|
|
411
|
+
`${prop}: ${value}. ${axis} is one axis. Set every axis from one text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
412
|
+
);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Of the literals only absolute type values are a finding. `em`, `%`,
|
|
417
|
+
// and a unitless line-height are relative to the inherited type, so
|
|
418
|
+
// they ride whatever the theme sets rather than overriding it.
|
|
419
|
+
if (
|
|
420
|
+
!value.includes('var(') &&
|
|
421
|
+
!/^(inherit|initial|unset|normal)$/.test(value) &&
|
|
422
|
+
/\d(px|rem|pt)\b|^[a-z"']/i.test(value)
|
|
423
|
+
) {
|
|
424
|
+
add(
|
|
425
|
+
'raw-text-axis',
|
|
426
|
+
at(index),
|
|
427
|
+
`${prop}: ${value}. Set type from a text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
428
|
+
);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
350
431
|
}
|
|
351
432
|
|
|
352
433
|
if (THEMED_GEOMETRY.test(prop) && hasDimensionLiteral(painted)) {
|