@motion-proto/live-tokens 0.73.0 → 0.75.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 +84 -0
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
- 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 +149 -0
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +121 -0
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +63 -0
- package/.claude/skills/live-tokens-set-type/SKILL.md +93 -0
- package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
- package/CHANGELOG.md +194 -0
- package/README.md +27 -18
- package/bin/check-component.mjs +91 -19
- package/bin/check-page.mjs +105 -24
- package/bin/cli.mjs +154 -97
- 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/liveState.mjs +110 -0
- 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 +176 -0
- package/bin/set-colors.mjs +189 -0
- package/bin/{adjust.mjs → set-geometry.mjs} +25 -57
- package/bin/{set-fonts.mjs → set-type.mjs} +26 -62
- package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
- package/dist-plugin/{chunk-RIXO2E55.js → chunk-PDNL4NC5.js} +9 -2
- package/dist-plugin/{chunk-YLCOIGQC.js → chunk-SWXRVZKT.js} +98 -2
- package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.cts} +1 -0
- package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.ts} +1 -0
- package/dist-plugin/index.cjs +366 -148
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +226 -130
- package/dist-plugin/migrateData/index.cjs +121 -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/{generateColorsAndType → setColors}/index.cjs +594 -499
- package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +34 -25
- package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +34 -25
- package/dist-plugin/{generateColorsAndType → setColors}/index.js +48 -66
- package/dist-plugin/{adjust → setGeometry}/index.cjs +166 -48
- package/dist-plugin/{adjust → setGeometry}/index.d.cts +6 -6
- package/dist-plugin/{adjust → setGeometry}/index.d.ts +6 -6
- package/dist-plugin/{adjust → setGeometry}/index.js +51 -45
- package/dist-plugin/{fontPairing → setType}/index.cjs +19 -4
- package/dist-plugin/{fontPairing → setType}/index.d.cts +2 -2
- package/dist-plugin/{fontPairing → setType}/index.d.ts +2 -2
- package/dist-plugin/{fontPairing → setType}/index.js +1 -1
- package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
- package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +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 +36 -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/{generateColorsAndType.ts → buildColors.ts} +84 -98
- package/src/editor/core/themes/liveStateStream.ts +26 -0
- package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
- package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
- package/src/editor/core/themes/migrations/index.ts +4 -0
- package/src/editor/core/themes/themeDocumentSync.ts +22 -11
- package/src/editor/core/themes/themeService.ts +9 -2
- package/src/editor/docs/content/themes-workflow.md +2 -2
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
- package/src/editor/pages/ComponentEditorPage.svelte +17 -1
- package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +76 -574
- 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 +29 -24
- package/src/editor/skill-atlas/skillTrees.ts +19 -3687
- 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/colors-and-type/autumn.json +1 -37
- package/src/live-tokens/data/colors-and-type/default.json +1 -37
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
- package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
- package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
- package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
- package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
- package/src/live-tokens/data/themes/autumn.json +16 -52
- package/src/live-tokens/data/themes/halloween.json +16 -52
- package/src/live-tokens/data/themes/midnight-study.json +16 -52
- package/src/live-tokens/data/themes/ocean.json +16 -52
- package/src/live-tokens/data/themes/royal-velvet.json +16 -52
- package/src/live-tokens/data/themes/sketchy.json +16 -52
- package/src/live-tokens/data/themes/spring-meadow.json +16 -52
- package/src/live-tokens/data/themes/sunset.json +16 -52
- package/src/live-tokens/data/tokens.generated.css +0 -36
- 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 +2 -0
- package/template/package.json +2 -1
- package/template/src/pages/Home.svelte +4 -18
- package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +0 -93
- 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/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
- package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
- package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
- package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +0 -90
- package/bin/generate-theme.mjs +0 -260
package/bin/create.mjs
CHANGED
|
@@ -78,7 +78,7 @@ export function runCreate({ targetDir, pkgRoot, force = false }) {
|
|
|
78
78
|
export function formatCreateResult({ appName, targetDir }, targetArg) {
|
|
79
79
|
return [
|
|
80
80
|
``,
|
|
81
|
-
`Scaffolded ${appName}
|
|
81
|
+
`Scaffolded ${appName} in ${targetDir}`,
|
|
82
82
|
``,
|
|
83
83
|
`Next steps:`,
|
|
84
84
|
` cd ${targetArg}`,
|
package/bin/lib/catalogue.mjs
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
// The registry as a query. Every component a project has, shipped or its own,
|
|
2
2
|
// with the props each takes and the tokens each declares, and every theme token
|
|
3
|
-
// grouped by
|
|
3
|
+
// grouped by scale. Read from files through the same vocabulary the checkers
|
|
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
7
|
import { relative } from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { CONTRACT_SCALES } from './tokenVocabulary.mjs';
|
|
9
9
|
|
|
10
|
-
/** The runtime file's leading HTML comment, which is where a component says what
|
|
10
|
+
/** The runtime file's leading HTML comment, which is where a component says what
|
|
11
|
+
it is for. A labelled line (`Use for:`, `Not for:`, `Emphasis:`) opens a line
|
|
12
|
+
of the description and every other line continues the one above it, so the
|
|
13
|
+
comment wraps in the source and still reads as its four lines here. */
|
|
11
14
|
function descriptionOf(source) {
|
|
12
15
|
const m = source.match(/^\s*<!--([\s\S]*?)-->/);
|
|
13
16
|
if (!m) return '';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (const raw of m[1].split('\n')) {
|
|
19
|
+
const line = raw.trim().replace(/\s+/g, ' ');
|
|
20
|
+
if (!line) continue;
|
|
21
|
+
if (lines.length && !/^[A-Z][A-Za-z ]{0,20}:/.test(line)) lines[lines.length - 1] += ` ${line}`;
|
|
22
|
+
else lines.push(line);
|
|
23
|
+
}
|
|
24
|
+
if (lines.length) lines[0] = lines[0].replace(/^\S+\.svelte\s*(?:[—–-]+|\.)\s*/, '');
|
|
25
|
+
return lines.join('\n');
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
function
|
|
28
|
+
function scaleOf(name) {
|
|
24
29
|
const stem = name.replace(/^--/, '');
|
|
25
|
-
const hit =
|
|
30
|
+
const hit = CONTRACT_SCALES
|
|
26
31
|
.filter((f) => stem === f || stem.startsWith(`${f}-`))
|
|
27
32
|
.sort((a, b) => b.length - a.length)[0];
|
|
28
33
|
return hit ?? stem.split('-')[0];
|
|
@@ -62,15 +67,15 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
|
62
67
|
if (!values.has(m[1])) values.set(m[1], m[2].trim());
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
const
|
|
70
|
+
const byScale = new Map();
|
|
66
71
|
for (const name of vocab.themeTokens) {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
|
|
72
|
+
const scale = scaleOf(name);
|
|
73
|
+
if (!byScale.has(scale)) byScale.set(scale, []);
|
|
74
|
+
byScale.get(scale).push({ name, value: values.get(name) ?? '' });
|
|
70
75
|
}
|
|
71
76
|
return {
|
|
72
77
|
tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
|
|
73
|
-
|
|
78
|
+
scales: [...byScale].map(([scale, tokens]) => ({ scale, tokens })),
|
|
74
79
|
components: [...vocab.components.values()].map((c) => ({
|
|
75
80
|
id: c.id,
|
|
76
81
|
tokens: [...c.tokens].map(([name, value]) => ({ name, default: value })),
|
|
@@ -78,13 +83,15 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
|
78
83
|
};
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
const describeLines = (c) => (c.description ? c.description.split('\n') : []);
|
|
87
|
+
|
|
81
88
|
export function formatComponents(list, { id } = {}) {
|
|
82
89
|
const lines = [];
|
|
83
90
|
if (id) {
|
|
84
91
|
const c = list.find((x) => x.id === id);
|
|
85
92
|
if (!c) return `No component "${id}". Run \`live-tokens components\` for the list.`;
|
|
86
|
-
lines.push(`${c.name} (${c.id}, ${c.origin}${c.registered ? '' : ',
|
|
87
|
-
|
|
93
|
+
lines.push(`${c.name} (${c.id}, ${c.origin}${c.registered ? '' : ', unregistered'}) ${c.file}`);
|
|
94
|
+
for (const line of describeLines(c)) lines.push(` ${line}`);
|
|
88
95
|
if (c.props.length) {
|
|
89
96
|
lines.push(' props:');
|
|
90
97
|
for (const p of c.props) lines.push(` ${p.name}${p.values ? `: ${p.values.join(' | ')}` : p.type ? `: ${p.type}` : ''}`);
|
|
@@ -96,7 +103,7 @@ export function formatComponents(list, { id } = {}) {
|
|
|
96
103
|
for (const c of list) {
|
|
97
104
|
const variants = c.variants.length ? ` variants: ${c.variants.join(', ')}` : '';
|
|
98
105
|
lines.push(`${c.id.padEnd(20)} ${c.origin.padEnd(8)} ${c.name}${c.registered ? '' : ' (NOT registered)'}${variants}`);
|
|
99
|
-
|
|
106
|
+
for (const line of describeLines(c)) lines.push(` ${line}`);
|
|
100
107
|
}
|
|
101
108
|
lines.push('');
|
|
102
109
|
const unregistered = list.filter((c) => !c.registered).length;
|
|
@@ -107,21 +114,21 @@ export function formatComponents(list, { id } = {}) {
|
|
|
107
114
|
return lines.join('\n');
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
export function formatTokens(desc, {
|
|
117
|
+
export function formatTokens(desc, { scale } = {}) {
|
|
111
118
|
const lines = [];
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
return `No
|
|
119
|
+
const scales = scale ? desc.scales.filter((s) => s.scale === scale) : desc.scales;
|
|
120
|
+
if (scale && scales.length === 0) {
|
|
121
|
+
return `No token scale "${scale}". Scales: ${desc.scales.map((s) => s.scale).join(', ')}.`;
|
|
115
122
|
}
|
|
116
|
-
lines.push(`
|
|
117
|
-
for (const
|
|
123
|
+
lines.push(`Design tokens from ${desc.tokensCss ?? '(no tokens.css found)'}`);
|
|
124
|
+
for (const s of scales) {
|
|
118
125
|
lines.push('');
|
|
119
|
-
lines.push(`${
|
|
120
|
-
for (const t of
|
|
126
|
+
lines.push(`${s.scale} (${s.tokens.length})`);
|
|
127
|
+
for (const t of s.tokens) lines.push(` ${t.name}: ${t.value}`);
|
|
121
128
|
}
|
|
122
|
-
if (!
|
|
129
|
+
if (!scale) {
|
|
123
130
|
lines.push('');
|
|
124
|
-
lines.push(`
|
|
131
|
+
lines.push(`Semantic properties: ${desc.components.reduce((n, c) => n + c.tokens.length, 0)} across ${desc.components.length} component(s). \`live-tokens components <id>\` lists one component's.`);
|
|
125
132
|
}
|
|
126
133
|
return lines.join('\n');
|
|
127
134
|
}
|
package/bin/lib/findings.mjs
CHANGED
|
@@ -52,38 +52,65 @@ export function isExcluded(relPath, root) {
|
|
|
52
52
|
* Unrecognised flags are returned in `rest` for the caller to handle.
|
|
53
53
|
*/
|
|
54
54
|
export function parseCheckFlags(argv) {
|
|
55
|
-
const opts = { off: [], warn: [], error: [], strict: false, json: false, rest: [] };
|
|
55
|
+
const opts = { off: [], warn: [], error: [], strict: false, json: false, tests: false, rest: [] };
|
|
56
56
|
for (const arg of argv) {
|
|
57
57
|
const m = arg.match(/^--(off|warn|error)=(.+)$/);
|
|
58
58
|
if (m) {
|
|
59
59
|
opts[m[1]].push(...m[2].split(',').map((s) => s.trim()).filter(Boolean));
|
|
60
60
|
} else if (arg === '--strict') opts.strict = true;
|
|
61
61
|
else if (arg === '--json') opts.json = true;
|
|
62
|
+
else if (arg === '--tests') opts.tests = true;
|
|
62
63
|
else opts.rest.push(arg);
|
|
63
64
|
}
|
|
64
65
|
return opts;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/** The same last-wins resolution `applySeverity` applies per finding, exposed
|
|
69
|
+
* standalone so coverage (which has no findings to attach a severity to, but
|
|
70
|
+
* still has to honor `--off`) can ask the same question. */
|
|
71
|
+
export function resolveRuleSeverity(ruleId, rules, opts = {}, config = {}) {
|
|
72
|
+
const configured = config.rules ?? {};
|
|
73
|
+
let severity = rules[ruleId] ?? 'error';
|
|
74
|
+
if (SEVERITIES.includes(configured[ruleId])) severity = configured[ruleId];
|
|
75
|
+
if (opts.off?.includes(ruleId)) severity = 'off';
|
|
76
|
+
if (opts.warn?.includes(ruleId)) severity = 'warn';
|
|
77
|
+
if (opts.error?.includes(ruleId)) severity = 'error';
|
|
78
|
+
if (opts.strict && severity === 'warn') severity = 'error';
|
|
79
|
+
return severity;
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
83
|
* Resolve each finding's severity and drop the ones turned off.
|
|
69
84
|
* `rules` maps rule id to its default severity.
|
|
70
85
|
*/
|
|
71
|
-
export function applySeverity(findings, rules, opts = {}, config = {}) {
|
|
72
|
-
const configured = config.rules ?? {};
|
|
73
|
-
const resolve = (id) => {
|
|
74
|
-
let severity = rules[id] ?? 'error';
|
|
75
|
-
if (SEVERITIES.includes(configured[id])) severity = configured[id];
|
|
76
|
-
if (opts.off?.includes(id)) severity = 'off';
|
|
77
|
-
if (opts.warn?.includes(id)) severity = 'warn';
|
|
78
|
-
if (opts.error?.includes(id)) severity = 'error';
|
|
79
|
-
if (opts.strict && severity === 'warn') severity = 'error';
|
|
80
|
-
return severity;
|
|
81
|
-
};
|
|
86
|
+
export function applySeverity(findings, rules, opts = {}, config = {}, fixes = {}) {
|
|
82
87
|
return findings
|
|
83
|
-
.map((f) => ({
|
|
88
|
+
.map((f) => ({
|
|
89
|
+
...f,
|
|
90
|
+
severity: resolveRuleSeverity(f.rule, rules, opts, config),
|
|
91
|
+
...(fixes[f.rule] ? { fix: fixes[f.rule] } : {}),
|
|
92
|
+
}))
|
|
84
93
|
.filter((f) => f.severity !== 'off');
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A coverage entry for a rule resolved to severity 'off' becomes 'disabled',
|
|
98
|
+
* regardless of what actually happened: the plan requires `--off` to stay
|
|
99
|
+
* visible as disabled coverage rather than silently reading as a pass (an
|
|
100
|
+
* obligation that failed but was silenced is not the same as one that ran
|
|
101
|
+
* clean), and disabled coverage can never establish a complete pass.
|
|
102
|
+
*/
|
|
103
|
+
export function applyCoverageSeverity(coverage, rules, opts = {}, config = {}) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const [id, ruleMap] of Object.entries(coverage)) {
|
|
106
|
+
out[id] = {};
|
|
107
|
+
for (const [rule, entry] of Object.entries(ruleMap)) {
|
|
108
|
+
out[id][rule] = resolveRuleSeverity(rule, rules, opts, config) === 'off' ? { status: 'disabled' } : entry;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
export function countBySeverity(findings) {
|
|
88
115
|
return {
|
|
89
116
|
errors: findings.filter((f) => f.severity === 'error').length,
|
|
@@ -117,7 +144,11 @@ export function formatFindings(findings, { label, checked = 0 } = {}) {
|
|
|
117
144
|
return lines.join('\n');
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
export function toJson(findings, { label, checked = 0 } = {}) {
|
|
147
|
+
export function toJson(findings, { label, checked = 0, coverage } = {}) {
|
|
121
148
|
const { errors, warnings } = countBySeverity(findings);
|
|
122
|
-
return JSON.stringify(
|
|
149
|
+
return JSON.stringify(
|
|
150
|
+
{ check: label, checked, errors, warnings, findings, ...(coverage ? { coverage } : {}) },
|
|
151
|
+
null,
|
|
152
|
+
2,
|
|
153
|
+
);
|
|
123
154
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The live state the three `set-*` workers read, in one place.
|
|
2
|
+
//
|
|
3
|
+
// Every resolver mirrors the dev server's own resolution order — the unsaved
|
|
4
|
+
// buffer, then the open theme's copy by value, then the shipped default — so a
|
|
5
|
+
// CLI edits what the page runs. Pure file reads: the caller passes the resolved
|
|
6
|
+
// dirs, so nothing here loads `dist-plugin` and `bin/engineLoadsLazily.test.ts`
|
|
7
|
+
// holds by construction.
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const packageDataDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..', 'src/live-tokens/data');
|
|
14
|
+
|
|
15
|
+
function readJsonIfExists(path) {
|
|
16
|
+
if (!existsSync(path)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error(`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Local tree first, then the copy the installed package ships. */
|
|
25
|
+
function readData(localDir, packageSubdir, fileName) {
|
|
26
|
+
return (
|
|
27
|
+
readJsonIfExists(join(localDir, `${fileName}.json`)) ??
|
|
28
|
+
readJsonIfExists(join(packageDataDir, packageSubdir, `${fileName}.json`))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `_fileName` and `_source` are read-door markers, never part of a document. */
|
|
33
|
+
export function stripMarkers(value) {
|
|
34
|
+
if (!value || typeof value !== 'object') return value;
|
|
35
|
+
const { _fileName, _source, ...rest } = value;
|
|
36
|
+
return rest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function componentNames(componentConfigsDir) {
|
|
40
|
+
if (!existsSync(componentConfigsDir)) return [];
|
|
41
|
+
return readdirSync(componentConfigsDir, { withFileTypes: true })
|
|
42
|
+
.filter((e) => e.isDirectory())
|
|
43
|
+
.map((e) => e.name)
|
|
44
|
+
.sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readActiveTheme(themesDir) {
|
|
48
|
+
if (!themesDir) return null;
|
|
49
|
+
const slug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
|
|
50
|
+
const theme = readData(themesDir, 'themes', slug);
|
|
51
|
+
return theme ? { slug, theme } : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readLiveColorsAndType(colorsAndTypeDir, active) {
|
|
55
|
+
const working = readJsonIfExists(join(colorsAndTypeDir, '_working.json'));
|
|
56
|
+
if (working) return { colorsAndType: stripMarkers(working), source: 'working' };
|
|
57
|
+
if (active?.theme?.colorsAndType) {
|
|
58
|
+
return { colorsAndType: stripMarkers(active.theme.colorsAndType), source: 'theme' };
|
|
59
|
+
}
|
|
60
|
+
const shipped = readData(colorsAndTypeDir, 'colors-and-type', 'default');
|
|
61
|
+
if (!shipped) throw new Error(`no colors and type to read at ${colorsAndTypeDir}`);
|
|
62
|
+
return { colorsAndType: stripMarkers(shipped), source: 'default' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `updatedAt` records when a buffer was written, not what it holds, so the
|
|
66
|
+
* comparison that tells a discard from an edit ignores it. Both writers of
|
|
67
|
+
* `colors-and-type/_working.json` ask this one question, and a rule that kept
|
|
68
|
+
* `updatedAt` missed every buffer the other verb had stamped. */
|
|
69
|
+
export function sameContent(a, b) {
|
|
70
|
+
const strip = ({ updatedAt: _updatedAt, ...rest }) => rest;
|
|
71
|
+
return JSON.stringify(strip(a)) === JSON.stringify(strip(b));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Which layer sits under the buffer: the open theme when it carries colors and
|
|
75
|
+
* type, else the shipped default. A report that names the layer reads it from
|
|
76
|
+
* here rather than re-deriving the fall-through, which is how the discard
|
|
77
|
+
* message came to name a theme that was not open. */
|
|
78
|
+
export const savedColorsAndTypeSource = (active) => (active?.theme?.colorsAndType ? 'theme' : 'default');
|
|
79
|
+
|
|
80
|
+
/** The layer under the buffer. Returning the buffer to exactly this is a
|
|
81
|
+
* discard, not an edit, so a caller compares against it before writing. */
|
|
82
|
+
export function readSavedColorsAndType(colorsAndTypeDir, active) {
|
|
83
|
+
const saved =
|
|
84
|
+
savedColorsAndTypeSource(active) === 'theme'
|
|
85
|
+
? active.theme.colorsAndType
|
|
86
|
+
: readData(colorsAndTypeDir, 'colors-and-type', 'default');
|
|
87
|
+
return saved ? stripMarkers(saved) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Each component's live config, and whether a buffer or the open document
|
|
91
|
+
* answered. Every theme carries every component by value, but this is a raw
|
|
92
|
+
* file read, ahead of any server-side fill — a theme written before a
|
|
93
|
+
* component existed can still omit it, so a missing entry falls back to the
|
|
94
|
+
* shipped default the same way the theme's own fill would, and still reports
|
|
95
|
+
* as the document. */
|
|
96
|
+
export function readLiveComponentConfigs(componentConfigsDir, active) {
|
|
97
|
+
const configs = {};
|
|
98
|
+
const sources = {};
|
|
99
|
+
for (const comp of componentNames(componentConfigsDir)) {
|
|
100
|
+
const working = readJsonIfExists(join(componentConfigsDir, comp, '_working.json'));
|
|
101
|
+
const config =
|
|
102
|
+
working ??
|
|
103
|
+
active?.theme?.componentConfigs?.[comp] ??
|
|
104
|
+
readData(join(componentConfigsDir, comp), `component-configs/${comp}`, 'default');
|
|
105
|
+
if (!config) continue;
|
|
106
|
+
configs[comp] = stripMarkers(config);
|
|
107
|
+
sources[comp] = working ? 'working' : 'theme';
|
|
108
|
+
}
|
|
109
|
+
return { configs, sources };
|
|
110
|
+
}
|
package/bin/lib/report.mjs
CHANGED
|
@@ -120,7 +120,7 @@ const list = (items, max = 20) =>
|
|
|
120
120
|
|
|
121
121
|
export function formatReport(r) {
|
|
122
122
|
const out = [];
|
|
123
|
-
out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens}
|
|
123
|
+
out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens} design tokens from ${r.project.tokensCss ?? '(no tokens.css)'}`);
|
|
124
124
|
if (r.migrations) {
|
|
125
125
|
out.push('');
|
|
126
126
|
out.push(`Migrations: ${r.migrations.status}${r.migrations.pending?.length ? ` (${list(r.migrations.pending)})` : ''}`);
|
|
@@ -131,11 +131,11 @@ export function formatReport(r) {
|
|
|
131
131
|
const unread = r.components.filter((c) => c.unread.length);
|
|
132
132
|
out.push(` tokens declared and read by their own CSS: ${r.components.reduce((n, c) => n + c.tokens - c.unread.length, 0)} of ${r.components.reduce((n, c) => n + c.tokens, 0)}`);
|
|
133
133
|
for (const c of unread) out.push(` ${c.id}: ${c.unread.length} unread (${list(c.unread, 6)})`);
|
|
134
|
+
const undescribed = r.components.filter((c) => !c.described).map((c) => c.id);
|
|
135
|
+
if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
|
|
134
136
|
const custom = r.components.filter((c) => c.origin === 'custom');
|
|
135
137
|
out.push(` custom: ${custom.length}${custom.length ? ` (${list(custom.map((c) => c.id))})` : ''}`);
|
|
136
138
|
if (r.usage.customUnregistered.length) out.push(` not registered: ${list(r.usage.customUnregistered)}`);
|
|
137
|
-
const undescribed = custom.filter((c) => !c.described).map((c) => c.id);
|
|
138
|
-
if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
|
|
139
139
|
|
|
140
140
|
out.push('');
|
|
141
141
|
out.push('Usage');
|
|
@@ -25,18 +25,18 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
|
25
25
|
|
|
26
26
|
const SHIPPED_COMPONENTS_DIR = 'src/system/components';
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
export const
|
|
28
|
+
/** Token scales whose names are governed by the token contract (see TOKENS.md). */
|
|
29
|
+
export const CONTRACT_SCALES = [
|
|
30
30
|
'surface', 'text', 'border', 'color', 'space', 'radius', 'font', 'line-height',
|
|
31
31
|
'letter-spacing', 'shadow', 'blur', 'icon-size', 'scrim', 'tint', 'columns',
|
|
32
32
|
'heading', 'body', 'editorial', 'eyebrow', 'code', 'easing', 'duration', 'zoom',
|
|
33
33
|
'gradient', 'stroke',
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
-
/** True when `name` belongs to a contract-governed
|
|
36
|
+
/** True when `name` belongs to a contract-governed scale, so a miss is a typo. */
|
|
37
37
|
export function isContractToken(name) {
|
|
38
38
|
const stem = name.replace(/^--/, '');
|
|
39
|
-
return
|
|
39
|
+
return CONTRACT_SCALES.some((f) => stem === f || stem.startsWith(`${f}-`));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** Every `--name:` declared anywhere in a stylesheet or style block. */
|
package/bin/migrate-routes.mjs
CHANGED
|
@@ -149,8 +149,8 @@ export function formatRouteResult(result, { check = false } = {}) {
|
|
|
149
149
|
const { rewritten, pendingWrite, advisory } = result;
|
|
150
150
|
if (!rewritten.length && !pendingWrite.length && !advisory.length) return '';
|
|
151
151
|
|
|
152
|
-
const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}'
|
|
153
|
-
const lines = ['Route references
|
|
152
|
+
const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}' to '${h.next}'`;
|
|
153
|
+
const lines = ['Route references: /editor, /components, /docs moved to /live-tokens/* in 0.35.0:'];
|
|
154
154
|
|
|
155
155
|
if (rewritten.length) {
|
|
156
156
|
lines.push(` ✓ Rewrote ${rewritten.length} reference(s):`);
|
|
@@ -160,7 +160,7 @@ export function formatRouteResult(result, { check = false } = {}) {
|
|
|
160
160
|
lines.push(
|
|
161
161
|
check
|
|
162
162
|
? ` Would rewrite ${pendingWrite.length} reference(s) with --write:`
|
|
163
|
-
: ` ${pendingWrite.length} reference(s) can be rewritten
|
|
163
|
+
: ` ${pendingWrite.length} reference(s) can be rewritten. Re-run with --write to apply:`,
|
|
164
164
|
);
|
|
165
165
|
pendingWrite.forEach((h) => lines.push(ref(h)));
|
|
166
166
|
}
|
|
@@ -169,8 +169,8 @@ export function formatRouteResult(result, { check = false } = {}) {
|
|
|
169
169
|
advisory.forEach((h) => {
|
|
170
170
|
const why =
|
|
171
171
|
h.reason === 'docs-never'
|
|
172
|
-
? `/docs is never auto-rewritten
|
|
173
|
-
: `
|
|
172
|
+
? `/docs is never auto-rewritten. Update to '${h.next}' only when it points at the package guide`
|
|
173
|
+
: `the project declares or relocates '${h.old}'. Leave it when it is the project's own route`;
|
|
174
174
|
lines.push(` ${h.file}:${h.line} ${h.kind} '${h.old}' (${why})`);
|
|
175
175
|
});
|
|
176
176
|
}
|
package/bin/migrate.mjs
CHANGED
|
@@ -127,13 +127,13 @@ export function formatMigrateDataResult(result) {
|
|
|
127
127
|
// `sketch-presets/` → `sketch-styles/`. Told apart by the destination
|
|
128
128
|
// name, since `LegacyRename` carries no reason of its own.
|
|
129
129
|
const reason = basename(to) === 'sketch-styles' ? 'sketchstyle rename' : '0.48 layout';
|
|
130
|
-
lines.push(` ${planned ? 'would move' : 'moved'} ${rel(from)}
|
|
130
|
+
lines.push(` ${planned ? 'would move' : 'moved'} ${rel(from)} to ${rel(to)} (${reason})`);
|
|
131
131
|
}
|
|
132
132
|
for (const p of result.upgradedThemes) {
|
|
133
133
|
lines.push(` ${planned ? 'would carry' : 'carried'} ${rel(p)} by value (was a pre-v3 theme naming files)`);
|
|
134
134
|
}
|
|
135
135
|
if (result.production) {
|
|
136
|
-
lines.push(` production theme
|
|
136
|
+
lines.push(` production theme: "${result.production.slug}" (${HOW_SAID[result.production.how]})`);
|
|
137
137
|
}
|
|
138
138
|
if (result.recoveredThemePath) {
|
|
139
139
|
lines.push(` ${planned ? 'would write' : 'wrote'} ${rel(result.recoveredThemePath)}`);
|
|
@@ -148,13 +148,13 @@ export function formatMigrateDataResult(result) {
|
|
|
148
148
|
lines.push(` ${planned ? 'would delete' : 'deleted'} ${rel(p)}: a copy of a saved theme`);
|
|
149
149
|
}
|
|
150
150
|
for (const p of result.keptUserFiles) {
|
|
151
|
-
lines.push(` kept ${rel(p)}: it matches no theme, so it
|
|
151
|
+
lines.push(` kept ${rel(p)}: it matches no theme, so it stays`);
|
|
152
152
|
}
|
|
153
153
|
for (const note of result.droppedRefs) {
|
|
154
154
|
lines.push(` ${note}`);
|
|
155
155
|
}
|
|
156
156
|
for (const p of result.notThemes) {
|
|
157
|
-
lines.push(` left ${rel(p)} alone: it is a colors-and-type file
|
|
157
|
+
lines.push(` left ${rel(p)} alone: it is a colors-and-type file`);
|
|
158
158
|
}
|
|
159
159
|
for (const p of result.shadowedDefaults) {
|
|
160
160
|
lines.push(` kept ${rel(p)}: it shadows the default the package ships, which has moved on`);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// `live-tokens save-theme` worker.
|
|
2
|
+
//
|
|
3
|
+
// Composes the live state into a theme document at `themes/<slug>.json` and
|
|
4
|
+
// opens it. Live state, and only live state: each layer resolves the way the
|
|
5
|
+
// dev server resolves it — the unsaved buffer, then the open theme's copy by
|
|
6
|
+
// value, then the shipped default — so the theme this writes is the look the
|
|
7
|
+
// page renders. Opening clears every `_working.json` and points
|
|
8
|
+
// `themes/_active.json` at the new document, exactly as the editor's apply door
|
|
9
|
+
// does; `--no-activate` writes the file and leaves the live state alone, which
|
|
10
|
+
// is how a set of themes comes off one starting look.
|
|
11
|
+
//
|
|
12
|
+
// This is the only verb that writes a theme. The three `set-*` verbs write
|
|
13
|
+
// buffers.
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
componentNames,
|
|
21
|
+
readActiveTheme,
|
|
22
|
+
readLiveColorsAndType,
|
|
23
|
+
readLiveComponentConfigs,
|
|
24
|
+
} from './lib/liveState.mjs';
|
|
25
|
+
|
|
26
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
// The setColors bundle carries the slug helper, the component schema stamp and
|
|
28
|
+
// the dir resolution; composing a theme needs no color pipeline of its own.
|
|
29
|
+
const ENGINE = resolve(pkgRoot, 'dist-plugin/setColors/index.js');
|
|
30
|
+
// Source of truth: src/editor/core/themes/themeTypes.ts, which
|
|
31
|
+
// normalizeTheme.ts re-exports. This copy cannot import TS, so
|
|
32
|
+
// `bin/schemaVersionCopies.test.ts` is what catches a drift.
|
|
33
|
+
const THEME_SCHEMA_VERSION = 5;
|
|
34
|
+
|
|
35
|
+
async function loadEngine() {
|
|
36
|
+
if (!existsSync(ENGINE)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`setColors engine not found at ${relative(process.cwd(), ENGINE)}. ` +
|
|
39
|
+
`Build the plugin first (npm run build:plugin).`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return import(ENGINE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The destination file, read to keep its `createdAt`. Never the package copy:
|
|
46
|
+
* this is the file about to be overwritten, not a layer to fall through to. */
|
|
47
|
+
function readLocalJson(path) {
|
|
48
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `engine` is a test seam; the CLI always runs the compiled bundle. */
|
|
52
|
+
export async function runSaveTheme({
|
|
53
|
+
name,
|
|
54
|
+
activate = true,
|
|
55
|
+
dryRun = false,
|
|
56
|
+
colorsAndTypeDir,
|
|
57
|
+
componentConfigsDir,
|
|
58
|
+
themesDir,
|
|
59
|
+
engine,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const { sanitizeFileName, resolveDataDirs, CURRENT_COMPONENT_SCHEMA_VERSION } =
|
|
62
|
+
engine ?? (await loadEngine());
|
|
63
|
+
|
|
64
|
+
const themeName = typeof name === 'string' ? name.trim() : '';
|
|
65
|
+
if (!themeName) throw new Error('save-theme needs a theme name');
|
|
66
|
+
const slug = sanitizeFileName(themeName);
|
|
67
|
+
if (slug === 'default') {
|
|
68
|
+
throw new Error('"default" is the protected package theme; pick another name');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolved = colorsAndTypeDir && componentConfigsDir && themesDir ? null : resolveDataDirs();
|
|
72
|
+
const dirs = {
|
|
73
|
+
colorsAndTypeDir: colorsAndTypeDir ?? resolved.colorsAndTypeDir,
|
|
74
|
+
componentConfigsDir: componentConfigsDir ?? resolved.componentConfigsDir,
|
|
75
|
+
themesDir: themesDir ?? resolved.themesDir,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const active = readActiveTheme(dirs.themesDir);
|
|
79
|
+
const { colorsAndType, source } = readLiveColorsAndType(dirs.colorsAndTypeDir, active);
|
|
80
|
+
const { configs, sources } = readLiveComponentConfigs(dirs.componentConfigsDir, active);
|
|
81
|
+
|
|
82
|
+
const themePath = join(dirs.themesDir, `${slug}.json`);
|
|
83
|
+
const existing = readLocalJson(themePath);
|
|
84
|
+
const now = new Date().toISOString();
|
|
85
|
+
const sketchSettings = active?.theme?.sketchSettings;
|
|
86
|
+
|
|
87
|
+
const theme = {
|
|
88
|
+
name: themeName,
|
|
89
|
+
createdAt: existing?.createdAt ?? now,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
schemaVersion: THEME_SCHEMA_VERSION,
|
|
92
|
+
colorsAndType: { ...colorsAndType, name: themeName },
|
|
93
|
+
componentConfigs: configs,
|
|
94
|
+
componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
95
|
+
...(sketchSettings ? { sketchSettings } : {}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!dryRun) {
|
|
99
|
+
mkdirSync(dirs.themesDir, { recursive: true });
|
|
100
|
+
writeFileSync(themePath, JSON.stringify(theme, null, 2) + '\n');
|
|
101
|
+
if (activate) applyTheme(slug, dirs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// `working` is the one source label that proves a buffer answered: a
|
|
105
|
+
// component the open theme omits reports as `theme` after falling through to
|
|
106
|
+
// its shipped default, so the report says what came from a buffer and names
|
|
107
|
+
// the fall-through for the rest rather than claiming a layer per component.
|
|
108
|
+
const bufferedComponents = componentNames(dirs.componentConfigsDir).filter(
|
|
109
|
+
(comp) => sources[comp] === 'working',
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: themeName,
|
|
114
|
+
slug,
|
|
115
|
+
themePath,
|
|
116
|
+
existed: existing !== null,
|
|
117
|
+
dryRun,
|
|
118
|
+
activated: activate && !dryRun,
|
|
119
|
+
previousActive: active?.slug ?? 'default',
|
|
120
|
+
openTheme: active?.slug ?? null,
|
|
121
|
+
buffered: { colorsAndType: source === 'working', components: bufferedComponents },
|
|
122
|
+
components: Object.keys(configs).length,
|
|
123
|
+
sketchSettings: Boolean(sketchSettings),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The apply door's write set, reproduced: clear every working delta and point
|
|
128
|
+
* `themes/_active.json` at the open document. Production is untouched. */
|
|
129
|
+
function applyTheme(slug, dirs) {
|
|
130
|
+
const colorsWorking = join(dirs.colorsAndTypeDir, '_working.json');
|
|
131
|
+
if (existsSync(colorsWorking)) rmSync(colorsWorking);
|
|
132
|
+
|
|
133
|
+
for (const comp of componentNames(dirs.componentConfigsDir)) {
|
|
134
|
+
const workingPath = join(dirs.componentConfigsDir, comp, '_working.json');
|
|
135
|
+
if (existsSync(workingPath)) rmSync(workingPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
writeFileSync(join(dirs.themesDir, '_active.json'), JSON.stringify({ activeFile: slug }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function formatSaveThemeResult(result) {
|
|
142
|
+
const root = process.cwd();
|
|
143
|
+
const lines = [];
|
|
144
|
+
const wrote = result.dryRun ? 'Would write' : result.existed ? 'Updated' : 'Created';
|
|
145
|
+
lines.push(`${wrote} theme "${result.name}" at ${relative(root, result.themePath)}`);
|
|
146
|
+
lines.push(`It carries the colors and type and ${result.components} component config(s) by value.`);
|
|
147
|
+
|
|
148
|
+
const edited = [
|
|
149
|
+
...(result.buffered.colorsAndType ? ['colors and type'] : []),
|
|
150
|
+
...result.buffered.components,
|
|
151
|
+
];
|
|
152
|
+
const open = result.openTheme ? `the open theme "${result.openTheme}"` : 'the shipped defaults';
|
|
153
|
+
const kept = result.dryRun ? 'Would save' : 'Saved';
|
|
154
|
+
if (edited.length > 0) {
|
|
155
|
+
lines.push(`${kept} the buffer: ${edited.join(', ')}.`);
|
|
156
|
+
lines.push(`Everything else came from ${open}, or the shipped defaults where it carries no entry.`);
|
|
157
|
+
} else {
|
|
158
|
+
lines.push(`No buffer. ${kept} a copy of ${open}.`);
|
|
159
|
+
}
|
|
160
|
+
if (result.sketchSettings) lines.push(`Sketch settings rode through from the open theme.`);
|
|
161
|
+
|
|
162
|
+
if (result.activated) {
|
|
163
|
+
lines.push(
|
|
164
|
+
`\nLoaded "${result.slug}" (previously open: "${result.previousActive}"). ` +
|
|
165
|
+
`Adopt in the editor ships it to tokens.generated.css.`,
|
|
166
|
+
);
|
|
167
|
+
} else if (result.dryRun) {
|
|
168
|
+
lines.push(`\nDry run: nothing written under ${relative(root, dirname(result.themePath))}.`);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(
|
|
171
|
+
`\nNot loaded (--no-activate). The buffer is still open, so the next ` +
|
|
172
|
+
`save-theme starts from the same theme.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|