@motion-proto/live-tokens 0.73.0 → 0.74.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.
Files changed (73) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +2 -2
  2. package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -4
  4. package/.claude/skills/live-tokens-create-theme/SKILL.md +80 -0
  5. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  6. package/.claude/skills/live-tokens-set-colors/SKILL.md +140 -0
  7. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  8. package/.claude/skills/{live-tokens-adjust-geometry → live-tokens-set-geometry}/SKILL.md +12 -7
  9. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +61 -0
  10. package/.claude/skills/{live-tokens-pair-fonts → live-tokens-set-type}/SKILL.md +18 -15
  11. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  12. package/CHANGELOG.md +91 -0
  13. package/README.md +24 -15
  14. package/bin/check-page.mjs +3 -3
  15. package/bin/cli.mjs +92 -55
  16. package/bin/lib/liveState.mjs +110 -0
  17. package/bin/save-theme.mjs +177 -0
  18. package/bin/set-colors.mjs +191 -0
  19. package/bin/{adjust.mjs → set-geometry.mjs} +18 -50
  20. package/bin/{set-fonts.mjs → set-type.mjs} +21 -55
  21. package/dist-plugin/{chunk-RIXO2E55.js → chunk-7VRTBGJT.js} +1 -1
  22. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-V3YF6CGT.js} +56 -2
  23. package/dist-plugin/index.cjs +58 -3
  24. package/dist-plugin/index.js +11 -11
  25. package/dist-plugin/migrateData/index.cjs +56 -1
  26. package/dist-plugin/migrateData/index.js +2 -2
  27. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +1109 -1071
  28. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +33 -24
  29. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +33 -24
  30. package/dist-plugin/{generateColorsAndType → setColors}/index.js +45 -63
  31. package/dist-plugin/{adjust → setGeometry}/index.cjs +60 -5
  32. package/dist-plugin/{adjust → setGeometry}/index.d.cts +1 -1
  33. package/dist-plugin/{adjust → setGeometry}/index.d.ts +1 -1
  34. package/dist-plugin/{adjust → setGeometry}/index.js +1 -1
  35. package/dist-plugin/{fontPairing → setType}/index.cjs +4 -4
  36. package/dist-plugin/{fontPairing → setType}/index.d.cts +1 -1
  37. package/dist-plugin/{fontPairing → setType}/index.d.ts +1 -1
  38. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  39. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  40. package/package.json +2 -1
  41. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +82 -96
  42. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  43. package/src/editor/core/themes/migrations/index.ts +2 -0
  44. package/src/editor/docs/content/themes-workflow.md +2 -2
  45. package/src/editor/docs/content.generated.ts +1 -1
  46. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  47. package/src/editor/skill-atlas/SkillAtlas.svelte +2 -2
  48. package/src/editor/skill-atlas/skillSources.generated.ts +19 -15
  49. package/src/editor/skill-atlas/skillTrees.ts +785 -632
  50. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  51. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  52. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  53. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  54. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  55. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  56. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  57. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  58. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  59. package/src/live-tokens/data/themes/autumn.json +1 -37
  60. package/src/live-tokens/data/themes/halloween.json +1 -37
  61. package/src/live-tokens/data/themes/midnight-study.json +1 -37
  62. package/src/live-tokens/data/themes/ocean.json +1 -37
  63. package/src/live-tokens/data/themes/royal-velvet.json +1 -37
  64. package/src/live-tokens/data/themes/sketchy.json +1 -37
  65. package/src/live-tokens/data/themes/spring-meadow.json +1 -37
  66. package/src/live-tokens/data/themes/sunset.json +1 -37
  67. package/src/live-tokens/data/tokens.generated.css +0 -36
  68. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  69. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  70. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  71. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  72. package/bin/generate-theme.mjs +0 -260
  73. /package/dist-plugin/{fontPairing → setType}/index.js +0 -0
