@motion-proto/live-tokens 0.78.0 → 0.80.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 (172) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +49 -38
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +23 -28
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +61 -0
  4. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +2 -2
  5. package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -15
  6. package/.claude/skills/live-tokens-create-page/SKILL.md +6 -4
  7. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
  8. package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
  9. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +1 -1
  10. package/.claude/skills/live-tokens-set-geometry/SKILL.md +1 -1
  11. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +1 -1
  12. package/CHANGELOG.md +271 -0
  13. package/README.md +10 -16
  14. package/bin/check-component.mjs +154 -551
  15. package/bin/check-page.mjs +57 -541
  16. package/bin/cli.mjs +93 -16
  17. package/bin/contractRunner.mjs +37 -91
  18. package/bin/lib/buildChecks.mjs +32 -0
  19. package/bin/lib/catalogue.mjs +161 -34
  20. package/bin/lib/componentSource.mjs +152 -0
  21. package/bin/lib/cssValues.mjs +9 -0
  22. package/bin/lib/dataDir.mjs +126 -0
  23. package/bin/lib/findings.mjs +103 -12
  24. package/bin/lib/fixers.mjs +64 -0
  25. package/bin/lib/geometry.mjs +92 -0
  26. package/bin/lib/pageSource.mjs +230 -0
  27. package/bin/lib/report.mjs +57 -59
  28. package/bin/lib/tokenVocabulary.mjs +104 -34
  29. package/bin/migrate-build-script.mjs +66 -0
  30. package/bin/migrate.mjs +5 -0
  31. package/bin/rules/componentStructure.mjs +344 -0
  32. package/bin/rules/componentUse.mjs +313 -0
  33. package/bin/rules/importsAndRoutes.mjs +136 -0
  34. package/bin/rules/testRuns.mjs +122 -0
  35. package/bin/rules/tokens.mjs +426 -0
  36. package/bin/setup-claude.mjs +1 -2
  37. package/dist-plugin/{chunk-PDNL4NC5.js → chunk-D4WRIKEZ.js} +7 -2
  38. package/dist-plugin/{chunk-SWXRVZKT.js → chunk-REBHE3ZM.js} +414 -1
  39. package/dist-plugin/index.cjs +470 -19
  40. package/dist-plugin/index.d.cts +1 -0
  41. package/dist-plugin/index.d.ts +1 -0
  42. package/dist-plugin/index.js +87 -55
  43. package/dist-plugin/migrateData/index.cjs +422 -4
  44. package/dist-plugin/migrateData/index.js +2 -2
  45. package/dist-plugin/setColors/index.cjs +414 -1
  46. package/dist-plugin/setColors/index.d.cts +1 -1
  47. package/dist-plugin/setColors/index.d.ts +1 -1
  48. package/dist-plugin/setColors/index.js +1 -1
  49. package/dist-plugin/setGeometry/index.cjs +423 -13
  50. package/dist-plugin/setGeometry/index.d.cts +3 -3
  51. package/dist-plugin/setGeometry/index.d.ts +3 -3
  52. package/dist-plugin/setGeometry/index.js +10 -13
  53. package/dist-plugin/setType/index.d.cts +1 -1
  54. package/dist-plugin/setType/index.d.ts +1 -1
  55. package/dist-plugin/{themeTypes-BxRtuN5V.d.cts → themeTypes-B8_Idrp4.d.cts} +2 -2
  56. package/dist-plugin/{themeTypes-BxRtuN5V.d.ts → themeTypes-B8_Idrp4.d.ts} +2 -2
  57. package/package.json +2 -2
  58. package/src/editor/component-editor/CalloutEditor.svelte +2 -2
  59. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +14 -15
  60. package/src/editor/component-editor/CornerBadgeEditor.svelte +14 -14
  61. package/src/editor/component-editor/DialogEditor.svelte +5 -5
  62. package/src/editor/component-editor/InlineEditActionsEditor.svelte +2 -2
  63. package/src/editor/component-editor/RadioButtonEditor.svelte +21 -21
  64. package/src/editor/component-editor/SectionDividerEditor.svelte +7 -7
  65. package/src/editor/component-editor/SegmentedControlEditor.svelte +5 -5
  66. package/src/editor/component-editor/SideNavigationEditor.svelte +25 -25
  67. package/src/editor/component-editor/TabBarEditor.svelte +6 -6
  68. package/src/editor/component-editor/TableEditor.svelte +6 -6
  69. package/src/editor/component-editor/ToggleEditor.svelte +2 -2
  70. package/src/editor/component-editor/index.ts +3 -0
  71. package/src/editor/component-editor/registry.ts +57 -1
  72. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +14 -14
  73. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +3 -0
  74. package/src/editor/component-editor/scaffolding/types.ts +15 -0
  75. package/src/editor/core/components/adjustAliases.ts +4 -4
  76. package/src/editor/core/components/aliasKinds.ts +20 -19
  77. package/src/editor/core/sketch/sketchLayer.ts +3 -3
  78. package/src/editor/core/themes/migrateComponentConfig.ts +14 -6
  79. package/src/editor/core/themes/migrations/2026-09-13-badge-brand.ts +42 -0
  80. package/src/editor/core/themes/migrations/2026-09-13-collapsiblesection-open.ts +33 -0
  81. package/src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts +70 -0
  82. package/src/editor/core/themes/migrations/2026-09-13-hairline.ts +92 -0
  83. package/src/editor/core/themes/migrations/2026-09-13-indicator.ts +54 -0
  84. package/src/editor/core/themes/migrations/2026-09-13-sectiondivider-surface.ts +36 -0
  85. package/src/editor/core/themes/migrations/2026-09-13-selected-state.ts +124 -0
  86. package/src/editor/core/themes/migrations/2026-09-13-toggle-label.ts +31 -0
  87. package/src/editor/core/themes/migrations/index.ts +16 -0
  88. package/src/editor/core/themes/themeService.ts +3 -3
  89. package/src/editor/core/themes/themeTypes.ts +13 -15
  90. package/src/editor/docs/Docs.svelte +1 -1
  91. package/src/editor/docs/content/light-and-dark.md +3 -3
  92. package/src/editor/docs/content.generated.ts +1 -1
  93. package/src/editor/index.ts +1 -0
  94. package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
  95. package/src/editor/skill-atlas/SkillAtlas.svelte +4 -4
  96. package/src/editor/skill-atlas/TreeNodeCard.svelte +4 -4
  97. package/src/editor/skill-atlas/skillSources.generated.ts +11 -14
  98. package/src/editor/skill-atlas/skillTrees.ts +1 -3
  99. package/src/editor/skill-atlas/trees/check-compliance.ts +37 -149
  100. package/src/editor/skill-atlas/trees/create-component.ts +62 -64
  101. package/src/editor/skill-atlas/trees/create-page.ts +38 -17
  102. package/src/editor/skill-atlas/trees/pick-component.ts +3 -3
  103. package/src/editor/skill-atlas/trees/set-geometry.ts +1 -1
  104. package/src/editor/ui/UIPaletteSelector.svelte +11 -15
  105. package/src/editor/ui/variantScales.ts +8 -8
  106. package/src/live-tokens/data/themes/autumn.json +188 -188
  107. package/src/live-tokens/data/themes/halloween.json +188 -188
  108. package/src/live-tokens/data/themes/midnight-study.json +245 -245
  109. package/src/live-tokens/data/themes/ocean.json +188 -188
  110. package/src/live-tokens/data/themes/royal-velvet.json +188 -188
  111. package/src/live-tokens/data/themes/sketchy.json +188 -188
  112. package/src/live-tokens/data/themes/spring-meadow.json +188 -188
  113. package/src/live-tokens/data/themes/sunset.json +188 -188
  114. package/src/system/components/Badge.svelte +27 -23
  115. package/src/system/components/Button.svelte +13 -7
  116. package/src/system/components/Callout.svelte +16 -11
  117. package/src/system/components/Card.svelte +22 -15
  118. package/src/system/components/CodeSnippet.svelte +10 -6
  119. package/src/system/components/CollapsibleSection.svelte +81 -76
  120. package/src/system/components/CornerBadge.svelte +78 -72
  121. package/src/system/components/Dialog.svelte +21 -18
  122. package/src/system/components/IconButton.svelte +13 -9
  123. package/src/system/components/Image.svelte +14 -8
  124. package/src/system/components/ImageLightbox.svelte +10 -6
  125. package/src/system/components/InlineEditActions.svelte +17 -14
  126. package/src/system/components/Input.svelte +13 -7
  127. package/src/system/components/MenuSelect.svelte +13 -7
  128. package/src/system/components/Notification.svelte +17 -11
  129. package/src/system/components/Panel.svelte +13 -6
  130. package/src/system/components/ProgressBar.svelte +10 -5
  131. package/src/system/components/RadioButton.svelte +33 -30
  132. package/src/system/components/SectionDivider.svelte +28 -21
  133. package/src/system/components/SegmentedControl.svelte +27 -23
  134. package/src/system/components/SideNavigation.svelte +175 -171
  135. package/src/system/components/Slider.svelte +11 -7
  136. package/src/system/components/TabBar.svelte +53 -49
  137. package/src/system/components/Table.svelte +20 -15
  138. package/src/system/components/Toggle.svelte +14 -10
  139. package/src/system/components/Tooltip.svelte +10 -6
  140. package/src/system/styles/CONVENTIONS.md +3 -4
  141. package/src/testing-js/chunk-3UKGXCDL.js +48 -0
  142. package/src/testing-js/chunk-3UKGXCDL.js.map +1 -0
  143. package/src/testing-js/{chunk-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
  144. package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
  145. package/src/testing-js/{chunk-GNIUPIU2.js → chunk-U7OJE5DU.js} +18 -18
  146. package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
  147. package/src/testing-js/{chunk-4JQX6WWL.js → chunk-ZMZQZ33J.js} +474 -166
  148. package/src/testing-js/chunk-ZMZQZ33J.js.map +1 -0
  149. package/src/testing-js/component-behavior.contract.js +155 -0
  150. package/src/testing-js/component-behavior.contract.js.map +1 -0
  151. package/src/testing-js/component-editor.contract.js +6 -4
  152. package/src/testing-js/component-editor.contract.js.map +1 -1
  153. package/src/testing-js/component-render.contract.js +14 -10
  154. package/src/testing-js/component-render.contract.js.map +1 -1
  155. package/src/testing-js/index.d.ts +44 -4
  156. package/src/testing-js/index.js +13 -7
  157. package/src/testing-js/index.js.map +1 -1
  158. package/src/testing-js/page-compliance.contract.js +48 -10
  159. package/src/testing-js/page-compliance.contract.js.map +1 -1
  160. package/src/testing-js/registry.contract.js +5 -3
  161. package/src/testing-js/registry.contract.js.map +1 -1
  162. package/src/testing-js/{vitest-C-wNWcoA.d.ts → vitest-BMLIbDs2.d.ts} +8 -2
  163. package/src/testing-js/vitest.d.ts +1 -1
  164. package/src/testing-js/vitest.js +2 -1
  165. package/template/package.json +1 -2
  166. package/template/vite.config.ts +3 -2
  167. package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
  168. package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -105
  169. package/src/editor/skill-atlas/trees/fix-findings.ts +0 -504
  170. package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
  171. package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
  172. package/src/testing-js/chunk-GNIUPIU2.js.map +0 -1
@@ -5,39 +5,36 @@ export const SKILL_DOC = 'SKILL.md' as const;
5
5
 
6
6
  export const skillDocs: Record<string, Record<string, string[]>> = {
7
7
  "check-compliance": {
8
- "SKILL.md": ["---","name: live-tokens-check-compliance","description: Report an existing project's adherence to @motion-proto/live-tokens from one run of npx live-tokens report. Checks for correct use of components, properties, and tokens. The report names the tokens each component reads, the component each page renders, the findings of both checkers, and the recommended fixes. Called as the verification step by live-tokens-create-page and live-tokens-create-component. Use when the user asks to check, audit, or review the project. Edits no file. Hands the fix list to live-tokens-fix-findings.","---","","# Checking a project's adherence to live-tokens","","Run `npx live-tokens report`. The CLI prints a report with the sections in the Report sections table. Say what each finding means and what the fix would cost. When live-tokens-create-page or live-tokens-create-component calls this skill, lead with the findings on the file it built. Edit no file. When the user wants the fixes applied, hand the fix list to **live-tokens-fix-findings**.","","`report` never runs the component contract suites. The `check-component` test run does, and it is what live-tokens-create-component calls to validate a component's runtime behavior.","","`report` stays static for pages too. The `check-page` test run is what live-tokens-create-page calls for a rendered page, in a browser against the project's own route, and it reports `page-component-paint`, `page-text-style`, `page-contrast`, `page-grid`, and `page-overflow` findings the same way `report`'s static findings read: by rule, with a fix.","","## Workflow","","When `report` is an unknown command, route the dependency upgrade to **live-tokens-fix-findings**. Resume the audit after the upgrade.","","1. Run `npx live-tokens report --json`.","2. Read each section of the report with the Report sections table.","3. When a finding needs component or token scale details, run the matching inspection command below. Otherwise continue with classification.","4. Classify each finding as Mechanical, Judgement, or Deliberate. For Deliberate findings, name the narrower config entry.","5. Reply with the findings of each section in the table's order, each with its count.","6. List the recommended fixes, each marked with its finding class, in the order **live-tokens-fix-findings** takes them.","7. End with the hand-off: run **live-tokens-fix-findings** on the list, or on the subset the user chooses.","","For one component, run `npx live-tokens components <id>`. For one token scale, run `npx live-tokens tokens --scale <name>`. Both take `--json`.","","## Report sections","","| Section | Contents | Fix |","| --- | --- | --- |","| Pending token migrations (`migrations`) | Whether `tokens.css` is behind the installed package. `status` is `pending`, `none pending`, `no tokens.css`, or `unavailable`. A stale file shows downstream as unknown tokens. | Fix this first. Run `npx live-tokens migrate --check` to see the plan, then `npx live-tokens migrate` to apply it. `--write` also rewrites the route references the plan lists. `--tokens <path>` names a tokens.css in an unusual place. |","| Checker findings by rule (`findings.pages`, `findings.components`) | Both checkers' findings by rule under the project's severities, and one strict total with every warning as an error. Errors fail the build today. The strict total is what a fully tokenized project would fail. | Classify each finding under Finding classes. |","| Tokens a component never reads (`components[].unread`) | Tokens a component declares and never reads. Each is an editor row that edits nothing. | Wire each token into the CSS, or remove it. No checker rule covers this, so confirm with a second `report` run. |","| Component registration (`components[].registered`) | Whether the component has a `bootLiveTokens` or `registerComponent` entry. Without one it renders with no editor. | Register it. |","| Component usage comment (`components[].described`) | Whether the runtime file has the header comment the picker reads. Without one, `live-tokens components` cannot say what the component is for. | Add the comment. No checker rule covers this, so confirm with a second `report` run. |","| Components each page renders (`usage.byPage`) | Which component each page imports, and how many times the page renders it. | When a page renders none, say whether it is chrome or markup that a shipped component covers. |","| Shipped components no page renders (`usage.unusedShipped`) | Shipped components no page renders. | None. Information only. |","| Project components unregistered or unused (`usage.customUnregistered`, `usage.customUnused`) | The project's own components that are unregistered or unused. | Register or delete each. `check-component` sees a component only under `src/system/components`, so confirm with a second `report` run. |","","## Finding classes","","Every finding is one of three. Say which.","","- **Mechanical.** The value determines the token, such as a spacing literal and its nearest `--space-*` step. When the fix shifts a rendered value, name the shift.","- **Judgement.** A role determines the token, such as a color literal and the role it plays. Say what the options are. Ask the user to choose.","- **Deliberate.** The finding records a decision, such as a layout size the project owns. Name the config entry that would record the decision. Leave the decision to the user. To lower a rule's severity everywhere, the entry is `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }` in `live-tokens.config.json`. To drop one file that is not a themed surface, the entry is `\"checks\": { \"exclude\": [\"src/art/hero.css\"] }`. The path is project-relative, and a directory covers what is under it. Prefer the narrower entry."],
8
+ "SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against the design system and fix it until check-page and check-component both exit 0. Checks for correct use of components, properties, and tokens. Runs both checkers, which bring tokens.css up to the installed package, apply every auto repair, and return each remaining finding with its own guidance, repair level, and details. Use when the user asks to check, audit, or review the project. Use when the user asks to fix the project or to make the build's design checks pass. Edits the files the checkers name. Changes tokens.css only through its migrations.","---","","# Checking and fixing a project's adherence to live-tokens","","Check the project, then fix every finding of `check-page` and `check-component` until both exit 0. `check-page` checks pages. Every component comes from the catalogue, every prop is declared, and every value in page CSS is a design token. `check-component` checks the project's own components. Every token names a semantic property, and its default is the design token that property reads. Every finding carries its own `guidance`, and the repair follows it.","","Both checkers are static. They read the project's source, never open a browser, and never mount a runtime. `npx live-tokens check-page <file> --tests` proves a page's rendered paint, and `npx live-tokens check-component <id> --tests` proves a component's declared behavior. **live-tokens-create-page** and **live-tokens-create-component** run those, one file or one id at a time, and their findings carry guidance the same way.","","## Workflow","","1. Run both checkers with `--json`. Each first brings `tokens.css` up to the installed package by applying every additive migration, then applies every finding whose `repair` is `auto`, checks again, and returns the fixes it applied in `fix.applied` beside the findings that remain. A pending breaking migration returns as a `tokens-breaking-migration` finding, since it renames design tokens the project may read. A fix in `fix.skipped` found its text moved, and the next run applies it. `--no-fix` reports without editing, for a build or CI."," ```sh"," npx live-tokens check-page --json"," npx live-tokens check-component --json"," ```","2. Read what remains. Each finding carries the fields under Finding fields, with a `repair` of `choice` or `authored`.","3. Group the findings by rule. Take the largest error group first, then the remaining errors. Take warnings only when the repair scope includes warnings.","4. Make every repair in the group from its `guidance`, within Scope below.","5. Run both checkers again. When repairable findings remain in scope, return to step 3. When no token fits a remaining finding, leave it and continue to the reply with its reason.","6. When the errors are clear, run both checkers with `--strict`. Report what `--strict` adds. Clear warnings within the existing request. Otherwise ask whether to clear the warnings now.","7. When the repair scope includes warnings, return to step 3 with `--strict`. When strict checks pass or the user defers warnings, continue to the reply.","8. Reply with:"," - the fixes the checkers applied, each with its count and any visible shift"," - the remaining changes by rule, each with its count and any visible shift"," - the findings left, each with its reason and any config entry the user chose"," - both checker commands with their exit codes","","`check-page <path>` scopes a run to one page. `check-component <id>` scopes a run to one component: its runtime, its editor, and its registration. The checkers read tokens.css from its default location.","","## Finding fields","","Every finding from both checkers, under each checker's `--json`:","","| Field | Value |","| --- | --- |","| `rule`, `severity`, `file`, `line`, `message` | What the finding is and where. |","| `guidance` | How to make the repair: the token for the role or the scale step, the command that prints the candidates, or the section of a create skill that owns the fix. |","| `repair` | `auto`, `choice`, or `authored`. See Repair levels. |","| `exception` | The narrower config entry that records a decision to leave the finding as it is: `{ \"checks\": { \"exclude\": [\"<file>\"] } }` for a page or CSS file, otherwise `{ \"checks\": { \"rules\": { \"<rule>\": \"warn\" } } }`. Applying it steps the rule down one level, error to warn and warn to off. |","| `details` | Per-rule data the message already states in prose, such as the accepted values behind `unknown-prop-value` or the nearest design-token candidates behind `dimension-literal`. Absent when a rule has nothing to add. |","","## Repair levels","","Every finding's `repair` says what moving it costs.","","- **`auto`.** The value determines the fix, such as a spacing literal with one nearest design-token step. The checkers apply it and list it in `fix.applied`.","- **`choice`.** A role or an ambiguous value determines the fix, such as a color literal and the role it plays, or a spacing literal tied between two steps. `details` lists the candidates, and `guidance` says how to pick one.","- **`authored`.** New code is the fix: a runtime that has to start behaving, an editor schema that has to name a token, a route that has to move. `guidance` names what to write. No candidate list applies.","","## Scope","","- Add no token to `tokens.css`. Map a literal with no matching token to the nearest existing token by role. When no token fits, leave the finding and say so.","- When the nearest token differs from the literal, use the token and name the shift in the reply, such as `14px` to `--space-16`.","- Apply a `tokens-breaking-migration` finding with `npx live-tokens migrate`, after `npx live-tokens migrate --check` prints the plan. `--tokens <path>` names a tokens.css in an unusual place, and `--write` also rewrites the route references the plan lists. Name each renamed design token in the reply.","- Any finding, at any repair level, can stay as a deliberate exception when the user chooses to keep it. Record that decision in the config entry its `exception` field names, and prefer the narrower entry. When the user has chosen to lower a rule's severity, record it in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`. `--off=<rule>` silences a rule for one run only."],
9
9
  },
10
10
  "create-component": {
11
- "SKILL.md": ["---","name: live-tokens-create-component","description: Create an editable component for a @motion-proto/live-tokens project. A component is a runtime Svelte file, an editor Svelte file, and one registration. The runtime file declares one semantic property per editable CSS property and assigns each an existing design token. The property names are semantic, based on function, and reuse the names of the existing components. Use when live-tokens-pick-component finds no suitable component, or the user asks for a new component. Use when the user asks to make an existing Svelte component editable in the live-tokens editor. For page integration, read live-tokens-create-page.","---","","# Creating a component for a live-tokens project","","Create a component whose structure and behavior serve the user's purpose. Give each editable visual property a semantic name. Assign its default from the existing design tokens. Deliver the runtime file, the editor file, and the registration together.","","## Workflow","","1. Read the project: `package.json`, `live-tokens.config.json`, `src/main.ts`, the catalogue, the token scales the component will use, a shipped runtime and editor pair, and the property suffixes.","2. Design the properties: separate the component's parts, variants, and states, then write one row per editable role with its token and the CSS it controls, named the way the shipped components name the same role.","3. Write the runtime file: the usage comment and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.","4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.","5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name, and write its contract in the module `contractsModule` names.","6. Run **live-tokens-check-compliance**, then `npx live-tokens check-component <id> --tests --strict --json` until exit 0 with complete applicable coverage, then the Svelte check and the build.","7. Reply with the files, the id, the props, and each check's result. Then place the component on a page with **live-tokens-create-page**.","","## Design model","","A live-tokens project has two layers.","","| Layer | Responsibility | Example |","|---|---|---|","| Design tokens | Name the available colors, typography, geometry, and motion values. A theme sets the values. | `--space-16`, `--radius-md`, `--surface-neutral` |","| Semantic properties | Name the visual roles within a component and reference tokens. A component config records these assignments. | `--statcard-padding: var(--space-16)` |","","A token is assigned to a property, and a CSS declaration reads the property. The editor changes the assignment; the runtime reads the property. Keep the assignment a token reference, so a theme change reaches the component.","","A property describes its purpose: `--statcard-value`, `--statcard-radius`, `--statcard-label-font-size`. Its name stays stable when its assigned color or size changes. Use role names such as `surface` and `text`. Use full words for component ids and parts.","","A component is distinct in its anatomy, its proportions, its content hierarchy, and its behavior. Its appearance comes from the existing tokens. Create only the variants and states the task requires. The tokens stay as they are; a new token is a separate change to the design system.","","Props carry content and behavior: a value, a label, a callback. Properties carry the editable appearance. When a variant is a choice the page makes, expose it as a prop.","","## Source inspection","","Before writing a file:","","1. Read the project's `package.json`, `live-tokens.config.json`, and `src/main.ts`.","2. Run `npx live-tokens components`. The list holds every component the project has, with its variants and usage comment. `npx live-tokens components <id>` prints one component's props.","3. Run `npx live-tokens tokens --scale <name>` for each token scale the component will use. Those names are the tokens a property can reference.","4. Read a shipped runtime and editor pair: `Toggle` for interaction states, `Badge` for variants and linked values, `Card` for text and container parts.","5. Read `references/token-naming.md` for the suffixes that select editor controls.","","The shipped sources are in `node_modules/@motion-proto/live-tokens/src/`: the runtime at `system/components/<Name>.svelte`, the editor at `editor/component-editor/<Name>Editor.svelte`. Inside the live-tokens repository, read them from the repository root. The source is the contract.","","## Variants and states","","A component has three kinds of division. Keep them apart in the props, the names, and the editor.","","| Kind | Meaning | Example |","|---|---|---|","| Part | Regions present at once | Dialog's overlay, header, body, footer |","| Variant | Alternative presentations the page chooses | Badge's primary, danger |","| State | A runtime condition | Toggle's on, hover, disabled |","","States have two axes. A component state is one of a set that excludes the others: default, selected (or on), disabled. An interaction state layers on a component state: default, hover, and later focus or active. `disabled` is terminal: no other state layers on it, in the names or in the editor.","","The default state carries the shared geometry and typography. A state adds properties only for the values that change: `--toggle-on-track-surface`, `--toggle-on-hover-track-surface`.","","The preview renders the state being edited. Pair each `:hover` selector with a `.force-hover` selector and expose a `class` prop, so the editor shows hover without a pointer. Keep native disabled behavior, keyboard operation, and visible focus on an interactive control.","","A component supplies its variants. The page chooses the one primary action.","","## Property design","","Before writing a file, identify the component's parts, text roles, variants, and states. Then write a property map: one row per editable role, with the token it is assigned and the CSS property it controls. Keep separate roles independent even when they start with the same value.","","| Property | Assigned token | CSS use |","|---|---|---|","| `--statcard-surface` | `--surface-neutral` | `background` |","| `--statcard-border` | `--border-neutral` | `border-color` |","| `--statcard-border-width` | `--border-width-1` | `border-width` |","| `--statcard-radius` | `--radius-md` | `border-radius` |","| `--statcard-padding` | `--space-16` | `padding` |","| `--statcard-value` | `--text-primary` | `color` of the value |","| `--statcard-value-font-size` | `--font-size-2xl` | `font-size` of the value |","| `--statcard-label` | `--text-secondary` | `color` of the label |","","Assign from the tokens the project has. Match the token scale to the role: `--surface-*` for a fill, `--border-*` for an outline, `--text-*` for text, and the space, radius, border-width, and icon-size scales for geometry. Give each text role five properties: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`.","","A property name starts with the component id and ends with the property suffix. Use this shape for part-specific states:","","```text","--<componentId>[-<variant>][-<part>][-<state>]-<property>","```","","- `componentId` is the runtime file name in lowercase with no dashes: `StatCard.svelte` is `statcard`.","- `variant` is present when the component has more than one: `--card-default-surface`, `--card-bare-surface`. A component with one variant has no variant segment: `--toggle-track-surface`.","- `part` names a region inside the component: `header`, `body`, `track`, `thumb`. The editor's `element` tag groups rows in the panel and is never a name segment.","- `state` comes before the property: `--card-hover-border`. `disabled` is terminal, so no name pairs `disabled` with another state.","- `property` is the suffix, and the suffix selects the editor control: `-surface` for a fill, `-border` for a border color, `-border-width` for a stroke, `-radius` for corners, `-padding` and `-gap` for spacing, and the five typography suffixes. `references/token-naming.md` lists every suffix.","","For a state that affects several parts, follow Toggle: `--toggle-on-hover-track-surface`. State segments precede the affected part.","","Name a role as the shipped component that paints the same thing names it. A fill is `-surface` in every shipped component. A knob is `-thumb`. A text role's color sits on the role's own name, `-title`, `-body`, `-label`, `-value`, and its typography hangs off that name: `--card-default-title-font-size`. A component with one text role uses `-text`: `--badge-primary-text`.","","## Runtime component","","Create `src/system/components/StatCard.svelte`. `check-component` finds a runtime there only. A component in another directory is listed by `components` and `report` when that directory is named in `\"componentDirs\"` in `live-tokens.config.json`, and `check-component` does not check it. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.","","Open the file with an HTML comment in the shape every shipped component carries. `npx live-tokens components` prints the comment beside the id, and `components <id>` prints the props.","","```svelte","<!--"," StatCard.svelte. A figure with its label."," Use for: one number the reader takes in at a glance."," Not for: a set of records (Table); a titled block of content (Card).","-->","```","","Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.","","```svelte","<style>"," :global(:root) {"," --statcard-surface: var(--surface-neutral);"," --statcard-border: var(--border-neutral);"," --statcard-border-width: var(--border-width-1);"," --statcard-radius: var(--radius-md);"," --statcard-padding: var(--space-16);"," --statcard-value: var(--text-primary);"," --statcard-value-font-size: var(--font-size-2xl);"," --statcard-label: var(--text-secondary);"," }",""," .statcard {"," display: grid;"," background: var(--statcard-surface);"," border: var(--statcard-border-width) solid var(--statcard-border);"," border-radius: var(--statcard-radius);"," padding: var(--statcard-padding);"," }",""," .value { color: var(--statcard-value); font-size: var(--statcard-value-font-size); }"," .label { color: var(--statcard-label); }","</style>","```","","The excerpt shows the chain for part of the property map. Every editable value reads a property. Structural CSS (`display: grid`, `width: 100%`, `align-items: center`) stays in the layout rules. A value beyond a scale is a token expression: `calc(var(--space-64) * 4)`. A property that carries a structural choice, an alignment or a visibility, is an intrinsic: read `references/intrinsics.md`.","","## Component editor","","Create `src/system/components/StatCardEditor.svelte` beside the runtime file. The editor has three parts.","","1. A `<script module>` block exports `component`, the id, and `allTokens`, one row per property in the map. A row is `{ label, variable, element? }`; `element` groups rows in the panel by part, and `label` names the property in the row.","2. The instance script imports the runtime component and the editor primitives from the package's public paths, and maps the state being edited to preview props.","3. The markup mounts `ComponentEditorBase` with one `VariantGroup` per variant, each rendering a preview.","","```svelte","<script module lang=\"ts\">"," import type { Token } from '@motion-proto/live-tokens/component-editor';",""," export const component = 'statcard';"," const states: Record<string, Token[]> = {"," default: ["," { label: 'surface', element: 'frame', variable: '--statcard-surface' },"," { label: 'border', element: 'frame', variable: '--statcard-border' },"," { label: 'border width', element: 'frame', variable: '--statcard-border-width' },"," { label: 'radius', element: 'frame', variable: '--statcard-radius' },"," { label: 'padding', element: 'frame', variable: '--statcard-padding' },"," { label: 'text', element: 'value', variable: '--statcard-value' },"," { label: 'font size', element: 'value', variable: '--statcard-value-font-size' },"," { label: 'text', element: 'label', variable: '--statcard-label' },"," ],"," };"," export const allTokens: Token[] = Object.values(states).flat();","</script>","","<script lang=\"ts\">"," import { ComponentEditorBase, VariantGroup } from '@motion-proto/live-tokens/component-editor';"," import StatCard from './StatCard.svelte';","</script>","","<ComponentEditorBase {component} title=\"Stat Card\" tokens={allTokens}>"," <VariantGroup name=\"statcard\" title=\"Stat Card\" {states} {component}>"," <StatCard value=\"1,204\" label=\"Sessions\" />"," </VariantGroup>","</ComponentEditorBase>","```","","The shipped editor for the closest component gives the preview snippet for a component with states. Custom chrome in an editor takes `--ui-*` tokens and no accent color; its copy uses periods and commas, never em-dashes.","","When variants share a value, read `references/linked-siblings.md`. A `groupKey` is scoped to the text role: `value-font-size` and `label-font-size` stay separate keys. A `buildTypeGroup*` helper takes `{ component, variants }` so it derives one key per role.","","## Registration","","Register the component in `src/registerComponents.ts`, a registration-only module, beside any registration already there. The id is unique; a registration that repeats a shipped id replaces that component.","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import StatCardEditor, { allTokens as statCardTokens } from './system/components/StatCardEditor.svelte';","","registerComponent({"," id: 'statcard',"," label: 'Stat Card',"," icon: 'fas fa-chart-simple',"," sourceFile: 'src/system/components/StatCard.svelte',"," editorComponent: StatCardEditor,"," schema: statCardTokens,","});","```","","Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app. The component's contract, one `ComponentContract` in the module `contractsModule` names, is what the eight component suites run; without it `--tests` reports `contract-missing`. Read `references/contract-tests.md` for both. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.","","At boot the plugin reads the `:global(:root)` block and writes `component-configs/<id>/default.json`, one token per property. An edit in the editor writes `_working.json`; Save As writes a named config. The assignments stay token references through that flow.","","Inside the live-tokens repository, a first-party component keeps its editor in `src/editor/component-editor/` and takes an entry in `builtInRegistry` in `src/editor/component-editor/registry.ts`.","","## Sketch mode and overlays","","Every component joins the sketch layer: read `references/sketch-mode.md`. A suitable root or inner wrapper carries a reserved class and names five `--sketch-*` values from its own properties. Preserve positioning, clipping, and pseudo-elements as the reference specifies. A first-party component adds a `PartSpec` row instead.","","A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A container that owns the typography of its content follows `Card` and its `prose` prop.","","## Verification","","1. Run **live-tokens-check-compliance** and address its findings with **live-tokens-fix-findings**. Then run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. `--tests` runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites; its JSON reports coverage by rule. A `tests-not-installed` finding names the missing package; install `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then `npx playwright install chromium`. Fix every finding and rerun until exit 0 with complete applicable coverage and no disabled checks.","2. Run the project's Svelte check and its build.","3. Reply with the files, the component id, the props, and the results of steps 1 and 2, naming any check the environment prevented.","","Every finding carries a `fix` slug naming the section that fixes it.","","| `fix` | Section |","|---|---|","| `property-name` | Property design, the name |","| `property-token` | Property design, the assigned token |","| `runtime` | Runtime component |","| `runtime-defaults` | Runtime component, the `:global(:root)` defaults |","| `editor` | Component editor |","| `registration` | Registration |","| `sketch` | Sketch mode and overlays |","| `tooling` | The message names the missing tool or the broken path; fix it and rerun |","| `coverage` | Add the missing contract, or complete the run the message names |","","`--tests` covers every line a reviewer once checked by eye: the component's listing, its controls and preview, persistence and reset, theme projection, linked properties, and Sketch mode.","","Then place the component on a page with **live-tokens-create-page**."],
12
- "references/contract-tests.md": ["# The registry contract as a test in a consumer project","","The package ships the contract as a test file. `checkRegistryEntry` is the","assertion behind it, exported so a project can write its own file instead. The","contract takes one registry entry and returns a violation line per failure; an","empty array is the pass.","","## The shipped path","","`npx live-tokens check-component <id> --tests` runs the shipped file for you,","under vitest, alongside the Playwright component contract suites, and maps","every failure to a finding with a rule id and a line. The compiled file is","`src/testing-js/registry.contract.js`; the `.ts` source it compiles from is not","in the tarball.","","It resolves a shipped component's `sourceFile` against the package and yours","against your project, and reports a component that exists as files and never","reached a registration.","","A project scaffolded by `create` has `@playwright/test`, `vitest`, and","`happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,","and `tests/contracts.ts` in place. Any other project adds the three as","devDependencies. Both run `npx playwright install chromium` once. A missing","one is a `tests-not-installed` finding naming the install command.","","Name the module that registers your components, in `live-tokens.testing.ts` at","the project root, as a plain quoted string:","","```ts","// live-tokens.testing.ts","import { defineTestingConfig } from '@motion-proto/live-tokens/testing/vitest';","","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',","});","```","","The setup module registers and stops there, exactly as the Registration","section of live-tokens-create-component wires it up:","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import MyWidgetEditor, { allTokens } from './system/components/MyWidgetEditor.svelte';","","registerComponent({"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: allTokens,","});","```","","Import the same module from `src/main.ts`, so one list of registrations serves","the app and the tests. Importing an editor registers nothing, and importing","`main.ts` would mount the app, which is why the registrations live in a module","of their own.","","`LIVE_TOKENS_COMPONENT=<id>` is what `check-component <id> --tests` sets for","you; it narrows the run to one component and fails when no component is","registered under that id.","","## The component contract","","The eight Playwright suites drive one `ComponentContract` per component. The","package ships one for each of its own components and none for yours. A run","with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one","`contract-missing` finding, which names the setting that unlocks the suites.","","Name the module that exports your contracts as `contractsModule`, next to","`registrySetup`:","","```ts","// live-tokens.testing.ts","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',"," contractsModule: 'tests/contracts.ts',","});","```","","The module exports a `ComponentContract[]`, as the default export or a named","`contracts` export. The type ships from `@motion-proto/live-tokens/testing`,","and the doc comment on each field is the reference for it. A contract","declares the component's parts as selectors inside the preview, then one","expectation per obligation:","","```ts","// tests/contracts.ts","import type { ComponentContract } from '@motion-proto/live-tokens/testing';","","export const contracts: ComponentContract[] = ["," {"," id: 'statcard',"," origin: 'custom',"," root: 'root',"," parts: { root: '.statcard', value: '.statcard-value' },"," properties: ["," {"," paints: {"," root: {"," backgroundColor: '--statcard-surface',"," borderTopColor: '--statcard-border',"," borderTopWidth: '--statcard-border-width',"," borderRadius: '--statcard-radius',"," paddingTop: '--statcard-padding',"," },"," value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },"," },"," },"," ],"," states: { applicable: false, reason: 'a stat card renders one state' },"," persistence: {"," cases: ["," { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },"," ],"," resetVariable: '--statcard-radius',"," },"," theme: {"," theme: 'brand',"," changed: ['--statcard-radius'],"," unchanged: ['--statcard-padding'],"," aliasedTo: { '--statcard-radius': '--radius-none' },"," observe: { part: 'root', css: 'borderRadius', variable: '--statcard-radius' },"," },"," interaction: { applicable: false, reason: 'a stat card carries no interactive role' },"," sketch: {"," style: 'pencil',"," parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],"," },"," },","];","```","","Each obligation and what it needs from the project:","","- `properties`: part key, then CSS property, then the semantic property that"," drives it. One entry per variant tab and state tab the editor renders;"," omit `variant` and `state` when the editor renders one view. The paint maps"," and `uncovered` together name every property in `default.json`. A property"," no computed style shows verbatim, one consumed inside `calc()` or a gradient"," function, goes in `uncovered` with its reason.","- `states`: one entry per state tab, with the class or attributes the preview"," forces. `applicable: false` when the editor renders no state strip.","- `persistence`: an edit the run makes through the editor, saves, reloads, and"," reads back. `resetVariable` is a property one of the cases moves.","- `theme`: a theme in the project's data tree whose block for the component"," reassigns a property. Save one with the editor before the first run."," `changed` names the properties the theme moves, `unchanged` the ones it"," leaves, and `aliasedTo` the design token each reassigned property resolves"," to under it.","- `interaction`: the interactive part's role and its cases. `applicable:"," false` for a component with no interactive role.","- `sketch`: a sketch style id and the fill and stroke each reserved-class part"," resolves to under it. `applicable: false` only for a component"," `references/sketch-mode.md` exempts.","","## Running vitest yourself","","`check-component --tests` covers the shipped path. Run vitest directly only","when you need to drive it outside the CLI. `createVitestConfig` lives at","`@motion-proto/live-tokens/testing/vitest`, which never imports","`@playwright/test`, so a project holding only `vitest` and `happy-dom` can","still build this config:","","```ts","// vitest.contract.config.ts","import { createVitestConfig } from '@motion-proto/live-tokens/testing/vitest';","import viteConfig from './vite.config';","import settings from './live-tokens.testing';","","export default createVitestConfig(viteConfig, { registrySetup: settings.registrySetup });","```","","```bash","npx vitest run --config vitest.contract.config.ts","```","","## Writing your own file","","`checkRegistryEntry` is exported at","`@motion-proto/live-tokens/component-editor/contract`, so a project that wants","its own suite writes two lines against its own registrations:","","```ts","// tests/registryContract.test.ts","// @vitest-environment happy-dom","import { describe, it, expect } from 'vitest';","import { getComponentRegistryEntries } from '@motion-proto/live-tokens';","import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';","import '../src/registerComponents';","","const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');","","describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {"," it('meets the registry contract', () => {"," expect(checkRegistryEntry(entry)).toEqual([]);"," });","});","```","","Filter on `origin`. The registry always carries the shipped components too, and","their `sourceFile` paths are relative to the package root. Without the filter","every built-in fails on a path that does not exist in your project.","","The helper reads the runtime file and `default.json` off disk, which is why it","is node-only and has its own subpath.","","## Paths","","Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.","A project that moved either passes them:","","```ts","checkRegistryEntry(entry, { projectRoot, componentConfigsDir });","```","","`componentConfigsDir` is the same directory `live-tokens.config.json` names.","","## What it holds","","1. **Registration**: `sourceFile` resolves to a real file, the schema is non-empty.","2. **Uniqueness**: no schema variable is declared twice.","3. **Editor to runtime**: every editable token's CSS var is declared in the"," runtime's `<style>` block, so an edit has something to repaint.","4. **Editor to default config**: every editable token has a seed alias in"," `component-configs/<id>/default.json`, so the component adopts with full"," defaults. A component with no `default.json` is editor-only; this check and"," the next one skip it.","5. **Opacity floors**: a token declaring `minOpacity` ships a default at or"," above it, so a floating panel starts out legible over page content.","6. **Round-trip**: `setComponentAlias` persists into the slice under the same key.","","Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored","as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes","(written on demand by the split-padding UI and read through the `themed-padding`","mixin's fallback chain, so they exist as neither `:root` declarations nor seeds)."],
11
+ "SKILL.md": ["---","name: live-tokens-create-component","description: Create an editable component for a @motion-proto/live-tokens project. A component is a runtime Svelte file, an editor Svelte file, and one registration. The runtime file declares one semantic property per editable CSS property and assigns each an existing design token. The property names are semantic, based on function, and reuse the names of the existing components. Use when live-tokens-pick-component finds no suitable component, or the user asks for a new component. Use when the user asks to make an existing Svelte component editable in the live-tokens editor. For page integration, read live-tokens-create-page.","---","","# Creating a component for a live-tokens project","","Create a component whose structure and behavior serve the user's purpose. Give each editable visual property a semantic name. Assign its default from the existing design tokens. Deliver the runtime file, the editor file, and the registration together.","","## Workflow","","1. Read the project: `package.json`, `live-tokens.config.json`, `src/main.ts`, the catalogue, the token scales the component will use, a shipped runtime and editor pair, and the property suffixes.","2. Design the properties: separate the component's parts, variants, and states, then write one row per editable role with its token and the CSS it controls, named the way the shipped components name the same role.","3. Write the runtime file: the catalogue export and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.","4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.","5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name, and write its contract in the module `contractsModule` names.","6. Run `npx live-tokens check-component <id> --tests --strict --json` until it exits 0 with complete applicable coverage, then the Svelte check and the build.","7. Reply with the files, the id, the props, and each check's result. Then place the component on a page with **live-tokens-create-page**.","","## Design model","","A live-tokens project has two layers.","","| Layer | Responsibility | Example |","|---|---|---|","| Design tokens | Name the available colors, typography, geometry, and motion values. A theme sets the values. | `--space-16`, `--radius-md`, `--surface-neutral` |","| Semantic properties | Name the visual roles within a component and reference tokens. A component config records these assignments. | `--statcard-padding: var(--space-16)` |","","A token is assigned to a property, and a CSS declaration reads the property. The editor changes the assignment; the runtime reads the property. Keep the assignment a token reference, so a theme change reaches the component.","","A property describes its purpose: `--statcard-value`, `--statcard-radius`, `--statcard-label-font-size`. Its name stays stable when its assigned color or size changes. Use role names such as `surface` and `text`. Use full words for component ids and parts.","","A component is distinct in its anatomy, its proportions, its content hierarchy, and its behavior. Its appearance comes from the existing tokens. Create only the variants and states the task requires. The tokens stay as they are; a new token is a separate change to the design system.","","Props carry content and behavior: a value, a label, a callback. Properties carry the editable appearance. When a variant is a choice the page makes, expose it as a prop.","","Prop names follow the shipped components. `label` names a control, `title` heads content, `text` is body copy. `open` is the one prop for an open state. `value` holds a selection's id. A size prop's values are `default` and `small`. A callback prop is `on` followed by the event name, all lowercase, as in `onchange`, `onclose`, and `onsave`.","","## Source inspection","","Before writing a file:","","1. Read the project's `package.json`, `live-tokens.config.json`, and `src/main.ts`.","2. Run `npx live-tokens components`. The list holds every component the project has, with its variants and catalogue entry. `npx live-tokens components <id>` prints one component's props.","3. Run `npx live-tokens tokens --scale <name>` for each token scale the component will use. Those names are the tokens a property can reference.","4. Read a shipped runtime and editor pair: `Toggle` for interaction states, `Badge` for variants and linked values, `Card` for text and container parts.","5. Read `references/token-naming.md` for the suffixes that select editor controls.","","The shipped sources are in `node_modules/@motion-proto/live-tokens/src/`: the runtime at `system/components/<Name>.svelte`, the editor at `editor/component-editor/<Name>Editor.svelte`. Inside the live-tokens repository, read them from the repository root. The source is the contract.","","## Variants and states","","A component has three kinds of division. Keep them apart in the props, the names, and the editor.","","| Kind | Meaning | Example |","|---|---|---|","| Part | Regions present at once | Dialog's overlay, header, body, footer |","| Variant | Alternative presentations the page chooses | Badge's brand, danger |","| State | A runtime condition | Toggle's on, hover, disabled |","","States have two axes. A component state is one of a set that excludes the others: default, selected (or on), disabled. An interaction state layers on a component state: default, hover, and later focus or active. `disabled` is terminal: no other state layers on it, in the names or in the editor.","","The default state carries the shared geometry and typography. A state adds properties only for the values that change: `--toggle-on-track-surface`, `--toggle-on-hover-track-surface`.","","The preview renders the state being edited. Pair each `:hover` selector with a `.force-hover` selector and expose a `class` prop, so the editor shows hover without a pointer. Keep native disabled behavior, keyboard operation, and visible focus on an interactive control.","","A component supplies its variants. The page chooses the one primary action.","","## Property design","","Before writing a file, identify the component's parts, text roles, variants, and states. Then write a property map: one row per editable role, with the token it is assigned and the CSS property it controls. Keep separate roles independent even when they start with the same value.","","| Property | Assigned token | CSS use |","|---|---|---|","| `--statcard-surface` | `--surface-neutral` | `background` |","| `--statcard-border` | `--border-neutral` | `border-color` |","| `--statcard-border-width` | `--border-width-1` | `border-width` |","| `--statcard-radius` | `--radius-md` | `border-radius` |","| `--statcard-padding` | `--space-16` | `padding` |","| `--statcard-value` | `--text-primary` | `color` of the value |","| `--statcard-value-font-size` | `--font-size-2xl` | `font-size` of the value |","| `--statcard-label` | `--text-secondary` | `color` of the label |","","Assign from the tokens the project has. Match the token scale to the role: `--surface-*` for a fill, `--border-*` for an outline, `--text-*` for text, and the space, radius, border-width, and icon-size scales for geometry. Give each text role five properties: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`.","","A property name starts with the component id and ends with the property suffix. Use this shape for part-specific states:","","```text","--<componentId>[-<variant>][-<part>][-<state>]-<property>","```","","- `componentId` is the runtime file name in lowercase with no dashes: `StatCard.svelte` is `statcard`.","- `variant` is present when the component has more than one: `--card-default-surface`, `--card-bare-surface`. A component with one variant has no variant segment: `--toggle-track-surface`.","- `part` names a region inside the component: `header`, `body`, `track`, `thumb`. The editor's `element` tag groups rows in the panel and is never a name segment.","- `state` comes before the property: `--card-hover-border`. `disabled` is terminal, so no name pairs `disabled` with another state.","- `property` is the suffix, and the suffix selects the editor control: `-surface` for a fill, `-border` for a border color, `-border-width` for a stroke, `-radius` for corners, `-padding` and `-gap` for spacing, and the five typography suffixes. `references/token-naming.md` lists every suffix.","","For a state that affects several parts, follow Toggle: `--toggle-on-hover-track-surface`. State segments precede the affected part.","","Name a role as the shipped component that paints the same thing names it. A fill is `-surface` in every shipped component. A knob is `-thumb`. A text role's color sits on the role's own name, `-title`, `-body`, `-label`, `-value`, and its typography hangs off that name: `--card-default-title-font-size`. A component with one text role uses `-text`: `--badge-brand-text`.","","## Runtime component","","Create `src/system/components/StatCard.svelte`. `check-component` finds a runtime there only. A component in another directory is listed by `components` and `report` when that directory is named in `\"componentDirs\"` in `live-tokens.config.json`, and `check-component` does not check it. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.","","Open the file with a `<script module lang=\"ts\">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Each field is `key: <string literal>`, in single, double, or backtick quotes; no `${}` interpolation, no concatenation, no identifier reference. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.","","```svelte","<script module lang=\"ts\">"," import type { CatalogueEntry } from '@motion-proto/live-tokens';",""," export const catalogue = {"," description: 'A figure with its label.',"," useFor: 'one number the reader takes in at a glance.',"," notFor: 'a set of records (Table); a titled block of content (Card).',"," } satisfies CatalogueEntry;","</script>","```","","Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.","","```svelte","<style>"," :global(:root) {"," --statcard-surface: var(--surface-neutral);"," --statcard-border: var(--border-neutral);"," --statcard-border-width: var(--border-width-1);"," --statcard-radius: var(--radius-md);"," --statcard-padding: var(--space-16);"," --statcard-value: var(--text-primary);"," --statcard-value-font-size: var(--font-size-2xl);"," --statcard-label: var(--text-secondary);"," }",""," .statcard {"," display: grid;"," background: var(--statcard-surface);"," border: var(--statcard-border-width) solid var(--statcard-border);"," border-radius: var(--statcard-radius);"," padding: var(--statcard-padding);"," }",""," .value { color: var(--statcard-value); font-size: var(--statcard-value-font-size); }"," .label { color: var(--statcard-label); }","</style>","```","","The excerpt shows the chain for part of the property map. Every editable value reads a property. Structural CSS (`display: grid`, `width: 100%`, `align-items: center`) stays in the layout rules. A value beyond a scale is a token expression: `calc(var(--space-64) * 4)`. A property that carries a structural choice, an alignment or a visibility, is an intrinsic: read `references/intrinsics.md`.","","## Component editor","","Create `src/system/components/StatCardEditor.svelte` beside the runtime file. The editor has three parts.","","1. A `<script module>` block exports `component`, the id, and `allTokens`, one row per property in the map. A row is `{ label, variable, element? }`; `element` groups rows in the panel by part, and `label` names the property in the row.","2. The instance script imports the runtime component and the editor primitives from the package's public paths, and maps the state being edited to preview props.","3. The markup mounts `ComponentEditorBase` with one `VariantGroup` per variant, each rendering a preview.","","```svelte","<script module lang=\"ts\">"," import type { Token } from '@motion-proto/live-tokens/component-editor';",""," export const component = 'statcard';"," const states: Record<string, Token[]> = {"," default: ["," { label: 'surface', element: 'frame', variable: '--statcard-surface' },"," { label: 'border', element: 'frame', variable: '--statcard-border' },"," { label: 'border width', element: 'frame', variable: '--statcard-border-width' },"," { label: 'radius', element: 'frame', variable: '--statcard-radius' },"," { label: 'padding', element: 'frame', variable: '--statcard-padding' },"," { label: 'text', element: 'value', variable: '--statcard-value' },"," { label: 'font size', element: 'value', variable: '--statcard-value-font-size' },"," { label: 'text', element: 'label', variable: '--statcard-label' },"," ],"," };"," export const allTokens: Token[] = Object.values(states).flat();","</script>","","<script lang=\"ts\">"," import { ComponentEditorBase, VariantGroup } from '@motion-proto/live-tokens/component-editor';"," import StatCard from './StatCard.svelte';","</script>","","<ComponentEditorBase {component} title=\"Stat Card\" tokens={allTokens}>"," <VariantGroup name=\"statcard\" title=\"Stat Card\" {states} {component}>"," <StatCard value=\"1,204\" label=\"Sessions\" />"," </VariantGroup>","</ComponentEditorBase>","```","","The shipped editor for the closest component gives the preview snippet for a component with states. Custom chrome in an editor takes `--ui-*` tokens and no accent color; its copy uses periods and commas, never em-dashes.","","When variants share a value, read `references/linked-siblings.md`. A `groupKey` is scoped to the text role: `value-font-size` and `label-font-size` stay separate keys. A `buildTypeGroup*` helper takes `{ component, variants }` so it derives one key per role.","","## Registration","","Register the component in `src/registerComponents.ts`, a registration-only module, beside any registration already there. The id is unique; a registration that repeats a shipped id replaces that component.","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import { catalogue } from './system/components/StatCard.svelte';","import StatCardEditor, { allTokens as statCardTokens } from './system/components/StatCardEditor.svelte';","","registerComponent({"," id: 'statcard',"," label: 'Stat Card',"," icon: 'fas fa-chart-simple',"," sourceFile: 'src/system/components/StatCard.svelte',"," editorComponent: StatCardEditor,"," schema: statCardTokens,"," catalogue,","});","```","","Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app. The component's contract, one `ComponentContract` in the module `contractsModule` names, is what the eight component suites run; without it `--tests` reports `contract-missing`. Read `references/contract-tests.md` for both. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.","","At boot the plugin reads the `:global(:root)` block and writes `component-configs/<id>/default.json`, one token per property. An edit in the editor writes `_working.json`; Save As writes a named config. The assignments stay token references through that flow.","","Inside the live-tokens repository, a first-party component keeps its editor in `src/editor/component-editor/` and takes an entry in `builtInRegistry` in `src/editor/component-editor/registry.ts`.","","## Sketch mode and overlays","","Every component joins the sketch layer: read `references/sketch-mode.md`. A suitable root or inner wrapper carries a reserved class and names five `--sketch-*` values from its own properties. Preserve positioning, clipping, and pseudo-elements as the reference specifies. A first-party component adds a `PartSpec` row instead.","","A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A container that owns the typography of its content follows `Card` and its `prose` prop.","","## Verification","","1. Run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. It applies every `auto` repair, runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites, and returns the fixes it applied, the findings that remain, and coverage by rule. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","2. Each remaining finding carries a rule id, a line, and its `guidance`. Make each repair from its guidance, and run the command again until it exits 0 with complete applicable coverage and no disabled checks.","3. Run the project's Svelte check and its build.","4. Reply with the files, the component id, the props, and the result of each check, naming any check the environment prevented.","","`--tests` covers every line a reviewer once checked by eye: the component's listing, its controls and preview, persistence and reset, theme projection, linked properties, and Sketch mode.","","Then place the component on a page with **live-tokens-create-page**."],
12
+ "references/contract-tests.md": ["# The registry contract as a test in a consumer project","","The package ships the contract as a test file. `checkRegistryEntry` is the","assertion behind it, exported so a project can write its own file instead. The","contract takes one registry entry and returns a violation line per failure; an","empty array is the pass.","","## The shipped path","","`npx live-tokens check-component <id> --tests` runs the shipped file for you,","under vitest, alongside the Playwright component contract suites, and maps","every failure to a finding with a rule id and a line. The compiled file is","`src/testing-js/registry.contract.js`; the `.ts` source it compiles from is not","in the tarball.","","It resolves a shipped component's `sourceFile` against the package and yours","against your project, and reports a component that exists as files and never","reached a registration.","","A project scaffolded by `create` has `@playwright/test`, `vitest`, and","`happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,","and `tests/contracts.ts` in place. Any other project adds the three as","devDependencies. Both run `npx playwright install chromium` once. A missing","one is a `tests-not-installed` finding naming the install command.","","Name the module that registers your components, in `live-tokens.testing.ts` at","the project root, as a plain quoted string:","","```ts","// live-tokens.testing.ts","import { defineTestingConfig } from '@motion-proto/live-tokens/testing/vitest';","","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',","});","```","","The setup module registers and stops there, exactly as the Registration","section of live-tokens-create-component wires it up:","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import MyWidgetEditor, { allTokens } from './system/components/MyWidgetEditor.svelte';","","registerComponent({"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: allTokens,","});","```","","Import the same module from `src/main.ts`, so one list of registrations serves","the app and the tests. Importing an editor registers nothing, and importing","`main.ts` would mount the app, which is why the registrations live in a module","of their own.","","`LIVE_TOKENS_COMPONENT=<id>` is what `check-component <id> --tests` sets for","you; it narrows the run to one component and fails when no component is","registered under that id.","","## The component contract","","The eight Playwright suites drive one `ComponentContract` per component. The","package ships one for each of its own components and none for yours. A run","with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one","`contract-missing` finding, which names the setting that unlocks the suites.","","Name the module that exports your contracts as `contractsModule`, next to","`registrySetup`:","","```ts","// live-tokens.testing.ts","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',"," contractsModule: 'tests/contracts.ts',","});","```","","The module exports a `ComponentContract[]`, as the default export or a named","`contracts` export. The type ships from `@motion-proto/live-tokens/testing`,","and the doc comment on each field is the reference for it. A contract","declares the component's parts as selectors inside the preview, then one","expectation per obligation:","","```ts","// tests/contracts.ts","import type { ComponentContract } from '@motion-proto/live-tokens/testing';","","export const contracts: ComponentContract[] = ["," {"," id: 'statcard',"," origin: 'custom',"," root: 'root',"," parts: { root: '.statcard', value: '.statcard-value' },"," properties: ["," {"," paints: {"," root: {"," backgroundColor: '--statcard-surface',"," borderTopColor: '--statcard-border',"," borderTopWidth: '--statcard-border-width',"," borderRadius: '--statcard-radius',"," paddingTop: '--statcard-padding',"," },"," value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },"," },"," },"," ],"," states: { applicable: false, reason: 'a stat card renders one state' },"," persistence: {"," cases: ["," { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },"," ],"," resetVariable: '--statcard-radius',"," },"," theme: {"," theme: 'brand',"," changed: ['--statcard-radius'],"," unchanged: ['--statcard-padding'],"," aliasedTo: { '--statcard-radius': '--radius-none' },"," observe: { part: 'root', css: 'borderRadius', variable: '--statcard-radius' },"," },"," interaction: { applicable: false, reason: 'a stat card carries no interactive role' },"," sketch: {"," style: 'pencil',"," parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],"," },"," },","];","```","","Each obligation and what it needs from the project:","","- `properties`: part key, then CSS property, then the semantic property that"," drives it. One entry per variant tab and state tab the editor renders;"," omit `variant` and `state` when the editor renders one view. The paint maps"," and `uncovered` together name every property in `default.json`. A property"," no computed style shows verbatim, one consumed inside `calc()` or a gradient"," function, goes in `uncovered` with its reason.","- `states`: one entry per state tab, with the class or attributes the preview"," forces. `applicable: false` when the editor renders no state strip.","- `persistence`: an edit the run makes through the editor, saves, reloads, and"," reads back. `resetVariable` is a property one of the cases moves.","- `theme`: a theme in the project's data tree whose block for the component"," reassigns a property. Save one with the editor before the first run."," `changed` names the properties the theme moves, `unchanged` the ones it"," leaves, and `aliasedTo` the design token each reassigned property resolves"," to under it.","- `interaction`: the interactive part's role and its cases. `applicable:"," false` for a component with no interactive role.","- `behavior`: what a callback prop does. See Behavior below.","- `sketch`: a sketch style id and the fill and stroke each reserved-class part"," resolves to under it. `applicable: false` only for a component"," `references/sketch-mode.md` exempts.","","## Behavior","","`behavior` runs under Vitest with `mount` from Svelte and happy-dom, not in","the editor preview, because a controlled component's callback prop has","nothing driving it there: the preview edits the token, not the prop. One","case names the props the runtime mounts with, the action a user takes, and","what that should do.","","```ts","behavior: {"," cases: ["," {"," name: 'clicking an off toggle asks for on',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'callback', prop: 'onchange', args: [true] },"," },"," {"," name: 'clicking leaves the switch where the prop put it',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'false' },"," },"," {"," name: 'checked drives aria-checked',"," props: { checked: true },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'true' },"," },"," {"," name: 'a disabled toggle stays silent',"," props: { checked: false, disabled: true },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'no-callback', prop: 'onchange' },"," },"," ],","},","```","","Toggle is controlled: it never flips `checked` itself, so the click case","that asks for `on` and the mount case that reads `checked` are two separate","cases, not one. The disabled case pins that a disabled toggle calls nothing","back: the prop reaches the DOM as a real `disabled` attribute on the part,","and happy-dom withholds a dispatched click from a disabled `<button>`. A","component that reports through `input` rather than `click`, such as Slider,","gets no disabled case, because happy-dom delivers a dispatched `input` event","to a disabled control and the case would assert the environment rather than","the component.","","A component with no callback prop declares the obligation inapplicable, in","one line:","","```ts","behavior: { applicable: false, reason: 'a table wrapper declares no callback prop; its rows arrive as a snippet' },","```","","The fault this suite catches is a callback that fires with the wrong","argument, fires when a case says it should stay silent, or never fires at","all. `check-component <id> --tests` reports it as `contract-behavior`,","naming the case.","","## Running vitest yourself","","`check-component --tests` covers the shipped path. Run vitest directly only","when you need to drive it outside the CLI. `createVitestConfig` lives at","`@motion-proto/live-tokens/testing/vitest`, which never imports","`@playwright/test`, so a project holding only `vitest` and `happy-dom` can","still build this config:","","```ts","// vitest.contract.config.ts","import { createVitestConfig } from '@motion-proto/live-tokens/testing/vitest';","import viteConfig from './vite.config';","import settings from './live-tokens.testing';","","export default createVitestConfig(viteConfig, { registrySetup: settings.registrySetup });","```","","```bash","npx vitest run --config vitest.contract.config.ts","```","","## Writing your own file","","`checkRegistryEntry` is exported at","`@motion-proto/live-tokens/component-editor/contract`, so a project that wants","its own suite writes two lines against its own registrations:","","```ts","// tests/registryContract.test.ts","// @vitest-environment happy-dom","import { describe, it, expect } from 'vitest';","import { getComponentRegistryEntries } from '@motion-proto/live-tokens';","import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';","import '../src/registerComponents';","","const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');","","describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {"," it('meets the registry contract', () => {"," expect(checkRegistryEntry(entry)).toEqual([]);"," });","});","```","","Filter on `origin`. The registry always carries the shipped components too, and","their `sourceFile` paths are relative to the package root. Without the filter","every built-in fails on a path that does not exist in your project.","","The helper reads the runtime file and `default.json` off disk, which is why it","is node-only and has its own subpath.","","## Paths","","Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.","A project that moved either passes them:","","```ts","checkRegistryEntry(entry, { projectRoot, componentConfigsDir });","```","","`componentConfigsDir` is the same directory `live-tokens.config.json` names.","","## What it holds","","1. **Registration**: `sourceFile` resolves to a real file, the schema is non-empty.","2. **Uniqueness**: no schema variable is declared twice.","3. **Editor to runtime**: every editable token's CSS var is declared in the"," runtime's `<style>` block, so an edit has something to repaint.","4. **Editor to default config**: every editable token has a seed alias in"," `component-configs/<id>/default.json`, so the component adopts with full"," defaults. A component with no `default.json` is editor-only; this check and"," the next one skip it.","5. **Opacity floors**: a token declaring `minOpacity` ships a default at or"," above it, so a floating panel starts out legible over page content.","6. **Round-trip**: `setComponentAlias` persists into the slice under the same key.","","Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored","as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes","(written on demand by the split-padding UI and read through the `themed-padding`","mixin's fallback chain, so they exist as neither `:root` declarations nor seeds)."],
13
13
  "references/fixed-overlays.md": ["# Fixed overlays must portal to body","","Any `position: fixed` layer (modal, lightbox, full-screen backdrop) is trapped, clipped or painted under other chrome, by a transformed / `isolation` / `contain` / `will-change` ancestor. Real consumer pages and the editor's own preview pane both have one, so this is the normal case rather than the edge.","","Render the layer with `use:portal` from `src/system/internal/portal.ts` so it escapes to `<body>`. `use:portal={enabled}` keeps an in-flow preview variant where it is; `Dialog` is the worked example. `check:overlay-portal` fails the build when a component sets `position: fixed` without it. Anchored popovers are exempt: `Tooltip` is `position: absolute` against its trigger and belongs in the flow.","","Moving to `<body>` costs two things:","","- DOM events from the layer no longer bubble to a consumer ancestor, so pass component callbacks the way `Dialog` does.","- A subtree-scoped CSS-variable theme no longer reaches it. This library themes via `:root`, so nothing breaks here.","","A modal also needs `role=\"dialog\"` with `aria-modal`, focus moved in on open and restored on close, and `Tab` trapped inside. `ImageLightbox` is the worked example."],
