@motion-proto/live-tokens 0.72.1 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +37 -2
  2. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +48 -0
  3. package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -2
  4. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -4
  5. package/.claude/skills/live-tokens-create-theme/SKILL.md +80 -0
  6. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  7. package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
  8. package/.claude/skills/live-tokens-set-colors/SKILL.md +140 -0
  9. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  10. package/.claude/skills/{live-tokens-adjust-geometry → live-tokens-set-geometry}/SKILL.md +12 -7
  11. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +61 -0
  12. package/.claude/skills/{live-tokens-pair-fonts → live-tokens-set-type}/SKILL.md +18 -15
  13. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  14. package/CHANGELOG.md +141 -0
  15. package/README.md +24 -15
  16. package/bin/check-page.mjs +3 -3
  17. package/bin/cli.mjs +92 -55
  18. package/bin/lib/liveState.mjs +110 -0
  19. package/bin/save-theme.mjs +177 -0
  20. package/bin/set-colors.mjs +191 -0
  21. package/bin/{adjust.mjs → set-geometry.mjs} +18 -50
  22. package/bin/{set-fonts.mjs → set-type.mjs} +21 -55
  23. package/dist-plugin/{chunk-RIXO2E55.js → chunk-7VRTBGJT.js} +1 -1
  24. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-V3YF6CGT.js} +56 -2
  25. package/dist-plugin/index.cjs +58 -3
  26. package/dist-plugin/index.js +11 -11
  27. package/dist-plugin/migrateData/index.cjs +56 -1
  28. package/dist-plugin/migrateData/index.js +2 -2
  29. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +1109 -1071
  30. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +33 -24
  31. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +33 -24
  32. package/dist-plugin/{generateColorsAndType → setColors}/index.js +45 -63
  33. package/dist-plugin/{adjust → setGeometry}/index.cjs +60 -5
  34. package/dist-plugin/{adjust → setGeometry}/index.d.cts +1 -1
  35. package/dist-plugin/{adjust → setGeometry}/index.d.ts +1 -1
  36. package/dist-plugin/{adjust → setGeometry}/index.js +1 -1
  37. package/dist-plugin/{fontPairing → setType}/index.cjs +4 -4
  38. package/dist-plugin/{fontPairing → setType}/index.d.cts +1 -1
  39. package/dist-plugin/{fontPairing → setType}/index.d.ts +1 -1
  40. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  41. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  42. package/package.json +10 -2
  43. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +82 -96
  44. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  45. package/src/editor/core/themes/migrations/index.ts +2 -0
  46. package/src/editor/docs/content/creating-components.md +13 -0
  47. package/src/editor/docs/content/themes-workflow.md +2 -2
  48. package/src/editor/docs/content.generated.ts +2 -2
  49. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  50. package/src/editor/skill-atlas/SkillAtlas.svelte +836 -0
  51. package/src/editor/skill-atlas/SkillAtlas.svelte.d.ts +4 -0
  52. package/src/editor/skill-atlas/SourcePane.svelte +364 -0
  53. package/src/editor/skill-atlas/TreeNodeCard.svelte +206 -0
  54. package/src/editor/skill-atlas/skillSources.generated.ts +45 -0
  55. package/src/editor/skill-atlas/skillSources.ts +1 -0
  56. package/src/editor/skill-atlas/skillTrees.ts +3844 -0
  57. package/src/editor/skill-atlas/types.ts +65 -0
  58. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  59. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  60. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  61. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  62. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  63. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  64. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  65. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  66. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  67. package/src/live-tokens/data/themes/autumn.json +1 -37
  68. package/src/live-tokens/data/themes/halloween.json +1 -37
  69. package/src/live-tokens/data/themes/midnight-study.json +1 -37
  70. package/src/live-tokens/data/themes/ocean.json +1 -37
  71. package/src/live-tokens/data/themes/royal-velvet.json +1 -37
  72. package/src/live-tokens/data/themes/sketchy.json +1 -37
  73. package/src/live-tokens/data/themes/spring-meadow.json +1 -37
  74. package/src/live-tokens/data/themes/sunset.json +1 -37
  75. package/src/live-tokens/data/tokens.generated.css +0 -36
  76. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  77. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  78. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  79. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  80. package/bin/generate-theme.mjs +0 -260
  81. /package/dist-plugin/{fontPairing → setType}/index.js +0 -0