@@ -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
+ }
@@ -0,0 +1,177 @@
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}" → ${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 package defaults';
153
+ const kept = result.dryRun ? 'Would save' : 'Saved';
154
+ if (edited.length > 0) {
155
+ lines.push(`${kept} your unsaved edits: ${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 unsaved edits; ${kept.toLowerCase()} 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
+ `\nOpened "${result.slug}" (previously open: "${result.previousActive}"). ` +
165
+ `Reload the app to see it; switch back any time from Load in the editor's Theme panel. ` +
166
+ `Adopt it there to publish it to tokens.generated.css.`,
167
+ );
168
+ } else if (result.dryRun) {
169
+ lines.push(`\nDry run: nothing written under ${relative(root, dirname(result.themePath))}.`);
170
+ } else {
171
+ lines.push(
172
+ `\nNot opened (--no-activate). Your unsaved edits are still open, so the next ` +
173
+ `save-theme starts from the same look. Load "${result.slug}" from the editor's Theme panel to see it.`,
174
+ );
175
+ }
176
+ return lines.join('\n');
177
+ }
@@ -0,0 +1,191 @@
1
+ // `live-tokens set-colors` worker.
2
+ //
3
+ // Reads a base color file (JSON), builds the theme's whole color state via the
4
+ // compiled engine (dist-plugin/setColors — the CLI never imports TS sources),
5
+ // enforces the AA contrast gate, and writes the result into
6
+ // `colors-and-type/_working.json`: the same buffer the editor's own palette
7
+ // edits land in, so a recolor is an unsaved edit the user keeps by saving the
8
+ // open theme or by running `save-theme`. Named colors-and-type files, themes,
9
+ // tokens.css and fonts.css are never touched, and nothing is activated.
10
+ //
11
+ // Non-color content carries forward from the live colors and type, so a recolor
12
+ // replaces the color identity and nothing else. Swatch gradients ride along
13
+ // when user-tuned; stock ones are rebuilt from the new families by the engine.
14
+
15
+ import { existsSync, 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
+ readActiveTheme,
21
+ readLiveColorsAndType,
22
+ readSavedColorsAndType,
23
+ sameContent,
24
+ savedColorsAndTypeSource,
25
+ } from './lib/liveState.mjs';
26
+
27
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
28
+ const ENGINE = resolve(pkgRoot, 'dist-plugin/setColors/index.js');
29
+
30
+ const SOURCE_LABELS = {
31
+ working: 'your unsaved edits',
32
+ theme: 'the open theme',
33
+ default: 'the package default',
34
+ };
35
+
36
+ async function loadEngine() {
37
+ if (!existsSync(ENGINE)) {
38
+ throw new Error(
39
+ `setColors engine not found at ${relative(process.cwd(), ENGINE)}. ` +
40
+ `Build the plugin first (npm run build:plugin).`,
41
+ );
42
+ }
43
+ return import(ENGINE);
44
+ }
45
+
46
+ function readJson(path) {
47
+ return JSON.parse(readFileSync(path, 'utf8'));
48
+ }
49
+
50
+ /** `engine` is a test seam; the CLI always runs the compiled bundle. */
51
+ export async function runSetColors({
52
+ baseColorsPath,
53
+ dryRun = false,
54
+ root = process.cwd(),
55
+ colorsAndTypeDir,
56
+ themesDir,
57
+ engine,
58
+ } = {}) {
59
+ const { buildColors, resolveDataDirs } = engine ?? (await loadEngine());
60
+
61
+ const fullPath = resolve(root, baseColorsPath);
62
+ if (!existsSync(fullPath)) {
63
+ throw new Error(`base color file not found at ${relative(root, fullPath)}`);
64
+ }
65
+ let input;
66
+ try {
67
+ input = readJson(fullPath);
68
+ } catch (err) {
69
+ throw new Error(`base color file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
70
+ }
71
+
72
+ const resolved = colorsAndTypeDir && themesDir ? null : resolveDataDirs();
73
+ const colorsDir = colorsAndTypeDir ?? resolved.colorsAndTypeDir;
74
+ const themes = themesDir ?? resolved.themesDir;
75
+ if (!existsSync(colorsDir)) {
76
+ throw new Error(`no colors and type at ${relative(root, colorsDir)}. Run the dev server once to create it.`);
77
+ }
78
+
79
+ const active = readActiveTheme(themes);
80
+ const { colorsAndType: live, source } = readLiveColorsAndType(colorsDir, active);
81
+
82
+ const { colors, report } = buildColors(input, {
83
+ cssVariables: live.cssVariables,
84
+ gradients: live.gradients,
85
+ });
86
+
87
+ const next = {
88
+ ...live,
89
+ ...colors,
90
+ updatedAt: new Date().toISOString(),
91
+ schemaVersion: live.schemaVersion,
92
+ };
93
+
94
+ const workingPath = join(colorsDir, '_working.json');
95
+ const saved = readSavedColorsAndType(colorsDir, active);
96
+
97
+ // Returning the buffer to what the layer under it already holds is a discard,
98
+ // not an edit — the same call the dev server's own PUT makes.
99
+ const backToSaved = saved !== null && sameContent(next, saved);
100
+ let wrote = null;
101
+ if (!dryRun) {
102
+ if (backToSaved) {
103
+ if (existsSync(workingPath)) rmSync(workingPath);
104
+ wrote = 'cleared';
105
+ } else {
106
+ writeFileSync(workingPath, JSON.stringify(next, null, 2) + '\n');
107
+ wrote = 'buffer';
108
+ }
109
+ }
110
+
111
+ return {
112
+ baseColorsPath: fullPath,
113
+ colorsAndTypeDir: colorsDir,
114
+ workingPath,
115
+ openTheme: active?.slug ?? null,
116
+ source,
117
+ // Which layer the discard compared against, so the report can name it. It
118
+ // is the open theme only when one is open; with none, it is the default the
119
+ // package ships, and calling that "the open theme" named a file nobody had
120
+ // opened.
121
+ savedSource: savedColorsAndTypeSource(active),
122
+ // The base color file's `name` used to pick a theme file name. This verb
123
+ // writes no file of its own now, so it names nothing; say so rather than
124
+ // drop it in silence.
125
+ ignoredName: input.name === undefined ? null : String(input.name),
126
+ dryRun,
127
+ wrote,
128
+ report,
129
+ };
130
+ }
131
+
132
+ export function formatSetColorsResult(result) {
133
+ const root = process.cwd();
134
+ const lines = [];
135
+
136
+ const from =
137
+ result.source === 'theme' && result.openTheme
138
+ ? `theme "${result.openTheme}"`
139
+ : SOURCE_LABELS[result.source];
140
+ const verb = result.dryRun ? 'Would replace' : 'Replaced';
141
+ lines.push(`${verb} the color identity, carrying everything else forward from ${from}.`);
142
+ if (result.ignoredName !== null) {
143
+ lines.push(
144
+ `Ignored "name": "${result.ignoredName}". The base color file no longer names a theme; ` +
145
+ `name it when you run save-theme.`,
146
+ );
147
+ }
148
+
149
+ lines.push(`\nContrast report (${result.report.scheme} scheme):`);
150
+ const width = Math.max(...result.report.checks.map((c) => c.textVar.length));
151
+ for (const c of result.report.checks) {
152
+ const mark = c.pass ? '✓' : '✗';
153
+ const fixed = c.corrected ? ' (auto-corrected)' : '';
154
+ lines.push(
155
+ ` ${mark} ${c.textVar.padEnd(width)} ${c.ratio.toFixed(2).padStart(6)}:1 vs ${c.against} floor ${c.floor}:1${fixed}`,
156
+ );
157
+ }
158
+ if (result.report.failures.length > 0) {
159
+ lines.push(`\nUnmet floors — adjust the base colors and re-run:`);
160
+ for (const f of result.report.failures) lines.push(` ! ${f}`);
161
+ }
162
+
163
+ lines.push(
164
+ result.report.gradients === 'carried'
165
+ ? '\nGradients: kept your tuned swatch gradients.'
166
+ : '\nGradients: swatch tokens rebuilt from the theme families.',
167
+ );
168
+ lines.push(`Shadows: ${result.report.shadows}; carried geometry kept.`);
169
+ if (result.report.canvasGradient) {
170
+ lines.push(`Canvas sky: ${result.report.canvasGradient}.`);
171
+ }
172
+
173
+ if (result.wrote === 'buffer') {
174
+ lines.push(
175
+ `\nReload the app to see it. This is an unsaved edit: save the open theme in the ` +
176
+ `editor's Theme panel to keep it, or run save-theme to write a new one.`,
177
+ );
178
+ } else if (result.wrote === 'cleared') {
179
+ const held =
180
+ result.savedSource === 'theme' ? `theme "${result.openTheme}"` : 'the package default';
181
+ lines.push(
182
+ result.source === 'working'
183
+ ? `\nThat is what ${held} already holds, so the unsaved buffer was discarded. ` +
184
+ `Reload the app to see it.`
185
+ : `\nThat is what ${held} already holds, and there was no unsaved buffer, so nothing was written.`,
186
+ );
187
+ } else if (result.dryRun) {
188
+ lines.push(`\nDry run: nothing written under ${relative(root, result.colorsAndTypeDir)}.`);
189
+ }
190
+ return lines.join('\n');
191
+ }
@@ -1,19 +1,20 @@
1
- // `live-tokens adjust` worker.
1
+ // `live-tokens set-geometry` worker.
2
2
  //