14
14
  "references/intrinsics.md": ["# Extension: intrinsics","","Some components expose **structural or display choices** that are not token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a custom `<select>` or checkbox authored in an editor snippet, outside the generic token grid, so they do not belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).","","An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the \"change\" the user thinks they made. `:global(:root)` is the source of truth.","","Declare intrinsics so the editor and the contract test stay honest:","","1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:",""," ```css"," --mywidget-lg-align: start;"," --mywidget-lg-eyebrow-display: block;"," ```","","2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:",""," ```ts"," import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';",""," export const intrinsics: IntrinsicSpec[] = ["," {"," key: 'align',"," variants: ['lg', 'md', 'sm'],"," variable: (v) => `--mywidget-${v}-align`,"," values: ['start', 'center'],"," default: { lg: 'start', md: 'start', sm: 'start' },"," },"," ];"," ```","","3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:",""," ```ts"," const byKey = new Map(intrinsics.map((i) => [i.key, i]));"," function readIntrinsic(key: string, v: string): string {"," const spec = byKey.get(key)!;"," const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default"," return spec.normalize ? spec.normalize(raw) : raw;"," }"," function getAlign(v: string) {"," return readIntrinsic('align', v) === 'center' ? 'center' : 'start';"," }"," ```",""," Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.","","4. **Put `intrinsics` on the registry entry** so the contract test can see it. That is the same entry the recipe passes to `bootLiveTokens`, with one more field:",""," ```ts"," bootLiveTokens(App, '#app', {"," components: [{"," id: 'mywidget',"," // ...label, icon, sourceFile, editorComponent, schema..."," intrinsics: myWidgetIntrinsics,"," }],"," });"," ```","","Use `normalize` only when two raw values render identically and the dropdown lists one (SectionDivider folds `above-description` into `below-label`). Properties that resemble intrinsics but are not: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`."],
