@motion-proto/live-tokens 0.94.2 → 0.95.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 (108) hide show
  1. package/.claude/skills/live-tokens-create-page/SKILL.md +7 -3
  2. package/CHANGELOG.md +108 -0
  3. package/README.md +6 -6
  4. package/dist-plugin/{chunk-7XSUHJUH.js → chunk-4J6JPSVS.js} +53 -2
  5. package/dist-plugin/{chunk-W5VQV32K.js → chunk-GYRULB2X.js} +1 -1
  6. package/dist-plugin/{chunk-ONYAIXCZ.js → chunk-QQASYPW7.js} +32 -9
  7. package/dist-plugin/index.cjs +598 -17
  8. package/dist-plugin/index.d.cts +9 -1
  9. package/dist-plugin/index.d.ts +9 -1
  10. package/dist-plugin/index.js +513 -6
  11. package/dist-plugin/migrateData/index.cjs +32 -9
  12. package/dist-plugin/migrateData/index.js +2 -2
  13. package/dist-plugin/setColors/index.cjs +32 -22
  14. package/dist-plugin/setColors/index.js +2 -15
  15. package/dist-plugin/setGeometry/index.cjs +32 -9
  16. package/dist-plugin/setGeometry/index.js +1 -1
  17. package/dist-plugin/tokensCssMigrations/index.cjs +53 -2
  18. package/dist-plugin/tokensCssMigrations/index.d.cts +9 -2
  19. package/dist-plugin/tokensCssMigrations/index.d.ts +9 -2
  20. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  21. package/package.json +8 -3
  22. package/src/demo/ChromeSelect.svelte +111 -0
  23. package/src/demo/Demo.svelte +39 -5
  24. package/src/demo/DemoBar.svelte +33 -14
  25. package/src/demo/FloatingTokenTags.css +33 -4
  26. package/src/demo/FloatingTokenTags.svelte +81 -68
  27. package/src/demo/Section.svelte +1 -7
  28. package/src/demo/SketchSelect.svelte +36 -12
  29. package/src/demo/SourceLinks.svelte +5 -4
  30. package/src/demo/TestingLoops.svelte +128 -19
  31. package/src/demo/ThemeSelect.svelte +34 -11
  32. package/src/demo/evalResults.ts +7 -7
  33. package/src/demo/sections/SectionClaude.svelte +20 -13
  34. package/src/demo/sections/SectionFooter.svelte +4 -5
  35. package/src/demo/sections/SectionGetStarted.svelte +5 -6
  36. package/src/demo/sections/SectionHero.svelte +43 -23
  37. package/src/demo/sections/SectionKit.svelte +141 -45
  38. package/src/demo/sections/SectionLayers.svelte +3 -3
  39. package/src/demo/sections/SectionOffering.svelte +8 -23
  40. package/src/editor/bootstrap.ts +12 -6
  41. package/src/editor/component-editor/ImageEditor.svelte +1 -1
  42. package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
  43. package/src/editor/component-editor/registration.ts +36 -0
  44. package/src/editor/component-editor/registry.ts +5 -12
  45. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
  46. package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
  47. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
  48. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
  49. package/src/editor/core/store/editorPersistence.ts +6 -1
  50. package/src/editor/core/store/editorRenderer.ts +1 -4
  51. package/src/editor/core/store/editorStore.ts +5 -9
  52. package/src/editor/core/store/editorTypes.ts +2 -2
  53. package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
  54. package/src/editor/core/themes/migrations/index.ts +2 -0
  55. package/src/editor/core/themes/slices/columns.ts +112 -42
  56. package/src/editor/docs/Docs.svelte +25 -22
  57. package/src/editor/docs/chapters.ts +1 -0
  58. package/src/editor/docs/content/editing-tokens.md +5 -3
  59. package/src/editor/docs/content/production-css.md +116 -0
  60. package/src/editor/docs/content/themes-workflow.md +2 -0
  61. package/src/editor/docs/content.generated.ts +3 -2
  62. package/src/editor/index.ts +2 -1
  63. package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
  64. package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
  65. package/src/editor/pages/Editor.svelte +15 -1
  66. package/src/editor/pages/EditorShell.svelte +312 -99
  67. package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
  68. package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
  69. package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
  70. package/src/editor/skill-atlas/trees/create-page.ts +24 -24
  71. package/src/editor/skill-atlas/wireLayout.ts +62 -0
  72. package/src/editor/ui/BezierCurveEditor.svelte +3 -0
  73. package/src/editor/ui/FontStackEditor.svelte +2 -2
  74. package/src/editor/ui/PaletteEditor.svelte +64 -6
  75. package/src/editor/ui/UIPaletteSelector.svelte +2 -0
  76. package/src/editor/ui/colors/ColorsTab.svelte +5 -2
  77. package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
  78. package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
  79. package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
  80. package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
  81. package/src/editor/ui/sections/WashesSection.svelte +11 -0
  82. package/src/editor/ui/sketch/SketchTab.svelte +1 -1
  83. package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
  84. package/src/live-tokens/data/colors-and-type/default.json +1 -4
  85. package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
  86. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
  87. package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
  88. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
  89. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
  90. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
  91. package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
  92. package/src/live-tokens/data/themes/autumn.json +1 -4
  93. package/src/live-tokens/data/themes/halloween.json +1 -4
  94. package/src/live-tokens/data/themes/midnight-study.json +1 -4
  95. package/src/live-tokens/data/themes/ocean.json +1 -4
  96. package/src/live-tokens/data/themes/royal-velvet.json +1 -4
  97. package/src/live-tokens/data/themes/sketchy.json +1 -4
  98. package/src/live-tokens/data/themes/spring-meadow.json +1 -4
  99. package/src/live-tokens/data/themes/sunset.json +1 -4
  100. package/src/live-tokens/data/tokens.generated.css +1 -4
  101. package/src/system/components/SectionDivider.svelte +4 -0
  102. package/src/system/styles/CONVENTIONS.md +1 -1
  103. package/src/system/styles/tokens.css +30 -4
  104. package/template/src/main.ts +2 -2
  105. package/template/src/pages/Home.svelte +11 -8
  106. package/template/vite.config.ts +7 -3
  107. package/src/system/assets/offering.webp +0 -0
  108. /package/dist-plugin/{chunk-O6GQ6GBH.js → chunk-A4TBP4MM.js} +0 -0
