@motion-proto/live-tokens 0.55.1 → 0.57.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 (107) hide show
  1. package/.claude/skills/{live-tokens-adjust-shape-space → live-tokens-adjust-geometry}/SKILL.md +5 -5
  2. package/.claude/skills/live-tokens-build-page/SKILL.md +2 -0
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +20 -357
  4. package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +3 -0
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +58 -0
  6. package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +70 -0
  7. package/.claude/skills/live-tokens-generate-theme/SKILL.md +79 -98
  8. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +18 -0
  9. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +88 -0
  10. package/.claude/skills/live-tokens-pick-component/SKILL.md +4 -1
  11. package/CHANGELOG.md +197 -0
  12. package/README.md +21 -8
  13. package/bin/cli.mjs +42 -3
  14. package/bin/set-fonts.mjs +280 -0
  15. package/dist-plugin/adjust/index.cjs +1 -0
  16. package/dist-plugin/adjust/index.d.cts +3 -3
  17. package/dist-plugin/adjust/index.d.ts +3 -3
  18. package/dist-plugin/adjust/index.js +1 -1
  19. package/dist-plugin/{chunk-TKZBVIW5.js → chunk-E5QYON4L.js} +59 -2
  20. package/dist-plugin/{chunk-PB2JTK2H.js → chunk-LW4SR7AZ.js} +39 -6
  21. package/dist-plugin/{chunk-YR6GPXW2.js → chunk-XWXIMTWZ.js} +0 -5
  22. package/dist-plugin/{chunk-D3ZVKOR4.js → chunk-Y5CNFSSV.js} +1 -0
  23. package/dist-plugin/{dataPaths-DBN0RPuT.d.cts → dataPaths-CRfD1LdA.d.cts} +3 -0
  24. package/dist-plugin/{dataPaths-DBN0RPuT.d.ts → dataPaths-CRfD1LdA.d.ts} +3 -0
  25. package/dist-plugin/fontPairing/index.cjs +413 -0
  26. package/dist-plugin/fontPairing/index.d.cts +109 -0
  27. package/dist-plugin/fontPairing/index.d.ts +109 -0
  28. package/dist-plugin/fontPairing/index.js +321 -0
  29. package/dist-plugin/generateColorsAndType/index.cjs +22 -9
  30. package/dist-plugin/generateColorsAndType/index.d.cts +3 -3
  31. package/dist-plugin/generateColorsAndType/index.d.ts +3 -3
  32. package/dist-plugin/generateColorsAndType/index.js +8 -5
  33. package/dist-plugin/index-4N-Orzzi.d.cts +3 -0
  34. package/dist-plugin/index-4N-Orzzi.d.ts +3 -0
  35. package/dist-plugin/index.cjs +158 -9
  36. package/dist-plugin/index.d.cts +1 -1
  37. package/dist-plugin/index.d.ts +1 -1
  38. package/dist-plugin/index.js +71 -4
  39. package/dist-plugin/migrateData/index.cjs +1 -0
  40. package/dist-plugin/migrateData/index.d.cts +1 -1
  41. package/dist-plugin/migrateData/index.d.ts +1 -1
  42. package/dist-plugin/migrateData/index.js +1 -1
  43. package/dist-plugin/{index-DpTIRZ2H.d.cts → themeTypes-DSV3Zisf.d.cts} +13 -4
  44. package/dist-plugin/{index-DpTIRZ2H.d.ts → themeTypes-DSV3Zisf.d.ts} +13 -4
  45. package/dist-plugin/tokensCssMigrations/index.cjs +58 -1
  46. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  47. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  48. package/dist-plugin/tokensCssMigrations/index.js +3 -3
  49. package/package.json +5 -2
  50. package/src/editor/core/fonts/applyFontPairing.ts +158 -0
  51. package/src/editor/core/fonts/fontLoader.ts +1 -0
  52. package/src/editor/core/fonts/fontMigration.ts +31 -1
  53. package/src/editor/core/fonts/fontPairing.ts +13 -6
  54. package/src/editor/core/fonts/googleFontsUrl.ts +123 -0
  55. package/src/editor/core/fonts/weightCoverage.ts +92 -0
  56. package/src/editor/core/palettes/paletteDerivation.ts +9 -2
  57. package/src/editor/core/sketch/maskField.ts +381 -0
  58. package/src/editor/core/sketch/sketchLayer.ts +1028 -0
  59. package/src/editor/core/sketch/sketchPresetService.ts +65 -0
  60. package/src/editor/core/sketch/sketchPresets.ts +328 -0
  61. package/src/editor/core/sketch/sketchStore.ts +190 -0
  62. package/src/editor/core/store/editorPersistence.ts +28 -12
  63. package/src/editor/core/store/editorTypes.ts +4 -1
  64. package/src/editor/core/store/editorViewStore.ts +2 -2
  65. package/src/editor/core/store/gradientSource.ts +15 -1
  66. package/src/editor/core/themes/parsers/gradient.ts +62 -4
  67. package/src/editor/core/themes/slices/gradients.ts +75 -14
  68. package/src/editor/core/themes/themeTypes.ts +6 -1
  69. package/src/editor/docs/Docs.svelte +4 -3
  70. package/src/editor/docs/chapters.ts +1 -0
  71. package/src/editor/docs/content/01-overview.md +2 -1
  72. package/src/editor/docs/content/editing-tokens.md +5 -1
  73. package/src/editor/docs/content/sketch-mode.md +89 -0
  74. package/src/editor/docs/content/themes-workflow.md +39 -0
  75. package/src/editor/docs/content.generated.ts +4 -3
  76. package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
  77. package/src/editor/pages/EditorShell.svelte +43 -0
  78. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  79. package/src/editor/ui/FontStackEditor.svelte +3 -0
  80. package/src/editor/ui/GradientEditor.svelte +38 -1
  81. package/src/editor/ui/ProjectFontsSection.svelte +24 -36
  82. package/src/editor/ui/UIReveal.svelte +7 -1
  83. package/src/editor/ui/UISegmentedControl.svelte +4 -8
  84. package/src/editor/ui/sections/GradientsSection.svelte +47 -1
  85. package/src/editor/ui/sketch/SketchDial.svelte +124 -0
  86. package/src/editor/ui/sketch/SketchPreview.svelte +119 -0
  87. package/src/editor/ui/sketch/SketchRange.svelte +185 -0
  88. package/src/editor/ui/sketch/SketchTab.svelte +1263 -0
  89. package/src/live-tokens/data/colors-and-type/autumn.json +17 -0
  90. package/src/live-tokens/data/colors-and-type/default.json +17 -0
  91. package/src/live-tokens/data/colors-and-type/halloween.json +17 -0
  92. package/src/live-tokens/data/colors-and-type/midnight-study.json +17 -0
  93. package/src/live-tokens/data/colors-and-type/ocean.json +17 -0
  94. package/src/live-tokens/data/colors-and-type/royal-velvet.json +17 -0
  95. package/src/live-tokens/data/colors-and-type/sketches.json +2520 -0
  96. package/src/live-tokens/data/colors-and-type/spring-meadow.json +17 -0
  97. package/src/live-tokens/data/colors-and-type/sunset.json +17 -0
  98. package/src/live-tokens/data/themes/autumn.json +17 -0
  99. package/src/live-tokens/data/themes/halloween.json +17 -0
  100. package/src/live-tokens/data/themes/midnight-study.json +17 -0
  101. package/src/live-tokens/data/themes/ocean.json +17 -0
  102. package/src/live-tokens/data/themes/royal-velvet.json +17 -0
  103. package/src/live-tokens/data/themes/sketches.json +3984 -0
  104. package/src/live-tokens/data/themes/spring-meadow.json +17 -0
  105. package/src/live-tokens/data/themes/sunset.json +17 -0
  106. package/src/live-tokens/data/tokens.generated.css +1 -0
  107. package/src/system/styles/tokens.css +15 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Live Tokens