15
15
  "references/linked-siblings.md": ["# Extension: linked siblings","","Read this when the component has more than one variant and those variants share base properties (surface, radius, padding) that should move together. Toggle and SectionDivider have no linked tokens and skip all of it.","","Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.","","1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.",""," ```ts"," function variantBaseTokens(v: Variant): Token[] {"," return ["," { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },"," { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },"," ];"," }"," // Colors omit canBeLinked. Per-variant by design."," function variantColorTokens(v: Variant): Token[] {"," return ["," { label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },"," { label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },"," ];"," }"," ```","","2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `\"success base\"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.",""," ```ts"," const linkableContexts = new Map<string, string>("," variants.flatMap((v) =>"," variantBaseTokens(v)"," .filter((t) => t.canBeLinked)"," .map((t) => [t.variable, `${v} base`] as [string, string]),"," ),"," );"," ```","","3. **Compute `linked` and mask currently-linked rows** out of per-state lists, so they render once inside the LinkedBlock instead of twice.",""," ```ts"," import { editorState } from '@motion-proto/live-tokens';"," import { computeLinkedBlock, withLinkedDisabled, buildSiblings }"," from '@motion-proto/live-tokens/component-editor';",""," let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));"," let visibleVariantStates = $derived((v: Variant) => Object.fromEntries("," Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),"," ));"," ```","","4. **Pass `{linked}` to `ComponentEditorBase`** so the LinkedBlock renders above the variant groups.","","5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.",""," ```svelte"," <ComponentEditorBase {component} title=\"Badge\" tokens={allTokens} {linked} variants={variantOptions}>"," {#each variants as v}"," <VariantGroup"," name={v}"," title={v}"," states={visibleVariantStates(v)}"," {component}"," siblings={buildSiblings(variants, v, variantStates)}"," >"," ...preview snippet"," </VariantGroup>"," {/each}"," </ComponentEditorBase>"," ```","","Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps; `ComponentEditorBase` renders without a `{linked}` prop."],