@@ -1,7 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { onMount, tick } from 'svelte';
3
+ import { MediaQuery } from 'svelte/reactivity';
3
4
  import { navigate, route } from '../core/routing/router';
5
+ import LabeledSelect from '../../demo/LabeledSelect.svelte';
4
6
  import Button from '../../system/components/Button.svelte';
7
+ import SegmentedControl from '../../system/components/SegmentedControl.svelte';
5
8
  import TabBar from '../../system/components/TabBar.svelte';
6
9
  import SourcePane from './SourcePane.svelte';
7
10
  import TreeCanvas from './TreeCanvas.svelte';
@@ -19,6 +22,8 @@
19
22
  let selection: Selection | null = $state(linked?.target ?? null);
20
23
  /** Which of the skill's documents the source pane shows. */
21
24
  let doc: string = $state(SKILL_DOC);
25
+ /** Which pane shows below the desktop lock, where the two would stack. */
26
+ let view: 'graph' | 'text' = $state('graph');
22
27
 
23
28
  let tree = $derived(skillTrees[active]);
24
29
  let docs = $derived(skillDocs[active]);
@@ -30,6 +35,13 @@
30
35
  label: `${t.title}\n${skillDocs[id][SKILL_DOC].length} lines`,
31
36
  }));
32
37
 
38
+ const skillItems = Object.entries(skillTrees).map(([id, t]) => ({ value: id, label: t.title }));
39
+
40
+ const views = [
41
+ { value: 'graph', label: 'Graph' },
42
+ { value: 'text', label: 'Text' },
43
+ ];
44
+
33
45
  let docTabs = $derived(
34
46
  Object.keys(docs).map((name) => ({
35
47
  id: name,
@@ -37,9 +49,19 @@
37
49
  })),
38
50
  );
39
51
 
52
+ // Matches the desktop lock in the styles: above it each pane scrolls on its
53
+ // own, below it the page does.
54
+ const locked = new MediaQuery('min-width: 64rem');
55
+
56
+ function toTop(pane: HTMLElement | undefined) {
57
+ if (locked.current) pane?.scrollTo({ top: 0 });
58
+ else window.scrollTo({ top: 0 });
59
+ }
60
+
40
61
  function openDoc(name: string) {
41
62
  doc = name;
42
- sourcePane?.scrollTo({ top: 0 });
63
+ view = 'text';
64
+ toTop(sourcePane);
43
65
  }
44
66
 
45
67
  function selectTarget(key: string, label: string, range: LineRange) {
@@ -84,13 +106,26 @@
84
106
  ?.scrollIntoView({ block: 'center', behavior: 'smooth' });
85
107
  }
86
108
 
