@motion-proto/live-tokens 0.81.0 → 0.83.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 (99) hide show
  1. package/.claude/skills/live-tokens-create-component/SKILL.md +16 -4
  2. package/.claude/skills/live-tokens-create-page/SKILL.md +19 -17
  3. package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +3 -3
  4. package/.claude/skills/live-tokens-pick-component/SKILL.md +10 -83
  5. package/CHANGELOG.md +78 -0
  6. package/README.md +1 -1
  7. package/bin/cli.mjs +21 -8
  8. package/bin/lib/catalogue.mjs +140 -41
  9. package/bin/rules/componentStructure.mjs +12 -4
  10. package/dist-plugin/{chunk-REBHE3ZM.js → chunk-6JKUYCPL.js} +29 -9
  11. package/dist-plugin/{chunk-D4WRIKEZ.js → chunk-LBZISJPG.js} +1 -1
  12. package/dist-plugin/index.cjs +33 -13
  13. package/dist-plugin/index.js +2 -2
  14. package/dist-plugin/migrateData/index.cjs +29 -9
  15. package/dist-plugin/migrateData/index.js +2 -2
  16. package/dist-plugin/setColors/index.cjs +29 -9
  17. package/dist-plugin/setColors/index.js +1 -1
  18. package/dist-plugin/setGeometry/index.cjs +29 -9
  19. package/dist-plugin/setGeometry/index.js +1 -1
  20. package/package.json +19 -1
  21. package/src/demo/Demo.svelte +97 -0
  22. package/src/demo/Demo.svelte.d.ts +7 -0
  23. package/src/demo/DemoBar.svelte +75 -0
  24. package/src/demo/FloatingTokenTags.css +284 -0
  25. package/src/demo/FloatingTokenTags.svelte +570 -0
  26. package/src/demo/LabeledSelect.svelte +243 -0
  27. package/src/demo/Section.svelte +46 -0
  28. package/src/demo/SketchSelect.svelte +58 -0
  29. package/src/demo/SourceLinks.svelte +156 -0
  30. package/src/demo/TestingLoops.svelte +876 -0
  31. package/src/demo/TestingLoops.svelte.d.ts +4 -0
  32. package/src/demo/ThemeSelect.svelte +52 -0
  33. package/src/demo/sections/SectionClaude.svelte +192 -0
  34. package/src/demo/sections/SectionFooter.svelte +67 -0
  35. package/src/demo/sections/SectionGetStarted.svelte +100 -0
  36. package/src/demo/sections/SectionHero.svelte +201 -0
  37. package/src/demo/sections/SectionKit.svelte +410 -0
  38. package/src/demo/sections/SectionLayers.svelte +54 -0
  39. package/src/demo/sections/SectionOffering.svelte +135 -0
  40. package/src/demo/sketchCarry.ts +91 -0
  41. package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -1
  42. package/src/editor/component-editor/scaffolding/types.ts +13 -8
  43. package/src/editor/core/sketch/sketchLayer.ts +1 -1
  44. package/src/editor/core/themes/migrations/2026-09-20-imagelightbox-scrim.ts +28 -0
  45. package/src/editor/core/themes/migrations/index.ts +2 -0
  46. package/src/editor/docs/content/creating-components.md +3 -0
  47. package/src/editor/docs/content.generated.ts +1 -1
  48. package/src/editor/skill-atlas/SkillAtlas.svelte +65 -38
  49. package/src/editor/skill-atlas/SkillValue.svelte +202 -0
  50. package/src/editor/skill-atlas/evalResults.ts +46 -0
  51. package/src/editor/skill-atlas/skillSources.generated.ts +4 -4
  52. package/src/editor/skill-atlas/trees/create-component.ts +18 -18
  53. package/src/editor/skill-atlas/trees/create-page.ts +28 -32
  54. package/src/editor/skill-atlas/trees/pick-component.ts +66 -196
  55. package/src/live-tokens/data/themes/autumn.json +3 -3
  56. package/src/live-tokens/data/themes/halloween.json +3 -3
  57. package/src/live-tokens/data/themes/midnight-study.json +3 -3
  58. package/src/live-tokens/data/themes/ocean.json +3 -3
  59. package/src/live-tokens/data/themes/royal-velvet.json +3 -3
  60. package/src/live-tokens/data/themes/sketchy.json +3 -3
  61. package/src/live-tokens/data/themes/spring-meadow.json +3 -3
  62. package/src/live-tokens/data/themes/sunset.json +3 -3
  63. package/src/system/components/Badge.svelte +5 -2
  64. package/src/system/components/Button.svelte +10 -2
  65. package/src/system/components/Callout.svelte +5 -2
  66. package/src/system/components/Card.svelte +8 -3
  67. package/src/system/components/CodeSnippet.svelte +6 -2
  68. package/src/system/components/CollapsibleSection.svelte +6 -2
  69. package/src/system/components/CornerBadge.svelte +5 -2
  70. package/src/system/components/Dialog.svelte +5 -2
  71. package/src/system/components/IconButton.svelte +5 -2
  72. package/src/system/components/Image.svelte +5 -2
  73. package/src/system/components/ImageLightbox.svelte +47 -4
  74. package/src/system/components/InlineEditActions.svelte +5 -2
  75. package/src/system/components/Input.svelte +6 -2
  76. package/src/system/components/MenuSelect.svelte +9 -2
  77. package/src/system/components/Notification.svelte +5 -2
  78. package/src/system/components/Panel.svelte +5 -2
  79. package/src/system/components/ProgressBar.svelte +5 -2
  80. package/src/system/components/RadioButton.svelte +5 -2
  81. package/src/system/components/SectionDivider.svelte +5 -2
  82. package/src/system/components/SegmentedControl.svelte +5 -2
  83. package/src/system/components/SideNavigation.svelte +5 -2
  84. package/src/system/components/Slider.svelte +6 -2
  85. package/src/system/components/TabBar.svelte +5 -2
  86. package/src/system/components/Table.svelte +4 -2
  87. package/src/system/components/Toggle.svelte +5 -2
  88. package/src/system/components/Tooltip.svelte +5 -2
  89. package/src/testing-js/{chunk-ZMZQZ33J.js → chunk-RDHCBQ6I.js} +2 -2
  90. package/src/testing-js/chunk-RDHCBQ6I.js.map +1 -0
  91. package/src/testing-js/{chunk-Q3YIAAG3.js → chunk-XV5CYADO.js} +2 -2
  92. package/src/testing-js/component-behavior.contract.js +1 -1
  93. package/src/testing-js/component-editor.contract.js +1 -1
  94. package/src/testing-js/component-render.contract.js +1 -1
  95. package/src/testing-js/index.js +2 -2
  96. package/src/testing-js/page-compliance.contract.js +1 -1
  97. package/src/testing-js/vitest.js +2 -2
  98. package/src/testing-js/chunk-ZMZQZ33J.js.map +0 -1
  99. /package/src/testing-js/{chunk-Q3YIAAG3.js.map → chunk-XV5CYADO.js.map} +0 -0