16
- "references/sketch-mode.md": ["# Joining the sketch layer","","Sketch mode blanks each part's real background and border and repaints them onto","`::before`/`::after` through a shared noise field. It draws a fixed set of","selectors: the shipped components, plus four classes reserved for everyone else.","A component is skipped until it opts in, so it stays crisp while the page","around it goes hand-drawn.","","The whole contract is CSS. There is nothing to import and no function to call:","the layer exports no runtime API, and a component joins it by carrying a class","and naming five custom properties.","","## What the layer takes over","","An opted-in element is no longer painting itself. On every drawn part the layer","forces:","","| It forces | So the component must |","|----------------------------------------------|----------------------------------------------------------|","| `background: transparent !important` | Name the fill again as `--sketch-fill` |","| `border-color: transparent !important` | Name the outline again as `--sketch-stroke` |","| `box-shadow: none !important` | Name the shadow again as `--sketch-shadow` |","| `overflow: visible !important` | Never put the class on a box whose clip carries meaning |","| `position: relative` | Never put the class on an absolutely-positioned root |","| `z-index: 0` | Expect a new stacking context on that element |","| `::before` (the fill), `::after` (the stroke) | Never own either pseudo-element on that element |","","`::after` survives on `sketch-rule` alone, which draws no outline. `::before` is","claimed on all four.","","## Opting in","","Put one class on the runtime component's root, chosen by **size, not by kind**.","A card and a modal are both containers; a badge and a pill are both chips.","","| Class | For |","|---------------------|-------------------------------------------------------------|","| `sketch-surface` | A box. The default treatment. |","| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |","| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |","| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |","","The class opts the component in and nothing more. It names no colour, so the layer emits","no rule for it and whatever the component declares survives.","","```svelte","<div class=\"mywidget sketch-container {variant}\">…</div>","","<style>"," .mywidget {"," background: var(--mywidget-surface);"," border: var(--mywidget-border-width) solid var(--mywidget-border);"," border-radius: var(--mywidget-radius);"," box-shadow: var(--mywidget-shadow);",""," /* The layer hides all four above. These are what it draws instead. */"," --sketch-fill: var(--mywidget-surface);"," --sketch-stroke: var(--mywidget-border);"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: var(--mywidget-radius);"," --sketch-shadow: var(--mywidget-shadow);"," }","</style>","```","","State all five. `--sketch-radius` is registered as an inheriting `<length>`, and","the other four inherit as ordinary custom properties, so a part that states","nothing is drawn with its **ancestor's** value: a badge inside a hatched card","stripes itself in the card's ink, and a square header inside a rounded card","picks up the card's corners.","","## Variants, states and inner parts","","Nothing competes for these values, so every case is one more","declaration at the specificity already in use.","","```css",".mywidget.danger { --sketch-fill: var(--mywidget-danger-surface); }",".mywidget:hover,",".mywidget.force-hover { --sketch-stroke: var(--mywidget-hover-border); }","```","","Pair every `:hover` with `.force-hover`, as elsewhere: that is the editor's","preview of the hover state, and a hover the sketch layer cannot paint reads as","no hover at all once the real background is transparent.","","An inner part that carries its own surface (a header strip, a footer) takes its","own class and its own five values. The class is easy to forget, because the part","already has its own values and looks finished without it. A part carrying only","the values is left crisp, and reads as a hard-edged rectangle dropped inside a","drawn box. No checker sees it. Where such a part draws no outline, bind the","hatch ink to the ink its **parent** is outlined in, so the component reads as one","drawing rather than a shaded panel dropped into a box:","","```svelte","<span class=\"mywidget-header sketch-chip\">{label}</span>","```","","```css",".mywidget-header {"," --sketch-fill: var(--mywidget-header-surface);"," --sketch-stroke: transparent;"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: 0px;","}","```","","A part with a visible stroke needs no `--sketch-hatch-color`; it falls back to","the stroke and follows it into hover. A part with no fill wants none, because","there is no surface there to shade.","","A gradient is a valid fill. The `background` shorthand's last layer takes a","colour or an image, so `--sketch-fill` accepts either.","","## Where the class does not go","","- **A positioned root.** The layer forces `position: relative` on parts that sit"," in flow, which drops an absolutely or fixed-positioned element back to its"," flow position. Put the class on an inner box instead.","- **A box that clips something real.** `overflow` is forced visible so the ink"," can travel past the border box, and there is no consumer opt-out. A scroller,"," a fill bar held to its track, or a picture held to its frame keeps its clip by"," keeping the class off that element and carrying it on a wrapper.","- **An element that owns `::before` or `::after`.** The layer claims both. A"," shimmer, a caret or a decorative arrow on the opted-in element is gone.","- **A shipped part's selector** (`.card`, `.panel`). Borrowing one to get drawn"," works, but it hands the component that part's colours and its damping, and it"," is package-internal. The reserved classes are the contract.","","## Rules, which are not boxes","","A `border` cannot be displaced: the effect moves boxes, and a border is not one.","Make the rule an element, give it `sketch-rule`, and name its ink as the fill.","","```svelte","<span class=\"mywidget-rule sketch-rule\" aria-hidden=\"true\"></span>","```","```css",".mywidget-rule {"," height: var(--border-width-2);"," background: var(--mywidget-divider);"," --sketch-fill: var(--mywidget-divider);","}","```","","## Media inside the component","","A drawn part's `overflow` is visible so the fill and outline can travel past the","box. A background that bleeds is the effect working. An image that bleeds is","not, since it keeps square corners while the part around it turns. Media running","to the component's edge has to carry the corners itself:","","```css",".mywidget-cover {"," overflow: hidden;"," border-top-left-radius: var(--sketch-radius, var(--mywidget-radius));"," border-top-right-radius: var(--sketch-radius, var(--mywidget-radius));","}","```","","`--sketch-radius` is the radius the layer drew and it inherits, so the fallback","covers the effect being off. Corner spread is per-corner and per-instance, so at","high spread the crop is the mean rather than an exact trace of the drawn edge.","","## Icons and SVG","","Icons and inline SVG take the wobble directly, since a glyph has no box to","redraw. Body type is left alone deliberately: an icon is a shape and survives a","wobble, a paragraph is not. Nothing opts into this; it applies to every","`[class*=\"fa-\"]` and every `svg` under the scope.","","`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It","inherits, so one declaration covers everything under it:","","```css","/* Crisp. Chrome, a logo, anything that has to stay exact. */",".mywidget-toolbar { --sketch-icon-off: none; }","","/* Drawn back rather than off, at a third of the travel. Small artwork, and"," type set as an SVG, which the layer reads as one large glyph. */",".mywidget-mark { --sketch-icon-off: var(--sketch-icon-soft); }","```","","Travel is stated in px against a glyph whose size the layer cannot know, so the","dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the","soft bank before reaching for `none`.","","## First-party components","","A component authored inside the package does not use the reserved classes. Add a","`PartSpec` row to `PART_SPECS` in `src/editor/core/sketch/sketchLayer.ts`","instead, which is keyed to the component's own token stem and gets the shipped","damping. `sketchPartTokens.test.ts` then holds the component to it: every colour the layer","paints must be one the component itself assigns to that same element, checked","against the compiled `<style>` block.","","## Verify","","Switch Sketch mode on from the editor's **Sketchstyle** view, then check the","component in place:","","- [ ] Drawn, not crisp, in every variant.","- [ ] Wearing its own colours, not its parent's, including inner parts.","- [ ] Hover repaints. The wobble holds still while it does.","- [ ] Hatched fill uses ink that belongs to the component.","- [ ] Media at the component's edge turns with the drawn corners.","- [ ] Nothing that has to stay exact is torn: icons, clipped content, overlays.","- [ ] Switch it off. Every trace is gone and the component is unchanged."],
17
- "references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-divider` | Divider / separator color |","| `-color` | Generic color, when none of the above name the role |","| `-shadow` | Box-shadow |","| `-opacity` | Opacity (0–1) |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-background` | Fill, where the component's own vocabulary says background |","| `-accent` | An accent bar or indicator's colour |","| `-indicator` | A selection indicator's colour |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-thickness` | Alternative to `-width` when fallback siblings would collide |","| `-accent-width` | An accent bar's thickness |","| `-indicator-width` | An indicator's thickness, moved with `-accent-width` |","| `-hairline-thickness` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-divider-width` | A divider's thickness |","| `-divider-thickness` | Alternative to `-divider-width` |","| `-divider-height` | A divider's length |","| `-divider-inset` | Inset trimmed from a stretched divider |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-inset` | Inset trimmed from a stretched element |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-divider-height`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
16
+ "references/sketch-mode.md": ["# Joining the sketch layer","","Sketch mode blanks each part's real background and border and repaints them onto","`::before`/`::after` through a shared noise field. It draws a fixed set of","selectors: the shipped components, plus four classes reserved for everyone else.","A component is skipped until it opts in, so it stays crisp while the page","around it goes hand-drawn.","","The whole contract is CSS. There is nothing to import and no function to call:","the layer exports no runtime API, and a component joins it by carrying a class","and naming five custom properties.","","## What the layer takes over","","An opted-in element is no longer painting itself. On every drawn part the layer","forces:","","| It forces | So the component must |","|----------------------------------------------|----------------------------------------------------------|","| `background: transparent !important` | Name the fill again as `--sketch-fill` |","| `border-color: transparent !important` | Name the outline again as `--sketch-stroke` |","| `box-shadow: none !important` | Name the shadow again as `--sketch-shadow` |","| `overflow: visible !important` | Never put the class on a box whose clip carries meaning |","| `position: relative` | Never put the class on an absolutely-positioned root |","| `z-index: 0` | Expect a new stacking context on that element |","| `::before` (the fill), `::after` (the stroke) | Never own either pseudo-element on that element |","","`::after` survives on `sketch-rule` alone, which draws no outline. `::before` is","claimed on all four.","","## Opting in","","Put one class on the runtime component's root, chosen by **size, not by kind**.","A card and a modal are both containers; a badge and a pill are both chips.","","| Class | For |","|---------------------|-------------------------------------------------------------|","| `sketch-surface` | A box. The default treatment. |","| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |","| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |","| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |","","The class opts the component in and nothing more. It names no colour, so the layer emits","no rule for it and whatever the component declares survives.","","```svelte","<div class=\"mywidget sketch-container {variant}\">…</div>","","<style>"," .mywidget {"," background: var(--mywidget-surface);"," border: var(--mywidget-border-width) solid var(--mywidget-border);"," border-radius: var(--mywidget-radius);"," box-shadow: var(--mywidget-shadow);",""," /* The layer hides all four above. These are what it draws instead. */"," --sketch-fill: var(--mywidget-surface);"," --sketch-stroke: var(--mywidget-border);"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: var(--mywidget-radius);"," --sketch-shadow: var(--mywidget-shadow);"," }","</style>","```","","State all five. `--sketch-radius` is registered as an inheriting `<length>`, and","the other four inherit as ordinary custom properties, so a part that states","nothing is drawn with its **ancestor's** value: a badge inside a hatched card","stripes itself in the card's ink, and a square header inside a rounded card","picks up the card's corners.","","## Variants, states and inner parts","","Nothing competes for these values, so every case is one more","declaration at the specificity already in use.","","```css",".mywidget.danger { --sketch-fill: var(--mywidget-danger-surface); }",".mywidget:hover,",".mywidget.force-hover { --sketch-stroke: var(--mywidget-hover-border); }","```","","Pair every `:hover` with `.force-hover`, as elsewhere: that is the editor's","preview of the hover state, and a hover the sketch layer cannot paint reads as","no hover at all once the real background is transparent.","","An inner part that carries its own surface (a header strip, a footer) takes its","own class and its own five values. The class is easy to forget, because the part","already has its own values and looks finished without it. A part carrying only","the values is left crisp, and reads as a hard-edged rectangle dropped inside a","drawn box. No checker sees it. Where such a part draws no outline, bind the","hatch ink to the ink its **parent** is outlined in, so the component reads as one","drawing rather than a shaded panel dropped into a box:","","```svelte","<span class=\"mywidget-header sketch-chip\">{label}</span>","```","","```css",".mywidget-header {"," --sketch-fill: var(--mywidget-header-surface);"," --sketch-stroke: transparent;"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: 0px;","}","```","","A part with a visible stroke needs no `--sketch-hatch-color`; it falls back to","the stroke and follows it into hover. A part with no fill wants none, because","there is no surface there to shade.","","A gradient is a valid fill. The `background` shorthand's last layer takes a","colour or an image, so `--sketch-fill` accepts either.","","## Where the class does not go","","- **A positioned root.** The layer forces `position: relative` on parts that sit"," in flow, which drops an absolutely or fixed-positioned element back to its"," flow position. Put the class on an inner box instead.","- **A box that clips something real.** `overflow` is forced visible so the ink"," can travel past the border box, and there is no consumer opt-out. A scroller,"," a fill bar held to its track, or a picture held to its frame keeps its clip by"," keeping the class off that element and carrying it on a wrapper.","- **An element that owns `::before` or `::after`.** The layer claims both. A"," shimmer, a caret or a decorative arrow on the opted-in element is gone.","- **A shipped part's selector** (`.card`, `.panel`). Borrowing one to get drawn"," works, but it hands the component that part's colours and its damping, and it"," is package-internal. The reserved classes are the contract.","","## Rules, which are not boxes","","A `border` cannot be displaced: the effect moves boxes, and a border is not one.","Make the rule an element, give it `sketch-rule`, and name its ink as the fill.","","```svelte","<span class=\"mywidget-rule sketch-rule\" aria-hidden=\"true\"></span>","```","```css",".mywidget-rule {"," height: var(--border-width-2);"," background: var(--mywidget-hairline-color);"," --sketch-fill: var(--mywidget-hairline-color);","}","```","","## Media inside the component","","A drawn part's `overflow` is visible so the fill and outline can travel past the","box. A background that bleeds is the effect working. An image that bleeds is","not, since it keeps square corners while the part around it turns. Media running","to the component's edge has to carry the corners itself:","","```css",".mywidget-cover {"," overflow: hidden;"," border-top-left-radius: var(--sketch-radius, var(--mywidget-radius));"," border-top-right-radius: var(--sketch-radius, var(--mywidget-radius));","}","```","","`--sketch-radius` is the radius the layer drew and it inherits, so the fallback","covers the effect being off. Corner spread is per-corner and per-instance, so at","high spread the crop is the mean rather than an exact trace of the drawn edge.","","## Icons and SVG","","Icons and inline SVG take the wobble directly, since a glyph has no box to","redraw. Body type is left alone deliberately: an icon is a shape and survives a","wobble, a paragraph is not. Nothing opts into this; it applies to every","`[class*=\"fa-\"]` and every `svg` under the scope.","","`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It","inherits, so one declaration covers everything under it:","","```css","/* Crisp. Chrome, a logo, anything that has to stay exact. */",".mywidget-toolbar { --sketch-icon-off: none; }","","/* Drawn back rather than off, at a third of the travel. Small artwork, and"," type set as an SVG, which the layer reads as one large glyph. */",".mywidget-mark { --sketch-icon-off: var(--sketch-icon-soft); }","```","","Travel is stated in px against a glyph whose size the layer cannot know, so the","dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the","soft bank before reaching for `none`.","","## First-party components","","A component authored inside the package does not use the reserved classes. Add a","`PartSpec` row to `PART_SPECS` in `src/editor/core/sketch/sketchLayer.ts`","instead, which is keyed to the component's own token stem and gets the shipped","damping. `sketchPartTokens.test.ts` then holds the component to it: every colour the layer","paints must be one the component itself assigns to that same element, checked","against the compiled `<style>` block.","","## Verify","","Switch Sketch mode on from the editor's **Sketchstyle** view, then check the","component in place:","","- [ ] Drawn, not crisp, in every variant.","- [ ] Wearing its own colours, not its parent's, including inner parts.","- [ ] Hover repaints. The wobble holds still while it does.","- [ ] Hatched fill uses ink that belongs to the component.","- [ ] Media at the component's edge turns with the drawn corners.","- [ ] Nothing that has to stay exact is torn: icons, clipped content, overlays.","- [ ] Switch it off. Every trace is gone and the component is unchanged."],
17
+ "references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-opacity` | Opacity (0–1) |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-indicator` | The colour of the bar or stripe that marks an item |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-indicator-width` | An indicator's thickness |","| `-hairline-width` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-hairline-inset` | Inset trimmed from a stretched hairline |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-hairline-width`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
18
18
  },