109
+ // A pane hidden behind the switch has no layout, so its scroll to the
110
+ // selection has to wait until it shows.
111
+ async function changeView(next: string) {
112
+ view = next as typeof view;
113
+ if (!selection) return;
114
+ if (view === 'graph') {
115
+ await tick();
116
+ scrollTreeTo(selection.key);
117
+ } else if (doc === SKILL_DOC) {
118
+ scrollSourceTo(selection.lines[0]);
119
+ }
120
+ }
121
+
87
122
  function changeTab(id: string) {
88
123
  active = id;
89
124
  selection = null;
90
125
  doc = SKILL_DOC;
91
126
  shareSelection();
92
- treePane?.scrollTo({ top: 0 });
93
- sourcePane?.scrollTo({ top: 0 });
127
+ toTop(treePane);
128
+ toTop(sourcePane);
94
129
  }
95
130
 
96
131
  function openLink(hash: string) {
@@ -129,18 +164,24 @@
129
164
  <div class="skill-atlas">
130
165
  <header class="masthead">
131
166
  <div class="masthead-top">
132
- <h1>Skill Atlas</h1>
133
167
  <Button variant="primary" onclick={() => navigate('/')} icon="fas fa-arrow-left" iconPosition="left">
134
168
  Back to Demo
135
169
  </Button>
170
+ <h1>Skill Atlas</h1>
136
171
  </div>
137
172
  </header>
138
173
 
139
174
  <div class="tabs">
140
- <TabBar {tabs} value={active} onchange={changeTab} />
175
+ <div class="tab-strip">
176
+ <TabBar {tabs} value={active} onchange={changeTab} />
177
+ </div>
178
+ <div class="picker">
179
+ <LabeledSelect label="Skill" items={skillItems} value={active} onchange={changeTab} />
180
+ <SegmentedControl segments={views} value={view} onchange={changeView} />
181
+ </div>
141
182
  </div>
142
183
 
143
- <div class="split">
184
+ <div class="split" data-view={view}>
144
185
  <section class="pane" aria-label="{tree.id} decision tree">
145
186
  <div class="pane-head">
146
187
  <span class="pane-title">{tree.id}</span>
@@ -192,22 +233,18 @@
192
233
 
193
234
  .masthead-top {
194
235
  display: flex;
195
- align-items: center;
196
- /* Beside the title, not at the far right: the editor overlay pins top-right
197
- and would cover a button parked there. */
198
- gap: var(--space-24);
199
- flex-wrap: wrap;
200
- /* The row carries the heading's own margins; align-items centres the margin
201
- box, so a margin left on the h1 offsets the title against the button by
202
- half of it. */
236
+ flex-direction: column;
237
+ align-items: flex-start;
238
+ gap: var(--space-32);
203
239
  }
204
240
 
241
+ /* The home page's heading style, so the two pages title alike. */
205
242
  .masthead-top h1 {
206
- font-family: var(--heading-xl-font-family);
207
- font-size: var(--heading-xl-font-size);
208
- font-weight: var(--heading-xl-font-weight);
209
- line-height: var(--heading-xl-line-height);
210
- letter-spacing: var(--heading-xl-letter-spacing);
243
+ font-family: var(--display-xl-font-family);
244
+ font-size: var(--display-xl-font-size);
245
+ font-weight: var(--display-xl-font-weight);
246
+ line-height: var(--display-xl-line-height);
247
+ letter-spacing: var(--display-xl-letter-spacing);
211
248
  color: var(--text-primary);
212
249
  margin: 0;
213
250
  }
@@ -223,9 +260,36 @@
223
260
  padding-bottom: var(--space-40);
224
261
  background: var(--page-bg);
225
262
  background-attachment: var(--page-bg-attachment, fixed);
263
+ }
264
+
265
+ /* Below the desktop lock a dropdown replaces the strip, which would scroll
266
+ sideways past most of the skills. */
267
+ .tab-strip {
268
+ display: none;
226
269
  overflow-x: auto;
227
270
  }
228
271
 
272
+ .picker {
273
+ display: flex;
274
+ flex-direction: column;
275
+ align-items: flex-start;
276
+ gap: var(--space-16);
277
+ }
278
+
279
+ .picker :global(.labeled-select) {
280
+ align-self: stretch;
281
+ align-items: stretch;
282
+ }
283
+
284
+ .picker :global(.segmented-control) {
285
+ align-self: stretch;
286
+ }
287
+
288
+ .picker :global(.segment) {
289
+ flex: 1;
290
+ justify-content: center;
291
+ }
292
+
229
293
  /* The label carries its own newline, so the line count sits under the skill
230
294
  name instead of wrapping wherever the strip happens to run out. */
231
295
  .tabs :global(.tab span) {
@@ -239,13 +303,19 @@
239
303
  gap: var(--space-32);
240
304
  }
241
305
 