@@ -101,9 +101,9 @@ Name a role as the shipped component that paints the same thing names it. A fill
101
101
 
102
102
  ## Runtime component
103
103
 
104
- Create `src/system/components/StatCard.svelte`. `check-component` finds a runtime there only. A component in another directory is listed by `components` and `report` when that directory is named in `"componentDirs"` in `live-tokens.config.json`, and `check-component` does not check it. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.
104
+ Create `src/system/components/StatCard.svelte`. A component in another directory is listed by `components` and `report`, and checked by `check-component <id>`, when that directory is named in `"componentDirs"` in `live-tokens.config.json`. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.
105
105
 
106
- Open the file with a `<script module lang="ts">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Each field is `key: <string literal>`, in single, double, or backtick quotes; no `${}` interpolation, no concatenation, no identifier reference. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.
106
+ Open the file with a `<script module lang="ts">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Every value is a literal: a string in single, double, or backtick quotes, an object of strings, or an array. No `${}` interpolation, no concatenation, no identifier reference. Each `whenNotToUse` row is `{ when, use? }`: `when` states the condition that rules this component out, and `use`, where a sibling covers it, names that sibling by its component id (`table`, never `Table`); `check-component` reports `missing-description` when no component has that id. An optional `constraints` array states each rule of use as one sentence. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.
107
107
 
108
108
  ```svelte
109
109
  <script module lang="ts">
@@ -111,12 +111,24 @@ Open the file with a `<script module lang="ts">` block that exports a `catalogue
111
111
 
112
112
  export const catalogue = {
113
113
  description: 'A figure with its label.',
114
- useFor: 'one number the reader takes in at a glance.',
115
- notFor: 'a set of records (Table); a titled block of content (Card).',
114
+ whenToUse: 'one number the reader takes in at a glance.',
115
+ whenNotToUse: [
116
+ { when: 'the reader scans and compares many records.', use: 'table' },
117
+ { when: 'the figure belongs to a titled block of content.', use: 'card' },
118
+ ],
119
+ constraints: ['The label names the figure in the words the page uses for it.'],
116
120
  } satisfies CatalogueEntry;
117
121
  </script>
118
122
  ```
119
123
 
124
+ An agent chooses by these sentences, so word them the way the shipped entries are worded:
125
+
126
+ - `description` is one sentence that says what the component is.
127
+ - `whenToUse` states the one condition that makes this component right.
128
+ - Each `when` states one condition that rules this component out. It reads correctly after "do not use <Name> when".
129
+ - A row takes a `use` when another component fits that condition, and no `use` when none does.
130
+ - When the nearest sibling is one of the project's own components, give that sibling a row that names the new component. An agent reaches a component by following `use`.
131
+
120
132
  Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.
121
133
 
122
134
  ```svelte
@@ -12,7 +12,7 @@ Assemble the page from the shipped components at their defaults and the theme's
12
12
  1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.
13
13
  2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.
14
14
  3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.
15
- 4. Give each section its container from the Containers by purpose list.
15
+ 4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.
16
16
  5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.
17
17
  6. Write the page CSS in design tokens.
18
18
  7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.
@@ -70,26 +70,28 @@ Show related items side by side when the width permits. A line of copy runs 45 t
70
70
 
71
71
  `references/layout-sources.md` names the sources for these laws.
72
72
 