19
19
  "create-page": {
20
- "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container from the Containers by purpose list.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run **live-tokens-check-compliance** and `npx live-tokens check-page <file> --tests --strict`, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the compliance result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Containers by purpose","","- `Panel` is a stage: a canvas, a player, a preview. `minHeight` holds its height while what it shows changes.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- `Card` is a titled block of content. Its `title` prop is the title, and the card's own tokens type it.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `--body-sm-*` in `--text-secondary`.","- A form puts the essential fields first and the secondary fields in a `CollapsibleSection`. Its actions sit on the bottom edge.","- A row of fields is a flex row with `gap: var(--space-20)`. Each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`, the primary last. A `danger` Button sits apart from the group it could be mistaken for.","- A vertical stack of Buttons sets `fullWidth` on each Button. A row omits it.","- `MenuSelect` renders its list open. For a picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints the declared props, the values each union accepts, and the usage comment. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Card title | the Card `title` prop |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Command or value | `code` in `--code-*` |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h4`, `p`, `code`, `pre`, and list items from these styles, so the tag carries the style. Type an element only when the table gives its tag a different style. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. The shipped default is the page's size.","","### Emphasis","","One `primary` Button per page: the action that completes the page's main task. An action that supports that task is `secondary`. An action unrelated to the task, or informational, is `outline`. An action that destroys saved work is `danger`.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run **live-tokens-check-compliance**, then `npx live-tokens check-page <file> --tests --strict`. The Playwright suite runs against the page's own route and proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. The two reports carry every finding by rule, and **live-tokens-fix-findings** takes the fix list. Repeat until the page is clean.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- An action that destroys saved work confirms in a `Dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
20
+ "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container from the Containers by purpose list.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Containers by purpose","","- `Panel` is a stage: a canvas, a player, a preview. `minHeight` holds its height while what it shows changes.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- `Card` is a titled block of content. Its `title` prop is the title, and the card's own tokens type it.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `--body-sm-*` in `--text-secondary`.","- A form puts the essential fields first and the secondary fields in a `CollapsibleSection`. Its actions sit on the bottom edge.","- A row of fields is a flex row with `gap: var(--space-20)`. Each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`, the primary last. A `danger` Button sits apart from the group it could be mistaken for.","- A vertical stack of Buttons sets `fullWidth` on each Button. A row omits it.","- `MenuSelect` renders its list open. For a picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints the declared props, the values each union accepts, and the catalogue entry. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Card title | the Card `title` prop |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Command or value | `code` in `--code-*` |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h4`, `p`, `code`, `pre`, and list items from these styles, so the tag carries the style. Type an element only when the table gives its tag a different style. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. The shipped default is the page's size.","","### Emphasis","","One `primary` Button per page: the action that completes the page's main task. An action that supports that task is `secondary`. An action unrelated to the task, or informational, is `outline`. An action that destroys saved work is `danger`.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- An action that destroys saved work confirms in a `Dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
21
21
  "references/interaction-sources.md": ["# Interaction sources","","Read this when a Verify check or an emphasis rule in SKILL.md needs its reason.","Each principle below is held by one of three parties. A shipped component","holds it when the component's own design answers it, so the page's rule is","the component and pick-component names the test. The page holds it when only","the page can get it right, so SKILL.md states a read or the checker a rule.","The product holds it when no page decision touches it, and the row says so","to keep a later edit from reopening it.","","## Jakob Nielsen, ten usability heuristics","","| Heuristic | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Visibility of system status | Page | Verify: an action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`. |","| Match between system and the real world | Page | Verify: labels use the user's words. |","| User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: an unrelated or informational action is `outline`. |","| Consistency and standards | Page | One size, one primary action, one text style per element. The components carry the rest. |","| Error prevention | Page | Verify: an action that destroys saved work confirms in a `Dialog`; the checker's `danger-without-dialog`. Verify: every field has a default and Reset restores it. |","| Recognition rather than recall | Component | `MenuSelect` lists the options; `Input` carries its label and hint; `Tooltip` defines in place. |","| Flexibility and efficiency of use | Product | Shortcuts and customisation are product decisions. |","| Aesthetic and minimalist design | Page | Layout: each element serves a purpose no other element serves. Verify: secondary settings sit in a `CollapsibleSection`. |","| Help users recognise, diagnose, and recover from errors | Component | `Input` carries the error line; `Callout variant=\"danger\"` carries a section's. |","| Help and documentation | Product | Contextual help is a product decision. |","","The complex-application version of the ten (Kaley, Nielsen Norman Group)","describes a tool with a stage and controls, which is the tool page SKILL.md","lays out. Its additions that the page holds: a wait past ten seconds shows","steps done and steps left, and the stage is the live preview of every","control. Undo, version history, and autosave are the product's.","","## Bruce Tognazzini, first principles of interaction design","","| Principle | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Anticipation | Page | Layout: show related items side by side. |","| Colour | Component | `Callout`, `Badge`, and `Notification` carry an icon or text beside the colour. |","| Consistency | Page | One size, one primary action. |","| Defaults | Page | Verify: every field has a default and Reset restores it. |","| Discoverability | Page | Verify: every control is in view. |","| Explorable interfaces | Page | Verify: every row of actions holds an action that leaves without committing. |","| Fitts's law | Component | The shipped default is the large target; SKILL.md's one-size rule keeps it. A toolbar sits on the section's bottom edge. |","| Protect users' work | Page | Verify: an action that destroys saved work confirms in a `Dialog`. |","| Readability | Component | live-tokens-set-colors gates every text pair at WCAG AA. |","| Simplicity | Page | Verify: secondary settings sit in a `CollapsibleSection`; no capability is removed for the sake of simplicity. |","| Visible navigation | Component | `SideNavigation` follows the current path. |","| Aesthetics, Autonomy, Efficiency of the user, Human-interface objects, Latency reduction, Learnability, Metaphors, State | Product | Measured, engineered, or researched outside a page. |","","Sources:","","- Nielsen, 10 Usability Heuristics for User Interface Design (1994, updated 2024). https://www.nngroup.com/articles/ten-usability-heuristics/","- Kaley, 10 Usability Heuristics Applied to Complex Applications. https://www.nngroup.com/articles/usability-heuristics-complex-applications/","- Tognazzini, First Principles of Interaction Design (revised 2014). https://asktog.com/atc/principles-of-interaction-design/","","## W3C Web Accessibility Initiative","","| Guidance | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Headings: one `h1`, levels in order, no skipped level | Page | Type: use the semantic element for each place. Verify: one `h1`, levels in order. |","| Images: text alternatives | Page | Verify: every `img` has `alt` text. |","| Focus order follows the reading order | Page | Verify: focus order follows the reading order. |","| Form labels and error messages | Component | `Input` carries its label, hint, and error line. |","","https://www.w3.org/WAI/tutorials/page-structure/headings/","https://www.w3.org/WAI/tutorials/images/","https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html"],
22
22
  "references/layout-sources.md": ["# Layout sources","","Read this when a layout decision in SKILL.md needs its reason. Each law in the","Layout section comes from one of these sources. The sources are for layout and","hierarchy only. Do not take color or type opinions from them: the theme owns","those.","","## Edward Tufte","","Tufte wrote about information graphics. His laws apply to a page because a page","is an information display with controls on it.","","| Law | Statement | Rule in SKILL.md |","|---|---|---|","| Smallest effective difference | Make all visual distinctions as subtle as possible, but still clear and effective. | Separate with space first, then a hairline rule, then a second surface. |","| 1+1=3 | Two heavy marks side by side make a third mark: the space between them. | A section of containers with borders and header bars reads as a set of posters. |","| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and chrome each take their own token. |","| Administrative debris | The metaphor for the interface is the information. Remove the chrome that the tool adds for itself. | The stage takes the space. Controls take their shipped default. |","| Spatial over temporal | Show information adjacent in space. A control that hides it stacks it in time. | Show related items side by side. Frames in a strip are small multiples. |","| Erase non-data ink | Remove each mark that carries no information. | The Verify question: does the page lose information when this element is removed? |","| Micro and macro readings | A good display reads at a distance and up close. | Verify from a distance, then closely. |","","Sources:","","- Envisioning Information (1990): layering and separation, small multiples, micro and macro. https://www.edwardtufte.com/book/envisioning-information/","- The Visual Display of Quantitative Information (1983): data-ink, chartjunk.","- iPhone interface design, edwardtufte.com notebook: administrative debris, spatial over temporal. https://www.edwardtufte.com/notebook/iphone-interface-design/","","## Josef Müller-Brockmann","","Grid Systems in Graphic Design (1981) is the discipline behind the page column","grid. The grid does the separating, so an element needs no border to show","where it sits. His stated aim is compact planning, intelligibility, and","clarity. That is Tufte's aim in a typographer's words.","","## Refactoring UI","","Adam Wathan and Steve Schoger, Refactoring UI (2018), turns both into working","rules for product screens:","","- Put more space around a group than within it.","- Start with too much white space, then remove some.","- Use fewer borders. Separate with space, a shadow, or a second background.","- Emphasize by de-emphasizing the secondary content.","- Labels are a last resort.","- Keep a spacing scale where no two steps are closer than a quarter. The `--space-*` scale is that scale.","","https://www.refactoringui.com/","","## Material Design 3, canonical layouts","","The Page layouts table takes its rows from Material's canonical layouts.","","| Layout | Statement | Row in SKILL.md |","|---|---|---|","| Supporting pane | The primary area takes about two thirds of the window; the secondary pane takes the rest. At compact width the pane moves below the main content. | Main with a supporting pane, two thirds and one third; the stacking sentence. |","| List-detail | The list and the detail of the selected item sit side by side at expanded width. | List with detail. |","| Feed | Equivalent items in an adaptive grid. | Grid of equals. |","","https://m3.material.io/foundations/layout/canonical-examples/overview","https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts","","## Cloudscape patterns","","| Pattern | Statement | Rule in SKILL.md |","|---|---|---|","| Dashboard | Three areas top to bottom: overview, data, support. \"Consider seven as the limit number for data representation.\" | Verify: the first section holds what the user came for. Grid of equals: up to seven per section. |","| Single-page create | One container; the essential fields first and few; secondary inputs in an expandable section; cancel then submit at the bottom. | Containers: a form. Single column. |","| Details page | The title with its actions, then a summary, then related blocks. | Stacked sections. |","| Empty state | A heading, an optional line, and one action. \"Always provide an action.\" Errors go elsewhere. | Containers: an empty stage. |","","https://cloudscape.design/patterns/general/service-dashboard/static-dashboard/","https://cloudscape.design/patterns/resource-management/create/single-page-create/","https://cloudscape.design/patterns/resource-management/details/details-page/","https://cloudscape.design/patterns/general/empty-states/","","## Matthew Butterick, line length","","\"45 to 90 characters per line, including spaces.\" The Separation paragraph and the Verify check carry the measure, and the half-width copy span holds it at body size.","","https://practicaltypography.com/line-length.html","","## Nielsen Norman Group, proximity","","\"Proximity is one of the most important grouping principles and can overpower competing visual cues such as similarity of color or shape.\" Space inside a group is smaller than space between groups. An unrelated action inside a group is camouflaged, so a `danger` Button sits apart from the toolbar group.","","https://www.nngroup.com/articles/gestalt-proximity/"],
23
23
  },
24
24
  "create-theme": {
25
25
  "SKILL.md": ["---","name: live-tokens-create-theme","description: Create or modify a complete live-tokens theme from a natural-language request. A theme has three dimensions: color, typography, and geometry. The skill adjusts design token values and their assignment to semantic properties to create a new theme. Derives one design direction and routes a color intent, a type intent, and a geometry intent to live-tokens-set-colors, live-tokens-set-type, and live-tokens-set-geometry. Use when the user asks for a theme, look, vibe, or brand feel by mood, style, era, season, holiday, or hue. Use when the user names only a color and wants a theme around it. Use when the user refines a theme across more than one dimension. For color, type, or geometry named on its own, read that set skill.","---","","# Creating a theme from a request","","A theme is built of three dimensions: color, type, and geometry. This skill","reads the user's prompt, the **request**, and derives the **design direction**,","a short summary covering three **intents**, one per dimension, each naming an","outcome rather than a value. Each set skill receives the design direction and","the intent for its own dimension as a goal, and reports back. The three reports","combine into one **assembled report** for the whole theme.","","One **anchor** carries the direction across all three dimensions. It is a row","label that `references/design-directions.md` and each set skill list under the","same names: a feeling, an idiom, or an occasion. Naming it once points every set","skill at the same row of its own table.","","Each set skill writes its dimension into the working buffers the app already","renders. This skill runs one CLI of its own, `save-theme`, which composes those","buffers into a **theme**, the document at `themes/<slug>.json`, and opens it.","Never hand-author theme JSON and never edit the data tree directly.","","## Workflow","","1. Read the request once and generate the design direction based on the prompt: the mood, and the color, typography, and geometry that mood implies. It describes the three intents for each set skill, and it names a default where the request leaves a dimension open. Keep it brief and clear. Every step below keys off it.","2. Read `references/design-directions.md` and name the **anchor** the request matches: a feeling, idiom, or occasion the reference lists, each one fixing color, type, and geometry. An idiom sets constraints and a feeling moves dials inside them, so a request matching both reads the idiom first. A request matching none takes the design direction alone.","3. Generate the three intents the design direction and the anchor imply, one line each: the color intent, the type intent, and the geometry intent. Each names an outcome. Pass the anchor and the matching intent to each set skill, because every set skill holds its own anchors for its own dimension under the same names. Never specify an OKLCH value, a font family, or a token on a set skill's behalf.","4. Invoke **live-tokens-set-colors** with the anchor and the color intent. Skip only when the user asked to leave the color alone.","5. Invoke **live-tokens-set-type** with the anchor and the type intent. Skip only when the user asked to leave the type alone.","6. Invoke **live-tokens-set-geometry** with the anchor and the geometry intent. Skip when the geometry intent is to leave the geometry alone.","7. Take the theme name from the design direction and run `npx live-tokens save-theme \"<name>\"`. It composes the buffers into `themes/<slug>.json` and loads it. `--dry-run` prints the file path and the layers instead. `--no-activate` writes the theme without loading it. A blank name and the name `default` exit 1. A name whose slug exists overwrites that theme in place. Adopt, in the editor, ships the theme to the site.","8. Assemble the three set skill responses into the assembled report: the design direction, what each set skill changed, any dimension left alone, and anything one of them flagged. Review the result in the running app. Offer refinements (see Refining a theme).","","## Set skill responsibilities","","Invoke set skills with the anchor and the matching intent.","","| Dimension | Set skill | It decides |","|---|---|---|","| color | live-tokens-set-colors | ten base colors, the scheme, harmony, the Canvas base color and its gradient, the contrast pass |","| type | live-tokens-set-type | the families for up to five slots, the form models behind them, the weights |","| geometry | live-tokens-set-geometry | radius, padding, gap, and border-width |","","A dimension the request left open still gets an intent, taken from the anchor.","A dimension the request excludes gets no invocation at all, and the assembled report says which.","","Component aliases and swatch gradients carry forward from the buffers by value","into the theme `save-theme` writes. At a set-colors run, gradients tuned in the","editor survive and stock ones rebuild from the new families.","","## Refining a theme","","A refinement operates on an existing theme, and one adjective usually names one","dimension. Route it to the matching set skill:","","| The user says | Goes to |","|---|---|","| warmer, cooler, calmer, louder, lighter, darker, moodier, more contrast | live-tokens-set-colors |","| more editorial, friendlier, more technical, a serif for headings | live-tokens-set-type |","| rounder, sharper, pill buttons, tighter, airier, thicker borders | live-tokens-set-geometry |","","The table gives examples. Route every refinement request, whether or not its","words appear there.","","When no refinement is requested, the theme is complete.","","Keep this skill for a refinement that spans dimensions (\"make it feel more","serious\"), or one that names no dimension at all. State a new design direction","and route all three again.","","Feedback about a page or a component (\"make the buttons bigger\", \"move the","hero up\") is not a theme change: read **live-tokens-create-page** or","**live-tokens-create-component**. A request for the previous theme is met by","loading it from the editor's Theme panel; loading clears the buffers.","","## Verify","","- Each invoked set skill reports its result. When invoked, `set-colors` exits 0 with every check passing (auto-corrected is fine).","- `save-theme` exits 0 and names the theme it wrote and opened.","- The app (dev server running) shows the whole theme, and the editor's Theme panel names that theme with no pending changes.","- The assembled report names one design direction, and the three intents come from it.","- To return to the previous theme, load it from the Theme panel; loading clears the buffers too."],
26
- "references/design-directions.md": ["# Design directions: feelings, idioms, and occasions","","Read this once the request names a feeling, a design idiom, an era, a genre, a","holiday, a season, or a natural scene. Each entry places the request and gives","the direction the three intents come from.","","The mechanics live with the executors. Color anchors are in","live-tokens-set-colors, type anchors in live-tokens-set-type, geometry anchors","in live-tokens-set-geometry, each keyed on the same names as the tables below.","Name the anchor in the intent, and the sibling reads its own column.","","Three axes place any request, including one no entry lists:","","| Axis | Reads as | Carried by |","|---|---|---|","| Valence | pleasant against unpleasant | lightness, above everything else |","| Energy | aroused against calm | chroma, and hue distance on screen |","| Dominance | assertive against yielding | surface contrast, type weight, border weight, tightness |","","An idiom sets constraints and a feeling moves dials, so a request that names","both (\"cozy brutalist\", \"clinical Swiss\") reads the idiom first and lets the","feeling move the dials inside it.","","## Feelings","","Valence and energy set the quadrant, and the table runs in quadrant order:","pleasant-aroused, pleasant-calm, unpleasant-aroused, unpleasant-calm. Dominance","separates confident from gentle inside one quadrant and lives almost entirely","outside color.","","| Request | Placement | Direction |","|---|---|---|","| Joyful, exuberant, energetic | pleasant, high energy | warm and bright throughout, with nothing held back |","| Playful, whimsical | pleasant, high energy, low dominance | four hue families at play, soft and generous |","| Optimistic, hopeful | pleasant, moderate energy | a cool ground lit by one warm counterpoint, like sunrise |","| Confident, bold | pleasant, high energy, high dominance | wide contrast, heavy weight, held tight |","| Serene, tranquil | pleasant, low energy | cool and quiet, with nothing loud anywhere |","| Tender, gentle, romantic | pleasant, low energy, low dominance | a soft warm ground, a narrow range, light weights |","| Cozy, comforting | pleasant, low energy | warm through and through, nothing cool on screen |","| Wistful, nostalgic, vintage, faded | pleasant, low energy | chroma withheld rather than light withheld |","| Earthy, grounded, natural | pleasant, low energy, moderate dominance | warm mineral hues, nothing synthetic |","| Clinical, sterile, precise | neutral, low energy, high dominance | an untinted ground, one cool hue, tightly set |","| Contemplative, focused | neutral, low energy | one cool hue and almost nothing else |","| Urgent, alarming | unpleasant, high energy, high dominance | a neutral ground so the alarm lands, heavy and tight |","| Tense, anxious | unpleasant, high energy | an uncomfortable ground under a pair that vibrates |","| Defiant, rebellious, loud | unpleasant, high energy, highest dominance | near-black under one acid hue, blunt everywhere |","| Melancholy, moody, sad | unpleasant, low energy | dark and cool, holding one moment of color |","| Somber, grave, mournful | unpleasant, low energy, high dominance | near-neutral dark, sharp and quiet |","| Ominous, dramatic, haunted | unpleasant, low energy, high dominance | dark with one hot accent and real atmosphere |","| Austere, severe, cold | unpleasant, lowest energy, highest dominance | monochrome at one extreme of lightness |","","## Idioms, eras, and genres","","The table runs modernist, digital, quiet, print, expressive.","","| Request | Placement | Direction |","|---|---|---|","| Swiss, International | neutral, low energy, high dominance | one hue on a near-white ground, rational, tight |","| Bauhaus | pleasant, high energy, high dominance | primaries at full commitment on paper, geometric, square but for the circle |","| Mid-century modern | pleasant, moderate energy | warm muted mid-tones, soft and open, no borders |","| Art deco, opulent, luxurious | pleasant, low energy, high dominance | dark with one metal, high-contrast type, sharp |","| Terminal, phosphor | neutral, moderate energy, high dominance | one phosphor hue on near-black, mono, bordered |","| Cyberpunk, neon noir, futuristic | unpleasant, high energy, high dominance | dark with two neons and a glow, wide type, sharp |","| Vaporwave | pleasant, moderate energy, low dominance | light sunset pastels with a gradient, retro display, soft |","| Y2K, bubble | pleasant, high energy | a chrome ground under electric color, geometric, pills |","| Blueprint | neutral, low energy, high dominance | a drafting ground with pale rules, technical type, gridded |","| Scandinavian, hygge | pleasant, low energy, low dominance | a chalk ground and muted naturals, soft and open |","| Japandi, wabi-sabi | pleasant, lowest energy | unbleached paper, near-monochrome, generous space, no borders |","| Cottagecore, botanical | pleasant, low energy, low dominance | warm cream and garden hues, serif display, soft |","| Editorial, magazine | neutral, low energy, high dominance | paper and ink with one strong hue, carried by rules |","| Newsprint, broadsheet | neutral, low energy | grey-warm paper under near-black ink, serif throughout, tight |","| Risograph, zine | pleasant, high energy, high dominance | two flat spot inks on paper, expressive display, heavy rules |","| Corporate, professional, trustworthy | pleasant, low energy | cool near-white with navy and teal, conventional everywhere |","| Brutalist | unpleasant, high energy, highest dominance | a pure ground, one alarming hue, heavy type, thick borders |","| Memphis, postmodern | pleasant, highest energy | a pastel ground under four hue families, shapes set against each other |","| Industrial, workshop, gritty | neutral, moderate energy, high dominance | concrete and steel with safety orange, condensed type, thick borders |","","## Occasions","","An occasion fixes color only, so its type and geometry intents come from the","feeling it implies or from the generic tables in the sibling skills.","","Every occasion is a statement request: the named color goes on the ground","rather than only on the buttons.","","| Request | Direction |","|---|---|","| Christmas | red and green with gold, one of the two owning the ground |","| Halloween | pumpkin, violet, and poison green, dark either way |","| St. Patrick's | green with gold over a pale ground |","| Ocean | blues held to one narrow band |","| Sunset | a hue sweep through red, falling in lightness |","| Autumn | parchment under rust, gold, and moss |","| Spring | pastels, greens and pinks over a mint ground |"],
27
- },
28
- "fix-findings": {
29
- "SKILL.md": ["---","name: live-tokens-fix-findings","description: Fix every finding of check-page and check-component in an existing @motion-proto/live-tokens project until both exit 0. Called with the fix list by live-tokens-check-compliance. Use when the user asks to fix the project. Edits the files the checkers name. Updates tokens.css only through the migration command.","---","","# Fixing the findings of check-page and check-component","","Fix every finding of `check-page` and `check-component` until both exit 0. `check-page` checks pages. Every component comes from the catalogue, every prop is declared, and every value in page CSS is a design token. `check-component` checks authored components. Every token names a semantic property, and its default is the design token that property reads. Update `tokens.css` only through the migration command. That command also heals the data tree, and with `--write` rewrites the route references it lists. When live-tokens-check-compliance hands over a fix list, the user's choices in it stand.","","## Workflow","","When `check-page` is an unknown command, upgrade `@motion-proto/live-tokens` first.","","1. Run `npx live-tokens migrate --check` to see the plan, then `npx live-tokens migrate` to apply it. `--tokens <path>` names a tokens.css in an unusual place.","2. Run both checkers with `--json`. Each finding carries a `rule`, a file, and a line."," ```sh"," npx live-tokens check-page --json"," npx live-tokens check-component --json"," ```","3. Group the findings by rule.","4. Take the largest error group first, then the remaining errors. Take warnings only when the repair scope includes warnings.","5. Fix every finding in the group with its section: Color by role, Geometry by scale, or The remaining rules.","6. Run both checkers again. When repairable findings remain in scope, return to step 3. When no token fits a remaining finding, leave it and continue to the reply with its reason.","7. When the errors are clear, run both checkers with `--strict`. Report what `--strict` adds. Clear warnings within the existing request. Otherwise ask whether to clear the warnings now.","8. When the repair scope includes warnings, return to step 3 with `--strict`. When strict checks pass or the user defers warnings, continue to the reply.","9. Reply with:"," - the changes by rule, each with its count and any visible shift"," - the findings left, each with its reason and any config entry the user chose"," - both checker commands with their exit codes","","`check-page <path>` scopes a run to one page. `check-component <id>` scopes a run to one component: its runtime, its editor, and its registration. The checkers read tokens.css from its default location.","","When `package.json` has no `check:design` script, add `\"check:design\": \"live-tokens check-page && live-tokens check-component\"`. When both checkers exit 0, prepend `npm run check:design &&` to the existing build command. Preserve its other build steps.","","## Scope","","- Add no token to `tokens.css`. Map a literal with no matching token to the nearest existing token by role. When no token fits, leave the finding and say so.","- When the user has chosen to lower a rule's severity, record it in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`. `--off=<rule>` silences a rule for one run only.","- When the nearest token differs from the literal, use the token and name the shift in the report, such as `14px` to `--space-16`.","","## Color by role","","`color-literal` is a judgement finding. The replacement is the token for the role the color plays. The theme moves every role together. `npx live-tokens tokens --scale <name>` prints a scale's names and values, with `--json` for data.","","| Literal | Token | Notes |","| --- | --- | --- |","| Text on a surface | `--text-primary` through `--text-disabled` | The neutral text scale. `--text-<family>` for a family color. |","| Light text on a dark chip | `--text-inverted` | No AA guarantee. |","| A surface fill | `--surface-<family>-<level>` | The role names the family: `neutral` for chrome, `brand` for emphasis, `danger` for status. |","| A stroke | `--border-<family>-<level>` | Levels run `faint` to `strong`. |","| A translucent layer that dims what is behind it | `--scrim-low`, `--scrim`, `--scrim-high` | Behind a modal. |","| A translucent wash on a surface | `--tint-low`, `--tint`, `--tint-high` | A hover state. |","| Any other translucent color | The role's token at an opacity: `color-mix(in srgb, var(--surface-brand) 80%, transparent)` | The editor reads that form. |","| Fully transparent | `--color-transparent` | |","| A gradient | `--gradient-*` | Or compose one from surface tokens. |","","## Geometry by scale","","`dimension-literal` is a mechanical finding. A size, such as a hero's height, is layout. Leave it.","","| Literal | Token | Notes |","| --- | --- | --- |","| Spacing | `--space-<px>` | `npx live-tokens tokens --scale space` prints the steps. Round to the nearest step. |","| A stroke width | `--border-width-1`, `-2`, `-4` | Also for `outline`. |","| A corner | `--radius-sm` through `--radius-4xl`, or `--radius-full` | |","| A shadow | `--shadow-sm` through `--shadow-xl` | Replace the whole value. |","| Part of a `calc()` | The token inside the calc | `calc(var(--space-64) * -2 + var(--space-8))` |","| A duration or easing | `--duration-*`, `--ease-*` | No rule reports it. Fix it while in the file. |","| A `blur()` | `--blur-*` | No rule reports it. Fix it while in the file. |","","## The remaining rules","","| Rule | Fix |","| --- | --- |","| `unknown-token` | Search `tokens.css` for the stem. When a contract-family name is gone, `npx live-tokens migrate --check` lists the migration that adds the current name. |","| `raw-text-axis` | Set every axis from one text style, `-font-family` through `-letter-spacing`. `npx live-tokens tokens --scale heading` prints one text style. The text styles are `heading`, `body`, `editorial`, and `code`. Rewrite a `font:` shorthand the same way. |","| `unknown-component` | Read **live-tokens-pick-component** for the shipped component that fits. When none fits, author one with **live-tokens-create-component**. |","| `unknown-prop` | `npx live-tokens components <id>` prints the declared props and their values. Map the prop to one of them, or delete it. |","| `unknown-prop-value` | Use a value from the union the message lists. |","| `control-size` | Delete the `size` prop. The shipped default is the page's size. When that default is wrong for the project, retune the component in `/live-tokens/components`. |","| `multiple-primary` | Keep the action that completes the main task `primary`. A Button with no `variant` counts as `primary`. Use `secondary` for supporting or related actions and `outline` for unrelated or informational actions. |","| `danger-without-dialog` | Open a `Dialog` from the danger Button or IconButton and run the action from the Dialog's confirm. The rule fires once per page, when the page imports no Dialog. For other actions, assign emphasis by the action's relationship to the main task. |","| `native-control` | Replace the native element with the shipped component the message names: Button or IconButton, Input, MenuSelect. |","| `property-override` | Delete the declaration from the page. Retune the component's token for the whole project at `/live-tokens/components`. |","| `page-component-paint` | The finding names the page file and the line of the instance whose part painted a value its semantic property never resolves to. Remove the global rule reaching past the component, from `site.css` or the page's own CSS, and retune the component's token for the whole project at `/live-tokens/components`. |","| `page-text-style` | The finding names the page file and the line of the text element and the nearest bundle it missed. Set the container's text style directly on the text element, one shipped bundle from `npx live-tokens tokens --scale heading` (or `body`, `editorial`, `code`), rather than leaving it to inherit from an ancestor typed for a different role. |","| `page-contrast` | The finding names the page file and the line of the text element, the surface ancestor, and both computed colors. Pick the text token the surface pairs with, from the Color by role table above. |","| `page-grid` | The finding names the page file and the line of the section whose edge sits off a column line. Move the edge to the line: place it by page-column numbers per the Grid section of **live-tokens-create-page**, and give a centered section symmetric insets rather than a margin, a width, or a transform. |","| `page-overflow` | The finding names the page file and the line of the element or the instance that overflows. Give the control its shipped width, remove a fixed width that does not fit its column at the viewport, and set `overflow-x: auto` only on an element deliberately meant to scroll, such as a code block. When the overflow is the page grid itself at a phone width, collapse it: `grid-template-columns: 1fr`, `column-gap: 0`, per the Grid section of **live-tokens-create-page**. |","| `hardcoded-columns` | `repeat(var(--columns-count), 1fr)` for the page grid. `calc(var(--columns-count) - 2)` for a sub-grid spanning fewer columns. |","| `site-css-in-main` | Delete the import from `main.ts`. Add it to each page's `<script>`. Page CSS then stays off the editor routes. |","| `missing-source` | Add `source: 'src/...'` to the route entry. |","| `reserved-route` | Move the route out of `/live-tokens/*`. |","| `deep-import` | Import from `@motion-proto/live-tokens`, `/component-editor`, or `/components/<Name>.svelte`. |","| `fix: property-name` | Rename the token to the name a shipped component uses for the same role. The vocabulary and the state model are in **live-tokens-create-component**. |","| `fix: property-token` | Make the `:global(:root)` default read a design token, composed when needed. Declare a structural keyword, such as `start`, in the editor's `intrinsics`. |","| `fix: runtime` | Wire the component as the recipe in **live-tokens-create-component** wires it. |","| `fix: runtime-defaults` | Make the `:global(:root)` default the value Reset should restore, per the Runtime component section of **live-tokens-create-component**. |","| `fix: editor` | The editor names a token the runtime never declares, or a property targets the wrong part. Fix the editor's schema, states, or preview props by the Component editor section of **live-tokens-create-component**. |","| `fix: registration` | Register the component in the shared module the Registration section of **live-tokens-create-component** wires up, importable by the app and by `check-component --tests`. |","| `fix: sketch` | Add the missing Sketch part or marker, per the Sketch mode and overlays section of **live-tokens-create-component**. |","| `fix: tooling` | Install the named package (`@playwright/test`, `vitest`, or `happy-dom`) as a devDependency, then `npx playwright install chromium` for a missing browser. A bad path or config is named in the message; fix it and rerun `check-component <id> --tests`. |","| `fix: coverage` | A contract obligation never ran to a result. Add the missing contract, or find why the suite skipped it, then rerun. |","","A `tests-*` finding names a problem with the run itself: the tool, the path, or a missing contract. Fix what the message names and rerun `check-component <id> --tests --json` until every applicable rule passes with no rule left `--off`."],
26
+ "references/design-directions.md": ["# Design directions: feelings, idioms, and occasions","","Read this once the request names a feeling, a design idiom, an era, a genre, a","holiday, a season, or a natural scene. Each entry places the request and gives","the direction the three intents come from.","","The mechanics live with the executors. Color anchors are in","live-tokens-set-colors, type anchors in live-tokens-set-type, geometry anchors","in live-tokens-set-geometry, each keyed on the same names as the tables below.","Name the anchor in the intent, and the sibling reads its own column.","","Three axes place any request, including one no entry lists:","","| Axis | Reads as | Carried by |","|---|---|---|","| Valence | pleasant against unpleasant | lightness, above everything else |","| Energy | aroused against calm | chroma, and hue distance on screen |","| Dominance | assertive against yielding | surface contrast, type weight, border weight, tightness |","","An idiom sets constraints and a feeling moves dials, so a request that names","both (\"cozy brutalist\", \"clinical Swiss\") reads the idiom first and lets the","feeling move the dials inside it.","","## Feelings","","Valence and energy set the quadrant, and the table runs in quadrant order:","pleasant-aroused, pleasant-calm, unpleasant-aroused, unpleasant-calm. Dominance","separates confident from gentle inside one quadrant and lives almost entirely","outside color.","","| Request | Placement | Direction |","|---|---|---|","| Joyful, exuberant, energetic | pleasant, high energy | warm and bright throughout, with nothing held back |","| Playful, whimsical | pleasant, high energy, low dominance | four hue families at play, soft and generous |","| Optimistic, hopeful | pleasant, moderate energy | a cool ground lit by one warm counterpoint, like sunrise |","| Confident, bold | pleasant, high energy, high dominance | wide contrast, heavy weight, held tight |","| Serene, tranquil | pleasant, low energy | cool and quiet, with nothing loud anywhere |","| Tender, gentle, romantic | pleasant, low energy, low dominance | a soft warm ground, a narrow range, light weights |","| Cozy, comforting | pleasant, low energy | warm through and through, nothing cool on screen |","| Wistful, nostalgic, vintage, faded | pleasant, low energy | chroma withheld rather than light withheld |","| Earthy, grounded, natural | pleasant, low energy, moderate dominance | warm mineral hues, nothing synthetic |","| Clinical, sterile, precise | neutral, low energy, high dominance | an untinted ground, one cool hue, tightly set |","| Contemplative, focused | neutral, low energy | one cool hue and almost nothing else |","| Urgent, alarming | unpleasant, high energy, high dominance | a neutral ground so the alarm lands, heavy and tight |","| Tense, anxious | unpleasant, high energy | an uncomfortable ground under a pair that vibrates |","| Defiant, rebellious, loud | unpleasant, high energy, highest dominance | near-black under one acid hue, blunt everywhere |","| Melancholy, moody, sad | unpleasant, low energy | dark and cool, holding one moment of color |","| Somber, grave, mournful | unpleasant, low energy, high dominance | near-neutral dark, sharp and quiet |","| Ominous, dramatic, haunted | unpleasant, low energy, high dominance | dark with one hot accent and real atmosphere |","| Austere, severe, cold | unpleasant, lowest energy, highest dominance | monochrome at one extreme of lightness |","","## Idioms, eras, and genres","","The table runs modernist, digital, quiet, print, expressive.","","| Request | Placement | Direction |","|---|---|---|","| Swiss, International | neutral, low energy, high dominance | one hue on a near-white ground, rational, tight |","| Bauhaus | pleasant, high energy, high dominance | primaries at full commitment on paper, geometric, square but for the circle |","| Mid-century modern | pleasant, moderate energy | warm muted mid-tones, soft and open, no borders |","| Art deco, opulent, luxurious | pleasant, low energy, high dominance | dark with one metal, high-contrast type, sharp |","| Terminal, phosphor | neutral, moderate energy, high dominance | one phosphor hue on near-black, mono, bordered |","| Cyberpunk, neon noir, futuristic | unpleasant, high energy, high dominance | dark with two neons and a glow, wide type, sharp |","| Vaporwave | pleasant, moderate energy, low dominance | light sunset pastels with a gradient, retro display, soft |","| Y2K, bubble | pleasant, high energy | a chrome ground under electric color, geometric, pills |","| Blueprint | neutral, low energy, high dominance | a drafting ground with pale rules, technical type, gridded |","| Scandinavian, hygge | pleasant, low energy, low dominance | a chalk ground and muted naturals, soft and open |","| Japandi, wabi-sabi | pleasant, lowest energy | unbleached paper, near-monochrome, generous space, no borders |","| Cottagecore, botanical | pleasant, low energy, low dominance | warm cream and garden hues, serif display, soft |","| Editorial, magazine | neutral, low energy, high dominance | paper and ink with one strong hue, carried by rules |","| Newsprint, broadsheet | neutral, low energy | grey-warm paper under near-black ink, serif throughout, tight |","| Risograph, zine | pleasant, high energy, high dominance | two flat spot inks on paper, expressive display, heavy rules |","| Corporate, professional, trustworthy | pleasant, low energy | cool near-white with navy and teal, conventional everywhere |","| Brutalist | unpleasant, high energy, highest dominance | a pure ground, one alarming hue, heavy type, thick borders |","| Memphis, postmodern | pleasant, highest energy | a pastel ground under four hue families, shapes set against each other |","| Industrial, workshop, gritty | neutral, moderate energy, high dominance | concrete and steel with safety orange, condensed type, thick borders |","","## Occasions","","An occasion fixes color only, so its type and geometry intents come from the","feeling it implies or from the generic tables in the sibling skills.","","Every occasion is a statement request: the named color goes on the ground","rather than only on the buttons.","","| Request | Direction |","|---|---|","| Christmas | red and green with gold, one of the two owning the ground |","| Halloween | pumpkin, violet, and poison green, dark either way |","| St. Patrick's | green with gold over a pale ground |","| Ocean | blues held to one narrow range |","| Sunset | a hue sweep through red, falling in lightness |","| Autumn | parchment under rust, gold, and moss |","| Spring | pastels, greens and pinks over a mint ground |"],
30
27
  },
