@motion-proto/live-tokens 0.73.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +33 -38
  2. package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
  4. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
  6. package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
  7. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
  8. package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
  9. package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
  10. package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
  11. package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
  12. package/.claude/skills/live-tokens-create-theme/SKILL.md +84 -0
  13. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  14. package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
  15. package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
  16. package/.claude/skills/live-tokens-set-colors/SKILL.md +149 -0
  17. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  18. package/.claude/skills/live-tokens-set-geometry/SKILL.md +121 -0
  19. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +63 -0
  20. package/.claude/skills/live-tokens-set-type/SKILL.md +93 -0
  21. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  22. package/CHANGELOG.md +194 -0
  23. package/README.md +27 -18
  24. package/bin/check-component.mjs +91 -19
  25. package/bin/check-page.mjs +105 -24
  26. package/bin/cli.mjs +154 -97
  27. package/bin/contractRunner.mjs +945 -0
  28. package/bin/create.mjs +1 -1
  29. package/bin/lib/catalogue.mjs +37 -30
  30. package/bin/lib/findings.mjs +46 -15
  31. package/bin/lib/liveState.mjs +110 -0
  32. package/bin/lib/report.mjs +3 -3
  33. package/bin/lib/tokenVocabulary.mjs +4 -4
  34. package/bin/migrate-routes.mjs +5 -5
  35. package/bin/migrate.mjs +4 -4
  36. package/bin/save-theme.mjs +176 -0
  37. package/bin/set-colors.mjs +189 -0
  38. package/bin/{adjust.mjs → set-geometry.mjs} +25 -57
  39. package/bin/{set-fonts.mjs → set-type.mjs} +26 -62
  40. package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
  41. package/dist-plugin/{chunk-RIXO2E55.js → chunk-PDNL4NC5.js} +9 -2
  42. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-SWXRVZKT.js} +98 -2
  43. package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.cts} +1 -0
  44. package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.ts} +1 -0
  45. package/dist-plugin/index.cjs +366 -148
  46. package/dist-plugin/index.d.cts +1 -1
  47. package/dist-plugin/index.d.ts +1 -1
  48. package/dist-plugin/index.js +226 -130
  49. package/dist-plugin/migrateData/index.cjs +121 -2
  50. package/dist-plugin/migrateData/index.d.cts +1 -1
  51. package/dist-plugin/migrateData/index.d.ts +1 -1
  52. package/dist-plugin/migrateData/index.js +3 -3
  53. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +594 -499
  54. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +34 -25
  55. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +34 -25
  56. package/dist-plugin/{generateColorsAndType → setColors}/index.js +48 -66
  57. package/dist-plugin/{adjust → setGeometry}/index.cjs +166 -48
  58. package/dist-plugin/{adjust → setGeometry}/index.d.cts +6 -6
  59. package/dist-plugin/{adjust → setGeometry}/index.d.ts +6 -6
  60. package/dist-plugin/{adjust → setGeometry}/index.js +51 -45
  61. package/dist-plugin/{fontPairing → setType}/index.cjs +19 -4
  62. package/dist-plugin/{fontPairing → setType}/index.d.cts +2 -2
  63. package/dist-plugin/{fontPairing → setType}/index.d.ts +2 -2
  64. package/dist-plugin/{fontPairing → setType}/index.js +1 -1
  65. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  66. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  67. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  68. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  69. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  70. package/package.json +36 -8
  71. package/src/app/site.css +19 -9
  72. package/src/editor/bootstrap.ts +2 -0
  73. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
  74. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  75. package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
  76. package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
  77. package/src/editor/component-editor/TabBarEditor.svelte +1 -1
  78. package/src/editor/component-editor/TableEditor.svelte +2 -2
  79. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
  80. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
  81. package/src/editor/core/components/adjustAliases.ts +59 -45
  82. package/src/editor/core/components/aliasKinds.ts +9 -5
  83. package/src/editor/core/preview/themePreview.ts +9 -2
  84. package/src/editor/core/sketch/sketchLayer.ts +22 -0
  85. package/src/editor/core/store/editorStore.ts +10 -1
  86. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +84 -98
  87. package/src/editor/core/themes/liveStateStream.ts +26 -0
  88. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  89. package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
  90. package/src/editor/core/themes/migrations/index.ts +4 -0
  91. package/src/editor/core/themes/themeDocumentSync.ts +22 -11
  92. package/src/editor/core/themes/themeService.ts +9 -2
  93. package/src/editor/docs/content/themes-workflow.md +2 -2
  94. package/src/editor/docs/content.generated.ts +1 -1
  95. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  96. package/src/editor/pages/ComponentEditorPage.svelte +17 -1
  97. package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
  98. package/src/editor/skill-atlas/SkillAtlas.svelte +76 -574
  99. package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
  100. package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
  101. package/src/editor/skill-atlas/edges.ts +31 -0
  102. package/src/editor/skill-atlas/skillSources.generated.ts +29 -24
  103. package/src/editor/skill-atlas/skillTrees.ts +19 -3687
  104. package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
  105. package/src/editor/skill-atlas/trees/create-component.ts +275 -0
  106. package/src/editor/skill-atlas/trees/create-page.ts +320 -0
  107. package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
  108. package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
  109. package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
  110. package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
  111. package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
  112. package/src/editor/skill-atlas/trees/set-type.ts +142 -0
  113. package/src/editor/skill-atlas/types.ts +3 -4
  114. package/src/editor/skill-atlas/wireLayout.ts +287 -0
  115. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  116. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  117. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  118. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  119. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  120. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  121. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  122. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  123. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  124. package/src/live-tokens/data/themes/autumn.json +16 -52
  125. package/src/live-tokens/data/themes/halloween.json +16 -52
  126. package/src/live-tokens/data/themes/midnight-study.json +16 -52
  127. package/src/live-tokens/data/themes/ocean.json +16 -52
  128. package/src/live-tokens/data/themes/royal-velvet.json +16 -52
  129. package/src/live-tokens/data/themes/sketchy.json +16 -52
  130. package/src/live-tokens/data/themes/spring-meadow.json +16 -52
  131. package/src/live-tokens/data/themes/sunset.json +16 -52
  132. package/src/live-tokens/data/tokens.generated.css +0 -36
  133. package/src/system/components/Badge.svelte +7 -0
  134. package/src/system/components/Button.svelte +7 -0
  135. package/src/system/components/Callout.svelte +10 -6
  136. package/src/system/components/Card.svelte +23 -4
  137. package/src/system/components/CodeSnippet.svelte +4 -3
  138. package/src/system/components/CollapsibleSection.svelte +23 -8
  139. package/src/system/components/CornerBadge.svelte +6 -0
  140. package/src/system/components/Dialog.svelte +13 -6
  141. package/src/system/components/IconButton.svelte +9 -0
  142. package/src/system/components/Image.svelte +8 -0
  143. package/src/system/components/ImageLightbox.svelte +6 -0
  144. package/src/system/components/InlineEditActions.svelte +7 -0
  145. package/src/system/components/Input.svelte +7 -0
  146. package/src/system/components/MenuSelect.svelte +7 -0
  147. package/src/system/components/Notification.svelte +7 -0
  148. package/src/system/components/Panel.svelte +6 -0
  149. package/src/system/components/ProgressBar.svelte +5 -0
  150. package/src/system/components/RadioButton.svelte +11 -5
  151. package/src/system/components/SectionDivider.svelte +8 -0
  152. package/src/system/components/SegmentedControl.svelte +6 -0
  153. package/src/system/components/SideNavigation.svelte +6 -0
  154. package/src/system/components/Slider.svelte +7 -4
  155. package/src/system/components/TabBar.svelte +15 -9
  156. package/src/system/components/Table.svelte +8 -3
  157. package/src/system/components/Toggle.svelte +4 -4
  158. package/src/system/components/Tooltip.svelte +6 -0
  159. package/src/testing-js/chunk-AO7EZHYV.js +776 -0
  160. package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
  161. package/src/testing-js/chunk-FAFOAWYL.js +39 -0
  162. package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
  163. package/src/testing-js/chunk-L73N4NSO.js +23 -0
  164. package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
  165. package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
  166. package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
  167. package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
  168. package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
  169. package/src/testing-js/component-alias.contract.js +81 -0
  170. package/src/testing-js/component-alias.contract.js.map +1 -0
  171. package/src/testing-js/component-editor.contract.js +62 -0
  172. package/src/testing-js/component-editor.contract.js.map +1 -0
  173. package/src/testing-js/component-render.contract.js +568 -0
  174. package/src/testing-js/component-render.contract.js.map +1 -0
  175. package/src/testing-js/index.d.ts +293 -0
  176. package/src/testing-js/index.js +222 -0
  177. package/src/testing-js/index.js.map +1 -0
  178. package/src/testing-js/registry.contract.js +39 -0
  179. package/src/testing-js/registry.contract.js.map +1 -0
  180. package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
  181. package/src/testing-js/vitest.d.ts +3 -0
  182. package/src/testing-js/vitest.js +13 -0
  183. package/src/testing-js/vitest.js.map +1 -0
  184. package/template/README.md +13 -0
  185. package/template/_gitignore +2 -0
  186. package/template/package.json +2 -1
  187. package/template/src/pages/Home.svelte +4 -18
  188. package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +0 -93
  189. package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
  190. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
  191. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  192. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  193. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  194. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  195. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +0 -90
  196. package/bin/generate-theme.mjs +0 -260