1
+ # LiveTokens
2
2
 
3
3
  A design system for styling and building Svelte + Vite microsites. Edit tokens and components in a dev-only editor and watch the running site repaint on every input. Save the result as a theme file and carry it between projects.
4
4
 
@@ -17,7 +17,7 @@ The editor is dev-only. Production builds get plain CSS variables and the compon
17
17
  - **Themes.** A theme is a whole look in one file: colors and type plus a config for every component, stored by value. Loading one changes a single pointer file, and nothing your site ships changes until you Adopt. Export a theme and import it into another project to restore the look in one step.
18
18
  - **Seven example looks.** Autumn, Halloween, Midnight Study, Ocean, Royal Velvet, Spring Meadow, and Sunset each ship as a full theme: colors, a Google Fonts pairing, and a shape personality of radius, padding, gap, and border-width aliases. They ship inside the package, so trying one needs no local files. Load Motion Proto to return to the default. Saving over a preset writes a local copy that shadows the shipped one; delete the copy and the shipped version returns.
19
19
  - **Vite plugin.** Hosts the `/api/live-tokens/{colors-and-type,component-configs,themes}/*` routes the editor reads and writes through. The single namespace keeps these routes clear of anything your app serves under `/api`.
20
- - **Claude Code skills.** Five bundled skills that drive the package from plain English. See [Claude Code skills](#claude-code-skills).
20
+ - **Claude Code skills.** Six bundled skills that drive the package from plain English. See [Claude Code skills](#claude-code-skills).
21
21
 
22
22
  ## Install
23
23
 
@@ -267,13 +267,14 @@ npx @motion-proto/live-tokens <command>
267
267
  | `check-component <id>` | Validate a component's runtime, editor, and registration against the authoring contract. |
268
268
  | `generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]` | Build a full theme from a 10-seed OKLCH brief, enforce AA contrast, write `themes/<slug>.json`, and open it. |
269
269
  | `adjust <ops.json> [--dry-run]` | Move radius, padding, gap, and border-width aliases along their token scales. |
270
+ | `set-fonts <brief.json> [--dry-run] [--no-verify]` | Bind Google Fonts families to the theme's font stacks, verified against the API. |
270
271
  | `migrate [--check] [--write] [--tokens <path>]` | Reconcile the project with the installed package: additive `tokens.css` migrations, the pre-0.48 data-tree move, and a report on source references to the routes that moved in 0.35.0. |
271
272
 
272
273
  Once installed in a project, the same commands are available as `npx live-tokens <command>`.
273
274
 
274
275
  ## Claude Code skills
275
276
 
276
- The package bundles five Claude Code skills. They encode the conventions this README cannot carry in full: which component fits a need, how a page is wired, what a valid theme looks like in OKLCH, and how shape and space move along the token scales. Each triggers from an ordinary request, so there are no slash commands to learn.
277
+ The package bundles six Claude Code skills. They encode the conventions this README cannot carry in full: which component fits a need, how a page is wired, what a valid theme looks like in OKLCH, how two typefaces sit together, and how geometry moves along the token scales. Each triggers from an ordinary request, so there are no slash commands to learn.
277
278
 
278
279
  ### Install
279
280
 
@@ -303,13 +304,25 @@ The skill composes the page from shipped components, styles every value with `va
303
304
 
304
305
  Ask for a look: "a dark, moody night theme", "a St Patrick's Day theme in green and gold", "warmer", "more contrast", "calmer".
305
306
 
306
- The skill translates the brief into ten OKLCH seeds (Brand, Accent, Special, Canvas, Neutral, Alternate, Info, Success, Warning, Danger) plus a light or dark scheme, then runs `npx live-tokens generate-theme <brief.json>`. The CLI assembles the curves, enforces AA contrast on derived text tokens and auto-corrects where it can, writes `themes/<slug>.json`, opens it, and prints a contrast report. Exit 1 means the seeds themselves are unworkable, and each failure line names the seed to change.
307
+ A theme is three decisions made from one brief: color, type, and geometry. The skill owns color and delegates the other two to `live-tokens-pair-fonts` and `live-tokens-adjust-geometry`, so the whole look comes from the same reading of the brief.
307
308
 
308
- Most of the skill is the judgment the generator cannot supply: a chroma budget scaled to how much screen area each palette covers, per-role lightness and hue bands for each scheme, gamut guardrails against impossible seeds, harmony modes, the optional canvas gradient, shadow weight for the canvas, and OKLCH anchors for named colors.
309
+ For color it translates the brief into ten OKLCH seeds (Brand, Accent, Special, Canvas, Neutral, Alternate, Info, Success, Warning, Danger) plus a light or dark scheme, then runs `npx live-tokens generate-theme <brief.json>`. The CLI assembles the curves, enforces AA contrast on derived text tokens and auto-corrects where it can, writes `themes/<slug>.json`, opens it, and prints a contrast report. Exit 1 means the seeds themselves are unworkable, and each failure line names the seed to change.
309
310
 
310
- Scope: colors only. Fonts, gradients, and component aliases carry forward from the open theme, or from `--carry-from <name>`. `--dry-run` prints the report without writing; `--no-activate` writes without opening. Opening a theme never changes what your site ships; Adopt does. Regenerating replaces that theme's whole color state, including palette edits made in the editor since the last run.
311
+ Most of the skill is the judgment the generator cannot supply: a chroma budget scaled to how much screen area each palette covers, per-role lightness and hue bands for each scheme, gamut guardrails against impossible seeds, harmony modes, the optional canvas gradient, and a voice-to-shape table for the shape step. OKLCH anchors for named holidays and seasons live in a reference file the skill reads on demand.
311
312
 
312
- ### `live-tokens-adjust-shape-space`
313
+ Color lands in the theme file; type and shape land in the unsaved buffers, and one Save keeps all three. `--dry-run` prints the report without writing; `--no-activate` writes without opening. Opening a theme never changes what your site ships; Adopt does. Regenerating replaces that theme's whole color state, including palette edits made in the editor since the last run, and carries the live buffers forward, so re-rolling color after setting fonts and shape keeps both.
314
+
315
+ ### `live-tokens-pair-fonts`
316
+
317
+ Ask for type: "pair some fonts for this theme", "what font should the headings use?", "make the type more editorial", "something friendlier", "a serif for headings".
318
+
319
+ The skill chooses the families and runs `npx live-tokens set-fonts <brief.json>`, which binds each one to `--font-display`, `--font-sans`, `--font-serif`, or `--font-mono`. Every family is checked against the Google Fonts API before it is written, and the URL is built from the weights that family actually has: a range for a variable font, an enumeration for a static one, a bare URL for a single-weight display face. The report names the weights your typography tokens ask for and the family does not carry.
320
+
321
+ The judgment is the skill's half. It anchors on the body face, because that is most of the words on the page and text faces survive small sizes where display faces do not. It classifies both candidates by form model (dynamic, rational, geometric) and applies the font matrix: two faces sharing a skeleton under different surfaces pair reliably, two faces sharing a surface over different skeletons fight, and two faces far apart on both read as a decision. It also carries the screen test a body face has to pass, a voice table from brief to type, and the Google Fonts superfamilies for when the type should stay quiet.
322
+
323
+ Scope: type only, and never color. Edits land in the colors-and-type `_working.json` buffer, so save the open theme to keep them. `--dry-run` reports without writing.
324
+
325
+ ### `live-tokens-adjust-geometry`
313
326
 
314
327
  Ask for shape or space: "make the buttons pill shaped", "sharper corners on the cards", "space it out", "tighter", "thinner borders".
315
328
 
@@ -323,7 +336,7 @@ Edits land in each affected component's `_working.json` buffer, which is what th
323
336
 
324
337
  Ask for something the catalogue lacks: "author a Rating component", "make my Chip component editable in the editor".
325
338
 
326
- The skill covers the four-step recipe: the runtime `.svelte` file with its `:global(:root)` token block, the editor `.svelte` file exporting `allTokens` and its variant groups, the `registerComponent()` call, and the catalogue entry that keeps `live-tokens-pick-component` current. It carries the naming scheme, the token suffix vocabulary, the state model (component states such as selected and disabled are separate from interaction states such as hover), linked siblings, the public-imports rule, and the shipped `Toggle` as a worked example from runtime file to registration.
339
+ The skill covers the recipe: the runtime `.svelte` file with its `:global(:root)` token block, the editor `.svelte` file exporting `allTokens` and its variant groups, the `registerComponent()` call, and the catalogue entry that keeps `live-tokens-pick-component` current. It carries the naming scheme, the token suffix vocabulary, the state model (component states such as selected and disabled are separate from interaction states such as hover), and the public-imports rule, and points at the shipped `Toggle` in `node_modules` as the worked example. Linked siblings, intrinsics, and the fixed-overlay portal rule sit in reference files the skill reads only when a component needs them.
327
340
 
328
341
  Verify the result:
329
342
 
package/bin/cli.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  // check-component <id> Validate a component against the add-component skill contract.
7
7
  // generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
8
8
  // adjust <ops.json> Apply radius/padding/gap/border-width ops to the open buffer.
9
+ // set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
9
10
  // migrate [...] Reconcile tokens.css, the data tree, and route references.
10
11
 
11
12
  import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
@@ -23,6 +24,7 @@ import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
23
24
  import { runCreate, formatCreateResult } from './create.mjs';
24
25
  import { runGenerateTheme, formatGenerateThemeResult } from './generate-theme.mjs';
25
26
  import { runAdjust, formatAdjustResult } from './adjust.mjs';
27
+ import { runSetFonts, formatSetFontsResult } from './set-fonts.mjs';
26
28
 
27
29
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
28
30
 
@@ -47,11 +49,23 @@ Commands:
47
49
  adjust <ops.json> [--dry-run]
48
50
  Move radius, padding, gap, and border-width
49
51
  aliases along their token scales (see the
50
- live-tokens-adjust-shape-space skill). Reads each
52
+ live-tokens-adjust-geometry skill). Reads each
51
53
  component's live config and writes the result to
52
54
  that component's unsaved buffer, so save the open
53
55
  theme in the editor to keep it. --dry-run prints
54
56
  the report without writing.
57
+ set-fonts <brief.json> [--dry-run] [--no-verify]
58
+ Bind Google Fonts families to --font-display,
59
+ --font-sans, --font-serif, --font-mono and
60
+ --font-editorial (see
61
+ the live-tokens-pair-fonts skill). Each family is
62
+ verified against the Google Fonts API and the URL
63
+ is negotiated from the weights it actually has.
64
+ Writes the result to the unsaved colors-and-type
65
+ buffer, so save the open theme in the editor to
66
+ keep it. --dry-run prints the report without
67
+ writing; --no-verify skips the network and
68
+ requires an explicit URL per family.
55
69
  migrate [--check] [--write] [--tokens <path>]
56
70
  Reconcile your project with the installed package:
57
71
  applies additive tokens.css migrations, moves a
@@ -148,6 +162,30 @@ if (command === 'adjust') {
148
162
  }
149
163
  }
150
164
 
165
+ if (command === 'set-fonts') {
166
+ const briefPath = rest.find((a) => !a.startsWith('-'));
167
+ if (!briefPath) {
168
+ fail(`Usage: npx @motion-proto/live-tokens set-fonts <brief.json> [--dry-run] [--no-verify]`);
169
+ }
170
+ if (rest.includes('--no-activate')) {
171
+ fail(
172
+ `set-fonts has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
173
+ `Drop the flag and re-run.`,
174
+ );
175
+ }
176
+ try {
177
+ const result = await runSetFonts({
178
+ briefPath,
179
+ dryRun: rest.includes('--dry-run'),
180
+ verify: !rest.includes('--no-verify'),
181
+ });
182
+ console.log(formatSetFontsResult(result));
183
+ process.exit(0);
184
+ } catch (err) {
185
+ fail(`set-fonts failed: ${err instanceof Error ? err.message : String(err)}`);
186
+ }
187
+ }
188
+
151
189
  if (command === 'migrate') {
152
190
  const check = rest.includes('--check');
153
191
  const write = rest.includes('--write');
@@ -229,8 +267,9 @@ const SAMPLE_PROMPTS = {
229
267
  'live-tokens-build-page': 'build a pricing page using live-tokens components',
230
268
  'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
231
269
  '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',
270
+ 'live-tokens-generate-theme': 'make me a bright and cheerful theme',
271
+ 'live-tokens-adjust-geometry': 'make the buttons pill shaped',
272
+ 'live-tokens-pair-fonts': 'pair some fonts for this theme',
234
273
  };
235
274
 
236
275
  const installedSamples = skills
@@ -0,0 +1,280 @@
1
+ // `live-tokens set-fonts` worker.
2
+ //
3
+ // Reads a pairing brief (JSON), verifies each family against Google Fonts,
4
+ // binds it to a font stack via the compiled engine (dist-plugin/fontPairing —
5
+ // the CLI never imports TS sources), and writes the result into
6
+ // `colors-and-type/_working.json`: the same buffer the editor's own font edits
7
+ // land in, so a retype is an unsaved edit the user saves into a theme when they
8
+ // want to keep it. Named colors-and-type files, themes, tokens.css and
9
+ // fonts.css are never touched, and nothing is activated.
10
+
11
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { resolveTokensCssPath } from './migrate.mjs';
16
+
17
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const ENGINE = resolve(pkgRoot, 'dist-plugin/fontPairing/index.js');
19
+ const packageDataDir = join(pkgRoot, 'src/live-tokens/data');
20
+
21
+ // Google serves a different stylesheet per user agent; ask as a browser would
22
+ // so the weight census matches what the page will actually load.
23
+ const BROWSER_UA =
24
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
25
+
26
+ const SOURCE_LABELS = {
27
+ working: 'your unsaved edits',
28
+ theme: 'the open theme',
29
+ default: 'the package default',
30
+ };
31
+
32
+ async function loadEngine() {
33
+ if (!existsSync(ENGINE)) {
34
+ throw new Error(
35
+ `font pairing engine not found at ${relative(process.cwd(), ENGINE)}. ` +
36
+ `Build the plugin first (npm run build:plugin).`,
37
+ );
38
+ }
39
+ return import(ENGINE);
40
+ }
41
+
42
+ function readJson(path) {
43
+ return JSON.parse(readFileSync(path, 'utf8'));
44
+ }
45
+
46
+ function readJsonIfExists(path) {
47
+ return existsSync(path) ? readJson(path) : null;
48
+ }
49
+
50
+ function browserFetch(url) {
51
+ return fetch(url, { headers: { 'user-agent': BROWSER_UA } });
52
+ }
53
+
54
+ /** The open theme, read the way every other door reads it: the local file
55
+ * first, then the copy the installed package ships. */
56
+ function readActiveTheme(themesDir) {
57
+ const slug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
58
+ const theme =
59
+ readJsonIfExists(join(themesDir, `${slug}.json`)) ??
60
+ readJsonIfExists(join(packageDataDir, 'themes', `${slug}.json`));
61
+ return theme ? { slug, theme } : null;
62
+ }
63
+
64
+ /** Mirrors the dev server's live colors-and-type resolution: the unsaved
65
+ * buffer, then the open theme's own copy, then the package default. */
66
+ function readLiveColorsAndType(colorsAndTypeDir, active) {
67
+ const working = readJsonIfExists(join(colorsAndTypeDir, '_working.json'));
68
+ if (working) return { colorsAndType: working, source: 'working' };
69
+ if (active?.theme?.colorsAndType) return { colorsAndType: active.theme.colorsAndType, source: 'theme' };
70
+ const shipped =
71
+ readJsonIfExists(join(colorsAndTypeDir, 'default.json')) ??
72
+ readJsonIfExists(join(packageDataDir, 'colors-and-type', 'default.json'));
73
+ if (!shipped) throw new Error(`no colors and type to read at ${colorsAndTypeDir}`);
74
+ return { colorsAndType: shipped, source: 'default' };
75
+ }
76
+
77
+ function normalizeFace(slot, value) {
78
+ if (typeof value === 'string') return { name: value };
79
+ if (value && typeof value === 'object' && typeof value.name === 'string') {
80
+ return { name: value.name, ...(value.url ? { url: value.url } : {}) };
81
+ }
82
+ throw new Error(`brief slot "${slot}" must be a family name or { "name": "...", "url": "..." }`);
83
+ }
84
+
85
+ function sameJson(a, b) {
86
+ return JSON.stringify(a) === JSON.stringify(b);
87
+ }
88
+
89
+ /** `engine` and `fetcher` are test seams; the CLI always runs the compiled
90
+ * bundle against the live API. */
91
+ export async function runSetFonts({
92
+ briefPath,
93
+ dryRun = false,
94
+ verify = true,
95
+ root = process.cwd(),
96
+ colorsAndTypeDir,
97
+ themesDir,
98
+ tokensCssPath,
99
+ engine,
100
+ fetcher = browserFetch,
101
+ } = {}) {
102
+ const {
103
+ applyFontPairing,
104
+ resolveGoogleFont,
105
+ requiredWeights,
106
+ weightCoverage,
107
+ SLOT_ORDER,
108
+ SLOT_VARIABLES,
109
+ readLiveTokensConfig,
110
+ resolveDataDirs,
111
+ } = engine ?? (await loadEngine());
112
+
113
+ const briefFull = resolve(root, briefPath);
114
+ if (!existsSync(briefFull)) throw new Error(`brief file not found at ${relative(root, briefFull)}`);
115
+ let brief;
116
+ try {
117
+ brief = readJson(briefFull);
118
+ } catch (err) {
119
+ throw new Error(`brief file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
120
+ }
121
+
122
+ const requested = SLOT_ORDER.filter((slot) => brief[slot] !== undefined).map((slot) => ({
123
+ slot,
124
+ face: normalizeFace(slot, brief[slot]),
125
+ }));
126
+ if (requested.length === 0) {
127
+ throw new Error(`brief names no slot. Use one or more of: ${SLOT_ORDER.join(', ')}.`);
128
+ }
129
+
130
+ const resolved = colorsAndTypeDir && themesDir ? null : resolveDataDirs();
131
+ const colorsDir = colorsAndTypeDir ?? resolved.colorsAndTypeDir;
132
+ const themes = themesDir ?? resolved.themesDir;
133
+ if (!existsSync(colorsDir)) {
134
+ throw new Error(`no colors and type at ${relative(root, colorsDir)}. Run the dev server once to create it.`);
135
+ }
136
+
137
+ const active = readActiveTheme(themes);
138
+ const { colorsAndType, source } = readLiveColorsAndType(colorsDir, active);
139
+
140
+ const pairing = {};
141
+ const faces = [];
142
+ for (const { slot, face } of requested) {
143
+ if (!verify) {
144
+ if (!face.url) {
145
+ throw new Error(
146
+ `--no-verify needs an explicit URL for "${slot}". Give it as ` +
147
+ `{ "name": "${face.name}", "url": "https://fonts.googleapis.com/css2?..." } ` +
148
+ `or drop the flag and let the CLI negotiate one.`,
149
+ );
150
+ }
151
+ pairing[slot] = { name: face.name, url: face.url };
152
+ faces.push({ slot, name: face.name, url: face.url, weights: [], italics: false, verified: false });
153
+ continue;
154
+ }
155
+ const found = face.url
156
+ ? { name: face.name, url: face.url, weights: [], italics: false }
157
+ : await resolveGoogleFont(face.name, fetcher);
158
+ pairing[slot] = { name: found.name, url: found.url, weights: found.weights, italics: found.italics };
159
+ faces.push({ slot, ...found, verified: true, pinned: Boolean(face.url) });
160
+ }
161
+
162
+ const { colorsAndType: next, report } = applyFontPairing(colorsAndType, pairing);
163
+
164
+ const tokensPath = resolveTokensCssPath(tokensCssPath, readLiveTokensConfig().tokensCssPath, root);
165
+ let coverage = [];
166
+ if (tokensPath && existsSync(tokensPath)) {
167
+ const covered = {};
168
+ for (const face of faces) {
169
+ if (!face.verified || face.weights.length === 0) continue;
170
+ covered[SLOT_VARIABLES[face.slot]] = { name: face.name, weights: face.weights, italics: face.italics };
171
+ }
172
+ coverage = weightCoverage(
173
+ requiredWeights(readFileSync(tokensPath, 'utf8'), next.cssVariables ?? {}),
174
+ covered,
175
+ );
176
+ }
177
+
178
+ const workingPath = join(colorsDir, '_working.json');
179
+ const savedColorsAndType =
180
+ active?.theme?.colorsAndType ??
181
+ readJsonIfExists(join(colorsDir, 'default.json')) ??
182
+ readJsonIfExists(join(packageDataDir, 'colors-and-type', 'default.json'));
183
+
184
+ // Returning the buffer to what the open theme already holds is a discard,
185
+ // not an edit — the same call the dev server's own PUT makes.
186
+ const backToSaved = savedColorsAndType !== null && sameJson(next, savedColorsAndType);
187
+ let wrote = null;
188
+ if (!dryRun && report.changed) {
189
+ if (backToSaved) {
190
+ if (existsSync(workingPath)) rmSync(workingPath);
191
+ wrote = 'cleared';
192
+ } else {
193
+ writeFileSync(workingPath, JSON.stringify(next, null, 2) + '\n');
194
+ wrote = 'buffer';
195
+ }
196
+ }
197
+
198
+ return {
199
+ briefPath: briefFull,
200
+ colorsAndTypeDir: colorsDir,
201
+ workingPath,
202
+ openTheme: active?.slug ?? null,
203
+ source,
204
+ verified: verify,
205
+ dryRun,
206
+ faces,
207
+ changes: report.changes,
208
+ dropped: report.dropped,
209
+ coverage,
210
+ changed: report.changed,
211
+ wrote,
212
+ };
213
+ }
214
+
215
+ export function formatSetFontsResult(result) {
216
+ const root = process.cwd();
217
+ const lines = [];
218
+
219
+ if (!result.changed) {
220
+ lines.push('Nothing to change: those faces are already bound to those stacks.');
221
+ return lines.join('\n');
222
+ }
223
+
224
+ const from =
225
+ result.source === 'theme' && result.openTheme
226
+ ? `theme "${result.openTheme}"`
227
+ : SOURCE_LABELS[result.source];
228
+ const verb = result.dryRun ? 'Would set' : 'Set';
229
+ lines.push(`${verb} ${result.changes.length} font stack(s), reading from ${from}.`);
230
+
231
+ const width = Math.max(0, ...result.changes.map((c) => c.variable.length));
232
+ for (const change of result.changes) {
233
+ lines.push(` ${change.variable.padEnd(width)} ${change.from ?? '(none)'} → ${change.to}`);
234
+ }
235
+
236
+ for (const face of result.faces) {
237
+ if (!face.verified) continue;
238
+ const weights = face.weights.length > 0 ? `${face.weights.join(', ')}` : 'single weight';
239
+ lines.push(`\n${face.name} (${face.slot})`);
240
+ lines.push(` weights: ${weights}${face.italics ? ' + italics' : ', no italics'}`);
241
+ lines.push(` ${face.url}`);
242
+ }
243
+
244
+ if (result.dropped.length > 0) {
245
+ lines.push(
246
+ `\nDropped ${result.dropped.length} source(s) no stack references any more: ` +
247
+ `${result.dropped.map((d) => d.names.join(', ')).join('; ')}.`,
248
+ );
249
+ }
250
+
251
+ const gaps = result.coverage.filter((c) => c.missing.length > 0);
252
+ if (gaps.length > 0) {
253
+ lines.push('\nWeight coverage:');
254
+ for (const gap of gaps) {
255
+ lines.push(
256
+ ` ${gap.family} (${gap.stack}) has no ${gap.missing.join(', ')}; ` +
257
+ `tokens ask for ${gap.required.join(', ')}. The browser will synthesize the rest.`,
258
+ );
259
+ }
260
+ }
261
+ const body = result.coverage.find((c) => c.stack === '--font-sans');
262
+ if (body && !body.italics) {
263
+ lines.push(`\n${body.family} has no italics. Emphasis in body text will be synthesized.`);
264
+ }
265
+
266
+ if (result.wrote === 'buffer') {
267
+ lines.push(
268
+ `\nReload the app to see it. This is an unsaved edit: save the open theme in the ` +
269
+ `editor's Theme panel to keep it, or load a theme to discard it.`,
270
+ );
271
+ } else if (result.wrote === 'cleared') {
272
+ lines.push(
273
+ `\nThat is what the open theme already holds, so the unsaved buffer was discarded. ` +
274
+ `Reload the app to see it.`,
275
+ );
276
+ } else if (result.dryRun) {
277
+ lines.push(`\nDry run: nothing written under ${relative(root, result.colorsAndTypeDir)}.`);
278
+ }
279
+ return lines.join('\n');
280
+ }
@@ -270,6 +270,7 @@ function resolveDataDirs(opts = {}) {
270
270
  colorsAndTypeDir: opts.colorsAndTypeDir ? import_path.default.resolve(opts.colorsAndTypeDir) : fileConfig.colorsAndTypeDir ? import_path.default.resolve(fileConfig.colorsAndTypeDir) : sub("colors-and-type"),
271
271
  componentConfigsDir: opts.componentConfigsDir ? import_path.default.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? import_path.default.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
272
272
  themesDir: opts.themesDir ? import_path.default.resolve(opts.themesDir) : fileConfig.themesDir ? import_path.default.resolve(fileConfig.themesDir) : sub("themes"),
273
+ sketchPresetsDir: sub("sketch-presets"),
273
274
  legacyManifestsDir: fileConfig.manifestsDir ? import_path.default.resolve(fileConfig.manifestsDir) : void 0
274
275
  };
275
276
  }