31
28
  "pick-component": {
32
- "SKILL.md": ["---","name: live-tokens-pick-component","description: Recommend which shipped @motion-proto/live-tokens component fits a UX need, with a decision test for each confusable family. Called by live-tokens-create-page when more than one component could fit, and by live-tokens-create-component before it authors anything. Use when the user asks which component to use, or what the difference between two components is. Use when the user asks how to show or capture a UX outcome. Edits no file. For size, emphasis, or placement, read live-tokens-create-page. When the catalogue lacks a component with chrome, read live-tokens-create-component.","---","","# Picking a live-tokens component","","When more than one shipped component could fit, find the family below that names the candidates. Apply its test, which asks what the choice means to the reader.","","## Catalogue","","Before choosing, run `npx live-tokens components`. The list holds every component the project has, shipped and custom, with each one's variants and usage comment. The family tests below name the shipped set only. Weigh a custom component by the same tests.","","## Action family","","- The action needs a word to be unambiguous: `Button`.","- The glyph alone is plain (close, edit, delete) and space is short: `IconButton`.","- The pair that confirms or cancels an inline edit: `InlineEditActions`.","","## Single-selection family","","Four components pick one option from a set. The test is the option count, whether the selection swaps the content below, and how much the choice asks of the reader.","","| Component | Test | Option count |","|---|---|---|","| `SegmentedControl` | An inline switch between views of the same data. It sits in a row of controls. | 2 to 4 |","| `TabBar` | The content area below swaps. | 2 to 7 |","| `RadioButton` | The reader reads every option as text inside a larger form. | any |","| `MenuSelect` | The options would overflow a row. | any |","","- When a label would wrap in a `SegmentedControl`, use `RadioButton` rows.","- The URL changes: `SideNavigation`. Sections inside one page: `TabBar`.","","## Text entry","","The test is whether the answer comes from a predefined list of options.","","- A predefined list (a status, a currency, a size, a country): the single-selection family, by its own test. Up to four options sit in a row; more go in a `MenuSelect`, which scrolls.","- No list (a name, a search string, an amount, a message, a street address): `Input`. Validation keeps a typed answer well-formed.","- A long list the reader would rather filter by typing (a city): no shipped component filters a list. Use `Input` with validation, or author a filtering select with **live-tokens-create-component**.","- A number where the position on a track carries the meaning (a volume, a price band, a percentage): `Slider`. A number the reader knows and would rather type: `Input` with `type=\"number\"`.","","## On and off","","Three components express a binary choice. The test is whether the two states have names of their own.","","| Component | Test |","|---|---|","| `Toggle` | A setting that takes effect at once. The label names the setting. The switch position is the state. |","| `SegmentedControl` | Two named alternatives the reader compares (Light / Dark, List / Grid). Both labels show at once. |","| `RadioButton` pair | A yes or no the reader answers inside a larger form. |","","When the two states share the feature's one name, use `Toggle`. \"Email notifications\" has no \"off\" label.","","## Container family","","Four components hold a block of content. The test is what the block is to the reader: one item, a section of the page, secondary content that stays collapsed until opened, or a decision.","","| Component | Modality | Test |","|---|---|---|","| `Card` | Inline, always open | One item, or each item in a set: a product, a record, a plan. It has a title and can react to hover. |","| `Panel` | Inline, always open | One section of the page's content in a frame: a stage, a list, a form, a block of copy. `minHeight` holds its height while the content changes. |","| `CollapsibleSection` | Inline, collapsed until the reader opens it | Secondary content most readers skip. |","| `Dialog` | Modal, blocks the page | A decision the page cannot continue without: a destructive confirmation, payment, sign-in. |","","A set of items is one `Card` per item. A routine form goes inline in a `Panel`.","","## Messaging family","","Five components carry a message. The test is what the message is about, what brings it on, and whether the reader dismisses it.","","| Component | Scope | Trigger | Dismissable | Test |","|---|---|---|---|---|","| `Callout` | A section | Always present | No | Something the reader must know about the content around it |","| `Notification` | The system | An action or event | Yes | Feedback about something that just happened |","| `Tooltip` | An element | Hover or focus | On leave | A definition or hint the reader can do without |","| `Badge` | An element | Always present | No | A standing label read at a glance (\"Beta\", \"New\", \"v2\") |","| `CornerBadge` | A parent's corner | Always present | No | A count or status marker on the thing it describes |","","`Badge` and `CornerBadge` differ in position only.","","## Display family","","Each pair holds a block the reader views and one the reader interacts with. The test is which of the two the page needs.","","- A picture the page shows: `Image`. A picture whose detail the reader must open, or a gallery: `ImageLightbox`.","- Records the reader scans and compares: `Table`. A set of items the reader acts on: one `Card` per item.","- A read-out of progress: `ProgressBar`. A number the reader sets: `Slider`.","- Text the reader runs or pastes (an install command, a key, an id): `CodeSnippet`. Prose the reader only reads: a paragraph in its `Card` or `Panel`.","- A titled break between the sections of one page: `SectionDivider`. Movement between pages: `SideNavigation`.","","## Nothing fits","","A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage. When nothing in the catalogue fits a piece with chrome (a `DatePicker`, a `Stepper`), author the component with **live-tokens-create-component**. Size, emphasis, and placement are **live-tokens-create-page**'s.","","`npx live-tokens components <id>` prints one component's usage comment, its declared props, and the values each union accepts. `--json` returns the same as data."],
29
+ "SKILL.md": ["---","name: live-tokens-pick-component","description: Recommend which shipped @motion-proto/live-tokens component fits a UX need, with a decision test for each confusable family. Called by live-tokens-create-page when more than one component could fit, and by live-tokens-create-component before it authors anything. Use when the user asks which component to use, or what the difference between two components is. Use when the user asks how to show or capture a UX outcome. Edits no file. For size, emphasis, or placement, read live-tokens-create-page. When the catalogue lacks a component with chrome, read live-tokens-create-component.","---","","# Picking a live-tokens component","","When more than one shipped component could fit, find the family below that names the candidates. Apply its test, which asks what the choice means to the reader.","","## Catalogue","","Before choosing, run `npx live-tokens components`. The list holds every component the project has, shipped and custom, with each one's variants and catalogue entry. The family tests below name the shipped set only. Weigh a custom component by the same tests.","","## Action family","","- The action needs a word to be unambiguous: `Button`.","- The glyph alone is plain (close, edit, delete) and space is short: `IconButton`.","- The pair that confirms or cancels an inline edit: `InlineEditActions`.","","## Single-selection family","","Four components pick one option from a set. The test is the option count, whether the selection swaps the content below, and how much the choice asks of the reader.","","| Component | Test | Option count |","|---|---|---|","| `SegmentedControl` | An inline switch between views of the same data. It sits in a row of controls. | 2 to 4 |","| `TabBar` | The content area below swaps. | 2 to 7 |","| `RadioButton` | The reader reads every option as text inside a larger form. | any |","| `MenuSelect` | The options would overflow a row. | any |","","- When a label would wrap in a `SegmentedControl`, use `RadioButton` rows.","- The URL changes: `SideNavigation`. Sections inside one page: `TabBar`.","","## Text entry","","The test is whether the answer comes from a predefined list of options.","","- A predefined list (a status, a currency, a size, a country): the single-selection family, by its own test. Up to four options sit in a row; more go in a `MenuSelect`, which scrolls.","- No list (a name, a search string, an amount, a message, a street address): `Input`. Validation keeps a typed answer well-formed.","- A long list the reader would rather filter by typing (a city): no shipped component filters a list. Use `Input` with validation, or author a filtering select with **live-tokens-create-component**.","- A number where the position on a track carries the meaning (a volume, a price range, a percentage): `Slider`. A number the reader knows and would rather type: `Input` with `type=\"number\"`.","","## On and off","","Three components express a binary choice. The test is whether the two states have names of their own.","","| Component | Test |","|---|---|","| `Toggle` | A setting that takes effect at once. The label names the setting. The switch position is the state. |","| `SegmentedControl` | Two named alternatives the reader compares (Light / Dark, List / Grid). Both labels show at once. |","| `RadioButton` pair | A yes or no the reader answers inside a larger form. |","","When the two states share the feature's one name, use `Toggle`. \"Email notifications\" has no \"off\" label.","","## Container family","","Four components hold a block of content. The test is what the block is to the reader: one item, a section of the page, secondary content that stays collapsed until opened, or a decision.","","| Component | Modality | Test |","|---|---|---|","| `Card` | Inline, always open | One item, or each item in a set: a product, a record, a plan. It has a title and can react to hover. |","| `Panel` | Inline, always open | One section of the page's content in a frame: a stage, a list, a form, a block of copy. `minHeight` holds its height while the content changes. |","| `CollapsibleSection` | Inline, collapsed until the reader opens it | Secondary content most readers skip. |","| `Dialog` | Modal, blocks the page | A decision the page cannot continue without: a destructive confirmation, payment, sign-in. |","","A set of items is one `Card` per item. A routine form goes inline in a `Panel`.","","## Messaging family","","Five components carry a message. The test is what the message is about, what brings it on, and whether the reader dismisses it.","","| Component | Scope | Trigger | Dismissable | Test |","|---|---|---|---|---|","| `Callout` | A section | Always present | No | Something the reader must know about the content around it |","| `Notification` | The system | An action or event | Yes | Feedback about something that just happened |","| `Tooltip` | An element | Hover or focus | On leave | A definition or hint the reader can do without |","| `Badge` | An element | Always present | No | A standing label read at a glance (\"Beta\", \"New\", \"v2\") |","| `CornerBadge` | A parent's corner | Always present | No | A count or status marker on the thing it describes |","","`Badge` and `CornerBadge` differ in position only.","","## Display family","","Each pair holds a block the reader views and one the reader interacts with. The test is which of the two the page needs.","","- A picture the page shows: `Image`. A picture whose detail the reader must open, or a gallery: `ImageLightbox`.","- Records the reader scans and compares: `Table`. A set of items the reader acts on: one `Card` per item.","- A read-out of progress: `ProgressBar`. A number the reader sets: `Slider`.","- Text the reader runs or pastes (an install command, a key, an id): `CodeSnippet`. Prose the reader only reads: a paragraph in its `Card` or `Panel`.","- A titled break between the sections of one page: `SectionDivider`. Movement between pages: `SideNavigation`.","","## Nothing fits","","A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage. When nothing in the catalogue fits a piece with chrome (a `DatePicker`, a `Stepper`), author the component with **live-tokens-create-component**. Size, emphasis, and placement are **live-tokens-create-page**'s.","","`npx live-tokens components <id>` prints one component's catalogue entry, its declared props, and the values each union accepts. `--json` returns the same as data."],
33
30
  },
