@motion-proto/live-tokens 0.47.0 → 0.48.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 (124) hide show
  1. package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
  4. package/CHANGELOG.md +292 -0
  5. package/README.md +37 -26
  6. package/bin/adjust.mjs +254 -0
  7. package/bin/cli.mjs +94 -7
  8. package/bin/generate-theme.mjs +251 -0
  9. package/bin/migrate.mjs +95 -6
  10. package/dist-plugin/adjust/index.cjs +259 -0
  11. package/dist-plugin/adjust/index.d.cts +45 -0
  12. package/dist-plugin/adjust/index.d.ts +45 -0
  13. package/dist-plugin/adjust/index.js +176 -0
  14. package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
  15. package/dist-plugin/chunk-6OZFXIQI.js +316 -0
  16. package/dist-plugin/chunk-76TFDTJO.js +556 -0
  17. package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
  18. package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
  19. package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
  20. package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
  21. package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
  22. package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
  23. package/dist-plugin/generateColorsAndType/index.js +1210 -0
  24. package/dist-plugin/index.cjs +901 -533
  25. package/dist-plugin/index.d.cts +3 -2
  26. package/dist-plugin/index.d.ts +3 -2
  27. package/dist-plugin/index.js +658 -1082
  28. package/dist-plugin/migrateData/index.cjs +728 -0
  29. package/dist-plugin/migrateData/index.d.cts +60 -0
  30. package/dist-plugin/migrateData/index.d.ts +60 -0
  31. package/dist-plugin/migrateData/index.js +348 -0
  32. package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
  33. package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
  34. package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
  36. package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
  37. package/dist-plugin/tokensCssMigrations/index.js +4 -2
  38. package/package.json +18 -4
  39. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
  40. package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
  41. package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
  42. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
  44. package/src/editor/core/components/adjustAliases.ts +180 -0
  45. package/src/editor/core/components/aliasKinds.ts +73 -0
  46. package/src/editor/core/components/componentConfigService.ts +26 -38
  47. package/src/editor/core/flashStatus.ts +1 -1
  48. package/src/editor/core/fonts/fontMigration.ts +13 -13
  49. package/src/editor/core/fonts/fontPairing.ts +35 -0
  50. package/src/editor/core/palettes/paletteDerivation.ts +128 -16
  51. package/src/editor/core/preview/lookPreview.ts +141 -0
  52. package/src/editor/core/productionPulse.ts +34 -21
  53. package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
  54. package/src/editor/core/store/editorConfigStore.ts +2 -2
  55. package/src/editor/core/store/editorPersistence.ts +0 -1
  56. package/src/editor/core/store/editorStore.ts +114 -51
  57. package/src/editor/core/store/editorTypes.ts +0 -1
  58. package/src/editor/core/store/gradientSource.ts +2 -2
  59. package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
  60. package/src/editor/core/themes/generateColorsAndType.ts +433 -0
  61. package/src/editor/core/themes/loadRows.ts +64 -0
  62. package/src/editor/core/themes/lookSummary.ts +75 -0
  63. package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
  64. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
  65. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
  66. package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
  67. package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
  68. package/src/editor/core/themes/migrations/index.ts +19 -11
  69. package/src/editor/core/themes/slices/components.ts +4 -4
  70. package/src/editor/core/themes/slices/fonts.ts +1 -1
  71. package/src/editor/core/themes/slices/gradients.ts +22 -0
  72. package/src/editor/core/themes/slices/palettes.ts +2 -2
  73. package/src/editor/core/themes/themeInit.ts +21 -29
  74. package/src/editor/core/themes/themeService.ts +218 -78
  75. package/src/editor/core/themes/themeTypes.ts +94 -51
  76. package/src/editor/docs/Docs.svelte +1 -0
  77. package/src/editor/docs/chapters.ts +1 -0
  78. package/src/editor/docs/content/01-overview.md +1 -1
  79. package/src/editor/docs/content/editing-tokens.md +6 -6
  80. package/src/editor/docs/content/getting-started.md +9 -7
  81. package/src/editor/docs/content/themes-workflow.md +74 -34
  82. package/src/editor/docs/content/where-themes-live.md +61 -0
  83. package/src/editor/docs/content.generated.ts +5 -4
  84. package/src/editor/index.ts +27 -27
  85. package/src/editor/pages/ComponentEditorPage.svelte +2 -2
  86. package/src/editor/pages/EditorShell.svelte +4 -30
  87. package/src/editor/ui/BezierCurveEditor.svelte +2 -2
  88. package/src/editor/ui/ColorEditPanel.svelte +1 -1
  89. package/src/editor/ui/FileLoadList.svelte +67 -7
  90. package/src/editor/ui/GradientEditor.svelte +3 -3
  91. package/src/editor/ui/PaletteEditor.svelte +26 -12
  92. package/src/editor/ui/ThemePanel.svelte +1067 -0
  93. package/src/editor/ui/UIDialog.svelte +11 -9
  94. package/src/editor/ui/UIPillButton.svelte +7 -2
  95. package/src/editor/ui/colors/ColorWheel.svelte +34 -1
  96. package/src/editor/ui/curveEngine.ts +48 -0
  97. package/src/editor/ui/index.ts +4 -2
  98. package/src/editor/ui/palette/PaletteBase.svelte +75 -51
  99. package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -8
  100. package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
  101. package/src/live-tokens/data/{themes → colors-and-type}/default.json +816 -293
  102. package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
  103. package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
  104. package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
  105. package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
  106. package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
  107. package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
  108. package/src/live-tokens/data/themes/autumn.json +3918 -0
  109. package/src/live-tokens/data/themes/halloween.json +3992 -0
  110. package/src/live-tokens/data/themes/midnight-study.json +3932 -0
  111. package/src/live-tokens/data/themes/ocean.json +3917 -0
  112. package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
  113. package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
  114. package/src/live-tokens/data/themes/sunset.json +3931 -0
  115. package/src/live-tokens/data/tokens.generated.css +106 -117
  116. package/src/system/styles/CONVENTIONS.md +1 -1
  117. package/src/system/styles/fonts.css +1 -1
  118. package/template/README.md +7 -5
  119. package/template/_gitignore +0 -6
  120. package/src/editor/core/components/componentPersist.ts +0 -62
  121. package/src/editor/core/manifests/manifestService.ts +0 -172
  122. package/src/editor/ui/ManifestFileManager.svelte +0 -446
  123. package/src/editor/ui/ThemeFileManager.svelte +0 -785
  124. package/src/live-tokens/data/manifests/default.json +0 -35