3
3
  // Reads an ops file (JSON), applies it to every component's LIVE config via the
4
- // compiled engine (dist-plugin/adjust — the CLI never imports TS sources), and
4
+ // compiled engine (dist-plugin/setGeometry — the CLI never imports TS sources), and
5
5
  // writes the result into that component's `_working.json` buffer: the same slot
6
6
  // the editor's own edits land in, so an adjustment is an unsaved edit the user
7
7
  // saves into a theme when they want to keep it. `default.json`, named preset
8
8
  // files, themes, colors and type, and tokens.css are never touched.
9
9
 
10
- import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
10
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { dirname, join, relative, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
+ import { componentNames, readActiveTheme, readLiveComponentConfigs } from './lib/liveState.mjs';
15
+
14
16
  const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
- const ENGINE = resolve(pkgRoot, 'dist-plugin/adjust/index.js');
16
- const packageThemesDir = join(pkgRoot, 'src/live-tokens/data/themes');
17
+ const ENGINE = resolve(pkgRoot, 'dist-plugin/setGeometry/index.js');
17
18
 
18
19
  const SOURCE_LABELS = {
19
20
  working: 'your unsaved edits',
@@ -30,7 +31,7 @@ const SKIP_LABELS = [
30
31
  async function loadEngine() {
31
32
  if (!existsSync(ENGINE)) {
32
33
  throw new Error(
33
- `adjust engine not found at ${relative(process.cwd(), ENGINE)}. ` +
34
+ `setGeometry engine not found at ${relative(process.cwd(), ENGINE)}. ` +
34
35
  `Build the plugin first (npm run build:plugin).`,
35
36
  );
36
37
  }
@@ -41,10 +42,6 @@ function readJson(path) {
41
42
  return JSON.parse(readFileSync(path, 'utf8'));
42
43
  }
43
44
 
44
- function readJsonIfExists(path) {
45
- return existsSync(path) ? readJson(path) : null;
46
- }
47
-
48
45
  /** Successive ops can touch the same alias (soften, then pill the buttons);
49
46
  * the report shows one entry per alias, first `from` to last `to`. */
50
47
  function collapseChanges(changes) {
@@ -57,49 +54,20 @@ function collapseChanges(changes) {
57
54
  return [...byVariable.values()].filter((c) => c.from !== c.to);
58
55
  }
59
56
 
60
- /** The open theme, read the way every other door reads it: the local file
61
- * first, then the copy the installed package ships. */
62
- function readActiveTheme(themesDir) {
63
- if (!themesDir) return null;
64
- const slug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
65
- const theme =
66
- readJsonIfExists(join(themesDir, `${slug}.json`)) ??
67
- readJsonIfExists(join(packageThemesDir, `${slug}.json`));
68
- return theme ? { slug, theme } : null;
69
- }
70
-
71
- /** Each component's live config, and whether a buffer or the open document
72
- * answered. The document's own copy falls back to the shipped default for a
73
- * component it somehow lacks, which reports as the document either way.
74
- * Mirrors the dev server's `resolveLiveComponentConfig`, so the CLI adjusts
75
- * what the page runs. */
76
- function readLiveConfigs(dir, active) {
57
+ /** A config document names its own component, and a theme-embedded copy can
58
+ * omit it, so stamp it back on before the buffer is written from it. */
59
+ function stampedLiveConfigs(dir, active) {
60
+ const { configs: live, sources } = readLiveComponentConfigs(dir, active);
77
61
  const configs = {};
78
- const sources = {};
79
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
80
- if (!entry.isDirectory()) continue;
81
- const comp = entry.name;
82
- const componentDir = join(dir, comp);
83
- const working = readJsonIfExists(join(componentDir, '_working.json'));
84
- // Every theme carries every component by value, but this CLI reads the
85
- // file straight off disk, ahead of any server-side fill — a theme
86
- // written before this component existed can still omit it, so a missing
87
- // entry falls back to the shipped default the same way the theme's own
88
- // fill would.
89
- const embedded =
90
- active?.theme?.componentConfigs?.[comp] ?? readJsonIfExists(join(componentDir, 'default.json'));
91
- const config = working ?? embedded;
92
- if (!config) {
93
- throw new Error(`component "${comp}": default.json is missing`);
94
- }
95
- configs[comp] = { ...config, component: comp };
96
- sources[comp] = working ? 'working' : 'theme';
62
+ for (const comp of componentNames(dir)) {
63
+ if (!live[comp]) throw new Error(`component "${comp}": default.json is missing`);
64
+ configs[comp] = { ...live[comp], component: comp };
97
65
  }
98
66
  return { configs, sources };
99
67
  }
100
68
 
101
69
  /** `engine` is a test seam; the CLI always runs the compiled bundle. */
102
- export async function runAdjust({
70
+ export async function runSetGeometry({
103
71
  opsPath,
104
72
  dryRun = false,
105
73
  root = process.cwd(),
@@ -132,7 +100,7 @@ export async function runAdjust({
132
100
  }
133
101
 
134
102
  const active = readActiveTheme(themesDir ?? resolved?.themesDir);
135
- const { configs, sources } = readLiveConfigs(dir, active);
103
+ const { configs, sources } = stampedLiveConfigs(dir, active);
136
104
  const now = new Date().toISOString();
137
105
  const { configs: next, report } = adjustAliases(configs, ops, now);
138
106
 
@@ -175,7 +143,7 @@ export async function runAdjust({
175
143
  };
176
144
  }
177
145
 
178
- export function formatAdjustResult(result) {
146
+ export function formatSetGeometryResult(result) {
179
147
  const root = process.cwd();
180
148
  const lines = [];
181
149
  const { components: changedCount, aliases, skips } = result.totals;
@@ -192,7 +160,7 @@ export function formatAdjustResult(result) {
192
160
  }
193
161
  if (result.ignoredName !== null) {
194
162
  lines.push(
195
- `Ignored "name": "${result.ignoredName}". adjust edits the open buffer now, ` +
163
+ `Ignored "name": "${result.ignoredName}". set-geometry edits the open buffer, ` +
196
164
  `so it writes no file of its own.`,
197
165
  );
198
166
  }