34
31
  "set-colors": {
35
32
  "SKILL.md": ["---","name: live-tokens-set-colors","description: Set a live-tokens theme's color: ten OKLCH base colors, a light or dark scheme, and a WCAG AA-gated contrast check. Called with an anchor and a color intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks for a palette, colors, or hues by mood, style, era, season, holiday, or hue. Use when the user names only a color. Use when the user refines a theme's color: warmer, cooler, calmer, louder, lighter, darker, moodier, more contrast. Changes color only. For a request that also names type or geometry, read live-tokens-create-theme.","---","","# Setting a theme's colors","","Choose ten base colors. The CLI builds every ramp from them, checks AA","contrast, and prints a contrast report. Never write theme JSON by hand and","never edit the data tree.","","The result is on screen as soon as the run finishes. The three set skills","write the same buffer, so color, type, and geometry compose in any order. When","the user accepts the result, run `save-theme` to keep it as a theme. Loading a","theme in the editor discards it.","","## Workflow","","1. Read the color intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, an occasion), read its entry in `references/color-anchors.md`; it overrides the generic ranges below.","2. Translate the intent into ten base colors with the framework below and write them to `scratch/<slug>-base-colors.json`. Keep this file for later refinements. The saved theme also records the base colors.","3. Run `npx live-tokens set-colors scratch/<slug>-base-colors.json`.","4. Read the report. Exit 0 passes, and auto-corrected values count as passing. On a contrast failure, exit 1 names the base color to change and the move: raise its lightness or reduce its chroma. On a bad file, exit 1 names the field. Fix the file and re-run.","5. Reply with the anchor if any, the scheme, the hue families, the Canvas base color, and anything the contrast report auto-corrected.","","`--dry-run` prints the report without writing.","","## The base color file","","```json","{"," \"scheme\": \"light\","," \"baseColors\": {"," \"Brand\": { \"l\": 0.62, \"c\": 0.17, \"h\": 145 },"," \"Accent\": { \"l\": 0.80, \"c\": 0.15, \"h\": 95 },"," \"Special\": { \"l\": 0.60, \"c\": 0.19, \"h\": 300 },"," \"Canvas\": { \"l\": 0.93, \"c\": 0.04, \"h\": 120 },"," \"Neutral\": { \"l\": 0.55, \"c\": 0.012, \"h\": 140 },"," \"Alternate\": { \"l\": 0.58, \"c\": 0.009, \"h\": 60 },"," \"Info\": { \"l\": 0.60, \"c\": 0.15, \"h\": 255 },"," \"Success\": { \"l\": 0.60, \"c\": 0.16, \"h\": 150 },"," \"Warning\": { \"l\": 0.75, \"c\": 0.15, \"h\": 85 },"," \"Danger\": { \"l\": 0.58, \"c\": 0.20, \"h\": 25 }"," }","}","```","","A base color is the one color a palette's whole ramp derives from.","","- `baseColors`: all ten required. Each is an OKLCH triple or a `\"#rrggbb\"` string in its place. `l` is lightness, above 0 and below 1. `c` is chroma, 0 for grey and at most 0.4. `h` is hue in degrees.","- `scheme`: `\"light\"` or `\"dark\"`.","- `canvasGradient` (optional): a boolean, default off. See Canvas sky and shadows.","- `harmony` (optional): `{ \"mode\": \"<mode>\" }`, a record of the harmony the base colors follow. The CLI validates the mode and derives nothing from it. The modes are the ones the Harmony section names, plus `custom`.","","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.","","## Chroma budget","","The more area a palette covers, the less chroma it gets.","","| Tier | Palettes | Chroma |","|---|---|---|","| Ground (about 60% of every screen) | Neutral, Alternate | C 0.008 to 0.02 |","| Canvas (the largest single area) | Canvas | C 0.02 to 0.14, by commitment level (see Canvas commitment) |","| Dominant chromatic (about 30%) | Brand | C 0.10 to 0.20 |","| Garnish (about 10%) | Accent, Special | may exceed Brand; at most one at the gamut cap for its hue (see Gamut guardrails) |","| Conditional | Info, Success, Warning, Danger | C 0.12 to 0.19 |","","A good theme reads as 3 or 4 hue families on screen, never 10. Neutral and Alternate stay near-grey, tinted toward the theme: Neutral near Brand's hue, Alternate offset 15 to 60 degrees or a warm/cool counterpoint. Pure C = 0 only when an anchor calls for it.","","## Per-role ranges","","| Base color | Light scheme | Dark scheme | Hue |","|---|---|---|---|","| 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 |","| Neutral, Alternate | L about 0.55, C 0.008 to 0.02 | same | per the chroma budget |","| 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 request's identity hue |","| Accent | harmony slot, or at least 0.25 L from Brand when the mode collapses hue distance | lighten and desaturate like Brand | harmony slot |","| Special | most expressive; default Brand hue +60 at about 65% of Brand's C | same transform | harmony slot |","| Info | shared status L (0.55 to 0.65 light) | lighten like Brand | H 230 to 260 |","| Success | shared status L | same | H 140 to 155 |","| Warning | L 0.75 or higher (vivid yellow must be light) | same | H 70 to 90 |","| Danger | shared status L, C 0.15 to 0.20 | same | H 20 to 30 |","","Three rules cross every role:","","- A dark scheme transforms every base color: each chromatic one lightens to L 0.75 to 0.85 and drops about a third of its chroma, because saturated color vibrates on dark grounds.","- 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.","- Status hues never rotate with the harmony; only their L and C adapt to the mood.","","**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, so near-white is a choice for a clean or minimal intent, never a default. The canvas sits in one of three ranges:","","1. *Tinted paper* (most UI intents): C 0.02 to 0.06, with L down to 0.92 where the hue needs room.","2. *Colored ground* (expressive intents): L 0.85 to 0.92 at C 0.05 to 0.10. The page is unmistakably mint, parchment, sky.","3. *Full-color ground* (holiday and statement intents): 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.","","Blue tints cap very low at high L (H 264 at L 0.95 barely reaches C 0.03), so lower L for a blue canvas rather than fighting the ceiling; yellow, green, and cream tint generously at high L. Across 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.","","## Mood dials","","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.","","Warm is hues 20 to 110 plus pink 290 to 360; cool is 140 to 290. For an intent that names a feeling, read `references/color-anchors.md` rather than guessing the dial settings.","","Avoid mid-lightness yellow-green (H 100 to 120 at L 0.5 to 0.7, C about 0.1) unless the intent asks for olive or toxic.","","## Gamut guardrails","","The engine clamps to gamut regardless; these keep the intent achievable.","","- 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.","- 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.","- Teal and sky cap at C 0.15.","- 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.","","## Harmony","","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.","","- A vague or single-adjective intent 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%.","- An intent naming two colors: measure their hue gap and pick the matching mode (green plus gold is 60 to 90 degrees, so analogous or compound).","- Drama or maximum contrast: complementary, triadic, or tetradic, and then tone one side down, since max-chroma text on a near-black ground vibrates.","","## Canvas sky and shadows","","`\"canvasGradient\": true` renders the page background as a vertical gradient from the Canvas ramp. Default off. Turn it on only when the intent evokes atmosphere (sky, night, dusk, glow, underwater) or asks for a gradient outright; keep it off for crisp, flat, minimal, or corporate intents and whenever in doubt, because a sky on every theme stops meaning anything. It needs a Canvas base color with L above 0.10 and below 0.90. Outside that range the engine skips it and says so. Say why it is on, in one line.","","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 base color's L.","","## Refining a theme's color","","\"Warmer\", \"calmer\", \"more contrast\" arrive against a theme that is already open, and the answer is a new base color file. Edit `scratch/<slug>-base-colors.json` when it is still there. When it is not, recover the ten base colors from `src/live-tokens/data/themes/<slug>.json`: each one sits at `colorsAndType.editorConfigs.<Palette>.baseColor` as an OKLCH triple, and the Canvas base color's lightness gives the scheme. Rebuild the base color file from those values, move the dial the user named, and re-run.","","A re-run replaces the buffer's palette state, including palette edits made in the editor since the last run. Swatch gradients tuned in the editor carry through, and the report says which. Say so once when iterating.","","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 base color 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.","","## Scope","","Color only. Fonts, geometry, saved themes, `tokens.css`, and `fonts.css` are","untouched: `set-colors` replaces the color state in the buffer and carries","every other value in it forward. `save-theme` keeps the result; Adopt ships it.","","## Verify","","- The CLI exits 0 with every check passing (auto-corrected is fine), and the report names which layer the non-color values came from.","- The app (dev server running) shows the new palette.","- The editor's Theme panel marks the open theme as edited. A dry run marks nothing, and neither does a run whose report says the open theme or the shipped default already holds these colors.","- The canvas is committed: on screen it reads as the theme's color rather than as generic near-white.","- To revert, re-run with the previous base color file, or load the open theme again to discard the buffer."],
36
- "references/color-anchors.md": ["# Color anchors: feelings, idioms, and occasions","","Read this when the color intent names one of these. Entries are starting","points: apply the chroma budget, the per-role bands, and the canvas commitment","rules from SKILL.md on top of them.","","An idiom sets constraints and overrides the generic defaults in SKILL.md. The","polished-UI Accent at 45% of Brand's chroma is right for a vague intent and","wrong for Bauhaus. A feeling moves dials, and moves them inside an idiom's","constraints when the intent names both.","","Energy spent on the ground tier fights the contrast gate, so keep chroma on the","garnish. A low-valence, low-energy intent taken literally reads as broken","rather than sad, which is why every dark entry below holds one moment of color.","","Riso, Memphis, and brutalist break the chroma budget on purpose. Break it in","the one layer the style is about and hold the rest of the ground tier down.","","## Feelings","","| Anchor | Anchors (L, C, H) |","|---|---|","| 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 |","| Playful, whimsical | tinted canvas (0.93, 0.05, 330), Special at full chroma; tetradic, the rare request that wants four hue families |","| 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 |","| 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 |","| Serene, tranquil | cool canvas (0.95, 0.03, 200), nothing above C 0.08, analogous 160 to 240 |","| 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 |","| 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 |","| Wistful, nostalgic, vintage, faded | faded canvas (0.92, 0.03, 70), every chromatic base color capped at C 0.10, hues warm and close. The feeling is chroma withheld, not darkness |","| 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 |","| Clinical, sterile, precise | near-white canvas at C 0.01, the one request an untinted ground suits; one cool Brand 200 to 260 at C 0.10; statuses carry the only other color |","| 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 |","| 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 |","| 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 |","| Defiant, rebellious, loud | near-black canvas (0.15, 0.01, 0), one acid hue (0.85, 0.20, 120), nothing else chromatic |","| Melancholy, moody, sad | dark; canvas (0.22, 0.03, 250), chromatic base colors 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 |","| Somber, grave, mournful | near-neutral dark canvas (0.18, 0.01, 260), one desaturated Brand, gradient off |","| Ominous, dramatic, haunted | dark canvas (0.15, 0.04, 300), one hot accent (0.75, 0.16, 30) used sparingly, canvasGradient on |","| Austere, severe, cold | monochrome; canvas at either L extreme at C 0.01 or below, one low-chroma Brand, muted statuses |","","## Idioms, eras, and genres","","| Anchor | Anchors (L, C, H) |","|---|---|","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| Y2K, bubble | chrome canvas (0.96, 0.015, 240), electric blue Brand (0.62, 0.18, 255), lime Accent (0.85, 0.17, 130) |","| Blueprint | canvas (0.35, 0.07, 245), pale rules (0.90, 0.02, 240), one warm accent (0.75, 0.14, 60); dark |","| 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 |","| 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 |","| 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 |","| 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 |","| Newsprint, broadsheet | grey-warm canvas (0.91, 0.02, 80), near-black ink, nothing chromatic above C 0.10 |","| 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 |","| 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 |","| 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) |","| 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 |","| 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 |","","## Occasions","","An occasion is a statement request. Default to canvas commitment level 2 or 3,","never cream, and put the named color on the ground rather than only on the","buttons.","","| Anchor | Anchors (L, C, H) | Strongest form |","|---|---|---|","| 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. |","| 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. |","| St. Patrick's | green (0.51, 0.13, 152) | green Brand, gold Accent, white or beige neutrals. |","| Ocean | deep blue (0.35, 0.08, 237), aqua (0.78, 0.12, 214) | hues held to 180 to 240. |","| Sunset | hues 90 to 320 through red | L falls 0.85 to 0.40 across the sweep. |","| 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. |","| Spring | pastels L 0.85 to 0.95, C 0.04 to 0.10 | greens 130 to 150, pinks 0 to 20, mint canvas. |"],
33
+ "references/color-anchors.md": ["# Color anchors: feelings, idioms, and occasions","","Read this when the color intent names one of these. Entries are starting","points: apply the chroma budget, the per-role ranges, and the canvas commitment","rules from SKILL.md on top of them.","","An idiom sets constraints and overrides the generic defaults in SKILL.md. The","polished-UI Accent at 45% of Brand's chroma is right for a vague intent and","wrong for Bauhaus. A feeling moves dials, and moves them inside an idiom's","constraints when the intent names both.","","Energy spent on the ground tier fights the contrast gate, so keep chroma on the","garnish. A low-valence, low-energy intent taken literally reads as broken","rather than sad, which is why every dark entry below holds one moment of color.","","Riso, Memphis, and brutalist break the chroma budget on purpose. Break it in","the one layer the style is about and hold the rest of the ground tier down.","","## Feelings","","| Anchor | Anchors (L, C, H) |","|---|---|","| 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 |","| Playful, whimsical | tinted canvas (0.93, 0.05, 330), Special at full chroma; tetradic, the rare request that wants four hue families |","| 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 |","| 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 |","| Serene, tranquil | cool canvas (0.95, 0.03, 200), nothing above C 0.08, analogous 160 to 240 |","| 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 |","| 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 |","| Wistful, nostalgic, vintage, faded | faded canvas (0.92, 0.03, 70), every chromatic base color capped at C 0.10, hues warm and close. The feeling is chroma withheld, not darkness |","| 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 |","| Clinical, sterile, precise | near-white canvas at C 0.01, the one request an untinted ground suits; one cool Brand 200 to 260 at C 0.10; statuses carry the only other color |","| 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 |","| 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 |","| 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 |","| Defiant, rebellious, loud | near-black canvas (0.15, 0.01, 0), one acid hue (0.85, 0.20, 120), nothing else chromatic |","| Melancholy, moody, sad | dark; canvas (0.22, 0.03, 250), chromatic base colors 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 |","| Somber, grave, mournful | near-neutral dark canvas (0.18, 0.01, 260), one desaturated Brand, gradient off |","| Ominous, dramatic, haunted | dark canvas (0.15, 0.04, 300), one hot accent (0.75, 0.16, 30) used sparingly, canvasGradient on |","| Austere, severe, cold | monochrome; canvas at either L extreme at C 0.01 or below, one low-chroma Brand, muted statuses |","","## Idioms, eras, and genres","","| Anchor | Anchors (L, C, H) |","|---|---|","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| 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 |","| Y2K, bubble | chrome canvas (0.96, 0.015, 240), electric blue Brand (0.62, 0.18, 255), lime Accent (0.85, 0.17, 130) |","| Blueprint | canvas (0.35, 0.07, 245), pale rules (0.90, 0.02, 240), one warm accent (0.75, 0.14, 60); dark |","| 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 |","| 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 |","| 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 |","| 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 |","| Newsprint, broadsheet | grey-warm canvas (0.91, 0.02, 80), near-black ink, nothing chromatic above C 0.10 |","| 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 |","| 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 |","| 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) |","| 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 |","| 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 |","","## Occasions","","An occasion is a statement request. Default to canvas commitment level 2 or 3,","never cream, and put the named color on the ground rather than only on the","buttons.","","| Anchor | Anchors (L, C, H) | Strongest form |","|---|---|---|","| 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. |","| 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. |","| St. Patrick's | green (0.51, 0.13, 152) | green Brand, gold Accent, white or beige neutrals. |","| Ocean | deep blue (0.35, 0.08, 237), aqua (0.78, 0.12, 214) | hues held to 180 to 240. |","| Sunset | hues 90 to 320 through red | L falls 0.85 to 0.40 across the sweep. |","| 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. |","| Spring | pastels L 0.85 to 0.95, C 0.04 to 0.10 | greens 130 to 150, pinks 0 to 20, mint canvas. |"],
37
34
  },