@@ -1,6 +1,6 @@
1
- import { A as AliasDiskValue, b as ComponentConfig } from '../index-DpTIRZ2H.cjs';
2
- export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-DpTIRZ2H.cjs';
3
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.cjs';
1
+ import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.cjs';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-CRfD1LdA.cjs';
3
+ export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.cjs';
4
4
 
5
5
  type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
6
6
  interface AdjustOp {
@@ -1,6 +1,6 @@
1
- import { A as AliasDiskValue, b as ComponentConfig } from '../index-DpTIRZ2H.js';
2
- export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-DpTIRZ2H.js';
3
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.js';
1
+ import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.js';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-CRfD1LdA.js';
3
+ export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.js';
4
4
 
5
5
  type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
6
6
  interface AdjustOp {
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  readLiveTokensConfig,
6
6
  resolveDataDirs
7
- } from "../chunk-D3ZVKOR4.js";
7
+ } from "../chunk-Y5CNFSSV.js";
8
8
 
9
9
  // src/editor/core/components/aliasKinds.ts
10
10
  var KIND_PATTERNS = [
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hexToOklch,
3
3
  oklchToCss
4
- } from "./chunk-YR6GPXW2.js";
4
+ } from "./chunk-XWXIMTWZ.js";
5
5
 
6
6
  // src/editor/core/themes/parsers/globalRootBlock.ts