73
- ## Containers by purpose
74
-
75
- - `Panel` is a stage: a canvas, a player, a preview. `minHeight` holds its height while what it shows changes.
76
- - An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant="danger"`.
77
- - `Card` is a titled block of content. Its `title` prop is the title, and the card's own tokens type it.
78
- - A container in a tool UI labels itself: `Card variant="bare"` with the label in the body as `--body-sm-*` in `--text-secondary`.
79
- - A form puts the essential fields first and the secondary fields in a `CollapsibleSection`. Its actions sit on the bottom edge.
80
- - A row of fields is a flex row with `gap: var(--space-20)`. Each field's wrapper takes `flex: 1`.
81
- - A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`, the primary last. A `danger` Button sits apart from the group it could be mistaken for.
82
- - A vertical stack of Buttons sets `fullWidth` on each Button. A row omits it.
83
- - `MenuSelect` renders its list open. For a picker, toggle it from a Button with a trailing chevron (`icon="fa-solid fa-chevron-down" iconPosition="right"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.
84
-
85
73
  ## Components
86
74
 
87
75
  - Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.
88
- - `npx live-tokens components <id>` prints the declared props, the values each union accepts, and the catalogue entry. `--json` prints the same as data. The list includes the project's own components.
76
+ - `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.
89
77
  - Pass only the props a component declares.
90
78
  - A shipped component fills its parent. To size one, size the element the page wraps it in.
91
79
  - A native element with no chrome of its own needs no component: an `<input type="file">` behind a Button, a `<canvas>`, an `<img>` inside a stage.
92
80
  - Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.
81
+ - An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant="danger"`.
82
+ - A container in a tool UI labels itself: `Card variant="bare"` with the label in the body as `--body-sm-*` in `--text-secondary`.
83
+ - A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.
84
+ - A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.
85
+ - For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon="fa-solid fa-chevron-down" iconPosition="right"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.
86
+
87
+ ## Rules the checker enforces
88
+
89
+ `check-page` fixes what it can and reports the rest by rule id.
90
+
91
+ - `multiple-primary`
92
+ - `danger-without-dialog`
93
+ - `control-size`
94
+ - `native-control`
93
95
 
94
96
  ## Tokens
95
97
 
@@ -119,11 +121,11 @@ Use the semantic element for each place: one `h1`, an `h2` for each section, `h3
119
121
 
120
122
  ### Size
121
123
 
122
- Omit `size` on every control and container. The shipped default is the page's size.
124
+ Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.
123
125
 
124
126
  ### Emphasis
125
127
 
126
- One `primary` Button per page: the action that completes the page's main task. An action that supports that task is `secondary`. An action unrelated to the task, or informational, is `outline`. An action that destroys saved work is `danger`.
128
+ `npx live-tokens components button` names each `variant` and the role it carries.
127
129
 
128
130
  In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.
129
131
 
@@ -178,7 +180,7 @@ The checkers cannot see a layout. Open the page at the width it is built for and
178
180
  - The containers in a section align at the bottom.
179
181
  - The actions sit where the eye goes last, with the one primary at the end.
180
182
  - Every row of actions holds an action that leaves without committing.
181
- - An action that destroys saved work confirms in a `Dialog`.
183
+ - Every action that destroys saved work meets the Button constraint `danger-without-dialog`.
182
184
  - An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.
183
185
  - Every field has a default, and Reset restores it.
184
186
  - Secondary settings sit in a `CollapsibleSection`. Every control is in view.
@@ -14,9 +14,9 @@ to keep a later edit from reopening it.
14
14
  |---|---|---|
15
15
  | Visibility of system status | Page | Verify: an action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`. |
16
16
  | Match between system and the real world | Page | Verify: labels use the user's words. |
17
- | User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: an unrelated or informational action is `outline`. |
17
+ | User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: `multiple-primary`. |
18
18
  | Consistency and standards | Page | One size, one primary action, one text style per element. The components carry the rest. |
19
- | Error prevention | Page | Verify: an action that destroys saved work confirms in a `Dialog`; the checker's `danger-without-dialog`. Verify: every field has a default and Reset restores it. |
19
+ | Error prevention | Page | Verify: `danger-without-dialog`. Verify: every field has a default and Reset restores it. |
20
20
  | Recognition rather than recall | Component | `MenuSelect` lists the options; `Input` carries its label and hint; `Tooltip` defines in place. |
21
21
  | Flexibility and efficiency of use | Product | Shortcuts and customisation are product decisions. |
22
22
  | Aesthetic and minimalist design | Page | Layout: each element serves a purpose no other element serves. Verify: secondary settings sit in a `CollapsibleSection`. |
@@ -40,7 +40,7 @@ control. Undo, version history, and autosave are the product's.
40
40
  | Discoverability | Page | Verify: every control is in view. |
41
41
  | Explorable interfaces | Page | Verify: every row of actions holds an action that leaves without committing. |
42
42
  | Fitts's law | Component | The shipped default is the large target; SKILL.md's one-size rule keeps it. A toolbar sits on the section's bottom edge. |
43
- | Protect users' work | Page | Verify: an action that destroys saved work confirms in a `Dialog`. |
43
+ | Protect users' work | Page | Verify: `danger-without-dialog`. |
44
44
  | Readability | Component | live-tokens-set-colors gates every text pair at WCAG AA. |
45
45
  | Simplicity | Page | Verify: secondary settings sit in a `CollapsibleSection`; no capability is removed for the sake of simplicity. |
46
46
  | Visible navigation | Component | `SideNavigation` follows the current path. |
@@ -1,96 +1,23 @@
1
1
  ---
2
2
  name: live-tokens-pick-component
3
- description: Recommend which shipped @motion-proto/live-tokens component fits a UX need, with a decision test for each confusable family. Called by live-tokens-create-page when more than one component could fit, and by live-tokens-create-component before it authors anything. Use when the user asks which component to use, or what the difference between two components is. Use when the user asks how to show or capture a UX outcome. Edits no file. For size, emphasis, or placement, read live-tokens-create-page. When the catalogue lacks a component with chrome, read live-tokens-create-component.
3
+ description: Recommend which shipped @motion-proto/live-tokens component fits a UX need, by reading each component's catalogue entry. Called by live-tokens-create-page when more than one component could fit, and by live-tokens-create-component before it authors anything. Use when the user asks which component to use, or what the difference between two components is. Use when the user asks how to show or capture a UX outcome. Edits no file. For size, emphasis, or placement, read live-tokens-create-page. When the catalogue lacks a component with chrome, read live-tokens-create-component.
4
4
  ---
5
5
 
6
6
  # Picking a live-tokens component
7
7
 
8
- When more than one shipped component could fit, find the family below that names the candidates. Apply its test, which asks what the choice means to the reader.
8
+ When more than one component could fit, read the catalogue entries. Each entry states the condition that makes the component right and the conditions that rule it out.
9
9
 
10
- ## Catalogue
10
+ ## Procedure
11
11
 
12
- Before choosing, run `npx live-tokens components`. The list holds every component the project has, shipped and custom, with each one's variants and catalogue entry. The family tests below name the shipped set only. Weigh a custom component by the same tests.
13
-
14
- ## Action family
15
-
16
- - The action needs a word to be unambiguous: `Button`.
17
- - The glyph alone is plain (close, edit, delete) and space is short: `IconButton`.
18
- - The pair that confirms or cancels an inline edit: `InlineEditActions`.
19
-
20
- ## Single-selection family
21
-
22
- Four components pick one option from a set. The test is the option count, whether the selection swaps the content below, and how much the choice asks of the reader.
23
-
24
- | Component | Test | Option count |
25
- |---|---|---|
26
- | `SegmentedControl` | An inline switch between views of the same data. It sits in a row of controls. | 2 to 4 |
27
- | `TabBar` | The content area below swaps. | 2 to 7 |
28
- | `RadioButton` | The reader reads every option as text inside a larger form. | any |
29
- | `MenuSelect` | The options would overflow a row. | any |
30
-
31
- - When a label would wrap in a `SegmentedControl`, use `RadioButton` rows.
32
- - The URL changes: `SideNavigation`. Sections inside one page: `TabBar`.
33
-
34
- ## Text entry
35
-
36
- The test is whether the answer comes from a predefined list of options.
37
-
38
- - A predefined list (a status, a currency, a size, a country): the single-selection family, by its own test. Up to four options sit in a row; more go in a `MenuSelect`, which scrolls.
39
- - No list (a name, a search string, an amount, a message, a street address): `Input`. Validation keeps a typed answer well-formed.
40
- - A long list the reader would rather filter by typing (a city): no shipped component filters a list. Use `Input` with validation, or author a filtering select with **live-tokens-create-component**.
41
- - A number where the position on a track carries the meaning (a volume, a price range, a percentage): `Slider`. A number the reader knows and would rather type: `Input` with `type="number"`.
42
-
43
- ## On and off
44
-
45
- Three components express a binary choice. The test is whether the two states have names of their own.
46
-
47
- | Component | Test |
48
- |---|---|
49
- | `Toggle` | A setting that takes effect at once. The label names the setting. The switch position is the state. |
50
- | `SegmentedControl` | Two named alternatives the reader compares (Light / Dark, List / Grid). Both labels show at once. |
51
- | `RadioButton` pair | A yes or no the reader answers inside a larger form. |
52
-
53
- When the two states share the feature's one name, use `Toggle`. "Email notifications" has no "off" label.
54
-
55
- ## Container family
56
-
57
- Four components hold a block of content. The test is what the block is to the reader: one item, a section of the page, secondary content that stays collapsed until opened, or a decision.
58
-
59
- | Component | Modality | Test |
60
- |---|---|---|
61
- | `Card` | Inline, always open | One item, or each item in a set: a product, a record, a plan. It has a title and can react to hover. |
62
- | `Panel` | Inline, always open | One section of the page's content in a frame: a stage, a list, a form, a block of copy. `minHeight` holds its height while the content changes. |
63
- | `CollapsibleSection` | Inline, collapsed until the reader opens it | Secondary content most readers skip. |
64
- | `Dialog` | Modal, blocks the page | A decision the page cannot continue without: a destructive confirmation, payment, sign-in. |
65
-
66
- A set of items is one `Card` per item. A routine form goes inline in a `Panel`.
67
-
68
- ## Messaging family
69
-
70
- Five components carry a message. The test is what the message is about, what brings it on, and whether the reader dismisses it.
71
-
72
- | Component | Scope | Trigger | Dismissable | Test |
73
- |---|---|---|---|---|
74
- | `Callout` | A section | Always present | No | Something the reader must know about the content around it |
75
- | `Notification` | The system | An action or event | Yes | Feedback about something that just happened |
76
- | `Tooltip` | An element | Hover or focus | On leave | A definition or hint the reader can do without |
77
- | `Badge` | An element | Always present | No | A standing label read at a glance ("Beta", "New", "v2") |
78
- | `CornerBadge` | A parent's corner | Always present | No | A count or status marker on the thing it describes |
79
-
80
- `Badge` and `CornerBadge` differ in position only.
81
-
82
- ## Display family
83
-
84
- Each pair holds a block the reader views and one the reader interacts with. The test is which of the two the page needs.
85
-
86
- - A picture the page shows: `Image`. A picture whose detail the reader must open, or a gallery: `ImageLightbox`.
87
- - Records the reader scans and compares: `Table`. A set of items the reader acts on: one `Card` per item.
88
- - A read-out of progress: `ProgressBar`. A number the reader sets: `Slider`.
89
- - Text the reader runs or pastes (an install command, a key, an id): `CodeSnippet`. Prose the reader only reads: a paragraph in its `Card` or `Panel`.
90
- - A titled break between the sections of one page: `SectionDivider`. Movement between pages: `SideNavigation`.
12
+ 1. Run `npx live-tokens components --json`. The list carries the catalogue entry of every shipped component and of each of the project's own components.
13
+ 2. Read each plausible candidate's `whenToUse`, `whenNotToUse`, and `constraints`.
14
+ 3. Drop a candidate whose `whenNotToUse` names a condition the requirement meets.
15
+ 4. When a dropped row names a `use`, weigh that component the same way.
16
+ 5. Choose the surviving candidate whose `whenToUse` condition the requirement meets.
17
+ 6. When no candidate fits, follow "Nothing fits".
91
18
 
92
19
  ## Nothing fits
93
20
 
94
- A native element with no chrome of its own needs no component: an `<input type="file">` behind a Button, a `<canvas>`, an `<img>` inside a stage. When nothing in the catalogue fits a piece with chrome (a `DatePicker`, a `Stepper`), author the component with **live-tokens-create-component**. Size, emphasis, and placement are **live-tokens-create-page**'s.
21
+ A native element with no chrome of its own needs no component: an `<input type="file">` behind a Button, a `<canvas>`, an `<img>` inside a stage. When nothing in the catalogue fits a piece with chrome, such as a `DatePicker`, a search-filtered list, or a `Stepper`, author the component with **live-tokens-create-component**. No shipped component filters a list by typing, so a searchable picker takes this same route. Size, emphasis, and placement are **live-tokens-create-page**'s.
95
22
 
96
23
  `npx live-tokens components <id>` prints one component's catalogue entry, its declared props, and the values each union accepts. `--json` returns the same as data.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.83.0 — The catalogue entry names what rules a component out
4
+
5
+ ### Added
6
+
7
+ - **`ImageLightbox` takes per-instance tile and scrim props.** Any CSS fill, written into
8
+ `--imagelightbox-tile-surface` for that one tile, the way `fit` writes
9
+ `--imagelightbox-tile-object-fit`. A theme's value still governs every tile
10
+ that passes nothing, and the default stays transparent. Art with transparency
11
+ wants a ground: without one the page shows through the art's own gaps while a
12
+ `box` shadow still casts from the rectangle around them. The open stage is
13
+ unaffected — it sits on the scrim and paints no tile fill.
14
+
15
+ `border` and `borderWidth` do the same for `--imagelightbox-tile-border` and
16
+ `--imagelightbox-tile-border-width`. Both are needed where a theme leaves the
17
+ width at zero, since a colour alone paints nothing.
18
+
19
+ `scrim` and `scrimOpacity` set the layer behind the open image, writing
20
+ `--imagelightbox-scrim-surface`. An opacity is mixed into the colour, so
21
+ `scrimOpacity` needs `scrim` to have something to mix. This is the one
22
+ reachable way to set it per page: the modal portals to `<body>`, out of reach
23
+ of any wrapper around the tile.
24
+
25
+ - **`CatalogueEntry` carries `constraints`.** `constraints` is an optional
26
+ `string[]` of rules of use, one sentence each. `components <id>` prints one
27
+ `Not for:` line per `whenNotToUse` row and one `Rule:` line per constraint.
28
+
29
+ - **Skill Atlas: a "Measured value" tab.** It reports an eval of twelve component
30
+ choices, run with the skills and without. Both arms chose correctly; the
31
+ skills cut the turns and the time. `#measured-value` links to it.
32
+
33
+ ### Changed (breaking)
34
+
35
+ - **`--imagelightbox-overlay-surface` is `--imagelightbox-scrim-surface`, and it
36
+ reads `--scrim-high`.** Dialog already called the layer that dims the page a
37
+ scrim and read the `--scrim-*` scale; ImageLightbox called it an overlay and
38
+ mixed its own colour, so a theme could not move both with one value. The
39
+ editor row reads "scrim color". Migration `2026-09-20-imagelightbox-scrim`
40
+ renames the key; the shipped presets carry the new one.
41
+
42
+ - **`CatalogueEntry` requires `description`, `whenToUse`, and `whenNotToUse`.**
43
+ `notFor` is gone. `whenToUse` is a required string: the condition that makes
44
+ the component the right choice. `whenNotToUse` is a required array of
45
+ `{ when, use? }` rows,
46
+ each naming a condition that rules the component out and, where a sibling
47
+ fits instead, that sibling's component id. A component with no disqualifying
48
+ condition declares `whenNotToUse: []`. `constraints` is an optional
49
+ `string[]`. A runtime's `catalogue` export, and any consumer's, must supply
50
+ all three required fields; `missing-description` reports any as missing, or a
51
+ `whenNotToUse` row's `use` that names no component.
52
+ - **`components` and `components --json` drop `tokens` from the list form.**
53
+ The id form (`components <id>`) is unchanged. The `--json` list drops from
54
+ ~179 KB to ~37 KB across the 26 shipped components.
55
+
56
+ ## 0.82.0 — The demo ships from the package
57
+
58
+ ### Added
59
+
60
+ - **The demo page and Testing Loops ship as exports.** Mount
61
+ `@motion-proto/live-tokens/demo` and `@motion-proto/live-tokens/demo/testing-loops`
62
+ as `lazy` routes. The demo takes `onThemePick(fileName)` and
63
+ `onSketchPick(id | null)`, called after the Theme or Sketch select applies a
64
+ pick. Testing Loops takes `homeHref` for its back link, which defaults to
65
+ `/demo`.
66
+ - **The Theme select keeps the sketchstyle on screen.** Picking a theme no
67
+ longer replaces a sketchstyle the visitor picked. The carry runs in every
68
+ document that calls `installSketchCarry()` from
69
+ `@motion-proto/live-tokens/demo/sketch-carry`, and the editor frame needs it
70
+ too, so call it from the app entry.
71
+
72
+ ### Changed
73
+
74
+ - **The demo's Theme and Sketch selects sit in a bar at the top of the page.**
75
+ The bar stays fixed while the page scrolls, and it shows only where the
76
+ editor runs. They left the "Swap tokens with a click" section. On a phone
77
+ they sit above the hero and scroll with the page.
78
+ - **The Sketch select reads Custom for adjusted dials.** Dials moved off every
79
+ named sketchstyle show the Custom placeholder.
80
+
3
81
  ## 0.81.0 — Skill Atlas links name a block
4
82
 
5
83
  ### Added
package/README.md CHANGED
@@ -366,7 +366,7 @@ A project created with `create` runs the forced form from `postinstall` and giti
366
366
 
367
367
  Ask "TabBar or SegmentedControl?", "how do I let someone pick one of four options?", or "what is the difference between a Callout and a Notification?".
368
368
 
369
- The skill holds the catalogue grouped by job (action, input, selection, containers, messaging, display) and a decision table for each confusable family: `SegmentedControl` vs `TabBar` vs `RadioButton` vs `MenuSelect`, `Card` vs `CollapsibleSection` vs `Dialog`, `Callout` vs `Notification` vs `Tooltip` vs `Badge`, `Button` vs `IconButton`, and the on/off case. It answers the question and writes nothing. Read it before authoring anything new.
369
+ The skill runs `npx live-tokens components --json` and reads each candidate's catalogue entry: the condition that makes it the right component, the conditions that rule it out with the component to use in its place, and its rules of use. The entries hold the decision, so a component you author joins the same query. It answers the question and writes nothing. Read it before authoring anything new.
370
370
 
371
371
  ### `live-tokens-create-page`
372
372
 
package/bin/cli.mjs CHANGED
@@ -21,7 +21,13 @@ import process from 'node:process';
21
21
  import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
22
22
  import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
23
23
  import { resolvePageTestTargets } from './lib/pageRoutes.mjs';
24
- import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
24
+ import {
25
+ describeComponents,
26
+ describeTokens,
27
+ formatComponents,
28
+ formatTokens,
29
+ withoutTokens,
30
+ } from './lib/catalogue.mjs';
25
31
  import { applyFixes } from './lib/fixers.mjs';
26
32
  import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
27
33
  import { loadVocabulary } from './lib/tokenVocabulary.mjs';
@@ -58,11 +64,14 @@ Commands:
58
64
  create <dir> [--force] Scaffold a new Svelte + Vite app wired up with
59
65
  live-tokens (editor, components, design tokens)
60
66
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
61
- components [id] [--json] List every component the project has, shipped and
67
+ components [id] [--json]
68
+ List every component the project has, shipped and
62
69
  its own (src/system/components plus any
63
- "componentDirs" in live-tokens.config.json). With
64
- an id, that component's props, variants, tokens,
65
- and defaults
70
+ "componentDirs" in live-tokens.config.json), each
71
+ with its catalogue entry (description, whenToUse,
72
+ whenNotToUse, constraints). The list form omits
73
+ tokens. With an id, that component's
74
+ props, variants, tokens, and defaults
66
75
  tokens [--scale <name>] [--json]