package/README.md CHANGED
@@ -8,12 +8,13 @@ A foundational design system for quickly styling and building Svelte + Vite micr
8
8
 
9
9
  - **Real-time token editing.** Pick a color, drag a hue slider, retype a font size — the page repaints on every input event via CSS-variable writes. No reload, no save-and-refresh, no build step. Works across colors, typography, spacing, radii, shadows, motion, palettes, and gradients.
10
10
  - **Real-time component editing.** Each of ~24 shipped Svelte components (Button, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, and more) declares its own design-token aliases in a `:global(:root)` block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox.
11
- - **Theme editor** (`/live-tokens/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
11
+ - **Theme editor** (`/live-tokens/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, then Adopt one to bake it into static CSS for the build.
12
12
  - **Per-component editor** (`/live-tokens/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
13
13
  - **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
14
- - **Manifests** — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
15
- - **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
16
- - **Claude Code skill suite** — three bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. One command to install all three: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
14
+ - **Themes** — a theme is a whole look in one file: colors and type plus a config for every component you changed, held by value. In the editor this is the Theme panel, with Colors & Type and Components as its parts. Themes are documents: loading one opens it, filling the editor's working buffer and returning every component it does not carry to its default, and nothing your site ships changes until you Adopt. A narrower load takes the colors and type alone and leaves your shapes. Export one and import it into another project to restore the full styling in one step.
15
+ - **Seven example looks** — Autumn, Halloween, Midnight Study, Ocean, Royal Velvet, Spring Meadow and Sunset each ship as a full theme: preset colors and type plus a shape personality of radius, padding, gap and border-width aliases. Each preset also names its own Google Fonts pairing, one display family and one body family, so a look carries type as well as colour and shape. They need no local files, so Load one to try a whole look on your own pages and load Motion Proto to come back. Saving over a preset writes a local copy that shadows the shipped one; delete that copy and the shipped version returns.
16
+ - **Vite plugin** — hosts the `/api/live-tokens/{colors-and-type,component-configs,themes}/*` routes the editor reads and saves through. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
17
+ - **Claude Code skill suite** — five bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. `generate-theme` turns a mood brief ("bright and cheerful", "dark night theme") into a complete AA-checked color theme. `adjust-shape-space` turns "make the buttons pill shaped" or "space it out" into new radius, padding, gap, and border-width aliases. One command to install them all: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
17
18
 
18
19
  ## Quick install
19
20
 
@@ -42,14 +43,20 @@ export default defineConfig({
42
43
  ```
43
44
 
44
45
  The `themeFileApi` plugin:
45
- - Seeds `src/live-tokens/data/themes/` with a default theme on first dev-server start.
46
+ - Resolves the `default` colors and type from the installed package, so you start on the shipped defaults without a local copy.
46
47
  - Discovers components at `src/components/*.svelte` (and `src/system/components/*.svelte` for back-compat) and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
48
+ - Writes `src/live-tokens/data/themes/default.json` on dev-server start: the Default theme, derived from the shipped colors and type and those component defaults, and regenerated whenever they change. It is protected, so the editor never deletes it and an outside deletion heals on the next start.
49
+ - Bakes `tokens.generated.css` from the production theme at startup, so a fresh checkout builds against the look you shipped.
47
50
  - Hosts the `/api/live-tokens/*` routes the editor uses to save and load themes + per-component configs.
48
51
  - Auto-injects `__PROJECT_ROOT__` for the overlay's "Page Source" link and `__LIVE_TOKENS_API_BASE__` so the client uses whatever `apiBase` you configured.
49
52
 
53
+ A project last opened on 0.47.1 or earlier still keeps its colors and type in `themes/` and its whole looks in `manifests/`, the names 0.48 reassigned. The plugin recognises that layout, writes nothing at all, and says so. `npx live-tokens migrate` moves `themes/` to `colors-and-type/` and `manifests/` to `themes/`, then heals what is inside: it reads what the retired per-layer `_active.json` / `_production.json` pointers resolved to, records it as the production theme, and clears them. Restart the dev server afterwards.
54
+
50
55
  ### Where data lands — and how to move it
51
56
 
52
- By default, the plugin reads and writes under one folder: `src/live-tokens/data/`. Inside that folder live three subdirectories — `themes/`, `manifests/`, `component-configs/` — each owned by the plugin.
57
+ By default, the plugin reads and writes under one folder: `src/live-tokens/data/`. Inside that folder live three subdirectories — `colors-and-type/`, `themes/`, `component-configs/` — each owned by the plugin.
58
+
59
+ `themes/` holds the documents: one file per whole look, plus `_active.json` naming the one the editor has open and `_production.json` naming the one your site ships. `colors-and-type/` and `component-configs/{comp}/` hold each layer's `default.json` baseline, any preset you save by name, and the `_working.json` buffer for edits you have not saved into a theme. A buffer exists only where the look sits off the shipped default, so a new project has none.
53
60
 
54
61
  To move them, create a `live-tokens.config.json` at your project root:
55
62
 
@@ -59,17 +66,19 @@ To move them, create a `live-tokens.config.json` at your project root:
59
66
  }
60
67
  ```
61
68
 
62
- All four keys are optional. `dataDir` is the headline knob — it relocates all three subfolders at once. The per-folder overrides exist for unusual layouts (e.g. a monorepo where themes are shared across packages but component-configs aren't):
69
+ All four keys are optional. `dataDir` is the headline knob — it relocates all three subfolders at once. The per-folder overrides exist for unusual layouts (e.g. a monorepo where colors and type are shared across packages but component-configs aren't):
63
70
 
64
71
  ```json
65
72
  {
66
73
  "dataDir": "src/live-tokens/data",
67
- "themesDir": "../shared/themes",
74
+ "colorsAndTypeDir": "../shared/colors-and-type",
68
75
  "componentConfigsDir": "src/live-tokens/data/component-configs",
69
- "manifestsDir": "src/live-tokens/data/manifests"
76
+ "themesDir": "src/live-tokens/data/themes"
70
77
  }
71
78
  ```
72
79
 
80
+ `colorsAndTypeDir` holds the colors-and-type files; `themesDir` holds the whole-look themes.
81
+
73
82
  Resolution order, per folder: explicit `themeFileApi(opts)` argument > matching key in `live-tokens.config.json` > `<dataDir>/<sub>`. The dev server reads the file once at startup — restart vite to pick up changes.
74
83
 
75
84
  ### Bootstrap in `main.ts`
@@ -204,8 +213,8 @@ import '@motion-proto/live-tokens/app/fonts.css'; // optional: Fraunces + Manro
204
213
  ```
205
214
 
206
215
  …or copy `node_modules/@motion-proto/live-tokens/src/system/styles/tokens.css` into
207
- your project and edit. The editor will seed `themes/default.json` on first
208
- run and you can promote your edits back into the file.
216
+ your project and edit. It stays hand-authored: the editor writes what you Adopt
217
+ into the sidecar `tokens.generated.css`, never back into `tokens.css`.
209
218
 
210
219
  ## Consuming live-tokens from scratch
211
220
 
@@ -269,8 +278,8 @@ src/
269
278
  system/styles/tokens.css # vendored Layer-1 tokens — committed
270
279
  live-tokens/data/ # editor state — committed
271
280
  tokens.generated.css # editor output
272
- themes/ manifests/ component-configs/
273
- **/_backups/ # gitignored (local-only snapshots)
281
+ themes/ # one file per whole look, plus the two pointers
282
+ colors-and-type/ component-configs/
274
283
  vite.config.ts # svelte({ preprocess: vitePreprocess() }) + themeFileApi
275
284
  svelte.config.js # vitePreprocess()
276
285
  ```
@@ -279,7 +288,7 @@ Conventions that make this work:
279
288
 
280
289
  - **Vendor `tokens.css` into `src/` and commit it.** Point `themeFileApi({ tokensCssPath })` at that file, not at one inside `node_modules`. The dev server writes your edits there; a copy under `node_modules` is wiped on every `npm install`.
281
290
  - **All editable state lives under `src/` and is committed** — `tokens.css`, `tokens.generated.css`, and everything in `live-tokens/data/`. This is the invariant that makes upgrades safe: `npm install` only ever touches `node_modules` + `package.json` + the lockfile, never your `src/`.
282
- - **`_backups/` is gitignored.** The dev server snapshots a file before overwriting it; those snapshots are local working state, not source.
291
+ - **Nothing is backed up for you.** The dev server keeps no snapshots, so git is the safety net: commit a theme you care about before editing over it.
283
292
  - **Preprocess with `vitePreprocess()`** (bundled in `@sveltejs/vite-plugin-svelte`), keeping `sass` installed for the components' `scss`. No `svelte-preprocess`, no `legacy-peer-deps` `.npmrc` — the dependency tree resolves cleanly on its own (since 0.19.1).
284
293
  - **Import only from the public surface** — `@motion-proto/live-tokens`, `/components/*`, `/vite-plugin`, `/app/*`.
285
294
 
@@ -311,13 +320,15 @@ The component appears in the `/live-tokens/components` page under a **CUSTOM** g
311
320
 
312
321
  ## Claude Code skills
313
322
 
314
- The package ships a suite of Claude Code skills that encode the project's conventions so Claude can drive the package in plain English. They cover the three jobs the README itself can't carry well: deciding which shipped component fits a need, composing a page from the catalogue, and (for the long-tail case) authoring a new editable component. Each skill auto-triggers from natural-language requests — no slash commands. (Plain `npm install` plus the README handle first-time setup.)
323
+ The package ships a suite of Claude Code skills that encode the project's conventions so Claude can drive the package in plain English. They cover the jobs the README itself can't carry well: deciding which shipped component fits a need, composing a page from the catalogue, generating a color theme from a mood brief, adjusting shape and space from plain language, and (for the long-tail case) authoring a new editable component. Each skill auto-triggers from natural-language requests — no slash commands. (Plain `npm install` plus the README handle first-time setup.)
315
324
 
316
325
  | Skill | Triggers on | What it knows |
317
326
  |--------------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
318
327
  | `live-tokens-build-page` | "build a pricing page using live-tokens components" | shipped-component catalogue, column grid, `pageSources` registration, token-only styling rule |
319
328
  | `live-tokens-pick-component` | "what's the difference between TabBar and SegmentedControl?" | decision tables for each confusable family (selection, container, messaging, on/off); when to author a new one instead |
320
329
  | `live-tokens-create-component` | "author a new Toggle component for my live-tokens project" | runtime + editor + `registerComponent()` recipe, naming scheme, state model, public-imports rule, verification checklist |
330
+ | `live-tokens-generate-theme` | "make me a bright and cheerful color theme" | mood → OKLCH seed framework (chroma budget, per-role bands, gamut guardrails, holiday palettes); drives `npx live-tokens generate-theme`, which enforces AA contrast |
331
+ | `live-tokens-adjust-shape-space` | "make the buttons pill shaped", "space it out" | shape and space idioms (pill, sharper, softer, tighter, airier) → radius/padding/gap/border-width ops; drives `npx live-tokens adjust`, which moves aliases along the shipped token scales |
321
332
 
322
333
  ### Install
323
334
 
@@ -345,8 +356,8 @@ It enforces the file layout, `:global(:root)` block, token-suffix vocabulary, th
345
356
 
346
357
  ## How the editor ships changes to prod
347
358
 
348
- 1. Edit in `/live-tokens/editor` or `/live-tokens/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
349
- 2. Promote a theme to "production." Its variables are written into `tokens.generated.css` next to your authored `tokens.css`.
359
+ 1. Edit in `/live-tokens/editor` or `/live-tokens/components`. Your edits sit in the working buffer (`_working.json`); **Save** in the Theme panel captures that buffer into the open theme at `<dataDir>/themes/{name}.json`.
360
+ 2. **Adopt** the theme. It becomes the production theme, and its variables are baked into `tokens.generated.css` next to your authored `tokens.css`. Nothing else writes that file, so trying a look never changes what you ship.
350
361
  3. `npm run build` bundles both as plain CSS. No editor code, no JSON lookups, no dev surfaces ship to prod.
351
362
 
352
363
  ## File ownership — what the plugin writes
@@ -362,19 +373,19 @@ Knowing which files the plugin touches matters when upgrading the package or wor
362
373
 
363
374
  It never writes to your project root, your `src/` outside the data folder, or anywhere else.
364
375
 
365
- **At dev-server startup, the plugin only fills gaps — it never overwrites authored files:**
376
+ **At dev-server startup, the plugin fills gaps and refreshes its own derived files — it never overwrites authored ones:**
366
377
 
367
- - `<dataDir>/themes/default.json` — written **only if missing**.
368
- - `<dataDir>/themes/_active.json` and `_production.json` — written **only if missing**.
369
- - `<dataDir>/component-configs/{comp}/_active.json` and `_production.json` — same: only if missing.
378
+ - `<dataDir>/themes/default.json` — the derived Default theme, regenerated when the shipped colors and type or a component default changes.
379
+ - `<dataDir>/themes/_active.json` and `_production.json` — written **only if missing**, and healed when they name a theme that no longer resolves.
370
380
  - `<dataDir>/component-configs/{comp}/default.json` — regenerated from the component's `:global(:root)` block **only when the `.svelte` source is newer than the existing default**. This file is a build artifact of the source; don't hand-edit it.
381
+ - `<tokensCssPath sibling>/tokens.generated.css` — rebaked from the production theme, so a fresh checkout builds against the look you shipped.
371
382
 
372
- **At dev-time editor actions, these files get rewritten by your explicit save/promote:**
383
+ **At dev-time editor actions, these files get rewritten by what you do:**
373
384
 
374
- - `<dataDir>/themes/{name}.json` — every save in the editor.
375
- - `<tokensCssPath sibling>/tokens.generated.css` — fully regenerated when you save or promote the production theme.
376
- - `<tokensCssPath sibling>/fonts.css` — same rule: regenerated from the theme's font sources.
377
- - `<dataDir>/component-configs/{comp}/{name}.json` — every save of a per-component config.
385
+ - `<dataDir>/colors-and-type/_working.json` and `<dataDir>/component-configs/{comp}/_working.json` — the buffer, written as you save each part of the look and cleared when a theme you open does not carry it.
386
+ - `<dataDir>/themes/{name}.json` — every Save and Save As in the Theme panel.
387
+ - `<dataDir>/colors-and-type/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json` — only when you save a preset by name. Nothing machine-written lands among them.
388
+ - `<tokensCssPath sibling>/tokens.generated.css` and `fonts.css` — regenerated from the production theme when you Adopt.
378
389
 
379
390
  The developer-authored `tokens.css` itself is **never written** by the plugin — it holds defaults you're free to hand-edit. The editor's overrides land in the sidecar `tokens.generated.css`, which the package imports immediately after `tokens.css`.
380
391
 
package/bin/adjust.mjs ADDED
@@ -0,0 +1,254 @@
1
+ // `live-tokens adjust` worker.
2
+ //
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
5
+ // writes the result into that component's `_working.json` buffer: the same slot
6
+ // the editor's own edits land in, so an adjustment is an unsaved edit the user
7
+ // saves into a theme when they want to keep it. `default.json`, named preset
8
+ // files, themes, colors and type, and tokens.css are never touched.
9
+
10
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join, relative, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ 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
+
18
+ const SOURCE_LABELS = {
19
+ working: 'your unsaved edits',
20
+ theme: 'the open theme',
21
+ default: 'the shipped default',
22
+ };
23
+
24
+ const SKIP_LABELS = [
25
+ ['raw-value', 'raw value, not a token'],
26
+ ['off-ladder', 'off the ladder'],
27
+ ['clamped', 'already at the ladder end'],
28
+ ['pill-preserved', 'pill preserved (pass "full": true to move it)'],
29
+ ];
30
+
31
+ async function loadEngine() {
32
+ if (!existsSync(ENGINE)) {
33
+ throw new Error(
34
+ `adjust engine not found at ${relative(process.cwd(), ENGINE)}. ` +
35
+ `Build the plugin first (npm run build:plugin).`,
36
+ );
37
+ }
38
+ return import(ENGINE);
39
+ }
40
+
41
+ function readJson(path) {
42
+ return JSON.parse(readFileSync(path, 'utf8'));
43
+ }
44
+
45
+ function readJsonIfExists(path) {
46
+ return existsSync(path) ? readJson(path) : null;
47
+ }
48
+
49
+ function numericRung(token) {
50
+ const match = /-(\d+)$/.exec(token);
51
+ return match ? Number(match[1]) : null;
52
+ }
53
+
54
+ /** Net direction the ops asked for on this alias. An explicit `set` owns the
55
+ * value outright, so it reports no direction. */
56
+ function requestedDirection(ops, matchesKind, component, variable) {
57
+ let total = 0;
58
+ for (const op of ops) {
59
+ if (op.target !== undefined && op.target !== component) continue;
60
+ if (!matchesKind(variable, op.kind)) continue;
61
+ if (op.shift === undefined) return 0;
62
+ total += op.shift;
63
+ }
64
+ return Math.sign(total);
65
+ }
66
+
67
+ /** A shift can land below where it started: an off-subset value (`--space-64`)
68
+ * snaps to its nearest writable rung first. Those changes are marked, never
69
+ * listed as an ordinary shift. */
70
+ function opposesShift(ops, matchesKind, component, change) {
71
+ const from = numericRung(change.from);
72
+ const to = numericRung(change.to);
73
+ if (from === null || to === null) return false;
74
+ const direction = requestedDirection(ops, matchesKind, component, change.variable);
75
+ return direction !== 0 && Math.sign(to - from) !== direction;
76
+ }
77
+
78
+ /** Successive ops can touch the same alias (soften, then pill the buttons);
79
+ * the report shows one entry per alias, first `from` to last `to`. */
80
+ function collapseChanges(changes) {
81
+ const byVariable = new Map();
82
+ for (const change of changes) {
83
+ const entry = byVariable.get(change.variable);
84
+ if (entry) entry.to = change.to;
85
+ else byVariable.set(change.variable, { ...change });
86
+ }
87
+ return [...byVariable.values()].filter((c) => c.from !== c.to);
88
+ }
89
+
90
+ /** The open theme, read the way every other door reads it: the local file
91
+ * first, then the copy the installed package ships. */
92
+ function readActiveTheme(themesDir) {
93
+ if (!themesDir) return null;
94
+ const slug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
95
+ const theme =
96
+ readJsonIfExists(join(themesDir, `${slug}.json`)) ??
97
+ readJsonIfExists(join(packageThemesDir, `${slug}.json`));
98
+ return theme ? { slug, theme } : null;
99
+ }
100
+
101
+ /** Each component's live config and where it came from: the buffer, else the
102
+ * open theme's embedded copy, else the shipped default. Mirrors the dev
103
+ * server's `resolveLiveComponentConfig`, so the CLI adjusts what the page runs. */
104
+ function readLiveConfigs(dir, active) {
105
+ const configs = {};
106
+ const sources = {};
107
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
108
+ if (!entry.isDirectory()) continue;
109
+ const comp = entry.name;
110
+ const componentDir = join(dir, comp);
111
+ const working = readJsonIfExists(join(componentDir, '_working.json'));
112
+ const embedded = active?.theme?.componentConfigs?.[comp];
113
+ const config = working ?? embedded ?? readJsonIfExists(join(componentDir, 'default.json'));
114
+ if (!config) {
115
+ throw new Error(`component "${comp}": default.json is missing`);
116
+ }
117
+ configs[comp] = { ...config, component: comp };
118
+ sources[comp] = working ? 'working' : embedded ? 'theme' : 'default';
119
+ }
120
+ return { configs, sources };
121
+ }
122
+
123
+ /** `engine` is a test seam; the CLI always runs the compiled bundle. */
124
+ export async function runAdjust({
125
+ opsPath,
126
+ dryRun = false,
127
+ root = process.cwd(),
128
+ componentConfigsDir,
129
+ themesDir,
130
+ engine,
131
+ } = {}) {
132
+ const { adjustAliases, matchesKind, resolveDataDirs } = engine ?? (await loadEngine());
133
+
134
+ const opsFull = resolve(root, opsPath);
135
+ if (!existsSync(opsFull)) {
136
+ throw new Error(`ops file not found at ${relative(root, opsFull)}`);
137
+ }
138
+ let doc;
139
+ try {
140
+ doc = readJson(opsFull);
141
+ } catch (err) {
142
+ throw new Error(`ops file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
143
+ }
144
+
145
+ const ops = doc?.ops;
146
+ if (!Array.isArray(ops) || ops.length === 0) {
147
+ throw new Error('ops file needs a non-empty "ops" array');
148
+ }
149
+
150
+ const resolved = componentConfigsDir && themesDir ? null : resolveDataDirs();
151
+ const dir = componentConfigsDir ?? resolved.componentConfigsDir;
152
+ if (!existsSync(dir)) {
153
+ throw new Error(`no component configs at ${relative(root, dir)}. Run the dev server once to create them.`);
154
+ }
155
+
156
+ const active = readActiveTheme(themesDir ?? resolved?.themesDir);
157
+ const { configs, sources } = readLiveConfigs(dir, active);
158
+ const now = new Date().toISOString();
159
+ const { configs: next, report } = adjustAliases(configs, ops, now);
160
+
161
+ const components = [];
162
+ for (const entry of report.components) {
163
+ const { component, skips } = entry;
164
+ const changes = collapseChanges(entry.changes);
165
+
166
+ if (changes.length > 0 && !dryRun) {
167
+ writeFileSync(
168
+ join(dir, component, '_working.json'),
169
+ JSON.stringify({ ...next[component], component }, null, 2),
170
+ );
171
+ }
172
+
173
+ components.push({
174
+ component,
175
+ source: sources[component],
176
+ changes: changes.map((c) => ({ ...c, snapped: opposesShift(ops, matchesKind, component, c) })),
177
+ skips,
178
+ });
179
+ }
180
+ components.sort((a, b) => a.component.localeCompare(b.component));
181
+
182
+ const changed = components.filter((c) => c.changes.length > 0);
183
+ return {
184
+ configsDir: dir,
185
+ openTheme: active?.slug ?? null,
186
+ // The ops file's `name` used to pick a file name. Buffers are fixed slots,
187
+ // so it now names nothing; say so rather than drop it in silence.
188
+ ignoredName: doc.name === undefined ? null : String(doc.name),
189
+ dryRun,
190
+ buffered: !dryRun && changed.length > 0,
191
+ components,
192
+ totals: {
193
+ components: changed.length,
194
+ aliases: changed.reduce((n, c) => n + c.changes.length, 0),
195
+ skips: components.reduce((n, c) => n + c.skips.length, 0),
196
+ },
197
+ };
198
+ }
199
+
200
+ export function formatAdjustResult(result) {
201
+ const root = process.cwd();
202
+ const lines = [];
203
+ const { components: changedCount, aliases, skips } = result.totals;
204
+
205
+ if (changedCount === 0) {
206
+ lines.push(
207
+ skips > 0
208
+ ? `Nothing changed: every matching alias was skipped (reasons below).`
209
+ : `Nothing to change: no alias matched the ops.`,
210
+ );
211
+ } else {
212
+ const verb = result.dryRun ? 'Would edit' : 'Edited';
213
+ lines.push(`${verb} the open buffer of ${changedCount} component(s).`);
214
+ }
215
+ if (result.ignoredName !== null) {
216
+ lines.push(
217
+ `Ignored "name": "${result.ignoredName}". adjust edits the open buffer now, ` +
218
+ `so it writes no file of its own.`,
219
+ );
220
+ }
221
+
222
+ for (const entry of result.components) {
223
+ const from =
224
+ entry.source === 'theme' && result.openTheme
225
+ ? `theme "${result.openTheme}"`
226
+ : SOURCE_LABELS[entry.source];
227
+ lines.push(`\n${entry.component} (from: ${from})`);
228
+
229
+ const width = Math.max(0, ...entry.changes.map((c) => c.variable.length));
230
+ for (const c of entry.changes) {
231
+ const note = c.snapped ? ' ← snapped to the nearest editor rung, against the requested shift' : '';
232
+ lines.push(` ${c.snapped ? '!' : ' '} ${c.variable.padEnd(width)} ${c.from} → ${c.to}${note}`);
233
+ }
234
+
235
+ for (const [reason, label] of SKIP_LABELS) {
236
+ const hit = entry.skips.filter((s) => s.reason === reason);
237
+ if (hit.length === 0) continue;
238
+ lines.push(` skipped, ${label}: ${hit.map((s) => s.variable).join(', ')}`);
239
+ }
240
+ }
241
+
242
+ lines.push(
243
+ `\n${changedCount} component(s) changed, ${aliases} alias(es), ${skips} skipped.`,
244
+ );
245
+ if (result.buffered) {
246
+ lines.push(
247
+ `Reload the app to see it. This is an unsaved edit: save the open theme in the ` +
248
+ `editor's Theme panel to keep it, or load a theme to discard it.`,
249
+ );
250
+ } else if (result.dryRun) {
251
+ lines.push(`Dry run: nothing written under ${relative(root, result.configsDir)}.`);
252
+ }
253
+ return lines.join('\n');
254
+ }
package/bin/cli.mjs CHANGED
@@ -4,15 +4,25 @@
4
4
  // create <dir> Scaffold a new app that depends on this package.
5
5
  // setup-claude [--force] Copy bundled Claude Code skills into ./.claude/skills/.
6
6
  // check-component <id> Validate a component against the add-component skill contract.
7
+ // generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
8
+ // adjust <ops.json> Apply radius/padding/gap/border-width ops to the open buffer.
9
+ // migrate [...] Reconcile tokens.css, the data tree, and route references.
7
10
 
8
11
  import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
9
12
  import { dirname, join, resolve } from 'node:path';
10
13
  import { fileURLToPath } from 'node:url';
11
14
  import process from 'node:process';
12
15
  import { checkComponent, formatReport } from './check-component.mjs';
13
- import { runMigrate, formatMigrateResult } from './migrate.mjs';
16
+ import {
17
+ runMigrate,
18
+ formatMigrateResult,
19
+ runMigrateData,
20
+ formatMigrateDataResult,
21
+ } from './migrate.mjs';
14
22
  import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
15
23
  import { runCreate, formatCreateResult } from './create.mjs';
24
+ import { runGenerateTheme, formatGenerateThemeResult } from './generate-theme.mjs';
25
+ import { runAdjust, formatAdjustResult } from './adjust.mjs';
16
26
 
17
27
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
18
28
 
@@ -22,15 +32,37 @@ Commands:
22
32
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
23
33
  check-component <id> Validate <id>'s runtime, editor, and registration
24
34
  against the live-tokens-create-component contract
35
+ generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]
36
+ Build a full theme from a 10-seed OKLCH brief
37
+ (see the live-tokens-generate-theme skill),
38
+ enforce AA contrast on derived text tokens, write
39
+ themes/<slug>.json, and open it in the editor.
40
+ Opening never changes what your site ships; Adopt
41
+ in the editor does that.
42
+ --no-activate writes the theme without opening it;
43
+ --dry-run prints the contrast report without
44
+ writing. Non-color content (gradients, fonts,
45
+ component aliases) carries forward from the live
46
+ look, or from theme <name> with --carry-from.
47
+ adjust <ops.json> [--dry-run]
48
+ Move radius, padding, gap, and border-width
49
+ aliases along their token scales (see the
50
+ live-tokens-adjust-shape-space skill). Reads each
51
+ component's live config and writes the result to
52
+ that component's unsaved buffer, so save the open
53
+ theme in the editor to keep it. --dry-run prints
54
+ the report without writing.
25
55
  migrate [--check] [--write] [--tokens <path>]