7
7
  function extractGlobalRootBody(source) {
@@ -375,6 +375,61 @@ var tokensCssMigration_2026_08_19_oklchColorValues = {
375
375
  }
376
376
  };
377
377
 
378
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-25-editorial-type-role.ts
379
+ var STACK = [{ name: "--font-editorial", value: "var(--font-sans)" }];
380
+ var BUNDLE2 = [
381
+ { name: "--editorial-font-family", value: "var(--font-editorial)" },
382
+ { name: "--editorial-font-size", value: "var(--font-size-md)" },
383
+ { name: "--editorial-font-weight", value: "var(--font-weight-normal)" },
384
+ { name: "--editorial-line-height", value: "var(--line-height-normal)" },
385
+ { name: "--editorial-letter-spacing", value: "var(--letter-spacing-normal)" }
386
+ ];
387
+ var tokensCssMigration_2026_08_25_editorialTypeRole = {
388
+ id: "2026-08-25-editorial-type-role",
389
+ kind: "additive",
390
+ description: "Add the editorial type role (--font-editorial and the --editorial-* bundle)",
391
+ apply(css) {
392
+ const withStack = ensureScale(css, {
393
+ anchorPrefixes: ["--font-mono", "--font-serif", "--font-sans", "--font-display"],
394
+ entries: STACK
395
+ });
396
+ return ensureScale(withStack, {
397
+ sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
398
+ anchorPrefixes: [
399
+ "--eyebrow-",
400
+ "--code-",
401
+ "--body-",
402
+ "--heading-",
403
+ "--letter-spacing-",
404
+ "--line-height-",
405
+ "--font-weight-",
406
+ "--font-size-",
407
+ "--font-"
408
+ ],
409
+ entries: BUNDLE2
410
+ });
411
+ }
412
+ };
413
+
414
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-26-gradient-stops.ts
415
+ var STOPS_RE = /^(?:linear|radial)-gradient\(\s*[^,]+,\s*(.+)\)$/i;
416
+ var tokensCssMigration_2026_08_26_gradientStops = {
417
+ id: "2026-08-26-gradient-stops",
418
+ kind: "additive",
419
+ description: "Add --gradient-N-stops companions to the gradient slots",
420
+ apply(css) {
421
+ const values = collectTokenValues(css);
422
+ const entries = [];
423
+ for (const [name, value] of values) {
424
+ if (!/^--gradient-\d+$/.test(name)) continue;
425
+ const stops = value.match(STOPS_RE);
426
+ if (stops) entries.push({ name: `${name}-stops`, value: stops[1].trim() });
427
+ }
428
+ if (entries.length === 0) return css;
429
+ return ensureScale(css, { anchorPrefixes: ["--gradient-"], entries });
430
+ }
431
+ };
432
+
378
433
  // vite-plugin/tokensCssMigrations/index.ts