242
- /* The reference tabs only exist on the source pane, so a body of its own
243
- fixed height would leave the two panes ending at different places. */
306
+ .split[data-view='graph'] .pane-source,
307
+ .split[data-view='text'] .pane:not(.pane-source) {
308
+ display: none;
309
+ }
310
+
311
+ /* Below the desktop lock a pane grows to its content and the page scrolls:
312
+ a scroll area nested in a scrolling page traps a phone's swipe. Isolated,
313
+ so the cards' raised badges stay under the pinned strip. */
244
314
  .pane {
245
315
  display: flex;
246
316
  flex-direction: column;
247
- height: 82vh;
248
317
  min-width: 0;
318
+ isolation: isolate;
249
319
  border: var(--border-width-1) solid var(--border-neutral-subtle);
250
320
  border-radius: var(--radius-3xl);
251
321
  background: var(--surface-neutral-lowest);
@@ -318,8 +388,7 @@
318
388
  }
319
389
 
320
390
  /* Desktop: the surface locks to the viewport and each pane scrolls on its
321
- own, so nothing ever slides under the tab strip. Placed after the base
322
- .pane rule: its 82vh would otherwise win on source order. The router wrapper
391
+ own, so nothing ever slides under the tab strip. The router wrapper
323
392
  assumes window-scroll pages (min-height + a 12rem bottom pad); the doubled
324
393
  .lt-app outranks its scoped rule on a specificity tie. The child
325
394
  combinator keeps the demo home out: it has an .atlas of its own. */
@@ -344,6 +413,18 @@
344
413
  flex: 0 0 auto;
345
414
  }
346
415
 
416
+ .tab-strip {
417
+ display: block;
418
+ }
419
+
420
+ .picker {
421
+ display: none;
422
+ }
423
+
424
+ .split[data-view] .pane {
425
+ display: flex;
426
+ }
427
+
347
428
  /* minmax(0, 1fr) pins the row to the remaining height; an auto row would
348
429
  grow to the panes' content and break their scroll. */
349
430
  .split {
@@ -359,6 +440,12 @@
359
440
  }
360
441
  }
361
442
 