26
56
  Reconcile your project with the installed package:
27
- applies additive tokens.css migrations (unless
28
- --check), and reports source references to the
57
+ applies additive tokens.css migrations, moves a
58
+ pre-0.48 data tree onto the current directory
59
+ names, heals what the retired pointer files named,
60
+ and reports source references to the
29
61
  editor/components/docs routes that moved to
30
62
  /live-tokens/* in 0.35.0. --write also rewrites the
31
63
  unambiguous route references (never /docs). --check
32
- reports without writing (exit 1 if token migrations
33
- are pending; route findings are advisory).
64
+ prints both plans without writing (exit 1 when
65
+ either is pending; route findings are advisory).
34
66
  `;
35
67
 
36
68
  function fail(message, code = 1) {
@@ -71,6 +103,51 @@ if (command === 'check-component') {
71
103
  process.exit(result.errors.length === 0 ? 0 : 1);
72
104
  }
73
105
 
106
+ if (command === 'generate-theme') {
107
+ const briefPath = rest.find((a) => !a.startsWith('-'));
108
+ if (!briefPath) {
109
+ fail(`Usage: npx @motion-proto/live-tokens generate-theme <brief.json> [--no-activate] [--dry-run]`);
110
+ }
111
+ try {
112
+ const carryIdx = rest.indexOf('--carry-from');
113
+ const carryFrom = carryIdx !== -1 ? rest[carryIdx + 1] : undefined;
114
+ if (carryIdx !== -1 && !carryFrom) fail(`--carry-from requires a theme name`);
115
+ const result = await runGenerateTheme({
116
+ briefPath,
117
+ activate: !rest.includes('--no-activate'),
118
+ dryRun: rest.includes('--dry-run'),
119
+ carryFrom,
120
+ });
121
+ console.log(formatGenerateThemeResult(result));
122
+ process.exit(result.report.failures.length === 0 ? 0 : 1);
123
+ } catch (err) {
124
+ fail(`generate-theme failed: ${err instanceof Error ? err.message : String(err)}`);
125
+ }
126
+ }
127
+
128
+ if (command === 'adjust') {
129
+ const opsPath = rest.find((a) => !a.startsWith('-'));
130
+ if (!opsPath) {
131
+ fail(`Usage: npx @motion-proto/live-tokens adjust <ops.json> [--dry-run]`);
132
+ }
133
+ if (rest.includes('--no-activate')) {
134
+ fail(
135
+ `adjust has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
136
+ `Drop the flag and re-run.`,
137
+ );
138
+ }
139
+ try {
140
+ const result = await runAdjust({
141
+ opsPath,
142
+ dryRun: rest.includes('--dry-run'),
143
+ });
144
+ console.log(formatAdjustResult(result));
145
+ process.exit(0);
146
+ } catch (err) {
147
+ fail(`adjust failed: ${err instanceof Error ? err.message : String(err)}`);
148
+ }
149
+ }
150
+
74
151
  if (command === 'migrate') {
75
152
  const check = rest.includes('--check');
76
153
  const write = rest.includes('--write');
@@ -81,15 +158,23 @@ if (command === 'migrate') {
81
158
  const result = await runMigrate({ tokensArg, check });
82
159
  console.log(formatMigrateResult(result, { check }));
83
160
 
161
+ // Data-tree pass: retires the pre-working-set pointer files and the copies
162
+ // they named. Runs on every migrate, --write included, because leaving a
163
+ // tree half on each model is what the heal exists to end.
164
+ const data = await runMigrateData({ check });
165
+ const dataOut = formatMigrateDataResult(data);
166
+ if (dataOut) console.log('\n' + dataOut);
167
+
84
168
  // Route-reference pass: advisory by default, rewrites the unambiguous hits
85
169
  // only with --write (and never under --check).
86
170
  const routes = runMigrateRoutes({ root: process.cwd(), apply: write && !check });
87
171
  const routeOut = formatRouteResult(routes, { check });
88
172
  if (routeOut) console.log('\n' + routeOut);
89
173
 
90
- // Route findings are advisory; only token migrations gate the exit code.
174
+ // Route findings are advisory; token migrations and the data heal gate the
175
+ // exit code.
91
176
  if (result.status === 'no-path') process.exit(1);
92
- if (check && result.status === 'would-change') process.exit(1);
177
+ if (check && (result.status === 'would-change' || data.status === 'planned')) process.exit(1);
93
178
  process.exit(0);
94
179
  } catch (err) {
95
180
  fail(`migrate failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -144,6 +229,8 @@ const SAMPLE_PROMPTS = {
144
229
  'live-tokens-build-page': 'build a pricing page using live-tokens components',
145
230
  'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
146
231
  'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
232
+ 'live-tokens-generate-theme': 'make me a bright and cheerful color theme',
233
+ 'live-tokens-adjust-shape-space': 'make the buttons pill shaped',
147
234
  };
148
235
 
149
236
  const installedSamples = skills