@motion-proto/live-tokens 0.72.1 → 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.
- package/.claude/skills/live-tokens-build-page/SKILL.md +37 -2
- package/.claude/skills/live-tokens-build-page/references/layout-sources.md +48 -0
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -2
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -4
- package/.claude/skills/live-tokens-create-theme/SKILL.md +80 -0
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
- package/.claude/skills/live-tokens-set-colors/SKILL.md +140 -0
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
- package/.claude/skills/{live-tokens-adjust-geometry → live-tokens-set-geometry}/SKILL.md +12 -7
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +61 -0
- package/.claude/skills/{live-tokens-pair-fonts → live-tokens-set-type}/SKILL.md +18 -15
- package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
- package/CHANGELOG.md +141 -0
- package/README.md +24 -15
- package/bin/check-page.mjs +3 -3
- package/bin/cli.mjs +92 -55
- package/bin/lib/liveState.mjs +110 -0
- package/bin/save-theme.mjs +177 -0
- package/bin/set-colors.mjs +191 -0
- package/bin/{adjust.mjs → set-geometry.mjs} +18 -50
- package/bin/{set-fonts.mjs → set-type.mjs} +21 -55
- package/dist-plugin/{chunk-RIXO2E55.js → chunk-7VRTBGJT.js} +1 -1
- package/dist-plugin/{chunk-YLCOIGQC.js → chunk-V3YF6CGT.js} +56 -2
- package/dist-plugin/index.cjs +58 -3
- package/dist-plugin/index.js +11 -11
- package/dist-plugin/migrateData/index.cjs +56 -1
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +1109 -1071
- package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +33 -24
- package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +33 -24
- package/dist-plugin/{generateColorsAndType → setColors}/index.js +45 -63
- package/dist-plugin/{adjust → setGeometry}/index.cjs +60 -5
- package/dist-plugin/{adjust → setGeometry}/index.d.cts +1 -1
- package/dist-plugin/{adjust → setGeometry}/index.d.ts +1 -1
- package/dist-plugin/{adjust → setGeometry}/index.js +1 -1
- package/dist-plugin/{fontPairing → setType}/index.cjs +4 -4
- package/dist-plugin/{fontPairing → setType}/index.d.cts +1 -1
- package/dist-plugin/{fontPairing → setType}/index.d.ts +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/package.json +10 -2
- package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +82 -96
- package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/docs/content/creating-components.md +13 -0
- package/src/editor/docs/content/themes-workflow.md +2 -2
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
- package/src/editor/skill-atlas/SkillAtlas.svelte +836 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte.d.ts +4 -0
- package/src/editor/skill-atlas/SourcePane.svelte +364 -0
- package/src/editor/skill-atlas/TreeNodeCard.svelte +206 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +45 -0
- package/src/editor/skill-atlas/skillSources.ts +1 -0
- package/src/editor/skill-atlas/skillTrees.ts +3844 -0
- package/src/editor/skill-atlas/types.ts +65 -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 +1 -37
- package/src/live-tokens/data/themes/halloween.json +1 -37
- package/src/live-tokens/data/themes/midnight-study.json +1 -37
- package/src/live-tokens/data/themes/ocean.json +1 -37
- package/src/live-tokens/data/themes/royal-velvet.json +1 -37
- package/src/live-tokens/data/themes/sketchy.json +1 -37
- package/src/live-tokens/data/themes/spring-meadow.json +1 -37
- package/src/live-tokens/data/themes/sunset.json +1 -37
- package/src/live-tokens/data/tokens.generated.css +0 -36
- 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/bin/generate-theme.mjs +0 -260
- /package/dist-plugin/{fontPairing → setType}/index.js +0 -0
package/bin/cli.mjs
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// report The project as facts: tokens read, components used, findings by rule. Always exits 0.
|
|
9
9
|
// check-component [id] Validate a component (or every authored one) against the create-component skill contract.
|
|
10
10
|
// check-page [paths...] Validate pages against the build-page skill contract.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// set-
|
|
11
|
+
// set-colors <colors> Build the color identity from 10 OKLCH base colors, into the open buffer.
|
|
12
|
+
// set-geometry <ops> Apply radius/padding/gap/border-width ops to the open buffer.
|
|
13
|
+
// set-type <pairing> Bind Google Fonts families to the theme's font stacks.
|
|
14
|
+
// save-theme <name> Compose the live state into themes/<slug>.json and open it.
|
|
14
15
|
// migrate [...] Reconcile tokens.css, the data tree, and route references.
|
|
15
16
|
|
|
16
17
|
import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeSync } from 'node:fs';
|
|
@@ -38,9 +39,10 @@ import {
|
|
|
38
39
|
} from './migrate.mjs';
|
|
39
40
|
import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
|
|
40
41
|
import { runCreate, formatCreateResult } from './create.mjs';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
42
|
+
import { runSetColors, formatSetColorsResult } from './set-colors.mjs';
|
|
43
|
+
import { runSetGeometry, formatSetGeometryResult } from './set-geometry.mjs';
|
|
44
|
+
import { runSetType, formatSetTypeResult } from './set-type.mjs';
|
|
45
|
+
import { runSaveTheme, formatSaveThemeResult } from './save-theme.mjs';
|
|
44
46
|
|
|
45
47
|
const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
|
|
46
48
|
|
|
@@ -69,7 +71,7 @@ Commands:
|
|
|
69
71
|
value a theme token. Checks every page under src/
|
|
70
72
|
when given no paths.
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
check-component and check-page also accept:
|
|
73
75
|
--json Machine-readable findings, for a skill to iterate
|
|
74
76
|
against until the exit code is 0
|
|
75
77
|
--strict Treat warnings as errors
|
|
@@ -77,31 +79,29 @@ Both check commands accept:
|
|
|
77
79
|
(or set "checks": { "rules": {...} } in
|
|
78
80
|
live-tokens.config.json; "checks": { "exclude":
|
|
79
81
|
[...] } drops paths from discovery entirely)
|
|
80
|
-
|
|
81
|
-
Build
|
|
82
|
-
(see the live-tokens-
|
|
83
|
-
enforce AA contrast on derived text
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
in the editor
|
|
87
|
-
|
|
88
|
-
--dry-run prints the
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
look, or from theme <name> with --carry-from.
|
|
92
|
-
adjust <ops.json> [--dry-run]
|
|
82
|
+
set-colors <base-colors.json> [--dry-run]
|
|
83
|
+
Build the theme's whole color identity from 10
|
|
84
|
+
OKLCH base colors (see the live-tokens-set-colors
|
|
85
|
+
skill) and enforce AA contrast on the derived text
|
|
86
|
+
tokens. Reads the live colors and type and writes
|
|
87
|
+
the result to the unsaved colors-and-type buffer,
|
|
88
|
+
so save the open theme in the editor or run
|
|
89
|
+
save-theme to keep it. Fonts and every override no
|
|
90
|
+
palette owns carry forward. --dry-run prints the
|
|
91
|
+
contrast report without writing.
|
|
92
|
+
set-geometry <ops.json> [--dry-run]
|
|
93
93
|
Move radius, padding, gap, and border-width
|
|
94
94
|
aliases along their token scales (see the
|
|
95
|
-
live-tokens-
|
|
95
|
+
live-tokens-set-geometry skill). Reads each
|
|
96
96
|
component's live config and writes the result to
|
|
97
97
|
that component's unsaved buffer, so save the open
|
|
98
98
|
theme in the editor to keep it. --dry-run prints
|
|
99
99
|
the report without writing.
|
|
100
|
-
set-
|
|
100
|
+
set-type <pairing.json> [--dry-run] [--no-verify]
|
|
101
101
|
Bind Google Fonts families to --font-display,
|
|
102
102
|
--font-sans, --font-serif, --font-mono and
|
|
103
103
|
--font-editorial (see
|
|
104
|
-
the live-tokens-
|
|
104
|
+
the live-tokens-set-type skill). Each family is
|
|
105
105
|
verified against the Google Fonts API and the URL
|
|
106
106
|
is negotiated from the weights it actually has.
|
|
107
107
|
Writes the result to the unsaved colors-and-type
|
|
@@ -109,6 +109,17 @@ Both check commands accept:
|
|
|
109
109
|
keep it. --dry-run prints the report without
|
|
110
110
|
writing; --no-verify skips the network and
|
|
111
111
|
requires an explicit URL per family.
|
|
112
|
+
save-theme <name> [--no-activate] [--dry-run]
|
|
113
|
+
Compose the live state (the unsaved buffers, the
|
|
114
|
+
open theme under them, the shipped defaults under
|
|
115
|
+
that) into themes/<slug>.json and open it, which
|
|
116
|
+
clears the buffers. With no unsaved edits it saves
|
|
117
|
+
a copy of the open theme under the new name.
|
|
118
|
+
Opening never changes what your site ships; Adopt
|
|
119
|
+
in the editor does that. --no-activate writes the
|
|
120
|
+
theme and leaves the live state alone, so a set of
|
|
121
|
+
themes comes off one starting look; --dry-run
|
|
122
|
+
prints the report without writing.
|
|
112
123
|
migrate [--check] [--write] [--tokens <path>]
|
|
113
124
|
Reconcile your project with the installed package:
|
|
114
125
|
applies additive tokens.css migrations, moves a
|
|
@@ -150,7 +161,7 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
150
161
|
|
|
151
162
|
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
152
163
|
|
|
153
|
-
if (command === 'create'
|
|
164
|
+
if (command === 'create') {
|
|
154
165
|
const targetArg = rest.find((a) => !a.startsWith('-'));
|
|
155
166
|
if (!targetArg) {
|
|
156
167
|
fail(`Usage: npx @motion-proto/live-tokens create <project-directory>`);
|
|
@@ -241,72 +252,97 @@ if (command === 'check-page') {
|
|
|
241
252
|
reportChecks('check-page', findings, checked, PAGE_RULES, opts);
|
|
242
253
|
}
|
|
243
254
|
|
|
244
|
-
if (command === '
|
|
245
|
-
const
|
|
246
|
-
if (!
|
|
247
|
-
fail(`Usage: npx @motion-proto/live-tokens
|
|
255
|
+
if (command === 'set-colors') {
|
|
256
|
+
const baseColorsPath = rest.find((a) => !a.startsWith('-'));
|
|
257
|
+
if (!baseColorsPath) {
|
|
258
|
+
fail(`Usage: npx @motion-proto/live-tokens set-colors <base-colors.json> [--dry-run]`);
|
|
259
|
+
}
|
|
260
|
+
if (rest.includes('--no-activate')) {
|
|
261
|
+
fail(
|
|
262
|
+
`set-colors has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
|
|
263
|
+
`Nothing is activated until save-theme, which takes the flag.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (rest.includes('--carry-from')) {
|
|
267
|
+
fail(
|
|
268
|
+
`set-colors has no --carry-from: it reads the live look and edits it in place, so a second theme ` +
|
|
269
|
+
`already starts from the first. Run save-theme --no-activate between themes.`,
|
|
270
|
+
);
|
|
248
271
|
}
|
|
249
272
|
try {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
if (carryIdx !== -1 && !carryFrom) fail(`--carry-from requires a theme name`);
|
|
253
|
-
const result = await runGenerateTheme({
|
|
254
|
-
briefPath,
|
|
255
|
-
activate: !rest.includes('--no-activate'),
|
|
273
|
+
const result = await runSetColors({
|
|
274
|
+
baseColorsPath,
|
|
256
275
|
dryRun: rest.includes('--dry-run'),
|
|
257
|
-
carryFrom,
|
|
258
276
|
});
|
|
259
|
-
console.log(
|
|
277
|
+
console.log(formatSetColorsResult(result));
|
|
260
278
|
process.exit(result.report.failures.length === 0 ? 0 : 1);
|
|
261
279
|
} catch (err) {
|
|
262
|
-
fail(`
|
|
280
|
+
fail(`set-colors failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
263
281
|
}
|
|
264
282
|
}
|
|
265
283
|
|
|
266
|
-
if (command === '
|
|
284
|
+
if (command === 'set-geometry') {
|
|
267
285
|
const opsPath = rest.find((a) => !a.startsWith('-'));
|
|
268
286
|
if (!opsPath) {
|
|
269
|
-
fail(`Usage: npx @motion-proto/live-tokens
|
|
287
|
+
fail(`Usage: npx @motion-proto/live-tokens set-geometry <ops.json> [--dry-run]`);
|
|
270
288
|
}
|
|
271
289
|
if (rest.includes('--no-activate')) {
|
|
272
290
|
fail(
|
|
273
|
-
`
|
|
291
|
+
`set-geometry has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
|
|
274
292
|
`Drop the flag and re-run.`,
|
|
275
293
|
);
|
|
276
294
|
}
|
|
277
295
|
try {
|
|
278
|
-
const result = await
|
|
296
|
+
const result = await runSetGeometry({
|
|
279
297
|
opsPath,
|
|
280
298
|
dryRun: rest.includes('--dry-run'),
|
|
281
299
|
});
|
|
282
|
-
console.log(
|
|
300
|
+
console.log(formatSetGeometryResult(result));
|
|
283
301
|
process.exit(0);
|
|
284
302
|
} catch (err) {
|
|
285
|
-
fail(`
|
|
303
|
+
fail(`set-geometry failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
286
304
|
}
|
|
287
305
|
}
|
|
288
306
|
|
|
289
|
-
if (command === 'set-
|
|
290
|
-
const
|
|
291
|
-
if (!
|
|
292
|
-
fail(`Usage: npx @motion-proto/live-tokens set-
|
|
307
|
+
if (command === 'set-type') {
|
|
308
|
+
const pairingPath = rest.find((a) => !a.startsWith('-'));
|
|
309
|
+
if (!pairingPath) {
|
|
310
|
+
fail(`Usage: npx @motion-proto/live-tokens set-type <pairing.json> [--dry-run] [--no-verify]`);
|
|
293
311
|
}
|
|
294
312
|
if (rest.includes('--no-activate')) {
|
|
295
313
|
fail(
|
|
296
|
-
`set-
|
|
314
|
+
`set-type has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
|
|
297
315
|
`Drop the flag and re-run.`,
|
|
298
316
|
);
|
|
299
317
|
}
|
|
300
318
|
try {
|
|
301
|
-
const result = await
|
|
302
|
-
|
|
319
|
+
const result = await runSetType({
|
|
320
|
+
pairingPath,
|
|
303
321
|
dryRun: rest.includes('--dry-run'),
|
|
304
322
|
verify: !rest.includes('--no-verify'),
|
|
305
323
|
});
|
|
306
|
-
console.log(
|
|
324
|
+
console.log(formatSetTypeResult(result));
|
|
325
|
+
process.exit(0);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
fail(`set-type failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (command === 'save-theme') {
|
|
332
|
+
const name = rest.find((a) => !a.startsWith('-'));
|
|
333
|
+
if (!name) {
|
|
334
|
+
fail(`Usage: npx @motion-proto/live-tokens save-theme <name> [--no-activate] [--dry-run]`);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const result = await runSaveTheme({
|
|
338
|
+
name,
|
|
339
|
+
activate: !rest.includes('--no-activate'),
|
|
340
|
+
dryRun: rest.includes('--dry-run'),
|
|
341
|
+
});
|
|
342
|
+
console.log(formatSaveThemeResult(result));
|
|
307
343
|
process.exit(0);
|
|
308
344
|
} catch (err) {
|
|
309
|
-
fail(`
|
|
345
|
+
fail(`save-theme failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
310
346
|
}
|
|
311
347
|
}
|
|
312
348
|
|
|
@@ -391,9 +427,10 @@ const SAMPLE_PROMPTS = {
|
|
|
391
427
|
'live-tokens-build-page': 'build a pricing page using live-tokens components',
|
|
392
428
|
'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
|
|
393
429
|
'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
|
|
394
|
-
'live-tokens-
|
|
395
|
-
'live-tokens-
|
|
396
|
-
'live-tokens-
|
|
430
|
+
'live-tokens-create-theme': 'make me a bright and cheerful theme',
|
|
431
|
+
'live-tokens-set-colors': 'give me a cooler palette, same fonts',
|
|
432
|
+
'live-tokens-set-type': 'pair some fonts for this theme',
|
|
433
|
+
'live-tokens-set-geometry': 'make the buttons pill shaped',
|
|
397
434
|
'live-tokens-fix-findings': 'make check:design pass',
|
|
398
435
|
'live-tokens-check-compliance': 'check this project against the design system',
|
|
399
436
|
};
|
|
@@ -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
|
+
}
|