@@ -1,156 +0,0 @@
1
- ---
2
- name: live-tokens-generate-theme
3
- description: Generate a complete live-tokens theme (color, type, and geometry) from a natural-language brief, delegating the type and geometry halves to its sibling skills. Use whenever the user asks for a theme, look, vibe, brand feel, color scheme, or palette by mood, style, era, season, holiday, or hue; when they name only a color (something red-based, green and gold for St. Patrick's Day); or when they refine an existing theme (warmer, more contrast, calmer). Examples: bright and cheerful, dark and moody, brutalist, mid-century modern, Swiss and minimal, cyberpunk neon, editorial magazine, make it feel like a terminal. Not for a single token (use the editor), type alone (live-tokens-pair-fonts), or geometry alone (live-tokens-adjust-geometry).
4
- ---
5
-
6
- # Generating a theme from a brief
7
-
8
- A theme is three decisions made from one brief: color, type, and geometry. This skill owns the color decision directly and delegates the other two, so the whole look comes from the same reading of the brief. Never hand-author theme JSON and never edit the data tree directly.
9
-
10
- ## Workflow
11
-
12
- 1. Read the brief once and name its voice in a sentence: the mood, the hue family, the scheme, and the type and geometry that mood implies. Everything below keys off that sentence.
13
- 2. Read the anchor reference that matches the voice (feeling, idiom, or occasion; see Anchor references) before seeding. Each entry fixes all three decisions together and overrides the generic defaults here.
14
- 3. Translate the brief into a seed file using the framework below. Write it to `scratch/<slug>-brief.json`. Nothing else records the seeds, so this file is the only copy; one per slug is what makes the refinement pass below cheap.
15
- 4. Run `npx live-tokens generate-theme scratch/<slug>-brief.json`. It writes `themes/<slug>.json`, opens that theme, and prints a contrast report. Auto-corrections are fine. Unmet floors (exit 1) mean the seeds themselves are unworkable; each failure line names the seed to change, usually by raising its lightness or cutting its chroma. Fix the brief and re-run; the same name overwrites. Regeneration replaces that theme's whole color state, including palette edits made in the editor since the last run, so say so once when iterating.
16
- 5. Invoke **live-tokens-pair-fonts** with the same voice. Skip only when the user asked for colors specifically and said to leave the type alone.
17
- 6. Invoke **live-tokens-adjust-geometry** with the geometry the voice implies (table below). Skip when the voice implies nothing about geometry.
18
- 7. Tell the user to look at the running app, and that type and geometry sit in the unsaved buffer until they save the open theme. Offer refinements as edits to the same brief (see Refining a theme that exists).
19
-
20
- Order matters only for safety, and the order above is safe: the color generator carries the live buffers forward into the new theme file, so a color re-roll after fonts and geometry keeps both.
21
-
22
- Flags: `--dry-run` prints the report without writing; `--no-activate` writes without opening; `--carry-from <theme>` takes the non-color content (gradients, fonts, component aliases) from a named theme rather than from the live look. Generating a set needs it: the first run becomes the live look, so a second run without it carries the first theme's fonts and geometry into the second. Opening a theme never changes what the site ships. Only Adopt, in the editor, does that.
23
-
24
- ## The brief
25
-
26
- ```json
27
- {
28
- "name": "Spring Meadow",
29
- "scheme": "light",
30
- "seeds": {
31
- "Brand": { "l": 0.62, "c": 0.17, "h": 145 },
32
- "Accent": { "l": 0.80, "c": 0.15, "h": 95 },
33
- "Special": { "l": 0.60, "c": 0.19, "h": 300 },
34
- "Canvas": { "l": 0.93, "c": 0.04, "h": 120 },
35
- "Neutral": { "l": 0.55, "c": 0.012, "h": 140 },
36
- "Alternate": { "l": 0.58, "c": 0.009, "h": 60 },
37
- "Info": { "l": 0.60, "c": 0.15, "h": 255 },
38
- "Success": { "l": 0.60, "c": 0.16, "h": 150 },
39
- "Warning": { "l": 0.75, "c": 0.15, "h": 85 },
40
- "Danger": { "l": 0.58, "c": 0.20, "h": 25 }
41
- }
42
- }
43
- ```
44
-
45
- All 10 seeds are required; a seed may also be a `"#rrggbb"` string. OKLCH: `l` is 0 to 1 lightness, `c` is chroma (0 grey, about 0.37 max), `h` is hue in degrees. `name` becomes the file slug; `"default"` is refused. `canvasGradient` is an optional boolean, see below.
46
-
47
- Roles: **Brand** is the dominant chromatic identity; **Accent** the supporting color; **Special** the rare expressive tertiary; **Canvas** is the page background verbatim; **Neutral** drives neutral surfaces and body text; **Alternate** is the second near-grey family; the four statuses are conventional signals.
48
-
49
- ## Chroma budget: color is inversely proportional to area
50
-
51
- | Tier | Palettes | Chroma |
52
- |---|---|---|
53
- | Ground (about 60% of every screen) | Canvas, Neutral, Alternate | C 0.005 to 0.03 |
54
- | Dominant chromatic (about 30%) | Brand | C 0.10 to 0.20 |
55
- | Garnish (about 10%) | Accent, Special | may exceed Brand; at most one at full saturation |
56
- | Conditional | Info, Success, Warning, Danger | C 0.12 to 0.19 |
57
-
58
- A good theme reads as 3 or 4 hue families on screen, never 10. Neutral and Alternate stay near-grey but tinted toward the theme (Neutral near Brand's hue; Alternate offset 15 to 60 degrees, or a warm/cool counterpoint), never pure C = 0.
59
-
60
- ## Per-role bands
61
-
62
- | Seed | Light scheme | Dark scheme | Hue |
63
- |---|---|---|---|
64
- | Canvas | L 0.92 to 0.98, C 0.02 to 0.06 | L 0.15 to 0.28, C 0.01 to 0.05 | Brand's hue or its harmony slot |
65
- | Neutral, Alternate | L about 0.55, C 0.008 to 0.02 | same | per the chroma budget |
66
- | Brand | L 0.45 to 0.62, C 0.12 to 0.20 | L 0.70 to 0.83, C cut by a third | the brief's identity hue |
67
- | Accent | harmony slot, or at least 0.25 L from Brand when the mode collapses hue distance | lighten and desaturate like Brand | harmony slot |
68
- | Special | most expressive; default Brand hue +60 at about 65% of Brand's C | same transform | harmony slot |
69
- | Info | shared status L (0.55 to 0.65 light) | lighten like Brand | H 230 to 260 |
70
- | Success | shared status L | same | H 140 to 155 |
71
- | Warning | L 0.75 or higher (vivid yellow must be light) | same | H 70 to 90 |
72
- | Danger | shared status L, C 0.15 to 0.20 | same | H 20 to 30 |
73
-
74
- **The canvas carries the theme's identity, so commit to it.** The page background is the largest area on screen and the strongest difference between themes; a timid canvas makes every theme look the same. Below C 0.015 at L 0.95 a tint is imperceptible, which makes near-white a deliberate choice for clean or minimal briefs and never the default. Three levels of commitment:
75
-
76
- 1. *Tinted paper* (most UI briefs): C 0.02 to 0.06, with L down to 0.92 where the hue needs room.
77
- 2. *Colored ground* (expressive briefs): L 0.85 to 0.92 at C 0.05 to 0.10. The page is unmistakably mint, parchment, sky.
78
- 3. *Full-color ground* (holiday and statement briefs): the canvas is the theme color, like a red Christmas page with green and gold on it. Keep canvas L at or below 0.48 or at or above 0.85 so text has somewhere to go; the contrast gate enforces legibility either way.
79
-
80
- Also:
81
-
82
- - Blue tints cap very low at high L (H 264 at L 0.95 barely reaches C 0.03): lower L for a blue canvas rather than fighting the ceiling. Yellow, green, and cream tint generously at high L.
83
- - When generating a set of themes, make the canvases pairwise distinct in hue or in L. Two light themes both near (0.97, 0.01) read as one theme with different buttons.
84
- - A dark scheme is a transform, not just a dark canvas: every chromatic seed lightens to L 0.75 to 0.85 and drops about a third of its chroma, because saturated color vibrates on dark grounds.
85
- - Equal lightness reads as equal weight: give the four statuses one shared L, and do the same for Brand and Accent when they should balance.
86
- - Status hues never rotate with the harmony; only their L and C adapt to the mood.
87
-
88
- ## Mood dials
89
-
90
- Pleasantness rises with lightness (strongly) and saturation (weakly); energy rises with saturation; drama rises with dark plus saturated. Dominance, the third axis, is carried by surface contrast, type weight, and tightness rather than by color at all.
91
-
92
- That is the whole mechanism, and one dial moves without a reference: warm is hues 20 to 110 plus pink 290 to 360, cool is 140 to 290. For a brief that names a feeling, read `references/mood-vocabulary.md` instead of guessing the dial settings; each entry places the emotion on the three axes and gives the color, type, and geometry together.
93
-
94
- Avoid mid-lightness yellow-green (H 100 to 120 at L 0.5 to 0.7, C about 0.1) unless the brief asks for olive or toxic.
95
-
96
- ## Gamut guardrails
97
-
98
- The engine clamps to gamut regardless; these keep your intent achievable rather than silently muted.
99
-
100
- - Dark saturated yellow does not exist: H 90 at L 0.4 caps at C 0.08 and reads olive. Vivid yellow needs L 0.8 or more. Brown is dark low-chroma orange.
101
- - Vivid light blue does not exist: H 264 at L 0.9 caps at C 0.05. Rich blue lives at L 0.40 to 0.55.
102
- - Teal and sky cap at C 0.15.
103
- - Peak chroma anchors: red H20 C 0.25 at L 0.63; orange H60 C 0.18 at L 0.76; yellow H90 C 0.18 at L 0.86; green H140 C 0.28 at L 0.88; blue H264 C 0.28 at L 0.50; magenta H320 C 0.31 at L 0.65.
104
-
105
- ## Harmony
106
-
107
- Hue offsets from Brand: complementary +180; split-complementary +150/+210; triadic +120/+240; tetradic +60/+180/+240; square +90 steps; compound +30/+180/+210; analogous plus or minus 30; monochromatic same hue.
108
-
109
- - A vague or single-adjective brief takes monochromatic or analogous, with Accent separated from Brand by L and C rather than hue. The polished-UI default: Accent at Brand's hue and about 45% of its chroma, Special at +60 and about 65%.
110
- - A brief naming two colors: measure their hue gap and pick the matching mode (green plus gold is 60 to 90 degrees, so analogous or compound).
111
- - Drama or maximum contrast: complementary, triadic, or tetradic, and then tone one side down, since max-chroma text on a near-black ground vibrates.
112
-
113
- ## Canvas sky and shadows
114
-
115
- `"canvasGradient": true` renders the page background as a vertical gradient from the Canvas ramp. Default off. Turn it on only when the brief evokes atmosphere (sky, night, dusk, glow, underwater) or asks for a gradient outright; keep it off for crisp, flat, minimal, or corporate briefs and whenever in doubt, because a sky on every theme stops meaning anything. It needs a committed canvas (level 2 or 3); at the ramp edge the engine skips it and says so. Say why you enabled it in one line.
116
-
117
- Shadow opacity derives from Canvas lightness and re-derives on every run, so there is nothing to choose. When shadows read heavy or muddy, raise the Canvas seed's L.
118
-
119
- ## Anchor references
120
-
121
- Read the matching reference before seeding, and apply the bands above on top of it.
122
-
123
- - `references/mood-vocabulary.md` covers feelings: joyful, playful, optimistic, confident, serene, tender, cozy, earthy, clinical, wistful, contemplative, urgent, tense, defiant, melancholy, somber, ominous, austere. It opens with the valence, energy, and dominance axes, so a feeling it does not list still places on them.
124
- - `references/style-vocabulary.md` covers named idioms, eras, and genres: Swiss, Bauhaus, mid-century, art deco, terminal, cyberpunk, vaporwave, Y2K, blueprint, Scandinavian, Japandi, cottagecore, editorial, newsprint, riso, corporate, brutalist, Memphis, industrial. Each entry fixes color, type, and geometry as one set, so hand its Type and Geometry columns to the sibling skills verbatim.
125
- - `references/named-themes.md` covers holidays, seasons, and natural scenes: Christmas, Halloween, St. Patrick's, Ocean, Sunset, Autumn, Spring. A holiday or season brief is a statement brief: commitment level 2 or 3, with the named color on the ground rather than only on the buttons.
126
-
127
- Most briefs hit the first file. A brief that matches two ("cozy brutalist", "clinical Swiss") reads the idiom first and lets the feeling move the dials inside it: an idiom sets constraints, and dials move within constraints. A brief that names no feeling, idiom, or occasion at all takes the bands above and the geometry table below.
128
-
129
- ## Geometry from the voice
130
-
131
- The geometry lives in radius, padding, gap, and border width, and `live-tokens-adjust-geometry` knows the mechanics. Hand it the intent:
132
-
133
- | Voice | Geometry |
134
- |---|---|
135
- | playful, friendly, soft | rounder, a step airier; pill buttons when the brief is warm |
136
- | luxurious, elegant, editorial | sharper corners, airier padding, thin borders |
137
- | technical, dense, systematic | tighter spacing, small radius, square corners on containers |
138
- | calm, minimal | leave geometry alone unless the brief says otherwise |
139
-
140
- This table is the fallback. When the brief matched an entry in the mood or style reference, take the geometry from that entry instead: it is tuned to the same reading the color came from, and a style's geometry is often targeted rather than global.
141
-
142
- ## Refining a theme that exists
143
-
144
- "Warmer", "calmer", "more contrast" arrive against a theme that is already open, and the answer is a new brief rather than hand-edits. Edit `scratch/<slug>-brief.json` when it is still there. When it is not, recover the seeds: `src/live-tokens/data/themes/<slug>.json` holds each one verbatim at `colorsAndType.editorConfigs.<Palette>.baseColor` as `{l, c, h}`, and the Canvas seed's lightness tells you the scheme. Rebuild the brief from those ten values, move the dial the user named, re-run under the same name.
145
-
146
- One adjective moves one dial. Warmer and cooler rotate hue; calmer and louder move chroma; lighter, darker, and moodier move Canvas L and the scheme; more contrast widens the L gap between Canvas and Brand and takes chroma out of the ground rather than adding it to the garnish. Leave every seed the user did not name alone, because a refinement that re-rolls the whole palette reads as a different theme and loses the thing they liked.
147
-
148
- ## Files each step writes
149
-
150
- Color writes `themes/<slug>.json` and opens it. Type and geometry write the unsaved buffers, which the page already runs. One Save in the editor keeps all three; Adopt ships them. Component aliases and gradients carry forward from the live look into a generated theme; user-tuned gradients survive, stock ones rebuild from the new families.
151
-
152
- ## Verify
153
-
154
- - The color CLI exits 0 with every check passing (auto-corrected is fine), and the two sibling skills report what they changed.
155
- - The app (dev server running) shows the whole look after a reload, and the editor's Theme panel names the theme.
156
- - To return to the previous look, load the theme the CLI output named from the Theme panel; that discards the buffers too.
@@ -1,43 +0,0 @@
1
- # Mood vocabulary: feelings
2
-
3
- Read this when the brief names a feeling. Entries are starting points, not seeds
4
- to copy: apply the chroma budget, per-role bands, and canvas commitment rules
5
- from SKILL.md on top of them.
6
-
7
- Three axes place any feeling, including one the table omits:
8
-
9
- | Axis | Reads as | Carried by |
10
- |---|---|---|
11
- | Valence | pleasant against unpleasant | lightness, above everything else |
12
- | Energy | aroused against calm | chroma, and hue distance on screen |
13
- | Dominance | assertive against yielding | surface contrast, type weight, border weight, tightness |
14
-
15
- Valence and energy set the quadrant, and the table runs in quadrant order:
16
- pleasant-aroused, pleasant-calm, unpleasant-aroused, unpleasant-calm. Dominance
17
- separates confident from gentle inside one quadrant and lives almost entirely
18
- outside color.
19
-
20
- Energy spent on the ground tier fights the contrast gate, so keep chroma on the
21
- garnish. And a low-valence, low-energy brief taken literally reads as broken
22
- rather than sad, which is why every dark entry below holds one moment of color.
23
-
24
- | Brief | Anchors (L, C, H) | Type | Geometry |
25
- |---|---|---|---|
26
- | Joyful, exuberant, energetic | butter canvas (0.95, 0.05, 90), Brand (0.80, 0.17, 75), coral Accent (0.72, 0.18, 20), green Special (0.75, 0.16, 145); analogous warm. Yellow stays yellow only above L 0.80 | dynamic sans in both slots | radius +2, padding +1, pill buttons |
27
- | Playful, whimsical | tinted canvas (0.93, 0.05, 330), Special at full chroma; tetradic, the rare brief that wants four hue families | expressive display over a plain workhorse | pill buttons, rounder cards, wide gaps |
28
- | Optimistic, hopeful | sky canvas (0.94, 0.03, 220), Brand (0.62, 0.14, 200), warm yellow Accent (0.85, 0.14, 90); complementary across the warm/cool line, which reads as sunrise rather than sky | humanist sans, low contrast | radius +1 |
29
- | Confident, bold | canvas (0.90, 0.07, 250), Brand C 0.18 at L 0.55, wide L range between surfaces. High dominance is the point | heavy rational display, plain body | radius -1, borders +1, tight gaps |
30
- | Serene, tranquil | cool canvas (0.95, 0.03, 200), nothing above C 0.08, analogous 160 to 240 | dynamic sans, one family | soft radius, padding +1, no borders |
31
- | Tender, gentle, romantic | blush canvas (0.95, 0.03, 20), rose Brand (0.70, 0.08, 10), sage Accent (0.68, 0.06, 150); narrow L range, light weights | soft serif display over humanist sans | rounder, hairline borders |
32
- | Cozy, comforting | amber-cream canvas (0.92, 0.05, 75), rust Brand (0.55, 0.12, 40), neutrals at H 60; nothing cool on screen | dynamic serif over dynamic sans | radius +1, padding +1 |
33
- | Wistful, nostalgic, vintage, faded | faded canvas (0.92, 0.03, 70), every chromatic seed capped at C 0.10, hues warm and close. The feeling is chroma withheld, not darkness | rational serif display, quiet body | unchanged, hairline rules |
34
- | Earthy, grounded, natural | canvas (0.90, 0.05, 90) at commitment level 2, hues 30 to 140 at C 0.06 to 0.14, neutrals H 60 to 80; no magenta, no cyan, nothing over C 0.16 | dynamic serif over humanist sans | radius +1, padding +1 |
35
- | Clinical, sterile, precise | near-white canvas at C 0.01, the one brief an untinted ground suits; one cool Brand 200 to 260 at C 0.10; statuses carry the only other color | one neo-grotesque, tightly set | radius sm, tight gaps, hairline borders |
36
- | Contemplative, focused | canvas (0.94, 0.015, 250) or its dark twin (0.20, 0.02, 250), one cool Brand at C 0.10, almost no other hue | one quiet superfamily | padding +1, minimal borders |
37
- | Urgent, alarming | ground held near-neutral so the alarm lands, red Brand (0.58, 0.22, 27) given real area, Warning and Danger on one shared L | condensed grotesque, heavy | radius -2, borders +2, tight padding |
38
- | Tense, anxious | an uncomfortable ground, (0.88, 0.04, 105) light or (0.22, 0.03, 280) dark, plus a near-complementary pair that vibrates with one side toned down | neo-grotesque, tightly set | tight gaps, radius sm |
39
- | Defiant, rebellious, loud | near-black canvas (0.15, 0.01, 0), one acid hue (0.85, 0.20, 120), nothing else chromatic | heavy display; a mono body works | radius none, borders +3 |
40
- | Melancholy, moody, sad | dark; canvas (0.22, 0.03, 250), chromatic seeds C 0.06 to 0.10 at L 0.72 to 0.80, blue through violet, with Accent held at C 0.14 as the moment of color | rational serif display over quiet sans | padding +1, hairline borders |
41
- | Somber, grave, mournful | near-neutral dark canvas (0.18, 0.01, 260), one desaturated Brand, gradient off | rational serif in both slots | sharp, tight gaps, hairline rules |
42
- | Ominous, dramatic, haunted | dark canvas (0.15, 0.04, 300), one hot accent (0.75, 0.16, 30) used sparingly, canvasGradient on | heavy high-contrast display | sharp, borders +2 |
43
- | Austere, severe, cold | monochrome; canvas at either L extreme at C 0.01 or below, one low-chroma Brand, muted statuses. Highest dominance, lowest energy | one rational family | radius none, tight padding, hairline borders |
@@ -1,18 +0,0 @@
1
- # Named themes: canonical OKLCH anchors
2
-
3
- Read this when the brief names a holiday, a season, or a natural scene. These
4
- are starting points, not seeds to copy: apply the chroma budget, per-role bands,
5
- and canvas commitment rules from SKILL.md on top of them.
6
-
7
- Holiday briefs are statement briefs. Default to canvas commitment level 2 or 3,
8
- never cream, and put the named color on the ground, not only on the buttons.
9
-
10
- | Brief | Anchors (L, C, H) | Strongest form |
11
- |---|---|---|
12
- | Christmas | red (0.53, 0.21, 22), green (0.46, 0.11, 155), gold (0.77, 0.14, 91) | red canvas (0.42, 0.14, 25), green Brand, gold Accent, dark scheme. Softer: evergreen canvas (0.35, 0.07, 160) or deep cream (0.95, 0.04, 85). One of red or green owns the ground; never a 50/50 split. |
13
- | Halloween | pumpkin (0.70, 0.20, 46), purple (0.51, 0.21, 313), poison green (0.73, 0.20, 137) | orange canvas (0.45, 0.13, 55) with violet and poison-green accents, or near-black violet canvas with pumpkin Brand. Dark scheme either way. |
14
- | St. Patrick's | green (0.51, 0.13, 152) | green Brand, gold Accent, white or beige neutrals. |
15
- | Ocean | deep blue (0.35, 0.08, 237), aqua (0.78, 0.12, 214) | hues held to 180 to 240. |
16
- | Sunset | hues 90 to 320 through red | L falls 0.85 to 0.40 across the sweep. |
17
- | Autumn | parchment canvas (0.87, 0.06, 80), rust Brand (0.55, 0.15, 40), gold Accent (0.75, 0.15, 85), moss Special (0.55, 0.10, 120) | warm brown neutrals H 50 to 70; deep red H 25 welcome. |
18
- | Spring | pastels L 0.85 to 0.95, C 0.04 to 0.10 | greens 130 to 150, pinks 0 to 20, mint canvas. |
@@ -1,35 +0,0 @@
1
- # Style vocabulary: idioms, eras, and genres
2
-
3
- Read this when the brief names a design idiom, a decade, or a genre. A mood
4
- moves dials; an idiom sets constraints, and it overrides the generic defaults in
5
- SKILL.md: the polished-UI Accent at 45% of Brand's chroma is right for a vague
6
- brief and wrong for Bauhaus.
7
-
8
- Entries are starting points, not seeds to copy: apply the chroma budget,
9
- per-role bands, and canvas commitment rules on top of them. Riso, Memphis, and
10
- brutalist break the chroma budget on purpose. Break it in the one layer the
11
- style is about and hold the rest of the ground tier down.
12
-
13
- The table runs modernist, digital, quiet, print, expressive.
14
-
15
- | Brief | Anchors (L, C, H) | Type | Geometry |
16
- |---|---|---|---|
17
- | Swiss, International | near-white canvas (0.97, 0.01, 0), or true black for the poster reading; one red Brand (0.55, 0.22, 27) as the only hue on screen; Neutral at C 0.005, untinted on purpose; monochromatic | one rational neo-grotesque across both slots | radius none, tight gaps, hairline borders |
18
- | Bauhaus | paper canvas (0.95, 0.02, 85) under primaries at full commitment: red (0.58, 0.21, 27), blue (0.48, 0.20, 264), yellow (0.86, 0.17, 90); triadic | geometric sans, heavy display | radius none on containers, radius full on buttons alone, so the circle reads as a decision |
19
- | Mid-century modern | canvas (0.90, 0.05, 75), mustard (0.75, 0.13, 85), teal (0.55, 0.10, 195), burnt orange (0.60, 0.15, 45), walnut neutrals H 60; nothing over C 0.16; compound | dynamic sans, or a geometric display over a dynamic body | radius +1 to +2, padding +1, no borders |
20
- | Art deco, opulent, luxurious | near-black canvas (0.20, 0.02, 280), gold (0.78, 0.13, 88), jade (0.60, 0.10, 165); dark, one metallic accent, everything else grey | rational high-contrast serif display, quiet body | sharp, padding +1, thin borders |
21
- | Terminal, phosphor | canvas (0.16, 0.01, 150), phosphor green Brand (0.80, 0.16, 145), amber Accent (0.80, 0.13, 80); monochromatic, dark, gradient off | mono in both slots | radius none, tight padding, a 1px border on everything |
22
- | Cyberpunk, neon noir, futuristic | canvas (0.18, 0.04, 300), magenta Brand (0.78, 0.18, 330), cyan Accent (0.82, 0.12, 200); complementary, dark, canvasGradient on for the glow | wide geometric display, neutral body | sharp, tight gaps |
23
- | Vaporwave | sunset canvas (0.88, 0.06, 330), pink (0.72, 0.16, 350), cyan (0.80, 0.11, 205), lilac Special; light, gradient on | wide retro display, serif welcome | rounder, airier |
24
- | Y2K, bubble | chrome canvas (0.96, 0.015, 240), electric blue Brand (0.62, 0.18, 255), lime Accent (0.85, 0.17, 130) | geometric sans, heavy display | radius full on buttons with the padding a pill needs, generous spacing |
25
- | Blueprint | canvas (0.35, 0.07, 245), pale rules (0.90, 0.02, 240), one warm accent (0.75, 0.14, 60); dark | mono, or a technical grotesque; a mono body works here and almost nowhere else | radius none, 1px borders, tight grid |
26
- | Scandinavian, hygge | chalk canvas (0.96, 0.012, 70), sage Brand (0.60, 0.06, 150), clay Accent (0.70, 0.08, 40); nothing above C 0.10; analogous | dynamic sans on both sides | soft radius, padding +1, hairline borders |
27
- | Japandi, wabi-sabi | unbleached paper canvas (0.93, 0.025, 80), ink Brand (0.35, 0.02, 250), one earth Accent (0.62, 0.09, 45); near-monochrome | rational serif display over a quiet humanist body | radius sm, padding +2, no borders |
28
- | Cottagecore, botanical | cream canvas (0.94, 0.04, 85), moss (0.55, 0.10, 135), dusty rose (0.70, 0.09, 15), butter (0.85, 0.11, 95); warm analogous | dynamic serif display over humanist sans | rounder, airier |
29
- | Editorial, magazine | paper canvas (0.97, 0.015, 85), ink neutrals, one strong Brand (0.50, 0.18, 20) carried by rules and pull quotes | high-contrast serif display over a text serif or humanist sans | sharp, padding +1, hairline rules |
30
- | Newsprint, broadsheet | grey-warm canvas (0.91, 0.02, 80), near-black ink, nothing chromatic above C 0.10 | rational serif in both slots | radius none, tight gaps, hairline rules |
31
- | Risograph, zine | paper canvas (0.94, 0.03, 80) with two flat spot inks, fluoro pink (0.68, 0.22, 5) and blue (0.52, 0.18, 260); complementary, no midtones between them | expressive display over a plain workhorse | radius none, borders +2 |
32
- | Corporate, professional, trustworthy | canvas (0.97, 0.012, 250), navy Brand (0.48, 0.12, 255), teal Accent (0.60, 0.09, 195), conventional statuses | rational sans body with a rational serif or same-family display | leave it alone |
33
- | Brutalist | pure canvas, (0.98, 0, 0) or (0.15, 0, 0), with Neutral at C 0, untinted because that is the point; one alarming Brand (0.58, 0.24, 27) | neo-grotesque or mono at heavy weight | radius none, borders +2 to +3, tight padding |
34
- | Memphis, postmodern | pastel canvas (0.95, 0.03, 60) carrying full-chroma primaries and a hot pink Special (0.70, 0.20, 350); tetradic or square, four hue families on purpose | geometric display, plain body | targeted rather than global: pill buttons against radius-none cards |
35
- | Industrial, workshop, gritty | concrete canvas (0.88, 0.008, 250), or (0.22, 0.01, 250) dark, safety orange Brand (0.68, 0.18, 50), steel neutrals | condensed grotesque display over a plain body | radius sm, thick borders, tight padding |
@@ -1,90 +0,0 @@
1
- ---
2
- name: live-tokens-pair-fonts
3
- description: Choose and apply a Google Fonts pairing for a live-tokens theme, binding families to the shipped --font-* stacks. Use whenever the user asks to pair fonts, pick a typeface, change or set the fonts, or describes type by voice: what font should the headings use, make the type more editorial, friendlier, more technical, more elegant, a serif for headings, a display font for this theme, less generic type, match the fonts to the theme. Also invoked by live-tokens-generate-theme for the type half of a whole look. Changes type only, never color. Not for a single token (use the editor) or for color (see live-tokens-generate-theme).
4
- ---
5
-
6
- # Pairing fonts for a theme
7
-
8
- You choose the families; the CLI verifies each against Google Fonts, builds the URL from the weights the family actually has, and writes the result into the unsaved buffer. Never hand-author font JSON and never edit the data tree directly. Google Fonts is the pool because it is freely licensable and loads by URL; other sources go in by hand through the editor's Project fonts section.
9
-
10
- ## Workflow
11
-
12
- 1. Choose the pairing with the framework below and write a brief to `scratch/font-brief.json`.
13
- 2. Run `npx live-tokens set-fonts scratch/font-brief.json`. It prints each stack that moved, each family's real weights and URL, and the weights your typography tokens ask for that the family lacks.
14
- 3. Read the report. A weight gap is a quality note: name it and offer an alternative only if it matters (a body face without 400, 700, or italic matters; a display face without 300 does not). A family not on Google Fonts fails the run; fix the spelling and re-run.
15
- 4. Tell the user to reload the editor page before saving. A running editor holds its own copy of the buffer this CLI just wrote and never re-reads it, so a Save without a reload writes the stale copy back and the pairing vanishes with a success report still on screen. After the reload the type is on the page, and unsaved until they save the open theme.
16
-
17
- State your reasoning when you propose the pairing: each face's form model and the matrix verdict, in one sentence, so the user can argue with the argument rather than only the result.
18
-
19
- Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires an explicit URL per family; use it only offline with a URL in hand.
20
-
21
- ## The brief
22
-
23
- ```json
24
- { "display": "Fraunces", "body": "Nunito Sans" }
25
- ```
26
-
27
- Every slot is optional and an omitted slot is left exactly as it is. `display` is `--font-display`, `body` is `--font-sans`; `serif`, `mono` and `editorial` exist when a theme needs them. `editorial` is `--font-editorial`, the long-reading face behind the `--editorial-*` text styles: it tracks the body face until a theme repoints it, so set it only when essays and articles should not carry the body face. A slot may be `{ "name": "...", "url": "..." }` to pin an exact URL. Spell families as Google does; the CLI reports the canonical spelling back.
28
-
29
- ## Choose the body face first
30
-
31
- The body face is the anchor. It carries most of the words, and text faces survive small sizes where display faces do not. Pick it against the brief, then pick the display face against it. A body face must have regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face failing any of these is a display face whatever its name says. Single-weight families are fine for `display` and disqualifying for `body`.
32
-
33
- The shipped text styles ask the display face for 600, across all four heading levels, and the body face for 400; prose markup adds 700 and italic for `strong` and `em`. Screen candidates against those four before running, so the report confirms a decision instead of reporting a surprise.
34
-
35
- ## The font matrix: the decision rule
36
-
37
- Classify each candidate on two layers. The **skeleton** is its form model; the **flesh** is its stroke contrast and serif treatment.
38
-
39
- | Form model | Construction | Reads as |
40
- |---|---|---|
41
- | **Dynamic** | diagonal stress, open apertures, written origin | open, warm, humane, timeless |
42
- | **Rational** | vertical stress, closed apertures, drawn not written | orderly, reserved, elegant, authoritative |
43
- | **Geometric** | monolinear, circle-and-line | technical, modern, systematic, sober |
44
-
45
- - **Same skeleton, different flesh: reliable.** Helvetica and Bodoni are both rational, one a linear sans and one a contrasting serif.
46
- - **Same flesh, different skeleton: the failure case.** The two look alike on the surface and fight underneath. This is why two arbitrary sans-serifs so often clash.
47
- - **Far apart on both: works, deliberately.** An unmistakable difference reads as a decision.
48
-
49
- Many faces sit between columns. When one straddles, say so and lean on the voice table and the x-height check instead.
50
-
51
- ## Voice
52
-
53
- | Brief says | Type voice |
54
- |---|---|
55
- | editorial, literary, considered | dynamic serif display over a humanist sans body |
56
- | elegant, luxurious, formal | rational high-contrast serif display; keep the body quiet |
57
- | friendly, warm, approachable | dynamic sans on both sides, or a soft serif display |
58
- | technical, systematic, precise | geometric or neo-grotesque sans; a mono for code |
59
- | playful, informal | an expressive display face over a plain workhorse body |
60
- | serious, institutional, trustworthy | rational sans body, rational serif display |
61
- | quiet, minimal, unbranded | one superfamily across both slots |
62
-
63
- Match the type to the same brief the color came from. A warm autumn palette under a cold geometric sans reads as two projects.
64
-
65
- ## Shortcuts
66
-
67
- These find an adequate pairing fast and skip the reasoning; use them when the brief is vague or the type should stay quiet.
68
-
69
- - **A superfamily.** Google Fonts families with both sans and serif siblings, among them Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not, so treat it as a starting set: `set-fonts` verifies every family against the API and fails loudly on one that is gone.
70
- - **One family across weights.**
71
- - **Same designer or foundry.**
72
- - **Serif display over sans body** when nothing else decides it.
73
-
74
- ## Watch for
75
-
76
- - **x-height parity.** Both faces are set from one size scale, so a small-x-height display face over a large-x-height body face gives a heading that looks weaker than its own body text. This is the one visual check that matters on screen; make it on the rendered page.
77
- - **Print faces at small sizes.** Delicate serifs and high stroke contrast turn to mud below 16px.
78
- - **Every family is a request.** Two is the target; three needs a reason.
79
- - **Sets of themes:** no two share a display face or a body face.
80
-
81
- ## Scope
82
-
83
- Type only. Color, component aliases, shape, and the type scale are untouched: `set-fonts` moves families between stacks and nothing else, writing only the unsaved colors-and-type buffer. Save the theme to keep it, Adopt to ship it. Adopt is also what rewrites `fonts.css`, which is how a build with no editor in it loads the family at all.
84
-
85
- ## Verify
86
-
87
- - The CLI exits 0 and names each stack that moved, before and after.
88
- - Each URL reflects the family's real weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face.
89
- - The app shows the new type after a reload, and the editor's Fonts section lists both families with their fallbacks intact.
90
- - To revert, run the inverse brief, or load the open theme again to discard the buffer.
@@ -1,260 +0,0 @@
1
- // `live-tokens generate-theme` worker.
2
- //
3
- // Reads a seed brief (JSON), builds a full validated colors-and-type layer via
4
- // the compiled engine (dist-plugin/generateColorsAndType — the CLI never imports
5
- // TS sources), enforces the AA contrast gate, and saves the result as a theme:
6
- // <themesDir>/<slug>.json, the document that carries the whole theme by value.
7
- // Unless --no-activate it then opens that theme the way the dev server's apply
8
- // door does — existing `_working` buffers are cleared and
9
- // `themes/_active.json` names it. Nothing else moves, so generating a theme
10
- // cannot change what the site ships.
11
- //
12
- // Non-color content (shadows, component aliases, fonts) carries forward from
13
- // the live state so generation only replaces the color identity. Swatch
14
- // gradients ride along too when user-tuned; stock ones are rebuilt from the new
15
- // theme's families by the engine.
16
-
17
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
18
- import { dirname, join, relative, resolve } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
-
21
- const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
- const ENGINE = resolve(pkgRoot, 'dist-plugin/generateColorsAndType/index.js');
23
- const packageDataDir = join(pkgRoot, 'src/live-tokens/data');
24
- // Source of truth: src/editor/core/themes/themeTypes.ts, which
25
- // normalizeTheme.ts re-exports. This copy cannot import TS, so
26
- // `check:preset-themes` and generate-theme.test.ts are what catch a drift.
27
- const THEME_SCHEMA_VERSION = 5;
28
-
29
- async function loadEngine() {
30
- if (!existsSync(ENGINE)) {
31
- throw new Error(
32
- `theme engine not found at ${relative(process.cwd(), ENGINE)}. ` +
33
- `Build the plugin first (npm run build:plugin).`,
34
- );
35
- }
36
- return import(ENGINE);
37
- }
38
-
39
- function readJson(path) {
40
- return JSON.parse(readFileSync(path, 'utf8'));
41
- }
42
-
43
- function readJsonIfExists(path) {
44
- try {
45
- return existsSync(path) ? readJson(path) : null;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- /** Local dir first, then the installed package's shipped copy — the same
52
- * fallback order the dev server's reads use. */
53
- function readData(localDir, packageSubdir, fileName) {
54
- return (
55
- readJsonIfExists(join(localDir, `${fileName}.json`)) ??
56
- readJsonIfExists(join(packageDataDir, packageSubdir, `${fileName}.json`))
57
- );
58
- }
59
-
60
- function componentNames(componentConfigsDir) {
61
- if (!existsSync(componentConfigsDir)) return [];
62
- return readdirSync(componentConfigsDir, { withFileTypes: true })
63
- .filter((e) => e.isDirectory())
64
- .map((e) => e.name)
65
- .sort();
66
- }
67
-
68
- /** `_fileName` and `_source` are read-door markers, never part of a document. */
69
- function stripMarkers(value) {
70
- if (!value || typeof value !== 'object') return value;
71
- const { _fileName, _source, ...rest } = value;
72
- return rest;
73
- }
74
-
75
- /** A component's derived default, local tree first, then the installed
76
- * package's shipped copy — same fallback order every other read here uses. */
77
- function defaultComponentConfig(componentConfigsDir, comp) {
78
- return readData(join(componentConfigsDir, comp), `component-configs/${comp}`, 'default');
79
- }
80
-
81
- /**
82
- * The look the new theme carries forward. Without --carry-from that is the LIVE
83
- * state — buffer first, then the open theme, then the shipped default — because
84
- * what the user sees is what they expect to keep. With it, the named theme,
85
- * then the shipped default for anything that theme omits, so an incomplete
86
- * source never produces an incomplete result.
87
- */
88
- function resolveCarrySource({ carryFrom, colorsAndTypeDir, componentConfigsDir, themesDir }) {
89
- const comps = componentNames(componentConfigsDir);
90
-
91
- if (carryFrom) {
92
- const theme = readData(themesDir, 'themes', carryFrom);
93
- if (!theme) throw new Error(`--carry-from theme "${carryFrom}" not found`);
94
- const componentConfigs = {};
95
- for (const comp of comps) {
96
- const cfg = theme.componentConfigs?.[comp] ?? defaultComponentConfig(componentConfigsDir, comp);
97
- if (cfg) componentConfigs[comp] = stripMarkers(cfg);
98
- }
99
- return { colorsAndType: stripMarkers(theme.colorsAndType), componentConfigs };
100
- }
101
-
102
- const activeSlug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
103
- const activeTheme = readData(themesDir, 'themes', activeSlug);
104
- const colorsAndType = stripMarkers(
105
- readJsonIfExists(join(colorsAndTypeDir, '_working.json')) ??
106
- activeTheme?.colorsAndType ??
107
- readData(colorsAndTypeDir, 'colors-and-type', 'default'),
108
- );
109
-
110
- // A working buffer is an unsaved delta from the active theme; absent theme
111
- // component entries fall through to the component defaults. Mirrors
112
- // `bin/adjust.mjs`'s `readLiveConfigs` and the dev server's
113
- // `resolveLiveComponentConfig` — all three layers, in the same order.
114
- const componentConfigs = {};
115
- for (const comp of comps) {
116
- const live =
117
- readJsonIfExists(join(componentConfigsDir, comp, '_working.json')) ??
118
- activeTheme?.componentConfigs?.[comp] ??
119
- defaultComponentConfig(componentConfigsDir, comp);
120
- if (live) componentConfigs[comp] = stripMarkers(live);
121
- }
122
- return { colorsAndType, componentConfigs };
123
- }
124
-
125
- /** `engine` is a test seam; the CLI always runs the compiled bundle. */
126
- export async function runGenerateTheme({
127
- briefPath,
128
- activate = true,
129
- dryRun = false,
130
- carryFrom,
131
- root = process.cwd(),
132
- colorsAndTypeDir,
133
- componentConfigsDir,
134
- themesDir,
135
- engine,
136
- } = {}) {
137
- const { buildColorsAndTypeFromSeeds, resolveDataDirs, CURRENT_COMPONENT_SCHEMA_VERSION } =
138
- engine ?? (await loadEngine());
139
-
140
- const briefFull = resolve(root, briefPath);
141
- if (!existsSync(briefFull)) {
142
- throw new Error(`brief not found at ${relative(root, briefFull)}`);
143
- }
144
- let brief;
145
- try {
146
- brief = readJson(briefFull);
147
- } catch (err) {
148
- throw new Error(`brief is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
149
- }
150
-
151
- const dirs =
152
- colorsAndTypeDir && componentConfigsDir && themesDir
153
- ? { colorsAndTypeDir, componentConfigsDir, themesDir }
154
- : resolveDataDirs();
155
-
156
- const previousActive = readJsonIfExists(join(dirs.themesDir, '_active.json'))?.activeFile ?? 'default';
157
- const carry = resolveCarrySource({ carryFrom, ...dirs });
158
-
159
- const { colorsAndType, slug, report } = buildColorsAndTypeFromSeeds(
160
- brief,
161
- {
162
- cssVariables: carry.colorsAndType?.cssVariables,
163
- fontSources: carry.colorsAndType?.fontSources,
164
- fontStacks: carry.colorsAndType?.fontStacks,
165
- gradients: carry.colorsAndType?.gradients,
166
- },
167
- new Date().toISOString(),
168
- );
169
-
170
- const themePath = join(dirs.themesDir, `${slug}.json`);
171
- const existing = readJsonIfExists(themePath);
172
- const theme = {
173
- name: colorsAndType.name,
174
- createdAt: existing?.createdAt ?? colorsAndType.createdAt,
175
- updatedAt: colorsAndType.updatedAt,
176
- schemaVersion: THEME_SCHEMA_VERSION,
177
- colorsAndType,
178
- componentConfigs: carry.componentConfigs,
179
- componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
180
- };
181
-
182
- if (!dryRun) {
183
- mkdirSync(dirs.themesDir, { recursive: true });
184
- writeFileSync(themePath, JSON.stringify(theme, null, 2) + '\n');
185
- if (activate) applyTheme(slug, dirs);
186
- }
187
-
188
- return {
189
- name: theme.name,
190
- slug,
191
- themePath,
192
- existed: existing !== null,
193
- activated: activate && !dryRun,
194
- previousActive,
195
- carriedFrom: carryFrom ?? null,
196
- componentsCarried: Object.keys(carry.componentConfigs).length,
197
- dryRun,
198
- report,
199
- };
200
- }
201
-
202
- /** The apply door's write set, reproduced: clear every working delta and point
203
- * `themes/_active.json` at the open document. Production is untouched. */
204
- function applyTheme(slug, dirs) {
205
- const colorsWorking = join(dirs.colorsAndTypeDir, '_working.json');
206
- if (existsSync(colorsWorking)) rmSync(colorsWorking);
207
-
208
- for (const comp of componentNames(dirs.componentConfigsDir)) {
209
- const workingPath = join(dirs.componentConfigsDir, comp, '_working.json');
210
- if (existsSync(workingPath)) rmSync(workingPath);
211
- }
212
-
213
- writeFileSync(join(dirs.themesDir, '_active.json'), JSON.stringify({ activeFile: slug }));
214
- }
215
-
216
- export function formatGenerateThemeResult(result) {
217
- const root = process.cwd();
218
- const lines = [];
219
- const wrote = result.dryRun ? 'Would write' : result.existed ? 'Updated' : 'Created';
220
- lines.push(`${wrote} theme "${result.name}" → ${relative(root, result.themePath)}`);
221
- lines.push(
222
- `Carried forward from ${result.carriedFrom ? `theme "${result.carriedFrom}"` : 'the live state'}: ` +
223
- `fonts, gradients and ${result.componentsCarried} component config(s).`,
224
- );
225
-
226
- lines.push(`\nContrast report (${result.report.scheme} scheme):`);
227
- const width = Math.max(...result.report.checks.map((c) => c.textVar.length));
228
- for (const c of result.report.checks) {
229
- const mark = c.pass ? '✓' : '✗';
230
- const fixed = c.corrected ? ' (auto-corrected)' : '';
231
- lines.push(
232
- ` ${mark} ${c.textVar.padEnd(width)} ${c.ratio.toFixed(2).padStart(6)}:1 vs ${c.against} floor ${c.floor}:1${fixed}`,
233
- );
234
- }
235
- if (result.report.failures.length > 0) {
236
- lines.push(`\nUnmet floors — adjust the brief's seeds and re-run:`);
237
- for (const f of result.report.failures) lines.push(` ! ${f}`);
238
- }
239
-
240
- lines.push(
241
- result.report.gradients === 'carried'
242
- ? '\nGradients: kept your tuned swatch gradients.'
243
- : '\nGradients: swatch tokens rebuilt from the theme families.',
244
- );
245
- lines.push(`Shadows: ${result.report.shadows}; carried geometry kept.`);
246
- if (result.report.canvasGradient) {
247
- lines.push(`Canvas sky: ${result.report.canvasGradient}.`);
248
- }
249
-
250
- if (result.activated) {
251
- lines.push(
252
- `\nOpened "${result.slug}" (previously open: "${result.previousActive}"). ` +
253
- `Reload the app to see it; switch back any time from Load in the editor's Theme panel. ` +
254
- `Adopt it there to publish it to tokens.generated.css.`,
255
- );
256
- } else if (!result.dryRun) {
257
- lines.push(`\nNot opened (--no-activate). Load "${result.slug}" from the editor's Theme panel to see it.`);
258
- }
259
- return lines.join('\n');
260
- }