379
434
  var TOKENS_CSS_MIGRATIONS = [
380
435
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
@@ -384,7 +439,9 @@ var TOKENS_CSS_MIGRATIONS = [
384
439
  tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions,
385
440
  tokensCssMigration_2026_07_20_lineHeightRename,
386
441
  tokensCssMigration_2026_07_20_semanticTextStyles,
387
- tokensCssMigration_2026_08_19_oklchColorValues
442
+ tokensCssMigration_2026_08_19_oklchColorValues,
443
+ tokensCssMigration_2026_08_25_editorialTypeRole,
444
+ tokensCssMigration_2026_08_26_gradientStops
388
445
  ];
389
446
  function runTokensCssMigrations(css) {
390
447
  return foldMigrations(css, () => true);
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  cssColorToOklch,
3
3
  gamutClamp,
4
- oklchToCssClamped
5
- } from "./chunk-YR6GPXW2.js";
4
+ oklchToCss
5
+ } from "./chunk-XWXIMTWZ.js";
6
6
 
7
7
  // src/editor/core/storage/files/versionedFileResourceClient.ts
8
8
  function sanitizeFileName(name) {
@@ -21,6 +21,30 @@ function formatColorOpacity(name, opacity) {
21
21
  }
22
22
 
23
23
  // src/editor/core/themes/parsers/gradient.ts
24
+ var LINEAR_DIRECTIONS = [
25
+ "to top",
26
+ "to top right",
27
+ "to right",
28
+ "to bottom right",
29
+ "to bottom",
30
+ "to bottom left",
31
+ "to left",
32
+ "to top left"
33
+ ];
34
+ var DIRECTION_ANGLES = {
35
+ "to top": 0,
36
+ "to top right": 45,
37
+ "to right": 90,
38
+ "to bottom right": 135,
39
+ "to bottom": 180,
40
+ "to bottom left": 225,
41
+ "to left": 270,
42
+ "to top left": 315
43
+ };
44
+ function parseDirection(raw) {
45
+ const norm = raw.trim().toLowerCase().replace(/\s+/g, " ");
46
+ return LINEAR_DIRECTIONS.includes(norm) ? norm : null;
47
+ }
24
48
  var RADIAL_BASE_PX = 100;
25
49
  function formatStopColor(s) {
26
50
  const opacity = s.opacity ?? 100;
@@ -48,11 +72,15 @@ function formatGradientValue(v) {
48
72
  return first ? formatStopColor(first) : "transparent";
49
73
  }
50
74
  const stops = formatGradientStops(v.stops);
51
- if (v.type === "linear") return `linear-gradient(${v.angle}deg, ${stops})`;
75
+ if (v.type === "linear") {
76
+ return `linear-gradient(${v.direction ?? `${v.angle}deg`}, ${stops})`;
77
+ }
52
78
  return `radial-gradient(${formatRadialShape(v)} at ${v.centerX ?? 50}% 50%, ${stops})`;
53
79
  }
54
80
  var NUM = String.raw`-?\d+(?:\.\d+)?`;
55
- var LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${NUM})deg\s*,\s*(.+)\)$`, "i");
81
+ var HEADING = String.raw`${NUM}deg|to\s+\w+(?:\s+\w+)?`;
82
+ var LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${HEADING})\s*,\s*(.+)\)$`, "i");
83
+ var DEG_RE = new RegExp(String.raw`^(${NUM})deg$`, "i");
56
84
  var RADIAL_RE = new RegExp(
57
85
  String.raw`^radial-gradient\(\s*(circle|circle\s+${NUM}px|ellipse\s+${NUM}px\s+${NUM}px)\s+at\s+(${NUM})%\s+50%\s*,\s*(.+)\)$`,
58
86
  "i"
@@ -111,8 +139,12 @@ function parseGradientValue(value) {
111
139
  const css = value.trim();
112
140
  const linear = css.match(LINEAR_RE);
113
141
  if (linear) {
142
+ const deg = linear[1].match(DEG_RE);
143
+ const direction = deg ? null : parseDirection(linear[1]);
144
+ if (!deg && !direction) return null;
114
145
  const stops2 = parseStops(linear[2]);
115
- return stops2 && { type: "linear", angle: parseFloat(linear[1]), stops: stops2 };
146
+ if (!stops2) return null;
147
+ return deg ? { type: "linear", angle: parseFloat(deg[1]), stops: stops2 } : { type: "linear", angle: 0, direction, stops: stops2 };
116
148
  }
117
149
  const radial = css.match(RADIAL_RE);
118
150
  if (!radial) return null;
@@ -307,7 +339,7 @@ function scaleStepToX(step, scale) {
307
339
  return scale.steps.length > 1 ? idx / (scale.steps.length - 1) * 100 : 50;
308
340
  }
309
341
  function serializeDerivedValue(value) {
310
- return value.kind === "raw" ? value.css : oklchToCssClamped(value.l, value.c, value.h);
342
+ return value.kind === "raw" ? value.css : oklchToCss(value.l, value.c, value.h);
311
343
  }
312
344
  function computePaletteOklch(index, base, lightnessCurve, saturationCurve, curveOffset, hueCurve) {
313
345
  const { c: baseC } = base;
@@ -587,6 +619,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
587
619
 
588
620
  export {
589
621
  parseColorOpacity,
622
+ DIRECTION_ANGLES,
590
623
  formatGradientValue,
591
624
  parseGradientValue,
592
625
  sanitizeFileName,