443
+ @media (max-width: 600px) {
444
+ .skill-atlas {
445
+ padding-inline: var(--space-16);
446
+ }
447
+ }
448
+
362
449
  @media (prefers-reduced-motion: reduce) {
363
450
  .pane-body {
364
451
  scroll-behavior: auto;
@@ -128,7 +128,15 @@
128
128
  marker-end={wire.arrow ? 'url(#atlas-arrow)' : undefined}
129
129
  />
130
130
  {/each}
131
+ </svg>
131
132
 
133
+ <svg
134
+ class="wires wire-labels"
135
+ viewBox="0 0 {canvasSize.w} {canvasSize.h}"
136
+ width={canvasSize.w}
137
+ height={canvasSize.h}
138
+ aria-hidden="true"
139
+ >
132
140
  {#each labels as label, i (i)}
133
141
  <text
134
142
  class="wire-label"
@@ -200,6 +208,12 @@
200
208
  }
201
209
 
202
210
  /* The halo breaks the wire so the answer reads on its own ground. */
211
+ /* Sketch mode filters every inline svg as artwork, which smears type. The
212
+ labels get their own svg so the wires stay drawn and the answers crisp. */
213
+ .wire-labels {
214
+ --sketch-icon-off: none;
215
+ }
216
+
203
217
  .wire-label {
204
218
  font-family: var(--code-font-family);
205
219
  font-size: var(--font-size-sm);
@@ -17,7 +17,7 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
17
17
  "references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-indicator` | The colour of the bar or stripe that marks an item |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-indicator-width` | An indicator's thickness |","| `-hairline-width` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-hairline-inset` | Inset trimmed from a stretched hairline |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-hairline-width`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
18
18
  },
19
19
  "create-page": {
20
- "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `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.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- 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\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `.body-sm` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. 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.","- 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.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","The text styles fall into five usages, each a scale of seven steps from `2xs` to `2xl`: display for type outside the page outline, heading for the outline, body for interface text and short copy, editorial for long reading, and code. Each step is a style named `--{usage}-{step}-*`, such as `--body-sm-*`. Headings use the whole set. The other usages mostly take `md` and `sm`.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Title inside a section | `h3` to `h6` in `--heading-md-*` to `--heading-2xs-*` |","| Card title | the Card `title` prop |","| Hero title | `--display-xl-*` |","| Big number | `--display-md-*` |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Article copy | `p` in `--editorial-md-*` |","| Code block | `pre` in `--code-md-*` |","| Command or value in a line | `code`, at `0.875em` of the text around it |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h6`, `p`, `small`, `code`, `pre`, and list items from these styles, so the tag carries the style. It also gives every step a class of the same name, such as `.display-md` or `.body-sm`, that sets all five axes. Type an element only when the table gives its tag a different style, and type it with the step's class. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
20
+ "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `box-sizing: content-box`, `max-width: var(--columns-max-width)`, `margin: 0 auto`, and `padding-inline: var(--columns-margin)`. Each section spans it with `grid-column: 1 / -1`. The page sets its top and bottom space with `padding-block`.","","The columns span `--columns-max-width`, and the margin sits outside them, where `ColumnsOverlay` draws it. `box-sizing: content-box` keeps a project-wide `border-box` reset from narrowing the columns.","","The page keeps the grid at every width. `tokens.css` narrows `--columns-gutter` and `--columns-margin` at 767px and again at 480px, and `--columns-count` holds at every width, so the page grid itself takes no `@media` rule.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","Spans count twelfths of the page at every width, so `span 6` is half the page on a desktop and on a phone. Widen the spans on a narrow screen. In a `@media (max-width: 767px)` rule, give a child that spans 4 on a desktop `span 6`, or give each child `grid-column: 1 / -1` to stack the section in reading order. A column at 390px is about 15px wide, so in a `@media (max-width: 480px)` rule each child spans 12, 6, or 4 columns.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `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.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- 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\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `.body-sm` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. 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.","- 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.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","The text styles fall into five usages, each a scale of seven steps from `2xs` to `2xl`: display for type outside the page outline, heading for the outline, body for interface text and short copy, editorial for long reading, and code. Each step is a style named `--{usage}-{step}-*`, such as `--body-sm-*`. Headings use the whole set. The other usages mostly take `md` and `sm`.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Title inside a section | `h3` to `h6` in `--heading-md-*` to `--heading-2xs-*` |","| Card title | the Card `title` prop |","| Hero title | `--display-xl-*` |","| Big number | `--display-md-*` |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Article copy | `p` in `--editorial-md-*` |","| Code block | `pre` in `--code-md-*` |","| Command or value in a line | `code`, at `0.875em` of the text around it |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h6`, `p`, `small`, `code`, `pre`, and list items from these styles, so the tag carries the style. It also gives every step a class of the same name, such as `.display-md` or `.body-sm`, that sets all five axes. Type an element only when the table gives its tag a different style, and type it with the step's class. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--columns-margin`, as the page grid's `padding-inline` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
21
21
  "references/interaction-sources.md": ["# Interaction sources","","Read this when a Verify check or an emphasis rule in SKILL.md needs its reason.","Each principle below is held by one of three parties. A shipped component","holds it when the component's own design answers it, so the page's rule is","the component and pick-component names the test. The page holds it when only","the page can get it right, so SKILL.md states a read or the checker a rule.","The product holds it when no page decision touches it, and the row says so","to keep a later edit from reopening it.","","## Jakob Nielsen, ten usability heuristics","","| Heuristic | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Visibility of system status | Page | Verify: an action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`. |","| Match between system and the real world | Page | Verify: labels use the user's words. |","| User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: `multiple-primary`. |","| Consistency and standards | Page | One size, one primary action, one text style per element. The components carry the rest. |","| Error prevention | Page | Verify: `danger-without-dialog`. Verify: every field has a default and Reset restores it. |","| Recognition rather than recall | Component | `MenuSelect` lists the options; `Input` carries its label and hint; `Tooltip` defines in place. |","| Flexibility and efficiency of use | Product | Shortcuts and customisation are product decisions. |","| Aesthetic and minimalist design | Page | Layout: each element serves a purpose no other element serves. Verify: secondary settings sit in a `CollapsibleSection`. |","| Help users recognise, diagnose, and recover from errors | Component | `Input` carries the error line; `Callout variant=\"danger\"` carries a section's. |","| Help and documentation | Product | Contextual help is a product decision. |","","The complex-application version of the ten (Kaley, Nielsen Norman Group)","describes a tool with a stage and controls, which is the tool page SKILL.md","lays out. Its additions that the page holds: a wait past ten seconds shows","steps done and steps left, and the stage is the live preview of every","control. Undo, version history, and autosave are the product's.","","## Bruce Tognazzini, first principles of interaction design","","| Principle | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Anticipation | Page | Layout: show related items side by side. |","| Colour | Component | `Callout`, `Badge`, and `Notification` carry an icon or text beside the colour. |","| Consistency | Page | One size, one primary action. |","| Defaults | Page | Verify: every field has a default and Reset restores it. |","| Discoverability | Page | Verify: every control is in view. |","| Explorable interfaces | Page | Verify: every row of actions holds an action that leaves without committing. |","| Fitts's law | Component | The shipped default is the large target; SKILL.md's one-size rule keeps it. A toolbar sits on the section's bottom edge. |","| Protect users' work | Page | Verify: `danger-without-dialog`. |","| Readability | Component | live-tokens-set-colors gates every text pair at WCAG AA. |","| Simplicity | Page | Verify: secondary settings sit in a `CollapsibleSection`; no capability is removed for the sake of simplicity. |","| Visible navigation | Component | `SideNavigation` follows the current path. |","| Aesthetics, Autonomy, Efficiency of the user, Human-interface objects, Latency reduction, Learnability, Metaphors, State | Product | Measured, engineered, or researched outside a page. |","","Sources:","","- Nielsen, 10 Usability Heuristics for User Interface Design (1994, updated 2024). https://www.nngroup.com/articles/ten-usability-heuristics/","- Kaley, 10 Usability Heuristics Applied to Complex Applications. https://www.nngroup.com/articles/usability-heuristics-complex-applications/","- Tognazzini, First Principles of Interaction Design (revised 2014). https://asktog.com/atc/principles-of-interaction-design/","","## W3C Web Accessibility Initiative","","| Guidance | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Headings: one `h1`, levels in order, no skipped level | Page | Type: use the semantic element for each place. Verify: one `h1`, levels in order. |","| Images: text alternatives | Page | Verify: every `img` has `alt` text. |","| Focus order follows the reading order | Page | Verify: focus order follows the reading order. |","| Form labels and error messages | Component | `Input` carries its label, hint, and error line. |","","https://www.w3.org/WAI/tutorials/page-structure/headings/","https://www.w3.org/WAI/tutorials/images/","https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html"],
22
22
  "references/layout-sources.md": ["# Layout sources","","Read this when a layout decision in SKILL.md needs its reason. Each law in the","Layout section comes from one of these sources. The sources are for layout and","hierarchy only. Do not take color or type opinions from them: the theme owns","those.","","## Edward Tufte","","Tufte wrote about information graphics. His laws apply to a page because a page","is an information display with controls on it.","","| Law | Statement | Rule in SKILL.md |","|---|---|---|","| Smallest effective difference | Make all visual distinctions as subtle as possible, but still clear and effective. | Separate with space first, then a hairline rule, then a second surface. |","| 1+1=3 | Two heavy marks side by side make a third mark: the space between them. | A section of containers with borders and header bars reads as a set of posters. |","| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and chrome each take their own token. |","| Administrative debris | The metaphor for the interface is the information. Remove the chrome that the tool adds for itself. | The stage takes the space. Controls take their shipped default. |","| Spatial over temporal | Show information adjacent in space. A control that hides it stacks it in time. | Show related items side by side. Frames in a strip are small multiples. |","| Erase non-data ink | Remove each mark that carries no information. | The Verify question: does the page lose information when this element is removed? |","| Micro and macro readings | A good display reads at a distance and up close. | Verify from a distance, then closely. |","","Sources:","","- Envisioning Information (1990): layering and separation, small multiples, micro and macro. https://www.edwardtufte.com/book/envisioning-information/","- The Visual Display of Quantitative Information (1983): data-ink, chartjunk.","- iPhone interface design, edwardtufte.com notebook: administrative debris, spatial over temporal. https://www.edwardtufte.com/notebook/iphone-interface-design/","","## Josef Müller-Brockmann","","Grid Systems in Graphic Design (1981) is the discipline behind the page column","grid. The grid does the separating, so an element needs no border to show","where it sits. His stated aim is compact planning, intelligibility, and","clarity. That is Tufte's aim in a typographer's words.","","## Refactoring UI","","Adam Wathan and Steve Schoger, Refactoring UI (2018), turns both into working","rules for product screens:","","- Put more space around a group than within it.","- Start with too much white space, then remove some.","- Use fewer borders. Separate with space, a shadow, or a second background.","- Emphasize by de-emphasizing the secondary content.","- Labels are a last resort.","- Keep a spacing scale where no two steps are closer than a quarter. The `--space-*` scale is that scale.","","https://www.refactoringui.com/","","## Material Design 3, canonical layouts","","The Page layouts table takes its rows from Material's canonical layouts.","","| Layout | Statement | Row in SKILL.md |","|---|---|---|","| Supporting pane | The primary area takes about two thirds of the window; the secondary pane takes the rest. At compact width the pane moves below the main content. | Main with a supporting pane, two thirds and one third; the stacking sentence. |","| List-detail | The list and the detail of the selected item sit side by side at expanded width. | List with detail. |","| Feed | Equivalent items in an adaptive grid. | Grid of equals. |","","https://m3.material.io/foundations/layout/canonical-examples/overview","https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts","","## Cloudscape patterns","","| Pattern | Statement | Rule in SKILL.md |","|---|---|---|","| Dashboard | Three areas top to bottom: overview, data, support. \"Consider seven as the limit number for data representation.\" | Verify: the first section holds what the user came for. Grid of equals: up to seven per section. |","| Single-page create | One container; the essential fields first and few; secondary inputs in an expandable section; cancel then submit at the bottom. | Containers: a form. Single column. |","| Details page | The title with its actions, then a summary, then related blocks. | Stacked sections. |","| Empty state | A heading, an optional line, and one action. \"Always provide an action.\" Errors go elsewhere. | Containers: an empty stage. |","","https://cloudscape.design/patterns/general/service-dashboard/static-dashboard/","https://cloudscape.design/patterns/resource-management/create/single-page-create/","https://cloudscape.design/patterns/resource-management/details/details-page/","https://cloudscape.design/patterns/general/empty-states/","","## Matthew Butterick, line length","","\"45 to 90 characters per line, including spaces.\" The Separation paragraph and the Verify check carry the measure, and the half-width copy span holds it at body size.","","https://practicaltypography.com/line-length.html","","## Nielsen Norman Group, proximity","","\"Proximity is one of the most important grouping principles and can overpower competing visual cues such as similarity of color or shape.\" Space inside a group is smaller than space between groups. An unrelated action inside a group is camouflaged, so a `danger` Button sits apart from the toolbar group.","","https://www.nngroup.com/articles/gestalt-proximity/"],
23
23
  },
@@ -2,7 +2,7 @@ import type { SkillTree } from '../types';
2
2
 
3
3
  export const createPage: SkillTree = {
4
4
  "id": "live-tokens-create-page",
5
- "digest": "sha256:dac3be326461ce44",
5
+ "digest": "sha256:a3d5c66949b280b6",
6
6
  "title": "create-page",
7
7
  "tagline": "Create a Page Using Live Tokens",
8
8
  "nodes": [
@@ -71,13 +71,13 @@ export const createPage: SkillTree = {
71
71
  "chips": [
72
72
  {
73
73
  "label": "Grid",
74
- "lines": [39, 52],
74
+ "lines": [39, 56],
75
75
  "anchor": "### Grid",
76
- "anchorEnd": "No scaffold collapses the page grid on a phone: the theme's "
76
+ "anchorEnd": "Spans count twelfths of the page at every width, so `span 6`"
77
77
  },
78
78
  {
79
79
  "label": "Separation",
80
- "lines": [54, 71],
80
+ "lines": [58, 75],
81
81
  "anchor": "### Separation",
82
82
  "anchorEnd": "`references/layout-sources.md` names the sources for these l"
83
83
  }
@@ -94,27 +94,27 @@ export const createPage: SkillTree = {
94
94
  "chips": [
95
95
  {
96
96
  "label": "Stage",
97
- "lines": [81, 81],
97
+ "lines": [85, 85],
98
98
  "anchor": "An empty stage shows a heading that names the condition and "
99
99
  },
100
100
  {
101
101
  "label": "Card",
102
- "lines": [82, 82],
102
+ "lines": [86, 86],
103
103
  "anchor": "A container in a tool UI labels itself: `Card variant=\"bare\""
104
104
  },
105
105
  {
106
106
  "label": "Form",
107
- "lines": [83, 83],
107
+ "lines": [87, 87],
108
108
  "anchor": "A row of fields is a flex row with `gap: var(--space-20)`; e"
109
109
  },
110
110
  {
111
111
  "label": "Buttons",
112
- "lines": [84, 84],
112
+ "lines": [88, 88],
113
113
  "anchor": "A toolbar is a flex row of Buttons on the section's bottom e"
114
114
  },
115
115
  {
116
116
  "label": "Picker",
117
- "lines": [85, 85],
117
+ "lines": [89, 89],
118
118
  "anchor": "For a `MenuSelect` picker, toggle it from a Button with a tr"
119
119
  }
120
120
  ]
@@ -130,7 +130,7 @@ export const createPage: SkillTree = {
130
130
  "chips": [
131
131
  {
132
132
  "label": "Components",
133
- "lines": [73, 80],
133
+ "lines": [77, 84],
134
134
  "anchor": "## Components",
135
135
  "anchorEnd": "Text inside a `Card` or a `CollapsibleSection` takes the con"
136
136
  },
@@ -158,25 +158,25 @@ export const createPage: SkillTree = {
158
158
  "chips": [
159
159
  {
160
160
  "label": "Tokens",
161
- "lines": [96, 101],
161
+ "lines": [100, 105],
162
162
  "anchor": "## Tokens",
163
163
  "anchorEnd": "A value that comes from data, such as a sheet's padding in p"
164
164
  },
165
165
  {
166
166
  "label": "Type",
167
- "lines": [105, 127],
167
+ "lines": [109, 131],
168
168
  "anchor": "### Type",
169
169
  "anchorEnd": "Use the semantic element for each place: one `h1`, an `h2` f"
170
170
  },
171
171
  {
172
172
  "label": "Size and emphasis",
173
- "lines": [129, 137],
173
+ "lines": [133, 141],
174
174
  "anchor": "### Size",
175
175
  "anchorEnd": "In a row of actions the primary sits last, on the right. Up "
176
176
  },
177
177
  {
178
178
  "label": "Spacing",
179
- "lines": [139, 153],
179
+ "lines": [143, 157],
180
180
  "anchor": "### Spacing",
181
181
  "anchorEnd": "Every section after the first opens with a hairline: `paddin"
182
182
  }
@@ -193,12 +193,12 @@ export const createPage: SkillTree = {
193
193
  "chips": [
194
194
  {
195
195
  "label": "LiveTokensRouter",
196
- "lines": [159, 159],
196
+ "lines": [163, 163],
197
197
  "anchor": "`<LiveTokensRouter pages={...}>`: add a `pages` entry with `"
198
198
  },
199
199
  {
200
200
  "label": "LiveEditorOverlay",
201
- "lines": [160, 160],
201
+ "lines": [164, 164],
202
202
  "anchor": "Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() "
203
203
  }
204
204
  ]
@@ -209,7 +209,7 @@ export const createPage: SkillTree = {
209
209
  "kind": "cli",
210
210
  "title": "Run check-page",
211
211
  "desc": "check-page --tests --strict --json applies every auto repair, proves the rendered page at its own route, and returns the fixes beside the remaining findings.",
212
- "lines": [177, 177],
212
+ "lines": [181, 181],
213
213
  "anchor": "Run `npx live-tokens check-page <file> --tests --strict --js"
214
214
  },
215
215
  {
@@ -218,7 +218,7 @@ export const createPage: SkillTree = {
218
218
  "kind": "gate",
219
219
  "title": "Repair from guidance",
220
220
  "desc": "Each remaining finding carries its guidance. Make each repair, then run check-page again.",
221
- "lines": [179, 179],
221
+ "lines": [183, 183],
222
222
  "anchor": "Each remaining finding carries a rule id, a line, and its `g"
223
223
  },
224
224
  {
@@ -227,31 +227,31 @@ export const createPage: SkillTree = {
227
227
  "kind": "chipset",
228
228
  "title": "Check the page in the browser",
229
229
  "desc": "The checkers cannot see a layout. Open the page at its width and check each line.",
230
- "lines": [181, 197],
230
+ "lines": [185, 201],
231
231
  "anchor": "The checkers cannot see a layout. Open the page at the width",
232
232
  "anchorEnd": "`references/interaction-sources.md` names the sources for th",
233
233
  "chips": [
234
234
  {
235
235
  "label": "Structure",
236
- "lines": [183, 187],
236
+ "lines": [187, 191],
237
237
  "anchor": "The first section holds what the user came for.",
238
238
  "anchorEnd": "The containers in a section align at the bottom."
239
239
  },
240
240
  {
241
241
  "label": "Actions",
242
- "lines": [188, 191],
242
+ "lines": [192, 195],
243
243
  "anchor": "The actions sit where the eye goes last, with the one primar",
244
244
  "anchorEnd": "An action that runs longer than a moment shows progress in a"
245
245
  },
246
246
  {
247
247
  "label": "Fields",
248
- "lines": [192, 193],
248
+ "lines": [196, 197],
249
249
  "anchor": "Every field has a default, and Reset restores it.",
250
250
  "anchorEnd": "Secondary settings sit in a `CollapsibleSection`. Every cont"
251
251
  },
252
252
  {
253
253
  "label": "Words and access",
254
- "lines": [194, 195],
254
+ "lines": [198, 199],
255
255
  "anchor": "Labels use the user's words, such as \"Export slices\".",
256
256
  "anchorEnd": "Every `img` has `alt` text. Focus order follows the reading "
257
257
  }
@@ -263,7 +263,7 @@ export const createPage: SkillTree = {
263
263
  "kind": "step",
264
264
  "title": "Read the page twice",
265
265
  "desc": "From a distance only the sections show. Up close, every border and bar earns its place or goes.",
266
- "lines": [199, 199],
266
+ "lines": [203, 203],
267
267
  "anchor": "Then read the page from a distance: the sections and their e"
268
268
  },
269
269
  {