38
35
  "set-geometry": {
39
- "SKILL.md": ["---","name: live-tokens-set-geometry","description: Set a live-tokens theme's geometry: corner radius, padding, gap, and border width. Each moves per component along its shipped scale. Called with an anchor and a geometry intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks for pill or capsule buttons. Use when the user asks for rounded, sharp, square, softer, or harder corners. Use when the user asks for thicker or thinner borders. Use when the user asks for density: space it out, tighter, denser, airier. Changes geometry only. For a request that also names color or type, read live-tokens-create-theme.","---","","# Setting a theme's geometry","","Write the request as an ops file. The CLI moves each matching alias along its scale, writes the result to each component's buffer, and prints a report. Never hand-edit the data tree.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the geometry intent and the anchor, when live-tokens-create-theme passed one. When the intent or the anchor names a feeling, an idiom, or a genre, read its entry in `references/geometry-anchors.md`.","2. Write the ops file to `scratch/geometry-ops.json`.","3. Run `npx live-tokens set-geometry scratch/geometry-ops.json`. It writes `component-configs/<id>/_working.json` for every component the ops change.","4. Read the report. It lists every changed alias, old and new, and every skip with its reason.","5. When the CLI exits 1, fix the op or the input the message names, then re-run.","6. Reply with every alias that moved and any skip worth naming.","","`--dry-run` prints the report without writing.","","Each run reads the live config, so \"a bit more\" and \"back one\" compound. The live config is the buffer, else the open theme. With no theme loaded, the open theme is the shipped default.","","## The ops file","","Global, relative:","","```json","{ \"ops\": [{ \"kind\": \"radius\", \"shift\": 1 }, { \"kind\": \"padding\", \"shift\": 1 }, { \"kind\": \"gap\", \"shift\": 1 }] }","```","","Targeted, absolute:","","```json","{ \"ops\": [{ \"target\": \"button\", \"kind\": \"radius\", \"set\": \"--radius-full\" }] }","```","","- `target` (optional): a component id, one of the folder names under `src/live-tokens/data/component-configs/`. \"Windows\" or \"modals\" is `dialog`, \"cards\" is `card`, \"tabs\" is `tabbar`. \"The UI\", \"everything\", or no noun means global, so omit `target`.","- `kind`: `radius | padding | gap | border-width | divider-width | accent-width`. `border-width` moves `-border-width` aliases. `divider-width` moves dividers, hairline rules, and `-thickness` aliases. `accent-width` moves accent bars and indicators.","- `set` or `shift`, one of the two. `set` takes a token on that kind's scale. `shift` is a whole number of steps and stops at the ends of the scale.","- `full` (radius shifts only): admits `--radius-full` as the top of the scale. A pill request is `set: \"--radius-full\"` with no `full` flag.","","## Idioms","","The table covers an intent that names no anchor. An anchor's entry in `references/geometry-anchors.md` overrides the table.","","| The intent says | Ops |","|---|---|","| pill, capsule | radius `set: \"--radius-full\"`, plus the padding the pill needs (see Compact containers before controls) |","| sharp, square corners | radius `set: \"--radius-none\"`, or `--radius-sm` for \"mostly sharp\" |","| rounded (a named component) | radius `shift: 2` |","| softer, rounder (global) | radius `shift: 1` to `2`, no `full` |","| harder, sharper | radius `shift: -1` to `-2` |","| increase the radius, less round, more round | radius `shift: 1` or `-1` with `\"full\": true` |","| space it out, airier, breathing room | padding and gap `shift: 1` |","| tighter, denser, more compact | padding and gap `shift: -1` |","| thicker, thinner borders | border-width `shift: 1` or `-1` |","","A theme intent names a direction.","","| The direction is | Geometry |","|---|---|","| playful, friendly, soft | rounder and a step airier. Warm adds pill buttons. |","| luxurious, elegant, editorial | sharper corners, airier padding, thin borders |","| technical, dense, systematic | tighter spacing, a small radius, square corners on containers |","| calm, minimal | unchanged |","","Magnitude follows the qualifier. \"Slightly\" or \"a bit\" is 1 step. No qualifier is 1 to 2 steps. \"Much\", \"way\", or \"really\" is 2 to 3 steps. A mood word often means both axes: \"softer\" is rounder plus airier, \"compact\" is tighter padding plus smaller gaps.","","## Compact containers before controls","","A global op spends the same number of steps everywhere, but a step costs a control more than a container. `padding shift: -2` takes a card from a 16px inset to 10px and it is still a card. The same op takes a button from 8px to its 6px floor, 12px at each end around an 18px line, and the floor stops it there. Below the floor the button stops reading as a button.","","So a global compaction is `shift: -1`. When the request wants more, spend the extra steps on the containers by name and leave the controls alone. The containers are:","","- `card`","- `dialog`","- `panel`","- `collapsiblesection`","- `sidenavigation`","- `table`","- `codesnippet`","","Airier is safe globally, because nothing breaks by growing.","","A pill needs the most room. `--radius-full` bends the corner in over the first and last glyph, so a capsule wants more horizontal inset than a square-cornered control. `--space-8` is the floor for a large-text pill. Compact Midnight Study sits there. The roomier pill presets, Ocean, Sunset, and Royal Velvet, run `--space-10` to `--space-12`. Pair the radius op with a padding `set` on the same target. In the ops list, place the padding `set` after any global padding shift. The later op wins:","","```json","{ \"ops\": ["," { \"kind\": \"padding\", \"shift\": -1 },"," { \"target\": \"button\", \"kind\": \"radius\", \"set\": \"--radius-full\" },"," { \"target\": \"button\", \"kind\": \"padding\", \"set\": \"--space-10\" }","] }","```","","## Scales","","Radius runs `none, sm, md, lg, xl, 2xl, 3xl, 4xl`, with `full` as the gated ninth step. Space (padding and gap) is the editor picker's subset, `0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 48`, so the editor can select every value the CLI writes. Border width is the `--border-width-*` scale from `1` to `24`. A shift never reaches `--border-width-0`, and an alias at 0 is skipped. \"No borders\" is `set: \"--border-width-0\"`.","","An alias off the subset spends its first step reaching the subset, so `--space-2` with `shift: 1` lands on `--space-4`.","","## Floors","","Content insets stop at `--space-4`. Below `--space-4` the text sits against its own edge, so `--space-0` and `--space-2` are values a person picks on purpose, through the editor picker or `set`. An alias below the floor still moves up. A shift that would push one under `--space-4` lands on `--space-4`. An alias already at `--space-4` is skipped, and the report says so.","","Padding around a line of type stops at `--space-6`. A variant that declares a `-text-font-size` holds text. A component that holds text doubles its padding horizontally, so `--space-4` there is 4px over an 18px line and 8px at each end. No shipped default puts text below `--space-6`.","","The floor guards `-padding` only. A 2px gap between an icon and its label, or a 2px margin under a bar, is ordinary design. `-margin` belongs to the `padding` kind, so a padding op moves margins too, without the floor.","","## Scope","","Geometry only. Color, type, saved themes, and `tokens.css` are untouched: `set-geometry` writes existing tokens into each component's buffer and creates no new ones. `save-theme` keeps the result; Adopt ships it.","","## Verify","","- The CLI exits 0 and the report lists the expected changes, with no unexpected skips.","- The app shows the new shape on each changed component.","- Buttons still read as buttons: the label has room at both ends, and a pill has more than a square-cornered control. A control whose padding sits at `--space-6` is on its floor. A control that also carries `--radius-full` needs a targeted lift.","- `component-configs/<id>/_working.json` exists for every component the report listed.","- To revert, run the inverse ops, or load the open theme to discard the buffer."],
40
- "references/geometry-anchors.md": ["# Geometry anchors: feelings, idioms, and genres","","Read this when the geometry intent names one of these. An anchor overrides the","Idioms table in SKILL.md, because it is tuned to the same direction the color","came from, and a style's geometry is often targeted rather than global.","","Entries are written in the ops vocabulary. \"radius +2\" is a radius shift of 2.","\"borders +1\" is a border-width shift of 1. \"padding +1\" is a padding and gap","shift of 1. \"hairline borders\" is border-width `set: \"--border-width-1\"`, and","\"no borders\" is `set: \"--border-width-0\"`. \"hairline rules\" is divider-width","`set: \"--border-width-1\"`. A named component means a targeted op. Controls squeeze before containers,","so a compaction of more than one step still spends its extra steps on","containers by name.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its geometry from","the feeling it implies, or leave geometry alone.","","## Feelings","","| Anchor | Geometry |","|---|---|","| Joyful, exuberant, energetic | radius +2, padding +1, pill buttons |","| Playful, whimsical | pill buttons, rounder cards, wide gaps |","| Optimistic, hopeful | radius +1 |","| Confident, bold | radius -1, borders +1, tight gaps |","| Serene, tranquil | soft radius, padding +1, no borders |","| Tender, gentle, romantic | rounder, hairline borders |","| Cozy, comforting | radius +1, padding +1 |","| Wistful, nostalgic, vintage, faded | unchanged, hairline rules |","| Earthy, grounded, natural | radius +1, padding +1 |","| Clinical, sterile, precise | radius sm, tight gaps, hairline borders |","| Contemplative, focused | padding +1, minimal borders |","| Urgent, alarming | radius -2, borders +2, tight padding |","| Tense, anxious | tight gaps, radius sm |","| Defiant, rebellious, loud | radius none, borders +3 |","| Melancholy, moody, sad | padding +1, hairline borders |","| Somber, grave, mournful | sharp, tight gaps, hairline rules |","| Ominous, dramatic, haunted | sharp, borders +2 |","| Austere, severe, cold | radius none, tight padding, hairline borders |","","## Idioms, eras, and genres","","| Anchor | Geometry |","|---|---|","| Swiss, International | radius none, tight gaps, hairline borders |","| Bauhaus | radius none on containers, radius full on buttons alone, so the circle reads as a decision |","| Mid-century modern | radius +1 to +2, padding +1, no borders |","| Art deco, opulent, luxurious | sharp, padding +1, thin borders |","| Terminal, phosphor | radius none, tight padding, a 1px border on everything |","| Cyberpunk, neon noir, futuristic | sharp, tight gaps |","| Vaporwave | rounder, airier |","| Y2K, bubble | radius full on buttons with the padding a pill needs, generous spacing |","| Blueprint | radius none, 1px borders, tight grid |","| Scandinavian, hygge | soft radius, padding +1, hairline borders |","| Japandi, wabi-sabi | radius sm, padding +2, no borders |","| Cottagecore, botanical | rounder, airier |","| Editorial, magazine | sharp, padding +1, hairline rules |","| Newsprint, broadsheet | radius none, tight gaps, hairline rules |","| Risograph, zine | radius none, borders +2 |","| Corporate, professional, trustworthy | leave it alone |","| Brutalist | radius none, borders +2 to +3, tight padding |","| Memphis, postmodern | targeted rather than global: pill buttons against radius-none cards |","| Industrial, workshop, gritty | radius sm, thick borders, tight padding |"],
36
+ "SKILL.md": ["---","name: live-tokens-set-geometry","description: Set a live-tokens theme's geometry: corner radius, padding, gap, and border width. Each moves per component along its shipped scale. Called with an anchor and a geometry intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks for pill or capsule buttons. Use when the user asks for rounded, sharp, square, softer, or harder corners. Use when the user asks for thicker or thinner borders. Use when the user asks for density: space it out, tighter, denser, airier. Changes geometry only. For a request that also names color or type, read live-tokens-create-theme.","---","","# Setting a theme's geometry","","Write the request as an ops file. The CLI moves each matching alias along its scale, writes the result to each component's buffer, and prints a report. Never hand-edit the data tree.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the geometry intent and the anchor, when live-tokens-create-theme passed one. When the intent or the anchor names a feeling, an idiom, or a genre, read its entry in `references/geometry-anchors.md`.","2. Write the ops file to `scratch/geometry-ops.json`.","3. Run `npx live-tokens set-geometry scratch/geometry-ops.json`. It writes `component-configs/<id>/_working.json` for every component the ops change.","4. Read the report. It lists every changed alias, old and new, and every skip with its reason.","5. When the CLI exits 1, fix the op or the input the message names, then re-run.","6. Reply with every alias that moved and any skip worth naming.","","`--dry-run` prints the report without writing.","","Each run reads the live config, so \"a bit more\" and \"back one\" compound. The live config is the buffer, else the open theme. With no theme loaded, the open theme is the shipped default.","","## The ops file","","Global, relative:","","```json","{ \"ops\": [{ \"kind\": \"radius\", \"shift\": 1 }, { \"kind\": \"padding\", \"shift\": 1 }, { \"kind\": \"gap\", \"shift\": 1 }] }","```","","Targeted, absolute:","","```json","{ \"ops\": [{ \"target\": \"button\", \"kind\": \"radius\", \"set\": \"--radius-full\" }] }","```","","- `target` (optional): a component id, one of the folder names under `src/live-tokens/data/component-configs/`. \"Windows\" or \"modals\" is `dialog`, \"cards\" is `card`, \"tabs\" is `tabbar`. \"The UI\", \"everything\", or no noun means global, so omit `target`.","- `kind`: `radius | padding | gap | border-width | hairline-width | indicator-width`. `border-width` moves `-border-width` aliases. `hairline-width` moves `-hairline-width` aliases. `indicator-width` moves the bar or stripe an indicator draws.","- `set` or `shift`, one of the two. `set` takes a token on that kind's scale. `shift` is a whole number of steps and stops at the ends of the scale.","- `full` (radius shifts only): admits `--radius-full` as the top of the scale. A pill request is `set: \"--radius-full\"` with no `full` flag.","","## Idioms","","The table covers an intent that names no anchor. An anchor's entry in `references/geometry-anchors.md` overrides the table.","","| The intent says | Ops |","|---|---|","| pill, capsule | radius `set: \"--radius-full\"`, plus the padding the pill needs (see Compact containers before controls) |","| sharp, square corners | radius `set: \"--radius-none\"`, or `--radius-sm` for \"mostly sharp\" |","| rounded (a named component) | radius `shift: 2` |","| softer, rounder (global) | radius `shift: 1` to `2`, no `full` |","| harder, sharper | radius `shift: -1` to `-2` |","| increase the radius, less round, more round | radius `shift: 1` or `-1` with `\"full\": true` |","| space it out, airier, breathing room | padding and gap `shift: 1` |","| tighter, denser, more compact | padding and gap `shift: -1` |","| thicker, thinner borders | border-width `shift: 1` or `-1` |","","A theme intent names a direction.","","| The direction is | Geometry |","|---|---|","| playful, friendly, soft | rounder and a step airier. Warm adds pill buttons. |","| luxurious, elegant, editorial | sharper corners, airier padding, thin borders |","| technical, dense, systematic | tighter spacing, a small radius, square corners on containers |","| calm, minimal | unchanged |","","Magnitude follows the qualifier. \"Slightly\" or \"a bit\" is 1 step. No qualifier is 1 to 2 steps. \"Much\", \"way\", or \"really\" is 2 to 3 steps. A mood word often means both axes: \"softer\" is rounder plus airier, \"compact\" is tighter padding plus smaller gaps.","","## Compact containers before controls","","A global op spends the same number of steps everywhere, but a step costs a control more than a container. `padding shift: -2` takes a card from a 16px inset to 10px and it is still a card. The same op takes a button from 8px to its 6px floor, 12px at each end around an 18px line, and the floor stops it there. Below the floor the button stops reading as a button.","","So a global compaction is `shift: -1`. When the request wants more, spend the extra steps on the containers by name and leave the controls alone. The containers are:","","- `card`","- `dialog`","- `panel`","- `collapsiblesection`","- `sidenavigation`","- `table`","- `codesnippet`","","Airier is safe globally, because nothing breaks by growing.","","A pill needs the most room. `--radius-full` bends the corner in over the first and last glyph, so a capsule wants more horizontal inset than a square-cornered control. `--space-8` is the floor for a large-text pill. Compact Midnight Study sits there. The roomier pill presets, Ocean, Sunset, and Royal Velvet, run `--space-10` to `--space-12`. Pair the radius op with a padding `set` on the same target. In the ops list, place the padding `set` after any global padding shift. The later op wins:","","```json","{ \"ops\": ["," { \"kind\": \"padding\", \"shift\": -1 },"," { \"target\": \"button\", \"kind\": \"radius\", \"set\": \"--radius-full\" },"," { \"target\": \"button\", \"kind\": \"padding\", \"set\": \"--space-10\" }","] }","```","","## Scales","","Radius runs `none, sm, md, lg, xl, 2xl, 3xl, 4xl`, with `full` as the gated ninth step. Space (padding and gap) is the editor picker's subset, `0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 48`, so the editor can select every value the CLI writes. Border width is the `--border-width-*` scale from `1` to `24`. A shift never reaches `--border-width-0`, and an alias at 0 is skipped. \"No borders\" is `set: \"--border-width-0\"`.","","An alias off the subset spends its first step reaching the subset, so `--space-2` with `shift: 1` lands on `--space-4`.","","## Floors","","Content insets stop at `--space-4`. Below `--space-4` the text sits against its own edge, so `--space-0` and `--space-2` are values a person picks on purpose, through the editor picker or `set`. An alias below the floor still moves up. A shift that would push one under `--space-4` lands on `--space-4`. An alias already at `--space-4` is skipped, and the report says so.","","Padding around a line of type stops at `--space-6`. A variant that declares a `-text-font-size` holds text. A component that holds text doubles its padding horizontally, so `--space-4` there is 4px over an 18px line and 8px at each end. No shipped default puts text below `--space-6`.","","The floor guards `-padding` only. A 2px gap between an icon and its label, or a 2px margin under a bar, is ordinary design. `-margin` belongs to the `padding` kind, so a padding op moves margins too, without the floor.","","## Scope","","Geometry only. Color, type, saved themes, and `tokens.css` are untouched: `set-geometry` writes existing tokens into each component's buffer and creates no new ones. `save-theme` keeps the result; Adopt ships it.","","## Verify","","- The CLI exits 0 and the report lists the expected changes, with no unexpected skips.","- The app shows the new shape on each changed component.","- Buttons still read as buttons: the label has room at both ends, and a pill has more than a square-cornered control. A control whose padding sits at `--space-6` is on its floor. A control that also carries `--radius-full` needs a targeted lift.","- `component-configs/<id>/_working.json` exists for every component the report listed.","- To revert, run the inverse ops, or load the open theme to discard the buffer."],
37
+ "references/geometry-anchors.md": ["# Geometry anchors: feelings, idioms, and genres","","Read this when the geometry intent names one of these. An anchor overrides the","Idioms table in SKILL.md, because it is tuned to the same direction the color","came from, and a style's geometry is often targeted rather than global.","","Entries are written in the ops vocabulary. \"radius +2\" is a radius shift of 2.","\"borders +1\" is a border-width shift of 1. \"padding +1\" is a padding and gap","shift of 1. \"hairline borders\" is border-width `set: \"--border-width-1\"`, and","\"no borders\" is `set: \"--border-width-0\"`. \"hairline rules\" is hairline-width","`set: \"--border-width-1\"`. A named component means a targeted op. Controls squeeze before containers,","so a compaction of more than one step still spends its extra steps on","containers by name.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its geometry from","the feeling it implies, or leave geometry alone.","","## Feelings","","| Anchor | Geometry |","|---|---|","| Joyful, exuberant, energetic | radius +2, padding +1, pill buttons |","| Playful, whimsical | pill buttons, rounder cards, wide gaps |","| Optimistic, hopeful | radius +1 |","| Confident, bold | radius -1, borders +1, tight gaps |","| Serene, tranquil | soft radius, padding +1, no borders |","| Tender, gentle, romantic | rounder, hairline borders |","| Cozy, comforting | radius +1, padding +1 |","| Wistful, nostalgic, vintage, faded | unchanged, hairline rules |","| Earthy, grounded, natural | radius +1, padding +1 |","| Clinical, sterile, precise | radius sm, tight gaps, hairline borders |","| Contemplative, focused | padding +1, minimal borders |","| Urgent, alarming | radius -2, borders +2, tight padding |","| Tense, anxious | tight gaps, radius sm |","| Defiant, rebellious, loud | radius none, borders +3 |","| Melancholy, moody, sad | padding +1, hairline borders |","| Somber, grave, mournful | sharp, tight gaps, hairline rules |","| Ominous, dramatic, haunted | sharp, borders +2 |","| Austere, severe, cold | radius none, tight padding, hairline borders |","","## Idioms, eras, and genres","","| Anchor | Geometry |","|---|---|","| Swiss, International | radius none, tight gaps, hairline borders |","| Bauhaus | radius none on containers, radius full on buttons alone, so the circle reads as a decision |","| Mid-century modern | radius +1 to +2, padding +1, no borders |","| Art deco, opulent, luxurious | sharp, padding +1, thin borders |","| Terminal, phosphor | radius none, tight padding, a 1px border on everything |","| Cyberpunk, neon noir, futuristic | sharp, tight gaps |","| Vaporwave | rounder, airier |","| Y2K, bubble | radius full on buttons with the padding a pill needs, generous spacing |","| Blueprint | radius none, 1px borders, tight grid |","| Scandinavian, hygge | soft radius, padding +1, hairline borders |","| Japandi, wabi-sabi | radius sm, padding +2, no borders |","| Cottagecore, botanical | rounder, airier |","| Editorial, magazine | sharp, padding +1, hairline rules |","| Newsprint, broadsheet | radius none, tight gaps, hairline rules |","| Risograph, zine | radius none, borders +2 |","| Corporate, professional, trustworthy | leave it alone |","| Brutalist | radius none, borders +2 to +3, tight padding |","| Memphis, postmodern | targeted rather than global: pill buttons against radius-none cards |","| Industrial, workshop, gritty | radius sm, thick borders, tight padding |"],
41
38
  },
42
39
  "set-type": {
43
40
  "SKILL.md": ["---","name: live-tokens-set-type","description: Set a live-tokens theme's type: a Google Fonts pairing for the shipped --font-* stacks. The CLI verifies each family for the weights it ships. Called with an anchor and a type intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks to pair fonts, pick a typeface, or set the fonts. Use when the user describes type by voice: editorial, friendlier, technical, elegant, less generic. Use when the user names a face for a role: a serif for headings, a display font. Changes type only. For a request that also names color or geometry, read live-tokens-create-theme.","---","","# Setting a theme's type","","Choose the families. The CLI verifies each against Google Fonts, builds the URL from the weights the family has, and writes the result to the buffer. Never hand-author font JSON or edit the data tree. Google Fonts is the pool because it is freely licensable and loads by URL. Other sources go in through the editor's Project fonts section.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the type intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, or a genre), read its entry in `references/type-anchors.md`; it overrides the Voice table below.","2. Choose the pairing and write it to `scratch/font-pairing.json`.","3. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's weights and URL, and, under Weight coverage, the weights the typography tokens ask for that the family lacks.","4. Read the report. Name a missing weight and offer an alternative only when it matters: a body face without 400, 700, or italic matters, and a display face without 300 does not. A family not on Google Fonts fails the run. Fix the spelling and re-run. A pairing the stacks already hold prints \"Nothing to change\" and writes nothing. A pairing equal to the open theme's discards the buffer, and the report says so.","5. Reply with the two families, the form model behind each, the matrix verdict, and any missing weight worth naming.","","Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires a URL per family; use it only offline.","","## The pairing file","","```json","{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }","```","","Every slot is optional; an omitted slot keeps its family. `display` is `--font-display` and `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. An omitted `editorial` keeps its family, so set it only when essays and articles need a face of their own. Weight coverage is reported for `display`, `body`, `serif`, and `mono`. A family bound to `editorial` gets no coverage line. A slot may be `{ \"name\": \"...\", \"url\": \"...\" }` to pin a URL. A pinned URL is not probed, so the report shows no weights for it and coverage skips it. Spell families as Google does; the CLI reports the canonical spelling.","","## Choose the body face first","","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 type intent, then pick the display face against it. A body face has regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face missing any of these is a display face, whatever its name says.","","The shipped text styles ask the display face for 600 and the body face for 400; `strong` and `em` add 700 and italic. Screen candidates against those four weights before running.","","## The font matrix","","Classify each candidate by form model and by stroke contrast and serifs.","","| Form model | Construction | Reads as |","|---|---|---|","| **Dynamic** | diagonal stress, open apertures, written origin | open, warm, humane, timeless |","| **Rational** | vertical stress, closed apertures, drawn not written | orderly, reserved, elegant, authoritative |","| **Geometric** | monolinear, circle-and-line | technical, modern, systematic, sober |","","- One form model with different stroke contrast or serifs pairs reliably. Helvetica and Bodoni are both rational, one a linear sans and one a high-contrast serif.","- Different form models with the same stroke contrast and serifs fail. The two look alike and fight underneath. Two arbitrary sans serifs clash for this reason.","- Different on both counts works. An unmistakable difference reads as a decision.","","Many faces sit between columns. When one straddles, say so and lean on the Voice table and the x-height check.","","## Voice","","| The intent says | Type voice |","|---|---|","| editorial, literary, considered | dynamic serif display over a humanist sans body |","| elegant, luxurious, formal | rational high-contrast serif display; keep the body quiet |","| friendly, warm, approachable | dynamic sans on both sides, or a soft serif display |","| technical, systematic, precise | geometric or neo-grotesque sans; a mono for code |","| playful, informal | an expressive display face over a plain workhorse body |","| serious, institutional, trustworthy | rational sans body, rational serif display |","| quiet, minimal, unbranded | one superfamily across both slots |","","The table covers an intent that names no anchor. An anchor's row in `references/type-anchors.md` wins.","","Match the type to the design direction the color came from. A warm autumn palette under a cold geometric sans reads as two projects.","","## Shortcuts","","Use these when the request is vague or the type should stay quiet.","","- **A superfamily.** A Google Fonts family with sans and serif siblings: Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not; `set-type` fails on a family that is gone.","- **One family across weights.**","- **Same designer or foundry.**","- **Serif display over sans body** when nothing else decides it.","","## Watch for","","- **x-height parity.** Both faces share one size scale, so a small-x-height display face over a large-x-height body face gives a heading weaker than its own body text. Check it on the rendered page.","- **Print faces at small sizes.** Delicate serifs and high stroke contrast turn to mud below 16px.","- **Every family is a download.** Two is the target; three needs a reason.","- **Sets of themes.** No two share a display face or a body face.","","## Scope","","Type only. Color, component aliases, shape, and the type scale are untouched: `set-type` writes the font entries in the buffer and carries every other value forward. `save-theme` keeps the result; Adopt ships it and rewrites `fonts.css`, which is how a build without the editor loads the family.","","## Verify","","- The CLI exits 0 and names each stack that moved, before and after.","- Each URL matches the family's weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face. A pinned URL is written as given.","- The app shows the new type, and the editor's Fonts section lists both families with their fallbacks.","- To revert, run the previous pairing file, or load the open theme to discard the buffer."],