67
76
  List every design token the project's tokens.css
68
77
  declares, by scale, with its value
@@ -265,10 +274,14 @@ function formatFixes({ applied, skipped }) {
265
274
 
266
275
  if (command === 'components') {
267
276
  const opts = parseCheckFlags(rest);
268
- const list = describeComponents(loadVocabulary());
269
277
  const id = opts.rest[0];
270
- if (id && !list.some((c) => c.id === id)) fail(formatComponents(list, { id }));
271
- writeOut(opts.json ? JSON.stringify(id ? list.find((c) => c.id === id) : list, null, 2) : formatComponents(list, { id }));
278
+ const list = describeComponents(loadVocabulary());
279
+ if (id) {
280
+ if (!list.some((c) => c.id === id)) fail(formatComponents(list, { id }));
281
+ writeOut(opts.json ? JSON.stringify(list.find((c) => c.id === id), null, 2) : formatComponents(list, { id }));
282
+ process.exit(0);
283
+ }
284
+ writeOut(opts.json ? JSON.stringify(withoutTokens(list), null, 2) : formatComponents(list, {}));
272
285
  process.exit(0);
273
286
  }
274
287
 
@@ -47,9 +47,11 @@ function literalValue(raw) {
47
47
  return inner.replace(/\s+/g, ' ').trim();
48
48
  }
49
49
 
50
- // Brace matching skips over quoted literals so a `}` inside a description
51
- // (or a description containing a stray brace) never closes the object early.
52
- function findBalanced(text, openIndex) {
50
+ // Brace/bracket matching skips over quoted literals so a closer inside a
51
+ // description (or a description containing a stray brace) never closes the
52
+ // group early. Shared by object literals (`{`/`}`) and array literals
53
+ // (`[`/`]`).
54
+ function findBalanced(text, openIndex, open = '{', close = '}') {
53
55
  let depth = 0;
54
56
  for (let i = openIndex; i < text.length; i++) {
55
57
  const ch = text[i];
@@ -59,8 +61,8 @@ function findBalanced(text, openIndex) {
59
61
  while (i < text.length && text[i] !== quote) i += text[i] === '\\' ? 2 : 1;
60
62
  continue;
61
63
  }
62
- if (ch === '{') depth++;
63
- else if (ch === '}') {
64
+ if (ch === open) depth++;
65
+ else if (ch === close) {
64
66
  depth--;
65
67
  if (depth === 0) return { content: text.slice(openIndex + 1, i), end: i + 1 };
66
68
  }
@@ -68,26 +70,127 @@ function findBalanced(text, openIndex) {
68
70
  return null;
69
71
  }
70
72
 
71
- function parseFieldObject(body) {
73
+ function skipSeparators(text, i) {
74
+ while (i < text.length && /[\s,]/.test(text[i])) i++;
75
+ return i;
76
+ }
77
+
78
+ // The comma that ends the expression starting at `i`. A key or a literal
79
+ // inside an expression the reader cannot read must stay unread with it.
80
+ function expressionEnd(text, i) {
81
+ let depth = 0;
82
+ for (; i < text.length; i++) {
83
+ const ch = text[i];
84
+ if (ch === "'" || ch === '"' || ch === '`') {
85
+ const quote = ch;
86
+ i++;
87
+ while (i < text.length && text[i] !== quote) i += text[i] === '\\' ? 2 : 1;
88
+ } else if ('([{'.includes(ch)) depth++;
89
+ else if (')]}'.includes(ch)) depth--;
90
+ else if (ch === ',' && depth === 0) return i;
91
+ }
92
+ return text.length;
93
+ }
94
+
95
+ function readWholeExpression(text, i) {
96
+ const end = expressionEnd(text, i);
97
+ const expression = text.slice(i, end);
98
+ const result = readValue(expression, 0);
99
+ const literal = result && !expression.slice(result.end).trim();
100
+ return { value: literal ? result.value : undefined, end };
101
+ }
102
+
103
+ /**
104
+ * One literal value starting at `text[i]`: a quoted string, a `[...]` array of
105
+ * values, or a `{...}` object of `key: value` pairs — the subset `catalogueOf`
106
+ * reads, recursively, so a key nested inside a `whenNotToUse` row, `props`, or a
107
+ * `{ rule, text }` constraint is read at its own depth and never mistaken for
108
+ * a top-level field. An identifier, a template with `${}`, a concatenation,
109
+ * or any other expression is outside the subset and reads as absent (`null`),
110
+ * the same tolerance `catalogueOf` always gave a non-literal field.
111
+ */
112
+ function readValue(text, i) {
113
+ i = skipSeparators(text, i);
114
+ const ch = text[i];
115
+ if (ch === "'" || ch === '"' || ch === '`') {
116
+ const m = STRING_LITERAL.exec(text.slice(i));
117
+ if (!m || m.index !== 0) return null;
118
+ const value = literalValue(m[0]);
119
+ return value === undefined ? null : { value, end: i + m[0].length };
120
+ }
121
+ if (ch === '[') {
122
+ const balanced = findBalanced(text, i, '[', ']');
123
+ if (!balanced) return null;
124
+ const items = [];
125
+ let j = 0;
126
+ while (j < balanced.content.length) {
127
+ const item = readWholeExpression(balanced.content, j);
128
+ if (item.value !== undefined) items.push(item.value);
129
+ j = item.end + 1;
130
+ }
131
+ return { value: items, end: balanced.end };
132
+ }
133
+ if (ch === '{') {
134
+ const balanced = findBalanced(text, i, '{', '}');
135
+ if (!balanced) return null;
136
+ return { value: readObject(balanced.content), end: balanced.end };
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /** `key: value` pairs read by depth: the scan resumes after each value's
142
+ * whole expression, read or unread, so a key inside a value's own text is
143
+ * never read as one of `body`'s own fields. */
144
+ function readObject(body) {
72
145
  const fields = {};
73
- const re = new RegExp(`([A-Za-z_$][A-Za-z0-9_$]*)\\s*:\\s*(${STRING_LITERAL.source})`, 'g');
146
+ const keyRe = /([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g;
74
147
  let m;
75
- while ((m = re.exec(body))) {
76
- if (m[1] in fields) continue;
77
- const value = literalValue(m[2]);
78
- if (value !== undefined) fields[m[1]] = value;
148
+ while ((m = keyRe.exec(body))) {
149
+ const result = readWholeExpression(body, keyRe.lastIndex);
150
+ if (result.value !== undefined && !(m[1] in fields)) fields[m[1]] = result.value;
151
+ keyRe.lastIndex = result.end;
79
152
  }
80
153
  return fields;
81
154
  }
82
155
 
156
+ function isPlainObject(v) {
157
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
158
+ }
159
+
160
+ /** An object's own string-valued entries; a non-string value is dropped, key
161
+ * by key, the same tolerance a top-level field gets. */
162
+ function stringEntries(v) {
163
+ if (!isPlainObject(v)) return undefined;
164
+ const out = {};
165
+ for (const [key, value] of Object.entries(v)) if (typeof value === 'string') out[key] = value;
166
+ return out;
167
+ }
168
+
169
+ /** `whenNotToUse` entries: a `{ when, use? }` object with `when` a string and
170
+ * `use`, if present, a string. A row whose `when` is not a string literal is
171
+ * dropped from the array. */
172
+ function whenNotToUseEntries(v) {
173
+ if (!Array.isArray(v)) return undefined;
174
+ const out = [];
175
+ for (const item of v) {
176
+ if (isPlainObject(item) && typeof item.when === 'string') {
177
+ out.push(typeof item.use === 'string' ? { when: item.when, use: item.use } : { when: item.when });
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function constraintEntries(v) {
184
+ if (!Array.isArray(v)) return undefined;
185
+ return v.filter((item) => typeof item === 'string');
186
+ }
187
+
83
188
  /**
84
- * Bounded, non-evaluating parse of the runtime file's `catalogue` export:
85
- * `key: <string literal>` pairs inside the `<script module>` block's
86
- * `export const catalogue = { ... }`, the same way `builtInIds` and
87
- * `componentProps` read the rest of the vocabulary without importing the
88
- * module. A field given as an identifier, a template with `${}`, or a
89
- * concatenation is not a string literal, so it is silently absent rather
90
- * than evaluated. Returns `null` when the file has no such export.
189
+ * Bounded, non-evaluating parse of the runtime file's `catalogue` export
190
+ * inside the `<script module>` block's `export const catalogue = { ... }`,
191
+ * the same way `builtInIds` and `componentProps` read the rest of the
192
+ * vocabulary without importing the module. Returns `null` when the file has
193
+ * no such export.
91
194
  */
92
195
  export function catalogueOf(source) {
93
196
  const moduleBlock = source.match(/<script\s+module[^>]*>([\s\S]*?)<\/script>/);
@@ -98,26 +201,16 @@ export function catalogueOf(source) {
98
201
  const balanced = findBalanced(moduleBlock[1], openIndex);
99
202
  if (!balanced) return null;
100
203
 
101
- let body = balanced.content;
102
- let props;
103
- const propsMatch = /\bprops\s*:\s*\{/.exec(body);
104
- if (propsMatch) {
105
- const propsOpen = propsMatch.index + propsMatch[0].length - 1;
106
- const propsBalanced = findBalanced(body, propsOpen);
107
- if (propsBalanced) {
108
- const parsedProps = parseFieldObject(propsBalanced.content);
109
- if (Object.keys(parsedProps).length) props = parsedProps;
110
- body = body.slice(0, propsMatch.index) + body.slice(propsBalanced.end);
111
- }
112
- }
113
-
114
- const fields = parseFieldObject(body);
115
- const catalogue = {
116
- description: fields.description,
117
- useFor: fields.useFor,
118
- notFor: fields.notFor,
119
- };
120
- if (props) catalogue.props = props;
204
+ const fields = readObject(balanced.content);
205
+ const catalogue = {};
206
+ if (typeof fields.description === 'string') catalogue.description = fields.description;
207
+ if (typeof fields.whenToUse === 'string') catalogue.whenToUse = fields.whenToUse;
208
+ const whenNotToUse = whenNotToUseEntries(fields.whenNotToUse);
209
+ if (whenNotToUse) catalogue.whenNotToUse = whenNotToUse;
210
+ const constraints = constraintEntries(fields.constraints);
211
+ if (constraints && constraints.length) catalogue.constraints = constraints;
212
+ const props = stringEntries(fields.props);
213
+ if (props && Object.keys(props).length) catalogue.props = props;
121
214
  return catalogue;
122
215
  }
123
216
 
@@ -190,6 +283,11 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
190
283
  return out.sort((a, b) => a.origin.localeCompare(b.origin) || a.id.localeCompare(b.id));
191
284
  }
192
285
 
286
+ /** The list form's payload. `tokens` is most of a full listing's bytes, and the id form keeps it. */
287
+ export function withoutTokens(list) {
288
+ return list.map(({ tokens, ...rest }) => rest);
289
+ }
290
+
193
291
  export function describeTokens(vocab, { root = process.cwd() } = {}) {
194
292
  return {
195
293
  tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
@@ -203,11 +301,12 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
203
301
 
204
302
  function describeLines(c) {
205
303
  if (!c.catalogue) return [];
206
- const { description, useFor, notFor, props } = c.catalogue;
304
+ const { description, whenToUse, whenNotToUse, constraints, props } = c.catalogue;
207
305
  const lines = [];
208
306
  if (description) lines.push(description);
209
- if (useFor) lines.push(`Use for: ${useFor}`);
210
- if (notFor) lines.push(`Not for: ${notFor}`);
307
+ if (whenToUse) lines.push(`When to use: ${whenToUse}`);
308
+ for (const row of whenNotToUse ?? []) lines.push(row.use ? `Not for: ${row.when} Use ${row.use}.` : `Not for: ${row.when}`);
309
+ for (const constraint of constraints ?? []) lines.push(`Rule: ${constraint}`);
211
310
  for (const [prop, text] of Object.entries(props ?? {})) lines.push(`${prop}: ${text}`);
212
311
  return lines;
213
312
  }