@@ -0,0 +1,45 @@
1
+ // AUTOGENERATED by scripts/sync-skill-sources.mjs — do not edit by hand.
2
+ // Source of truth: .claude/skills/*/SKILL.md and references/*.md · regenerate: npm run sync:skill-sources
3
+
4
+ export const SKILL_DOC = 'SKILL.md' as const;
5
+
6
+ export const skillDocs: Record<string, Record<string, string[]>> = {
7
+ "build-page": {
8
+ "SKILL.md": ["---","name: live-tokens-build-page","description: Apply the @motion-proto/live-tokens project conventions when building a page: shipped components, theme tokens over hex/pixel literals, dynamic route mounting, per-page site.css. Use when the user asks to build, create, lay out, or rearrange a page, route, hero, landing page, dashboard, settings screen, pricing page, or a tool screen with a stage and controls; add a route; place an existing component on a page; assemble a screen from the catalogue; or says the layout, label sizes, or control sizes of a page are off. For component choice see live-tokens-pick-component; for a brand-new component, live-tokens-create-component; for look and feel mid-build, live-tokens-create-theme or live-tokens-set-geometry.","---","","# Building pages in a live-tokens project","","Two rules above all else:","","1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See **live-tokens-pick-component** for the catalogue and the confusing-pair decisions. Pass only the props it declares, with variant and size values from its union: `npx live-tokens components <id>` prints them (`--json` for data), and the list includes the project's own components beside the shipped ones. A prop a component does not declare is dropped silently at runtime, and the checker reports it. Author custom markup only when nothing fits, and then consider **live-tokens-create-component** so the new piece is editable too.","2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`, whether it sits in the `<style>` block, an inline `style=` attribute, or a `style:` directive. No colour literals in any notation, `white` and `rgb()` included. No px or rem in spacing, stroke, radius, or shadow: that is the geometry the theme owns and `set-geometry` moves. Sizing is layout, not theme: a hero's height, a max content width, or a column's minimum width stays a literal. A change in `/live-tokens/editor` should repaint your page.","","For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--editorial-xl` through `--editorial-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.","","Text inside a `Card` or a `CollapsibleSection` is typed by that container, not by the page: the slot pins the axes the container owns onto nested `p`, `ul`, `ol`, and `li`, so a consumer's global element rules cannot break a card's body. Pass `prose={false}` when the page should own the type instead, which is also what full-bleed media wants.","","## Layout","","**The purpose of a layout.** The page shows one thing. All other content must stay out of its way. Each mark that is not content costs attention: a rule, a border, a header bar, a shadow. Each mark must do a job that no other mark does.","","Separate elements with the smallest difference that separates them. Use space first. If space is not sufficient, add a hairline rule. If a rule is not sufficient, use a second surface. Do not stack these separators. Two heavy edges side by side make a third shape between them. A band of boxes with borders and header bars looks like a set of posters.","","Put each element in one of three layers, and type it from that layer. Content is `--text-primary`. Labels are `--eyebrow-*` or `--text-secondary`. Scaffolding is `--border-neutral`.","","Show related items side by side when the width permits. Do not put them behind a toggle.","","On a tool page, the stage is the content. Each control is administration. Give the space to the stage. Give the controls the smallest size that still works.","","`references/layout-sources.md` names the sources for these laws.","","Decide the bands before the columns. Read the page top to bottom and name each band by its job: what the user looks at, what they type into, what they press. A content page runs hero, sections, footer. A tool page runs the stage on top (the canvas, player, or strip the work is about), the inputs under it, and one toolbar of actions along the bottom edge. Each band is a row of the page grid; a band that needs columns of its own spans the grid and redeclares it, as below.","","Separate bands with space and a rule, `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`, and stretch a band's boxes to one height (`align-items: stretch`) so their bottom edges make one line. Card chrome does not separate bands.","","Pages sit inside the column grid via `--columns-count`, `--columns-gutter`, `--columns-max-width`. The columns button in the overlay's header (the vertical-lines icon) draws the grid over the page while you place content.","","To place children at specific page-column positions, span the parent grid (`grid-column: 1 / -1`), redeclare `repeat(var(--columns-count), 1fr)` with `--columns-gutter`, then refer to children by real page-column numbers. Never fabricate a local `repeat(N, 1fr)` with a hardcoded count: the widths drift from the page grid and the numbers stop matching `ColumnsOverlay`.","","### Containers by job","","- `Panel` is a stage: a canvas, a player, a preview. It pins its height so the page holds still while what it shows changes.","- `Card` is a titled block of content. Its header is typed by the card's own tokens, `--card-default-title-*` at `--font-size-2xl` with a body at `--font-size-xl` by default; `size=\"compact\"` drops the title to md, the body to sm, and tightens the padding. That is a content card's voice, and the theme editor retunes it for the whole project.","- A box in a tool UI labels itself. Use `variant=\"bare\" size=\"compact\"` and put your own label in the body, typed from a text style: the `.eyebrow` class from `site.css` for a quiet section label, `.heading-sm` for one that leads. Leave the shipped header alone rather than shrinking it with a page rule.","- A toolbar is a flex row of small buttons on the band's bottom edge, grouped left and right with `justify-content: space-between`. No card around it.","","### Density","","- `Button` and `IconButton` take `size=\"small\"` in toolbars, compose rows, and any band that holds more than a couple of actions; the default size is for the page's primary action. `fullWidth` belongs to a stacked rail and comes off in a row.","- A project component that wraps shipped buttons forwards a `size` prop to them, so a page sets density the same way for shipped and custom pieces.","- Text in your own elements inside a `Card` inherits the card's body size unless you type it. A label, count, or status line inside a card sets a text style of its own (`--body-sm-*`, `--code-*`).","- `MenuSelect` renders its list open. For a picker, toggle it from a small `Button` with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list absolutely under the button, `top: 100%` with a `--space-*` margin.","","## Wiring","","- Add the route the way `App.svelte` already wires routes:"," - **`<LiveTokensRouter pages={...}>`** (the usual case): add a `pages` entry as `lazy: () => import('./YourPage.svelte')` with a `source: 'src/...'` (and a `label`/`icon` to show it in the nav rail). For a route you can't enumerate (a `/:id`, a path prefix, a gated page), add a `resolve(path) => RouteEntry | null` instead of a `pages` key; same entry shape, so `props` and `source` (hence \"Page Source\") work identically."," - **Manual `<LiveEditorOverlay>`**: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`."," Either way use `lazy`, not a static top-level import: static imports evaluate every page module at boot and leak page CSS into the editor routes.","- Import `site.css` from each page's `<script>` block, never from `main.ts` (would leak into editor routes).","","The entry shape, for a project whose `App.svelte` has moved on from the template:","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","`source` is what makes Page Source work; drop `label` to keep a route reachable by URL but off the nav rail.","","## Avoid","","- Colour literals, and px or rem in spacing, stroke, radius, or shadow.","- Hardcoded page-grid counts (`repeat(10, 1fr)`). Use `repeat(var(--columns-count), 1fr)`, or `calc(var(--columns-count) - 2)` for a sub-grid that spans fewer page columns. A local two-up or three-up is a layout and is fine.","- Utility classes overriding shipped components. Extend via the `/live-tokens/components` editor instead.","- A card header as a section label in a tool UI, and a page rule that shrinks it. Label the box yourself with a text style.","- Deep imports from `node_modules/@motion-proto/live-tokens/src/...`. Use public entry points only.","- Mounting `Editor` or `ComponentEditorPage` outside their dedicated routes.","- A page route under `/live-tokens/*`. That namespace is reserved for the package's own dev surfaces so they can never shadow your routes; the rest of the URL space is yours.","","## Verify","","Run the checker and fix what it reports. Repeat until it exits 0:","","```sh","npx live-tokens check-page src/pages/YourPage.svelte","# or: npx @motion-proto/live-tokens check-page (every page under src/)","```","","It fails on a component outside the catalogue, a prop or value the component does not declare, a deep import, a `var()` that resolves to nothing, a colour literal in any notation, a route under `/live-tokens/*`, and `site.css` imported from `main.ts`. It warns on a px or rem literal in the geometry the theme owns, a hardcoded page-column count, a raw type axis, and a route entry with no `source`. Inline `style=` attributes and `style:` directives are read the same way as the `<style>` block; a `var()` fallback is never a finding. The recipe for each rule is in **live-tokens-fix-findings**.","","Warnings do not fail the run. `--strict` makes them fail, which is the setting to use when the page is meant to be fully tokenized. `--json` prints findings with a stable `rule` id, so you can work through one rule at a time and re-run. `--off=<rule>` silences a rule for a run; `\"checks\": { \"rules\": { ... } }` in `live-tokens.config.json` sets it for the project. A project scaffolded by `create` runs the checker, with `check-component`, as `npm run check:design` before every `vite build`, so the page has to pass before it can ship.","","The checker cannot see a layout. Open the page at the width it is built for and read it band by band: the boxes in a band end on one line, no label is larger than the page's body copy, every control stays inside its box (a `width: 100%` field without `box-sizing: border-box` pushes past it by its padding), and the actions sit where the eye goes last. Fix what you see before you move on.","","Then look at the page from a distance. The bands and their edges must be the only shapes that you see. Then look closely. For each border, header bar, and box, ask this question: does the page lose information if this element is removed? If the answer is no, remove the element. Find the element that a reader sees first, second, and third. Make sure that this is the reading order the page needs.","","Then in dev: change a colour in `/live-tokens/editor` and confirm your page repaints (proves token usage). The overlay's \"Page Source\" button on the new route opens the page in VS Code (proves the route's `source`). The columns overlay shows content sitting inside `--columns-max-width`."],
9
+ "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 band of boxes with borders and header bars looks like a set of posters. |","| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and scaffolding 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 the smallest size that still works. |","| Spatial over temporal | Show information adjacent in space, not stacked in time behind controls. | 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 if 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/"],
10
+ },
11
+ "check-compliance": {
12
+ "SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against its design system and report, without changing a file: which tokens each component reads, which page renders which component, what the two checkers find, and a list of recommended fixes handed to live-tokens-fix-findings. Use when the user asks to check, audit, validate, or review the project, a page, or a component against the design system; asks how compliant it is, what is off, or what it would take to make the build pass; or wants a look before an upgrade. Not for making the changes (live-tokens-fix-findings), and not for a single token (use the editor).","---","","# Checking a project against its design system","","The answer to \"check this project\" is a report. Every fact in it comes from one command; the reading of those facts, and what fixing them would cost, is yours. This skill edits nothing. When the user wants the changes made, that is **live-tokens-fix-findings**, and this report is what it starts from.","","## Workflow","","1. Run `npx live-tokens report --json`. It always exits 0: it is a reading, not a gate. Unknown command means the installed package predates it; upgrade `@motion-proto/live-tokens` first.","2. Read each section against the table below. For every rule with findings, say in a line what the rule holds and whether the fix is mechanical or a judgement.","3. Where a finding looks deliberate, name the config entry that would record the decision, and leave the decision to the user.","4. Report in the order under Summary, each line carrying its count, and end by handing the list to **live-tokens-fix-findings**. Do not start applying fixes here, even one-liners, because the user asked how things stand.","","`npx live-tokens components <id>` and `npx live-tokens tokens --family <name>` (both take `--json`) answer any question the report raises about one component or one scale.","","## The report's sections","","| Section | Fact | When it is not clean |","| --- | --- | --- |","| `migrations` | Whether `tokens.css` is behind the installed package | A stale file shows up downstream as unknown tokens. This is the first fix, and it is one command: `npx live-tokens migrate --check`, then `--write` (`--tokens <path>` for a tokens.css in an unusual place). |","| `components[].unread` | Tokens a component declares that nothing in its file reads | An editor row that edits nothing. Each is a token to wire into the CSS or to remove. |","| `components[].registered` | A component file with no `bootLiveTokens` or `registerComponent` entry | It renders on the page but has no editor. |","| `components[].described` | Whether the runtime file has the header comment the picker reads | Without one, `live-tokens components` cannot say what it is for. |","| `usage.byPage` | Which catalogue component each page renders, and how many times | A page rendering none is either chrome or hand-rolled markup that a shipped component covers. |","| `usage.unusedShipped` | Shipped components no page renders | Information, not a finding. |","| `usage.customUnregistered`, `usage.customUnused` | The project's own components that are unregistered or unused | Dead or half-wired work. |","| `findings.pages`, `findings.components` | Both checkers' findings by rule, under the project's severities and again with every warning counted as an error | The errors are what fails the build today; the strict count is what a fully tokenized project would fail. |","","## Mechanical or judgement","","- **Mechanical**: a spacing literal to its nearest `--space-*` step, a stroke to `--border-width-*`, a hardcoded column count to `var(--columns-count)`, `site.css` moved out of `main.ts`, a route given its `source`. Name any visible shift, such as a `14px` margin becoming `16px`.","- **Judgement**: a colour literal mapped by the role it plays rather than its hue, a raw type axis set from a text style, a prop the component does not declare mapped or dropped. Say what the choice is, not what you would pick.","","## Deliberate findings","","A translucent overlay on an app shell, or a layout size the project owns, may be a decision rather than a miss. Say so and name the entry that would record it: `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }` in `live-tokens.config.json`. Where a whole file is not a themed surface at all, hand-tuned artwork or vendored CSS, the entry is `\"checks\": { \"exclude\": [\"src/art/hero.css\"] }`: a project-relative path, a directory covering what is under it, and naming the file on the command line still checks it. Prefer the narrower one: an exclusion drops one file, a severity change drops a rule everywhere. Recording either is the user's call, not yours.","","## Summary","","1. Migrations pending, and the one command that clears them.","2. What fails the build now: errors by rule, with the files.","3. What the strict count adds: warnings by rule.","4. Components: unread tokens, unregistered, undescribed.","5. Usage: what each page renders, and what is used nowhere.","6. Recommended fixes, in the order **live-tokens-fix-findings** would take them: migrations, then the largest group of errors, then the rest, then warnings. Mark each as mechanical or judgement.","","End with the hand-off: \"Run live-tokens-fix-findings to apply these\", or the subset the user chooses."],
13
+ },
14
+ "create-component": {
15
+ "SKILL.md": ["---","name: live-tokens-create-component","description: Author a brand-new editable component for a @motion-proto/live-tokens project when nothing in the shipped catalogue fits: runtime and editor Svelte files, registration, naming, state model, and verification. Use when the user asks to author, create, or build a new tokenized component; make an existing Svelte component editable in the live-tokens editor; add a component to the catalogue; register a custom component with the editor; or build a [Thing] component that does not exist in the shipped set. Not for placing an existing shipped component on a page (see live-tokens-build-page); read live-tokens-pick-component first to confirm nothing in the catalogue fits.","---","","# Authoring a component for a live-tokens project","","The end state is a runtime Svelte file, an editor Svelte file, one registration, and an entry on `/live-tokens/components` under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.","","## Worked examples ship inside the package","","Read a shipped component's source from the consumer's `node_modules` rather than from memory, because the files are the contract and this skill is not:","","- Runtime files: `node_modules/@motion-proto/live-tokens/src/system/components/<Name>.svelte`."," - Simplest reads (no state, no linked-block): `Card` (single variant with parts), `Badge` and `Callout` (multi-variant)."," - Multi-state (hover, disabled, focus): `Button`, `Input`."," - Multi-part (overlay / header / body / footer): `Dialog`."," - Multi-variant with linked siblings (`canBeLinked` + `groupKey`): `SegmentedControl`, `TabBar`. Composes another shipped component: `CodeSnippet`."," - Every rule below in the fewest lines: `Toggle`. Component states name themselves in the token (`--toggle-on-*`, `--toggle-disabled-*`), interaction states layer on top (`--toggle-hover-*`, `--toggle-on-hover-*`), disabled is terminal (no `--toggle-disabled-hover-*`), and each `:hover` selector has a `.force-hover` sibling so the editor's preview can paint hover tokens without a pointer.","- Editor files: `node_modules/@motion-proto/live-tokens/src/editor/component-editor/<Name>Editor.svelte`. `ToggleEditor` has no `groupKey` and no `canBeLinked`; for components that share base properties across variants, read `references/linked-siblings.md`.","","Shipped editors live in `src/editor/component-editor/` because they are library-internal. For *your* component, co-locate both files in `src/system/components/`. Read the shipped files for pattern, ignore their location.","","## The recipe","","1. **Runtime file**, `src/system/components/MyWidget.svelte`. Declare every editable slot as a CSS custom property inside `:global(:root)`, defaulting to a theme token (never a raw value). The plugin parses `:global(:root)` to seed `component-configs/<id>/default.json`; variables declared anywhere else cannot be edited.","2. **Editor file**, `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (read `references/linked-siblings.md`). Components with structural or display controls that are not token values (alignment, element visibility, layout position) also export an `intrinsics: IntrinsicSpec[]` (read `references/intrinsics.md`). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.","3. **Register** by passing the component to `bootLiveTokens` in `src/main.ts`, the boot the scaffold generates:"," ```ts"," import { bootLiveTokens } from '@motion-proto/live-tokens';"," import App from './App.svelte';"," import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';",""," bootLiveTokens(App, '#app', {"," components: [{"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: myWidgetTokens,"," }],"," });"," ```"," `bootLiveTokens` calls `registerComponent` for you after its editor init hooks and before it seeds configs, so a standalone `registerComponent(...)` placed *before* `bootLiveTokens` lands in the wrong window and can leave editor changes disconnected from the live page. Call `registerComponent` directly only when the app mounts manually, and then before `mount(App, ...)`. Registering against a built-in id wins with a console warning; the right call is a unique id.","4. **Say what it is for.** The runtime file's leading HTML comment is the component's description. `npx live-tokens components` prints it beside the id with the variants and props read from `interface Props` (`--json` for data), which is how **live-tokens-pick-component** weighs a project's own component against the shipped set: no skill file is edited, and nothing is lost when `setup-claude` refreshes the skills. Name the job it does and what it is not for. A directory other than `src/system/components` goes in `\"componentDirs\"` in `live-tokens.config.json`. A first-party component is also added to the picker's **Catalogue** line, which `check:skills` holds.","5. **Join the sketch layer.** The effect draws a fixed set of parts, so a new component stays crisp while the page around it goes hand-drawn until it opts in. A consumer component carries one of four reserved classes on its root and names the five `--sketch-*` values it is drawn with; a first-party component adds a `PartSpec` row instead. The layer also takes `background`, `border-color`, `box-shadow`, `overflow`, `position` and both pseudo-elements away from the element it draws, which constrains where the class can go. Read `references/sketch-mode.md`.","6. **Gate on the checker.** Run it, fix every error, and run it again. Do not call the component done while it reports one:"," ```bash"," npx live-tokens check-component <id> --strict --json"," ```"," `--json` gives each finding a stable `rule` id and a line number, so work one rule at a time. `--strict` fails on warnings too, the right setting for a new component: every warning is a naming or token decision that is cheaper to make now than to migrate later. `--off=<rule>` silences a rule for one run, which a component still being authored has no use for: the finding is a decision to make. Exit code 0 is the gate. With no id it checks every component under `src/system/components`; a project scaffolded by `create` runs that as `npm run check:design` before every `vite build`.",""," If it rejects a suffix, do not invent a new name for the role. Find a shipped component that paints the same thing and use the name it uses: every shipped component passes this same check, so the catalogue is the worked reference.","7. **Verify** with the checklist at the bottom of this file, then place the component on a page with **live-tokens-build-page**.","","## Token discipline","","### Naming scheme","","```","--<componentId>-<part|variant>[-<state>][-<element>]-<property>","```","","- `componentId`: the literal id passed to `registerComponent()`. Lowercase, no dashes, no abbreviations (`segmentedcontrol` not `sc`). The file id matches: `MyWidget.svelte` is id `mywidget`.","- `part` or `variant`: the sub-region (`bar`, `option`, `track`, `header`, `body`, `footer`, `overlay`, `value`, `label`), or, on a component whose variants differ in more than one property, the variant name: `--badge-accent-surface`, `--callout-danger-border`. A component with both stacks them outer to inner, so the bar in its small size is `--segmentedcontrol-bar-small-padding`.","- `state` (optional): interaction or component state (`hover`, `disabled`, `selected`, `focus`). **Always before the property.**","- `element` (optional): sub-element inside the part (`dot`, `icon`, `label`, `text`).","- `property`: theme role or CSS property. Always last.","","### Suffix vocabulary","","The editor picker is chosen by the token's suffix, so the suffix is the naming","decision that matters. Color and surface: `-surface`, `-border`, `-text`,","`-icon`, `-label`, `-fill`, `-divider`, `-color`, `-shadow`, `-opacity`,","`-tint`, `-background`, `-accent`, `-indicator`, `-thumb`, and the","element-named text roles `-title`, `-body`, `-eyebrow`, `-description`,","`-hint`, `-error`, `-placeholder`, `-value`. Geometry: `-radius`,","`-border-width`, `-accent-width`, `-hairline-thickness`, `-thickness`,","`-width`, `-height`, `-size`, `-padding`, `-margin`, `-gap`, `-inset`,","`-divider-width`, `-divider-thickness`, `-divider-height`, `-divider-inset`,","`-track-height`, `-dot-size`, `-thumb-size`, `-icon-size`, `-scale`, `-blur`.","Motion: `-duration`, `-easing`. Typography: `-font-family`, `-font-weight`,","`-font-size`, `-line-height`, `-letter-spacing`.","","A token that carries a structural keyword rather than a value takes no suffix","from this list. Declare it in the editor's `intrinsics` instead, which is what","exempts it, and never end its name in a state word, which reads as","state-after-property and fails.","","Read `references/token-naming.md` for what each one means and when two of them","compete. A suffix outside that list fails `check-component`. The list lives in","`KIND_RULES` in the editor's `aliasKinds.ts`, which the picker, the","`set-geometry` CLI, and `check-component` all read, so a name accepted here","always has a control behind it.","","### Rules that bite","","- **Fixed overlays must portal to `<body>`.** Any `position: fixed` layer is trapped by a transformed or `contain`ed ancestor, which real pages and the editor's preview pane both have. `check:overlay-portal` fails the build without it. Read `references/fixed-overlays.md` before authoring a modal, lightbox, or backdrop.","- **State before property.** `--mywidget-button-hover-surface` passes; `--mywidget-button-surface-hover` breaks sibling matching. Disabled is terminal in the name too: `-disabled-hover-` and `-selected-disabled-` describe states that never paint, and `check-component` rejects both.","- **Every default resolves to a theme token.** A component token names a semantic property; its default is the theme token that property reads, which is what makes the component repaint when the theme changes. `var(--surface-primary)` passes; `#6a4ce8`, `white`, `var(--surface-imaginary)`, and a bare `16rem` all fail, because `check-component` rejects a colour literal in any notation, a `var()` naming a token that does not exist, and a default with no token behind it. Composing tokens counts and is common: `color-mix(in srgb, var(--surface-neutral-lower) 70%, transparent)`, or `calc(var(--space-64) * 4)` for a width the spacing scale does not reach. The one value allowed without a token is a structural keyword (`contain`, `start`, `none`), and only when the editor declares it in `intrinsics`.","- **No abbreviations.** `bg` is `surface`; `fg` is `text`; component ids are never abbreviated.","- **Text aliases.** Neutral scale is `--text-primary` / `--text-secondary` / `--text-tertiary` / `--text-muted` / `--text-disabled`. Family-tinted is `--text-primary-color`, `--text-accent`, `--text-success`. There is no `--text-neutral`.","- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` stay distinct; a bare `groupKey: 'font-family'` silently merges the slots into one link tree. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group colours (two slots ending in `-text` collapsing to one `text` key).","- **Let the type-group helpers derive slot-scoped keys.** When you build typography tokens with `buildTypeGroupColorTokens` / `buildTypeGroupTokens` / `buildTypeGroupFontTokens`, pass `{ component, variants }` so each slot gets a distinct, structural `groupKey`:",""," ```ts"," // variants = the variant/state segment strings as they appear in the variable name"," const VARIANTS = ['default', 'hover'] as const;"," ...buildTypeGroupColorTokens(typeGroups, { component, variants: [...VARIANTS] }),"," ...buildTypeGroupFontTokens(typeGroups, { component, variants: [...VARIANTS] }),"," ```",""," The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text` becomes `header-text`, and `--mywidget-header-default-text-font-family` becomes `header-text-font-family`. Two parts ending in the same word stay distinct; one slot across variants collapses to one key. To override a single derived key, set `colorGroupKey` on that type-group config; it wins and is never recomputed. There is no name-based fallback: a bare `buildTypeGroupColorTokens` call emits un-grouped (solo) colours rather than guessing, and a bare *font* helper across multiple slots is a `check-component` warning, because its default keys would merge the slots' fonts.","","## State model","","Components *can* have two state axes. Many do not: container and messaging components (Card, Badge, Callout, CollapsibleSection) have only variants, no hover or disabled. Skip the rest of this section for those.","","When a component does have states, keep the two axes apart:","","- **Component states** are mutually exclusive top-level fieldsets: `default`, `selected`, `disabled` (names vary by component). One fieldset per component state.","- **Interaction states** are a select *inside* each component-state fieldset: `default`, `hover`. Add `focus` or `active` later if needed.","","Rules:","","- **Disabled is terminal.** A disabled component cannot be hovered or focused. The `disabled` fieldset is flat, with no interaction selector.","- **`selected-disabled` is impossible.** Do not author tokens or fieldsets for it.","- **Parts are not states.** Dialog's `overlay | header | body | footer` are *parts* (all present at once), not states. The VariantGroup tab strip defaults its label to \"Element\" (neutral). If you label tabs anywhere, use **part** for structure and **state** for runtime conditions. Never call a footer a state.","- **Do not call interaction states \"option states\" or \"selected states\"** in the UI. `selected` is a *component* state.","","Token naming consequence:","","```","--mywidget-disabled-surface ✓ component-state-level","--mywidget-option-disabled-surface ✗ implies disabled is an interaction state","--mywidget-option-hover-surface ✓ default-component-state, hover-interaction","--mywidget-selected-hover-surface ✓ selected-component-state, hover-interaction","--mywidget-selected-disabled-text ✗ selected-disabled does not exist","```","","## User-facing copy","","Strings you author for the editor UI use periods and commas, never em-dashes, which read as an AI tell. This applies to `title=` and `description=` on `ComponentEditorBase`, token row labels, info popovers, and any text inside `previewActions` / `canvasToolbarExtras` snippets. Code comments are unaffected.","","Custom chrome inside an editor snippet is rare, since `ComponentEditorBase` and `VariantGroup` carry the standard chrome. Where you add some, keep it greyscale (no accent colours) and reference heading sizes via `--ui-font-size-md` / `-lg` / `-2xl` rather than pixel literals.","","## Public imports only","","Imports in your runtime, editor, and `main.ts` come from exactly two paths:","","```ts","import { registerComponent, editorState } from '@motion-proto/live-tokens';","import {"," ComponentEditorBase, VariantGroup,"," computeLinkedBlock, withLinkedDisabled, buildSiblings,","} from '@motion-proto/live-tokens/component-editor';","import type { Token } from '@motion-proto/live-tokens/component-editor';","```","","That covers everything the worked examples use. Additional primitives (`LinkedBlock`, `TypeEditor`, `TokenLayout`, `buildTypeGroupTokens`, `buildTypeGroupColorTokens`, `buildTypeGroupFontTokens`, `buildTypeGroupShareableContexts`, the `TypeGroupConfig` type, more types) are exported from the same paths for advanced cases.","","**Never deep-import `node_modules/@motion-proto/live-tokens/src/...`.** Reading those files for pattern reference is fine; importing them at runtime is not. If you need something not exported, file an issue rather than reaching in.","","## Extensions","","Read the sketch reference for every component; the other two only when they apply.","","- `references/linked-siblings.md`: variants that share base properties and should move together (Badge, Card, SegmentedControl).","- `references/intrinsics.md`: structural or display choices that are not token values (an alignment, an element's visibility), where the runtime default and the editor's read-back must agree.","- `references/sketch-mode.md`: joining the sketch layer. **Every component needs this.** One class on the root, the five `--sketch-*` values the layer draws with, and the list of what it takes over from the element. Skip it and the component stays crisp while the page around it goes hand-drawn.","","## Verification checklist","","Step 6 of the recipe is the static gate: `npx live-tokens check-component <id>` at exit 0, with `--strict` clean or its warnings resolved. It enforces the file layout, the `:global(:root)` block, the suffix vocabulary, state-before-property, the terminal disabled state, public imports, that every token an editor row names is declared in the runtime, that every default reads a theme token, and that the id is registered through `bootLiveTokens({ components: [{ id }] })` or a direct `registerComponent({ id })` call.","","**Then run the registry contract test.** `checkRegistryEntry`, from `@motion-proto/live-tokens/component-editor/contract`, takes one registry entry and returns a violation line per failure, so a suite over your own components is a `describe.each` and one call. It verifies that the registration resolves to a real `sourceFile` and a non-empty schema, that schema variables are unique, that every editable token is declared in the runtime `<style>` block and seeded in `component-configs/<id>/default.json`, that a token declaring `minOpacity` seeds at or above its floor, and that `setComponentAlias` round-trips the alias through the slice. The test file and its path options are in `references/contract-tests.md`. Inside the package, `registryContract.test.ts` runs that same check over `builtInRegistry`, so a first-party component is covered the moment it lands there.","","**If your component declares `intrinsics`, the intrinsics contract test covers it too.** `intrinsicsContract.test.ts` asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, that it is one of the spec's `values`, and that the editor's `default` equals it. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`.","","Finally navigate to `/live-tokens/components` and confirm the runtime behaviours no static check can see:","","- [ ] The new component appears in the nav rail under the **CUSTOM** group (system entries above, custom below the labeled divider).","- [ ] Token rows render. Color pickers, radius selectors, font selectors all work.","- [ ] Linked-block (if your component has linked siblings): shared rows appear with the link toggle. Changing the linked value broadcasts across every variant.","- [ ] `component-configs/<id>/default.json` is derived from the `:global(:root)` block at boot. Save writes `_working.json`, the unsaved buffer the open theme captures; Save As also writes a named preset.","- [ ] Reset returns each variable to its `:global(:root)` default.","- [ ] Boot validation is clean (no warnings about the component being missing from the server scan, or about disk-vs-registry drift).","- [ ] Switch Sketch mode on in the editor and walk the checklist at the end of `references/sketch-mode.md`. The component is drawn in every variant and on hover, in its own colours, not crisp and not wearing another part's palette. Switch it off again and the component is unchanged."],
16
+ "references/contract-tests.md": ["# The registry contract as a test in your own project","","`checkRegistryEntry` is the contract the package holds its own 26 components","to, exported so a project outside the package can run it over its own. It takes","one registry entry and returns a violation line per failure; an empty array is","the pass.","","```ts","// tests/registryContract.test.ts","// @vitest-environment happy-dom","import { describe, it, expect } from 'vitest';","import { getComponentRegistryEntries, registerComponent } from '@motion-proto/live-tokens';","import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';","import MyWidgetEditor, { allTokens } from '../src/system/components/MyWidgetEditor.svelte';","","registerComponent({"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: allTokens,","});","","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([]);"," });","});","```","","Two lines there are load-bearing.","","- **Register at the top of the test file**, rather than importing `main.ts`."," The entries have to exist before `describe.each` reads them, and"," `bootLiveTokens` would mount the app.","- **Filter on `origin`.** The registry always carries the shipped components"," too, and their `sourceFile` paths are relative to the package root, not"," yours. Without the filter every built-in fails on a path that does not exist"," in your project.","","## Setup","","`vitest` and `happy-dom` as devDependencies, and the svelte plugin already in","`vite.config.ts` so the editor `.svelte` import resolves. The helper reads the","runtime file and `default.json` off disk, which is why it is node-only and has","its own subpath.","","The package ships Svelte and TypeScript source, and `bootLiveTokens` imports the","FontAwesome stylesheet. Left external, Node meets that `.css` and stops with","`Unknown file extension \".css\"`, before a single test runs. Inline both so Vite","transforms them:","","```ts","// vitest.config.ts","import { defineConfig, mergeConfig } from 'vitest/config';","import viteConfig from './vite.config';","","export default mergeConfig("," viteConfig,"," defineConfig({"," test: {"," server: { deps: { inline: [/@motion-proto\\/live-tokens/, /@fortawesome/] } },"," },"," }),",");","```","","## 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)."],
17
+ "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."],
18
+ "references/intrinsics.md": ["# Extension: intrinsics","","Some components expose **structural or display choices** that aren't token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a bespoke `<select>` or checkbox you author in an editor snippet, not the generic token grid, so they don't 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 just one (SectionDivider folds `above-description` into `below-label`). Properties that look like intrinsics but aren't: 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`."],
19
+ "references/linked-siblings.md": ["# Extension: linked siblings","","Read this when your 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 fine without a `{linked}` prop."],
20
+ "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.","Your 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 you 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 you in and nothing more. It names no colour, so the layer emits","no rule for it and whatever your 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 with you for these values, so every case is one more","declaration at the specificity you already 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 your 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 your 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 your 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. You opt into none of 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 you 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."],
21
+ "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 |","| `-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 you need 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."],
22
+ },
23
+ "create-theme": {
24
+ "SKILL.md": ["---","name: live-tokens-create-theme","description: Create a complete live-tokens theme from a natural-language request by stating one design direction and routing 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 whenever the user asks for a theme, look, vibe, or brand feel by mood, style, era, season, holiday, or hue; when they name only a color and want a theme around it; or when they refine a look across more than one dimension. Not for a single token (use the editor), and not for one dimension alone: color is live-tokens-set-colors, type is live-tokens-set-type, geometry is live-tokens-set-geometry.","---","","# Creating a theme from a request","","A look is three decisions: color, type, and geometry. This skill reads the","**request**, the user's own words, and states one **design direction**, a line","or two that fixes all three. From it come three **intents**, one per dimension,","each naming an outcome and never a value. Each goes to the contributing skill","that owns that dimension, and their three reports come back as one **assembled","report**, so the whole look comes from one reading.","","Every contributing skill writes its dimension into the unsaved buffers the app","already renders, and those three buffers are the **look**. This skill runs one","CLI of its own, `save-theme`, which turns the look into the **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 state the design direction to the user: the mood, the hue family, the scheme, and the type and geometry that mood implies. It fixes enough to derive the three intents in step 3, and it names the default where the request leaves a dimension open. Keep it to a line or two. Every step below keys off it.","2. Read `references/design-directions.md` and name the **anchor** the request matches: a feeling, an idiom, or an occasion that reference lists, each one fixing color, type, and geometry together. 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. State 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's name with each one, because every contributing skill holds its own anchors for its own dimension under the same names. Never reach for an OKLCH triple, a font family, or a token on a contributing skill's behalf.","4. Invoke **live-tokens-set-colors** with the color intent. This step never skips: a theme request names a color identity, so color is the one dimension every look fixes.","5. Invoke **live-tokens-set-type** with the type intent. Skip only when the user asked to leave the type alone.","6. Invoke **live-tokens-set-geometry** with 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 three buffers into `themes/<slug>.json` and opens it, so nothing is left unsaved. `--dry-run` prints what it would write.","8. Assemble the three reports into the assembled report: the design direction, what each contributing skill changed, the theme `save-theme` wrote, and anything one of them flagged. Tell the user to look at the running app. Offer refinements (see Refining a look).","","A set of themes runs steps 4 to 7 once per theme, with `--no-activate` on every","save but the last, so each theme starts from the same live look.","","## What each contributing skill owns","","Hand an outcome and the anchor's name. The mechanics stay where they are.","","| Dimension | Contributing skill | It decides |","|---|---|---|","| color | live-tokens-set-colors | ten base colors, the scheme, harmony, the canvas commitment, the contrast pass |","| type | live-tokens-set-type | the two families, the form models behind them, the weights |","| geometry | live-tokens-set-geometry | radius, padding, gap, and border-width moves, global or per component |","","A dimension the request leaves open still gets an intent, taken from the anchor.","A dimension the request rules out gets no invocation at all, and the assembled","report says which.","","## Refining a look","","A refinement arrives against a theme that is already open, and one adjective","usually names one dimension. Route it rather than re-reading the whole look:","","| 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 |","","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.","","## Files each step writes","","Color, type, and geometry each write an unsaved buffer, which the page already","runs. `save-theme` composes the three into `themes/<slug>.json` and opens it,","which clears the buffers; Adopt then ships the theme. Opening a theme never","changes what the site ships. Only Adopt, in the editor, does that. Component","aliases and gradients carry forward from the live look into the theme","`save-theme` writes; user-tuned gradients survive, stock ones rebuild from the","new families.","","## Verify","","- Each contributing skill reports back, and `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 look after a reload, and the editor's Theme panel names that theme with no unsaved marker.","- The assembled report names one design direction, and the three intents trace to it.","- To return to the previous look, load the earlier theme from the Theme panel."],
25
+ "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 when you state an 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 |"],
26
+ },
27
+ "fix-findings": {
28
+ "SKILL.md": ["---","name: live-tokens-fix-findings","description: Bring an existing @motion-proto/live-tokens project into line with its design system by running check-page and check-component, reading the findings, and fixing each by rule until both exit 0. Use when the user asks to make the build pass, fix the design-system errors or warnings, clean up the literals, replace hex or pixel values with tokens, make a page or component themeable, or apply what a check reported. Not for the check itself (live-tokens-check-compliance reports and edits nothing), not for building a new page (live-tokens-build-page) or a new component (live-tokens-create-component), which run the same gate as their last step, and not for a single token edit (use the editor).","---","","# Fixing the checkers' findings","","Two checkers hold a project to its design system. `check-page` holds pages: every component comes from the catalogue and is passed only the props it declares, and every value in page CSS is a theme token. `check-component` holds authored components: every token names a semantic property and its default is the theme token that property reads. A page or component that passes repaints when the theme changes. One that fails has opted out of the system silently, and its findings say where.","","This skill is the loop for code that already exists. When the user has not seen the state of the project yet, **live-tokens-check-compliance** presents `npx live-tokens report` without editing; this skill edits.","","## Workflow","","1. Run both checkers with `--json`. Each finding carries a stable `rule`, a file, and a line."," ```sh"," npx live-tokens check-page --json # every page under src/"," npx live-tokens check-component --json # every component authored under src/system/components"," ```"," `check-page src/pages/Home.svelte` and `check-component <id>` scope a run when the user names one thing. Unknown command means the installed package predates the checkers: upgrade `@motion-proto/live-tokens`, then run `npx live-tokens migrate --check` and apply what it plans with `--write` (`--tokens <path>` for a tokens.css outside the four default locations).","2. Group by rule. Take errors before warnings, and the rule with the most findings first, because one recipe clears the whole group.","3. Apply that rule's recipe to every finding in the group: colour by role, geometry by scale, or the row in the table below. A component outside the catalogue hands off to **live-tokens-pick-component** for the shipped one that fits, or to **live-tokens-create-component**.","4. Run again. New findings can appear as old ones clear: a token you reached for may not exist, or a moved import may land where a rule now sees it. Stop at exit 0.","5. Run once with `--strict` and report what it adds, so the user can decide whether warnings are worth clearing now. Then report by rule.","","A project scaffolded by `create` has a `check:design` script. Give any other project one in `package.json`, `\"check:design\": \"live-tokens check-page && live-tokens check-component\"`, and once it passes, gate the build: `\"build\": \"npm run check:design && vite build\"`.","","## Three things the loop never does","","- **Silence a rule to pass.** `--off=<rule>` is for a single run while working. A severity the project wants changed goes in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`, with the reason in the commit, and only when the user has made that call.","- **Mint a token.** A literal with no token behind it is remapped to the nearest existing token by role. No new `--surface-*`, `--text-*`, or `--space-*` is added to `tokens.css` to match a value the page happened to use. If nothing fits, say so and leave the finding.","- **Change what the page looks like without saying so.** Most remaps land on the same value. When the nearest token differs, `14px` to `--space-16` or a 55% black to `--scrim`, name the shift in the report.","","## Colour by role, never by hue","","`color-literal` is the finding that takes judgement. The replacement is the token for what the colour *does*, not the token nearest in hue, because the theme moves every role together and the page must move with it. `npx live-tokens tokens --family surface` prints a family's names and values (`text`, `border`, `scrim`, `tint` likewise; `--json` for data); the families are fixed.","","| The literal is | Token family | Notes |","| --- | --- | --- |","| Text on a surface | `--text-primary`, `-secondary`, `-tertiary`, `-muted`, `-disabled` | The neutral scale. Family colour is `--text-accent`, `--text-success`, and so on. |","| Light text on a dark chip over the page | `--text-inverted` | The one flip; no AA guarantee. |","| A box's fill | `--surface-<family>-<level>` | `neutral` for chrome; `brand`, `accent`, `special` for emphasis; `info`, `success`, `warning`, `danger` for status. |","| A stroke | `--border-<family>-<level>` | `faint`, `subtle`, base, `medium`, `strong` in the neutral family. |","| A translucent layer that dims what is behind it | `--scrim-low`, `--scrim`, `--scrim-high` | Behind a modal, under a floating control. |","| A translucent wash on a surface | `--tint-low`, `--tint`, `--tint-high` | Hover, an active tab, a code chip's background. |","| Fully transparent | `--color-transparent` | Never `transparent` inside a component default. |","| A gradient | `--gradient-*` | Or compose one from surface tokens. |","","A `var(--x, #fff)` fallback is not a finding. A named colour is: `white` and `rebeccapurple` are literals like any hex.","","## Geometry by scale","","`dimension-literal` fires only on the geometry the theme owns: padding, margin, gap, border and outline widths, inset offsets, radius, and shadow. Sizing (a hero's height, a max content width, a `minmax()` floor) is layout and is never reported, so leave it.","","| The literal is | Token | Notes |","| --- | --- | --- |","| Padding, margin, gap, an offset | `--space-<px>` | `npx live-tokens tokens --family space` prints the steps. Round to the nearest one and name the shift. |","| A stroke width | `--border-width-1`, `-2`, `-4` | Also for `outline`. |","| A corner | `--radius-sm` through `-4xl`, `--radius-full` | |","| A shadow | `--shadow-sm` through `-xl` | Replace the whole value, never one offset. |","| Part of a `calc()` | The token inside the calc | `calc(var(--space-64) * -2 + var(--space-8))` |","","While in the file, motion values take `--duration-*` and `--ease-*` even though no rule reports them, and a `blur()` takes `--blur-*`.","","## Every other rule","","| Rule | Fix |","| --- | --- |","| `unknown-token` | A typo or a rename. Search `tokens.css` for the stem. A contract-family name (`--surface-…`, `--text-…`) that is gone was renamed: `npx live-tokens migrate --check` names the migration. |","| `raw-text-axis` | Set the whole axis set from one text style: `--heading-xl` through `-sm`, `--body-md`, `--body-sm`, `--editorial-*`, `--eyebrow`, `--code`, each carrying `-font-family`, `-font-size`, `-font-weight`, `-line-height`, `-letter-spacing`. A `font:` shorthand is rewritten the same way. `em`, `%`, and a unitless line-height are relative and fine. |","| `unknown-component` | Not in the catalogue. Read **live-tokens-pick-component** for the shipped one that fits, or author it with **live-tokens-create-component**. |","| `unknown-prop` | The component drops it at runtime. `npx live-tokens components <id>` prints the props it declares and the values each union accepts; map the prop to one of them or delete it. A `class` on a component that declares none does nothing. |","| `unknown-prop-value` | Pick a value from the union the message lists. |","| `hardcoded-columns` | `repeat(var(--columns-count), 1fr)` for the page grid; `calc(var(--columns-count) - 2)` for a sub-grid spanning fewer page columns. A two-up or three-up is a layout and is not reported. |","| `site-css-in-main` | Delete the import from `main.ts` and add it to each page's `<script>`, so page CSS never reaches the editor routes. |","| `missing-source` | Add `source: 'src/...'` to the route entry so Page Source can open it. |","| `reserved-route` | Move the route out of `/live-tokens/*`; the package owns that namespace. |","| `deep-import` | Import from `@motion-proto/live-tokens` or `/component-editor` or `/components/<Name>.svelte`, never from `/src/`. |","| `unknown-suffix`, `state-after-property`, `disabled-is-terminal` | Rename the token. Borrow the name a shipped component uses for the same role; the vocabulary and the state model are in **live-tokens-create-component**. |","| `color-literal`, `unknown-token-ref`, `default-not-token` (component) | The `:global(:root)` default reads a theme token, composed if needed. A structural keyword (`start`, `contain`) is declared in the editor's `intrinsics`. |","| `phantom-editor-token`, `phantom-link` | The editor names a token the runtime never declares, or a bare font helper spans slots. Both are editor fixes; see the same skill. |","| `invalid-id`, `missing-file`, `missing-root-block`, `no-tokens`, `missing-component-const`, `missing-all-tokens`, `missing-registration` | The component is not wired the way the recipe in **live-tokens-create-component** wires it: a lowercase id, a runtime file with a `:global(:root)` block, an editor file exporting `component` and `allTokens`, and a `bootLiveTokens` entry. |","","## Report","","Say what changed by rule, one line per rule with the count and any visible shift. Say what was left and why, with the config entry if the user chose to lower a severity. End with the two commands and their exit codes.","","## Verify","","Open `/live-tokens/editor` in dev and change a surface colour and a spacing step. Every file the loop touched should repaint. One that does not still holds a literal the checker cannot see, which is worth reporting as a gap in the checker rather than patching around."],
29
+ },
30
+ "pick-component": {
31
+ "SKILL.md": ["---","name: live-tokens-pick-component","description: Recommend which shipped @motion-proto/live-tokens component fits a UX need, with decision trees for the confusable pairs (SegmentedControl / TabBar / RadioButton / MenuSelect, Card / CollapsibleSection / Dialog, Callout / Notification / Tooltip, and others). Use when the user asks which component to use, should I use X or Y, what is the difference between two components, how do I show / let the user / capture some UX outcome, or starts authoring a custom component before checking the catalogue. Read this before live-tokens-create-component. Not for placing the chosen component on a page (see live-tokens-build-page).","---","","# Picking the right live-tokens component","","This skill helps you choose between shipped components when several could plausibly fit. The catalogue is small; the hard part is semantic intent. A `RadioButton` set and a `SegmentedControl` can render identical-looking UIs but communicate different things.","","For composing a page once you've picked components, see **live-tokens-build-page**. For authoring a brand-new component when nothing fits, see **live-tokens-create-component** (but read this skill first to confirm nothing in the catalogue fits).","","## Catalogue","","Action: `Button`, `IconButton`, `InlineEditActions`. Input: `Input`, `Slider`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`, `Panel`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.","","That line is the shipped set. A project can register components of its own,","and those never appear in this file: run `npx live-tokens components` before","choosing. It lists every component the project has, shipped and custom, with","the variants each takes and the purpose its header comment states, so a custom","component is weighed against the shipped set on the same footing.","`npx live-tokens components <id>` prints one component's props, the values each","union accepts, and its tokens with defaults; `--json` returns the same as data.","","## Action family: Button vs IconButton","","Both trigger an action and share the same six variants (primary, secondary, outline, success, danger, warning), three states (default, hover, disabled) and two sizes (default, small). They differ only in content.","","- `Button` carries a text label, optionally with a leading or trailing icon. Use it whenever the action needs a word to be unambiguous.","- `IconButton` is icon-only and square. Use it for compact, space-constrained actions whose meaning is obvious from the glyph alone (toolbar controls, close/edit/delete affordances, card overflow menus). It has no text slot, so an `ariaLabel` is required for accessibility.","- **Don't reach for `IconButton` when the icon's meaning isn't self-evident.** A labelled `Button` (or a `Button` with an icon) avoids the guessing game.","- `InlineEditActions` is the confirm-and-cancel pair that follows an inline edit (rename a row, edit a value in place). Use it rather than two loose `IconButton`s so every inline edit on the page resolves the same way.","","## Single-selection family: SegmentedControl vs TabBar vs RadioButton vs MenuSelect","","All four pick one option from a set. The right one depends on **option count**, **whether the selection changes what's rendered below**, and **how much visual weight** you want.","","| Component | Best for | Visual weight | Option count |","|-------------------|-------------------------------------------------------------------------|------------------|--------------|","| `SegmentedControl`| Inline switch between alternative *views of the same data* | Compact pill | 2–4 |","| `TabBar` | Switching between *tab panels* (content area swaps below) | Page-section | 2–7 |","| `RadioButton` | Form-style selection where the user reviews all options as text | Form-row | Any |","| `MenuSelect` | A list of options, one checked; renders open, so a dropdown toggles it from a `Button` | Open list | Any |","","- `TabBar` implies \"this changes the page\"; `SegmentedControl` implies \"this is one knob among others.\"","- Use `RadioButton` when labels deserve room to breathe and the user is committing to a larger form.","- Use `MenuSelect` when options would overflow horizontally or there are too many to display at once.","- **Don't pick `SegmentedControl` when option labels are long enough to wrap.** It loses its compactness; use `RadioButton` rows instead.","","## Text entry: Input vs the selection family","","- `Input` takes an answer the page cannot enumerate: a name, an email, a search string, an amount. It ships the label, the hint line, and the error state as parts (`--input-label-*`, `--input-hint-*`, `--input-error-*`), so style those rather than stacking your own text under a bare field.","- The boundary is whether you can list the answers. A short fixed set is the single-selection family above; a long fixed set is `MenuSelect`; anything you cannot write down is `Input`.","- `Slider` takes a number inside a known range where the position carries the meaning: a volume, a price band, a percentage. Its `range` variant takes a low and a high bound on one track. A number the user knows exactly and would rather type is `Input` with `type=\"number\"`.","- **Don't use it for on/off.** That is `Toggle`, and a one-field form asking for yes or no is the usual way this goes wrong.","- Its four variants are `default`, `focused`, `disabled`, and `error`. A validation message belongs in the `error` variant, not in a `Callout` next to the field.","","## Container family: Card vs CollapsibleSection vs Dialog","","| Component | Modality | Use for |","|-----------------------|---------------------|-------------------------------------------------------------|","| `Card` | Inline, always open | Default container for grouped content |","| `CollapsibleSection` | Inline, toggleable | Progressive disclosure inside a longer page |","| `Dialog` | Modal, blocks page | Confirmations, focused tasks the page can't continue around |","| `Panel` | Inline, fixed stage | A demo or preview surface whose height must not reflow |","","- Default to `Card`. It's the workhorse. For full-bleed media (cover art, a poster, a chart that reaches its own border) pass `flush` (with `prose={false}`) rather than zeroing its padding tokens from the page.","- Reach for `CollapsibleSection` only when the content is *legitimately secondary* (advanced users open it; most skip). Don't use collapse as a styling choice when the content matters.","- `Panel` is a stage, not a content container. It pins its own height so what it shows can resize without moving the page, which is what a component preview or a live example needs and what article content does not. Content goes in `Card`.","- **Don't use `Dialog` for routine forms.** Reach for it only when the page cannot meaningfully continue until the user decides (destructive confirmations, payment, sign-in). Routine forms go inline in a `Card`.","","## Messaging family: Callout vs Notification vs Tooltip vs Badge","","| Component | Scope | Triggered by | Dismissable | Use for |","|-----------------|----------------|-----------------|-------------|------------------------------------------------------|","| `Callout` | Section-inline | Always present | No | \"Heads up about this section\" |","| `Notification` | System-level | Event / save | Yes | \"Your changes were saved\" |","| `Tooltip` | Element-inline | Hover / focus | Auto | Definition or hint anchored to an element |","| `Badge` | Element-inline | Always present | No | Status pill (\"Beta\", \"New\", \"v2\") |","| `CornerBadge` | Element-corner | Always present | No | Position-anchored marker (count, status dot) |","","- `Callout` is *content*. Part of the section, written into the markup, says something important about what surrounds it. Variants (`info`, `success`, `warning`, `danger`) set the tone.","- `Notification` is *feedback*. Appears in response to an action, then dismisses. **Don't use `Notification` for static content;** persistent messages belong in a `Callout`.","- `Tooltip` is for *what an element means*. **Don't use `Tooltip` as the primary location of important content;** it auto-dismisses and isn't accessible for must-read content.","- `Badge` and `CornerBadge` differ only in positioning. `CornerBadge` lives at a `top-right` / `bottom-left` anchor on a parent (notification counts, \"NEW\" stickers).","","## Display family: shown, not asked","","- `Image` frames a picture in the flow at one of four sizes, with an optional hover zoom. It is the default for any picture the page simply shows.","- `ImageLightbox` adds click-to-open at full size and takes an array for a gallery. Use it when the detail is the point (screenshots, artwork, charts that need reading), and not for decoration: it puts a modal behind every picture it wraps.","- `Table` themes your own rows and cells without owning the data. Records go here; a set of *things the user acts on* is a stack of `Card`s instead.","- `ProgressBar` reports progress against a labelled track. It is a read-out, never a control.","- `CodeSnippet` is for a single-line command or value the reader is meant to copy back into a terminal (install commands, generated keys, ids), with click-to-copy and a brief \"Copied\" popover. Use it whenever the page asks the reader to *run* something rather than just *read* it.","- `SectionDivider` separates sections of one page. `SideNavigation` moves between pages, driven by the current path. **Don't use `SideNavigation` to switch panels inside one page;** that is `TabBar`, and the difference is whether the URL changes.","","## Toggle vs SegmentedControl vs RadioButton (for on/off)","","All three can express a binary choice. The right one depends on what the choice *is*.","","| Component | Best for |","|--------------------|-----------------------------------------------------------------------------------------------------------------------|","| `Toggle` | A *setting* that's either on or off (notifications on/off, dark mode). The label names the setting; the switch shows the state. |","| `SegmentedControl` | A *choice between two named alternatives* (Light / Dark, List / Grid). Both labels are visible at once. |","| `RadioButton` pair | A *form-style choice* where the user reviews both labels before committing (Yes / No questions, opt-in selections). |","","- If the off and on states share a name (the feature itself), it's `Toggle`. \"Email notifications\" has no \"off\" label because the switch position is the state.","- If the two states have different names you want users to compare, it's `SegmentedControl`.","- `Toggle` flips immediately; `RadioButton` pair is for forms where the choice is part of a larger submission.","","---","","If nothing in the catalogue fits (a `DatePicker`, a `Stepper`, a custom widget), author it via **live-tokens-create-component**. **Don't reach for a custom component before checking the catalogue;** a custom component is a maintenance commitment."],
32
+ },
33
+ "set-colors": {
34
+ "SKILL.md": ["---","name: live-tokens-set-colors","description: Set a live-tokens theme's color from a color intent: ten OKLCH base colors, a light or dark scheme, and an AA-gated contrast pass, written into the unsaved color buffer the app already renders. Use whenever the user asks for a palette, colors, or hues by mood, style, era, season, holiday, or hue; when they name only a color; or when they refine the color of a look: warmer, cooler, calmer, louder, lighter, darker, moodier, more contrast. Also invoked by live-tokens-create-theme, which supplies the color intent for a whole look. Changes color only, never fonts or geometry. Not for a single token (use the editor), and not for a whole look (see live-tokens-create-theme).","---","","# Setting a theme's colors","","You choose ten base colors; the CLI builds every ramp from them, enforces AA","contrast on the derived text tokens, writes the result into the unsaved colors","buffer the app already renders, and prints a contrast report. Never hand-author","theme JSON and never edit the data tree directly.","","The run replaces the color state in that buffer and carries everything else","forward, so it composes with type and geometry in any order. Saving the open","theme in the editor, or running `save-theme`, turns the live look into a theme.","","## Workflow","","1. Read the color intent. When it names an anchor (a feeling, an idiom, or an occasion), read `references/color-anchors.md` for that entry; it overrides the generic bands below. Say which anchor you took.","2. Translate the intent into ten base colors using the framework below and write `scratch/<slug>-base-colors.json`. Nothing else records the base colors, so this file is the only copy; one per slug is what makes the refinement pass cheap.","3. Run `npx live-tokens set-colors scratch/<slug>-base-colors.json`. It writes the color state into the unsaved buffer the page already runs, and prints a contrast report.","4. Read the report. Exit 0 passes, and auto-corrected values count as passing. Exit 1 means the base colors are unworkable; each failure line names the base color to change, usually by raising its lightness or cutting its chroma. Fix the base color file and re-run.","5. Report back in a line: the scheme, the hue families on screen, the canvas commitment level, and anything the report auto-corrected.","","Flags: `--dry-run` prints the contrast 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. All 10 are required, and each may be given as a `\"#rrggbb\"` string instead. OKLCH: `l` is 0 to 1 lightness, `c` is chroma (0 grey, about 0.37 max), `h` is hue in degrees. The file names no theme: the slug in its own path is the theme name live-tokens-create-theme intends, or any label when this skill runs alone. `canvasGradient` is an optional boolean, see below.","","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: color is inversely proportional to area","","| Tier | Palettes | Chroma |","|---|---|---|","| Ground (about 60% of every screen) | Neutral, Alternate | C 0.008 to 0.02 |","| Canvas (the largest single area) | Canvas | Per the commitment levels below, C 0.02 to 0.14 |","| 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 but tinted toward the theme (Neutral near Brand's hue; Alternate offset 15 to 60 degrees, or a warm/cool counterpoint), never pure C = 0 unless an anchor calls for it.","","## Per-role bands","","| 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 |","","**The canvas carries the theme's identity, so commit to it.** The page background is the largest area on screen and the strongest difference between themes; a timid canvas makes every theme look the same. Below C 0.015 at L 0.95 a tint is imperceptible, which makes near-white a deliberate choice for clean or minimal intents and never the default. Three levels of commitment:","","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.","","Also:","","- Blue tints cap very low at high L (H 264 at L 0.95 barely reaches C 0.03): lower L for a blue canvas rather than fighting the ceiling. Yellow, green, and cream tint generously at high L.","- When generating a set of themes, make the canvases pairwise distinct in hue or in L. Two light themes both near (0.97, 0.01) read as one theme with different buttons.","- 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.","","## 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 at all.","","That is the whole mechanism, and one dial moves without a reference: warm is hues 20 to 110 plus pink 290 to 360, cool is 140 to 290. For an intent that names a feeling, read `references/color-anchors.md` instead of 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 committed canvas (level 2 or 3); at the ramp edge 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 the color of a theme that exists","","\"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 base colors: `src/live-tokens/data/themes/<slug>.json` holds each one verbatim at `colorsAndType.editorConfigs.<Palette>.baseColor` as `{l, c, h}`, and the Canvas base color's lightness gives the scheme. Rebuild the base color file from those ten values, move the dial the user named, and re-run. A re-run replaces the buffer's whole color state, including palette edits made in the editor since the last run, so say so once when iterating; a Save or a `save-theme` run keeps the result.","","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. Type and geometry are untouched: `set-colors` replaces the color","state in the unsaved buffer and carries every other value in it forward. Save","the open theme in the editor, or run `save-theme`, to keep the result; Adopt","ships it.","","## Verify","","- The CLI exits 0 with every check passing (auto-corrected is fine), and the report names the layer it carried the rest of the look forward from.","- The app (dev server running) shows the new palette after a reload. The editor's Theme panel marks the open theme unsaved, unless the run was a dry one or the report says the layer under the buffer, the open theme or the package 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."],
35
+ "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. |"],
36
+ },
37
+ "set-geometry": {
38
+ "SKILL.md": ["---","name: live-tokens-set-geometry","description: Adjust corner radius, padding, gap, and border width across live-tokens components by moving each token alias along the shipped scales. Use when the user asks for pill or capsule buttons; rounded, rounder, sharp, sharper, square, softer, or harder corners; thicker or thinner borders; or density: space it out, tighter, denser, more compact, airier, more breathing room. Also invoked by live-tokens-create-theme, which supplies the geometry intent for a whole look. Changes shape and space aliases per component, never color, fonts, or tokens.css. Not for editing a single token (use the editor) or for a whole look (see live-tokens-create-theme).","---","","# Adjusting geometry","","You translate the request into a small ops file; the CLI resolves each matching alias on its token ladder, writes the result into each component's unsaved buffer, and prints a report card. Never hand-edit the data tree.","","## Workflow","","1. Read the geometry intent. When it names an anchor (a feeling, an idiom, or a genre), read `references/geometry-anchors.md` for that entry; it overrides the Idioms table below. Write the ops file to `scratch/geometry-ops.json`.","2. Run `npx live-tokens set-geometry scratch/geometry-ops.json`. It writes `component-configs/<id>/_working.json` for every component the ops change, which is the buffer the page already runs. `--dry-run` prints the report without writing.","3. Read the report card: every changed alias old → new, plus skips (raw value, off the ladder, already at the ladder end, pill preserved). Exit 1 means the run was rejected; the message names the offending op or the missing input, so fix it and re-run. Read where the controls landed, not only that the run succeeded: a button, badge, input, or tab padding sitting at `--space-6` is on its floor, and one that also carries `--radius-full` wants a targeted lift.","4. Report back in a line: every alias that moved, and any skip or clamp worth naming.","5. Tell the user to reload the page before saving. The editor keeps the look in the browser and writes the buffers from that copy, so a Save in a tab that was open during the run puts the pre-run shape back and the report you just showed them becomes a lie. After the reload, offer the inverse op as the undo and say the edit is unsaved until they save the open theme.","","Each run reads the LIVE config (buffer, else the open theme, else the shipped default), so \"a bit more\" and \"back one\" compound naturally.","","## 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\" }] }","```","","- `name`: ignored. Buffers are fixed slots, so a name names no file, and the CLI says it dropped one. Leave it out.","- `target` (optional): a component id (the folder names under `src/live-tokens/data/component-configs/`, which the Catalogue in **live-tokens-pick-component** also names in full). A named component targets its id: \"windows\" or \"modals\" is `dialog`, \"cards\" is `card`, \"tabs\" is `tabbar`; an unknown target is a hard error. \"The UI\", \"everything\", or no noun at all means global, so omit it.","- `kind`: `radius | padding | gap | border-width`.","- `set` or `shift`, exactly one of the two. `set` takes an existing token on that kind's ladder. `shift` is a whole number of steps, clamped at the ladder ends.","- `full` (radius shifts only): admits `--radius-full` as the ladder's top rung. `set` plus `full` is an error, so a pill request is `set: \"--radius-full\"` with no `full` flag.","","## Idioms","","This table covers an intent that names no anchor. When the intent names one, `references/geometry-anchors.md` has the row and it wins.","","| The intent says | Ops |","|---|---|","| pill, capsule | radius `set: \"--radius-full\"`, plus the padding the pill needs (see below) |","| 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`, so repeated pushes reach pill and a pill can come back down |","| 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 whole-look intent often arrives as a direction rather than an op. Playful, friendly, or soft is rounder and a step airier, with pill buttons when the direction is warm. Luxurious, elegant, or editorial is sharper corners, airier padding, thin borders. Technical, dense, or systematic is tighter spacing, a small radius, and square corners on containers. Calm or minimal leaves geometry alone.","","Magnitude words: \"slightly\" or \"a bit\" is 1 step, unqualified is 1 to 2, \"much\", \"way\", or \"really\" is 2 to 3. Mood words often mean both axes: \"softer\" is rounder plus airier, \"compact\" is tighter padding plus smaller gaps.","","## Controls squeeze before containers","","A global op spends the same number of steps everywhere, but a step costs a control far more than a container. `padding shift: -2` takes a card from a 16px inset to 10px and it is still a card. It takes a button from 8 to 4, doubled to 8px at each end, around an 18px line. 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 (`card`, `dialog`, `panel`, `collapsiblesection`, `sidenavigation`, `table`, `codesnippet`) and leave the controls alone. Loosening is not symmetric: airier is safe globally, because nothing breaks by growing.","","A pill needs the room most. `--radius-full` bends the corner in over the first and last glyph, so a capsule wants more horizontal inset than a square-cornered control, never less. `--space-8` is the floor for a large-text pill, which is where compact Midnight Study sits; the roomier pill presets (Ocean, Sunset, Royal Velvet) run `--space-10` to `--space-12`. Pair the radius op with a padding `set` on the same target, placed after any global compaction so it wins outright:","","```json","{ \"ops\": ["," { \"kind\": \"padding\", \"shift\": -1 },"," { \"target\": \"button\", \"kind\": \"radius\", \"set\": \"--radius-full\" },"," { \"target\": \"button\", \"kind\": \"padding\", \"set\": \"--space-10\" }","] }","```","","## Ladders","","Radius runs `none, sm, md, lg, xl, 2xl, 3xl, 4xl`, with `full` as the gated ninth rung. Space (padding and gap) is the editor picker's subset: `0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 48`, so every written value stays re-editable by hand. Border width is the full `--border-width-*` scale. `set` values must be on the ladder (`--space-64` is rejected).","","Content insets stop at `--space-4`. Below it the text sits against its own edge, so `--space-0` and `--space-2` are destinations a person picks on purpose, not ones a relative \"tighter\" hands you. Both stay available through the editor picker and through `set`. An alias already below the floor still moves up, and a shift that would push one under `--space-4` reports as clamped and writes nothing.","","Padding that wraps a line of type stops a rung higher, at `--space-6`. The engine spots it in the config itself: a variant that also declares a `-text-font-size` is holding text, and the components that hold text double their 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. Outer space is exempt, because a 2px gap between an icon and its label, or a 2px margin under a bar, is ordinary design rather than a mistake. Note that `-margin` rides the `padding` kind, so a padding op moves margins too; it just does not floor them.","","An alias sitting off the subset spends its first step reaching the rung the shift points at, so `--space-2` with `shift: 1` lands on `--space-4` rather than jumping past it.","","## Scope","","Every value written is an existing token; nothing new is minted. `tokens.css`, saved themes, colors, and fonts are never touched, so any theme composes with any shape state. An adjustment is an unsaved edit: Save the open theme in the editor to keep it, and Adopt to ship it. Both stay human actions.","","## Verify","","- The CLI exits 0 and the report card lists the changes you expected, with no surprising skips.","- The app (dev server running) shows the new shape on each changed component after a reload.","- Buttons still read as buttons: the label has room at both ends, and a pill has more of it than a square-cornered control had.","- `component-configs/<id>/_working.json` exists for every component the report listed. That buffer is the whole change: it stays until the open theme is saved or another theme is loaded.","- To revert, run the inverse ops, or load a theme in the Theme panel to discard every unsaved edit."],
39
+ "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\" a border-width shift of 1, \"padding +1\" a padding and gap shift of","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 |"],
40
+ },
41
+ "set-type": {
42
+ "SKILL.md": ["---","name: live-tokens-set-type","description: Choose and apply a Google Fonts pairing for a live-tokens theme, binding families to the shipped --font-* stacks. Use whenever the user asks to pair fonts, pick a typeface, change or set the fonts, or describes type by voice: what font should the headings use, make the type more editorial, friendlier, more technical, more elegant, a serif for headings, a display font for this theme, less generic type, match the fonts to the theme. Also invoked by live-tokens-create-theme, which supplies the type intent for a whole look. Changes type only, never color. Not for a single token (use the editor) or for a whole look (see live-tokens-create-theme).","---","","# Setting a theme's fonts","","You choose the families; the CLI verifies each against Google Fonts, builds the URL from the weights the family actually has, and writes the result into the unsaved buffer. Never hand-author font JSON and never edit the data tree directly. Google Fonts is the pool because it is freely licensable and loads by URL; other sources go in by hand through the editor's Project fonts section.","","## Workflow","","1. Read the type intent. When it names an anchor (a feeling, an idiom, or a genre), read `references/type-anchors.md` for that entry; it overrides the Voice table below. Choose the pairing with the framework here and write the pairing file to `scratch/font-pairing.json`.","2. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's real weights and URL, and the weights your typography tokens ask for that the family lacks.","3. Read the report. A weight gap is a quality note: name it and offer an alternative only if it matters (a body face without 400, 700, or italic matters; a display face without 300 does not). A family not on Google Fonts fails the run; fix the spelling and re-run.","4. Report back in a line: the two families, the form model behind each, and any weight gap worth naming.","5. Tell the user to reload the editor page before saving. A running editor holds its own copy of the buffer this CLI just wrote and never re-reads it, so a Save without a reload writes the stale copy back and the pairing vanishes with a success report still on screen. After the reload the type is on the page, and unsaved until they save the open theme.","","State your reasoning when you propose the pairing: each face's form model and the matrix verdict, in one sentence, so the user can argue with the argument rather than only the result.","","Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires an explicit URL per family; use it only offline with a URL in hand.","","## The pairing file","","```json","{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }","```","","Every slot is optional and an omitted slot is left exactly as it is. `display` is `--font-display`, `body` is `--font-sans`; `serif`, `mono` and `editorial` exist when a theme needs them. `editorial` is `--font-editorial`, the long-reading face behind the `--editorial-*` text styles: it tracks the body face until a theme repoints it, so set it only when essays and articles should not carry the body face. A slot may be `{ \"name\": \"...\", \"url\": \"...\" }` to pin an exact URL. Spell families as Google does; the CLI reports the canonical spelling back.","","## 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 must have regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face failing any of these is a display face whatever its name says. Single-weight families are fine for `display` and disqualifying for `body`.","","The shipped text styles ask the display face for 600, across all four heading levels, and the body face for 400; prose markup adds 700 and italic for `strong` and `em`. Screen candidates against those four before running, so the report confirms a decision instead of reporting a surprise.","","## The font matrix: the decision rule","","Classify each candidate on two layers. The **skeleton** is its form model; the **flesh** is its stroke contrast and serif treatment.","","| 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 |","","- **Same skeleton, different flesh: reliable.** Helvetica and Bodoni are both rational, one a linear sans and one a contrasting serif.","- **Same flesh, different skeleton: the failure case.** The two look alike on the surface and fight underneath. This is why two arbitrary sans-serifs so often clash.","- **Far apart on both: works, deliberately.** 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 instead.","","## 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 |","","This table covers an intent that names no anchor. When the intent names one, `references/type-anchors.md` has the row and it wins.","","Match the type to the same design direction the color came from. A warm autumn palette under a cold geometric sans reads as two projects.","","## Shortcuts","","These find an adequate pairing fast and skip the reasoning; use them when the request is vague or the type should stay quiet.","","- **A superfamily.** Google Fonts families with both sans and serif siblings, among them Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not, so treat it as a starting set: `set-type` verifies every family against the API and fails loudly on one 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 are set from one size scale, so a small-x-height display face over a large-x-height body face gives a heading that looks weaker than its own body text. This is the one visual check that matters on screen; make it on the rendered page.","- **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` moves families between stacks and nothing else, writing only the font entries in the unsaved buffer. Save the theme to keep it, Adopt to ship it. Adopt is also what rewrites `fonts.css`, which is how a build with no editor in it loads the family at all.","","## Verify","","- The CLI exits 0 and names each stack that moved, before and after.","- Each URL reflects the family's real weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face.","- The app shows the new type after a reload, and the editor's Fonts section lists both families with their fallbacks intact.","- To revert, run the inverse pairing file, or load the open theme again to discard the buffer."],
43
+ "references/type-anchors.md": ["# Type anchors: feelings, idioms, and genres","","Read this when the type intent names one of these. An anchor overrides the","Voice table in SKILL.md, because it is tuned to the same direction the color","came from.","","An entry names a form model and a role, never a family. Take the families from","the font matrix and the body-face rule in SKILL.md, and screen them against the","weights the shipped text styles ask for.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its type from the","feeling it implies: Autumn reads cozy, Halloween reads ominous, Spring reads","tender.","","## Feelings","","| Anchor | Type voice |","|---|---|","| Joyful, exuberant, energetic | dynamic sans in both slots |","| Playful, whimsical | expressive display over a plain workhorse |","| Optimistic, hopeful | humanist sans, low contrast |","| Confident, bold | heavy rational display, plain body |","| Serene, tranquil | dynamic sans, one family |","| Tender, gentle, romantic | soft serif display over humanist sans, light weights |","| Cozy, comforting | dynamic serif over dynamic sans |","| Wistful, nostalgic, vintage, faded | rational serif display, quiet body |","| Earthy, grounded, natural | dynamic serif over humanist sans |","| Clinical, sterile, precise | one neo-grotesque, tightly set |","| Contemplative, focused | one quiet superfamily |","| Urgent, alarming | condensed grotesque, heavy |","| Tense, anxious | neo-grotesque, tightly set |","| Defiant, rebellious, loud | heavy display; a mono body works |","| Melancholy, moody, sad | rational serif display over quiet sans |","| Somber, grave, mournful | rational serif in both slots |","| Ominous, dramatic, haunted | heavy high-contrast display |","| Austere, severe, cold | one rational family |","","## Idioms, eras, and genres","","| Anchor | Type voice |","|---|---|","| Swiss, International | one rational neo-grotesque across both slots |","| Bauhaus | geometric sans, heavy display |","| Mid-century modern | dynamic sans, or a geometric display over a dynamic body |","| Art deco, opulent, luxurious | rational high-contrast serif display, quiet body |","| Terminal, phosphor | mono in both slots |","| Cyberpunk, neon noir, futuristic | wide geometric display, neutral body |","| Vaporwave | wide retro display, serif welcome |","| Y2K, bubble | geometric sans, heavy display |","| Blueprint | mono, or a technical grotesque; a mono body works here and almost nowhere else |","| Scandinavian, hygge | dynamic sans on both sides |","| Japandi, wabi-sabi | rational serif display over a quiet humanist body |","| Cottagecore, botanical | dynamic serif display over humanist sans |","| Editorial, magazine | high-contrast serif display over a text serif or humanist sans |","| Newsprint, broadsheet | rational serif in both slots |","| Risograph, zine | expressive display over a plain workhorse |","| Corporate, professional, trustworthy | rational sans body with a rational serif or same-family display |","| Brutalist | neo-grotesque or mono at heavy weight |","| Memphis, postmodern | geometric display, plain body |","| Industrial, workshop, gritty | condensed grotesque display over a plain body |"],
44
+ },
45
+ };
@@ -0,0 +1 @@
1
+ export { SKILL_DOC, skillDocs } from './skillSources.generated';