@motion-proto/live-tokens 0.74.0 → 0.76.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 (171) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +33 -38
  2. package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
  4. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
  6. package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
  7. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
  8. package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
  9. package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
  10. package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
  11. package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
  12. package/.claude/skills/live-tokens-create-theme/SKILL.md +52 -48
  13. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
  14. package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
  15. package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
  16. package/.claude/skills/live-tokens-set-colors/SKILL.md +47 -38
  17. package/.claude/skills/live-tokens-set-geometry/SKILL.md +58 -35
  18. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +5 -3
  19. package/.claude/skills/live-tokens-set-type/SKILL.md +30 -30
  20. package/CHANGELOG.md +138 -0
  21. package/README.md +17 -7
  22. package/bin/check-component.mjs +91 -19
  23. package/bin/check-page.mjs +102 -21
  24. package/bin/cli.mjs +87 -113
  25. package/bin/contractRunner.mjs +945 -0
  26. package/bin/create.mjs +1 -1
  27. package/bin/lib/catalogue.mjs +37 -30
  28. package/bin/lib/findings.mjs +46 -15
  29. package/bin/lib/report.mjs +3 -3
  30. package/bin/lib/tokenVocabulary.mjs +4 -4
  31. package/bin/migrate-routes.mjs +5 -5
  32. package/bin/migrate.mjs +4 -4
  33. package/bin/save-theme.mjs +8 -9
  34. package/bin/set-colors.mjs +9 -11
  35. package/bin/set-geometry.mjs +7 -7
  36. package/bin/set-type.mjs +5 -7
  37. package/bin/setup-claude.mjs +110 -0
  38. package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
  39. package/dist-plugin/{chunk-7VRTBGJT.js → chunk-PDNL4NC5.js} +9 -2
  40. package/dist-plugin/{chunk-V3YF6CGT.js → chunk-SWXRVZKT.js} +43 -1
  41. package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.cts} +1 -0
  42. package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.ts} +1 -0
  43. package/dist-plugin/index.cjs +310 -147
  44. package/dist-plugin/index.d.cts +1 -1
  45. package/dist-plugin/index.d.ts +1 -1
  46. package/dist-plugin/index.js +218 -122
  47. package/dist-plugin/migrateData/index.cjs +66 -2
  48. package/dist-plugin/migrateData/index.d.cts +1 -1
  49. package/dist-plugin/migrateData/index.d.ts +1 -1
  50. package/dist-plugin/migrateData/index.js +3 -3
  51. package/dist-plugin/setColors/index.cjs +61 -4
  52. package/dist-plugin/setColors/index.d.cts +1 -1
  53. package/dist-plugin/setColors/index.d.ts +1 -1
  54. package/dist-plugin/setColors/index.js +5 -5
  55. package/dist-plugin/setGeometry/index.cjs +107 -44
  56. package/dist-plugin/setGeometry/index.d.cts +5 -5
  57. package/dist-plugin/setGeometry/index.d.ts +5 -5
  58. package/dist-plugin/setGeometry/index.js +51 -45
  59. package/dist-plugin/setType/index.cjs +15 -0
  60. package/dist-plugin/setType/index.d.cts +1 -1
  61. package/dist-plugin/setType/index.d.ts +1 -1
  62. package/dist-plugin/setType/index.js +1 -1
  63. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  64. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  65. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  66. package/package.json +35 -8
  67. package/src/app/site.css +19 -9
  68. package/src/editor/bootstrap.ts +2 -0
  69. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
  70. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  71. package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
  72. package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
  73. package/src/editor/component-editor/TabBarEditor.svelte +1 -1
  74. package/src/editor/component-editor/TableEditor.svelte +2 -2
  75. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
  76. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
  77. package/src/editor/core/components/adjustAliases.ts +59 -45
  78. package/src/editor/core/components/aliasKinds.ts +9 -5
  79. package/src/editor/core/preview/themePreview.ts +9 -2
  80. package/src/editor/core/sketch/sketchLayer.ts +22 -0
  81. package/src/editor/core/store/editorStore.ts +10 -1
  82. package/src/editor/core/themes/buildColors.ts +3 -3
  83. package/src/editor/core/themes/liveStateStream.ts +26 -0
  84. package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
  85. package/src/editor/core/themes/migrations/index.ts +2 -0
  86. package/src/editor/core/themes/themeDocumentSync.ts +22 -11
  87. package/src/editor/core/themes/themeService.ts +9 -2
  88. package/src/editor/pages/ComponentEditorPage.svelte +17 -1
  89. package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
  90. package/src/editor/skill-atlas/SkillAtlas.svelte +75 -573
  91. package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
  92. package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
  93. package/src/editor/skill-atlas/edges.ts +31 -0
  94. package/src/editor/skill-atlas/skillSources.generated.ts +20 -19
  95. package/src/editor/skill-atlas/skillTrees.ts +19 -3840
  96. package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
  97. package/src/editor/skill-atlas/trees/create-component.ts +275 -0
  98. package/src/editor/skill-atlas/trees/create-page.ts +320 -0
  99. package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
  100. package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
  101. package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
  102. package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
  103. package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
  104. package/src/editor/skill-atlas/trees/set-type.ts +142 -0
  105. package/src/editor/skill-atlas/types.ts +3 -4
  106. package/src/editor/skill-atlas/wireLayout.ts +287 -0
  107. package/src/live-tokens/data/themes/autumn.json +15 -15
  108. package/src/live-tokens/data/themes/halloween.json +15 -15
  109. package/src/live-tokens/data/themes/midnight-study.json +15 -15
  110. package/src/live-tokens/data/themes/ocean.json +15 -15
  111. package/src/live-tokens/data/themes/royal-velvet.json +15 -15
  112. package/src/live-tokens/data/themes/sketchy.json +15 -15
  113. package/src/live-tokens/data/themes/spring-meadow.json +15 -15
  114. package/src/live-tokens/data/themes/sunset.json +15 -15
  115. package/src/system/components/Badge.svelte +7 -0
  116. package/src/system/components/Button.svelte +7 -0
  117. package/src/system/components/Callout.svelte +10 -6
  118. package/src/system/components/Card.svelte +23 -4
  119. package/src/system/components/CodeSnippet.svelte +4 -3
  120. package/src/system/components/CollapsibleSection.svelte +23 -8
  121. package/src/system/components/CornerBadge.svelte +6 -0
  122. package/src/system/components/Dialog.svelte +13 -6
  123. package/src/system/components/IconButton.svelte +9 -0
  124. package/src/system/components/Image.svelte +8 -0
  125. package/src/system/components/ImageLightbox.svelte +6 -0
  126. package/src/system/components/InlineEditActions.svelte +7 -0
  127. package/src/system/components/Input.svelte +7 -0
  128. package/src/system/components/MenuSelect.svelte +7 -0
  129. package/src/system/components/Notification.svelte +7 -0
  130. package/src/system/components/Panel.svelte +6 -0
  131. package/src/system/components/ProgressBar.svelte +5 -0
  132. package/src/system/components/RadioButton.svelte +11 -5
  133. package/src/system/components/SectionDivider.svelte +8 -0
  134. package/src/system/components/SegmentedControl.svelte +6 -0
  135. package/src/system/components/SideNavigation.svelte +6 -0
  136. package/src/system/components/Slider.svelte +7 -4
  137. package/src/system/components/TabBar.svelte +15 -9
  138. package/src/system/components/Table.svelte +8 -3
  139. package/src/system/components/Toggle.svelte +4 -4
  140. package/src/system/components/Tooltip.svelte +6 -0
  141. package/src/testing-js/chunk-AO7EZHYV.js +776 -0
  142. package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
  143. package/src/testing-js/chunk-FAFOAWYL.js +39 -0
  144. package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
  145. package/src/testing-js/chunk-L73N4NSO.js +23 -0
  146. package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
  147. package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
  148. package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
  149. package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
  150. package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
  151. package/src/testing-js/component-alias.contract.js +81 -0
  152. package/src/testing-js/component-alias.contract.js.map +1 -0
  153. package/src/testing-js/component-editor.contract.js +62 -0
  154. package/src/testing-js/component-editor.contract.js.map +1 -0
  155. package/src/testing-js/component-render.contract.js +568 -0
  156. package/src/testing-js/component-render.contract.js.map +1 -0
  157. package/src/testing-js/index.d.ts +293 -0
  158. package/src/testing-js/index.js +222 -0
  159. package/src/testing-js/index.js.map +1 -0
  160. package/src/testing-js/registry.contract.js +39 -0
  161. package/src/testing-js/registry.contract.js.map +1 -0
  162. package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
  163. package/src/testing-js/vitest.d.ts +3 -0
  164. package/src/testing-js/vitest.js +13 -0
  165. package/src/testing-js/vitest.js.map +1 -0
  166. package/template/README.md +13 -0
  167. package/template/_gitignore +6 -0
  168. package/template/package.json +3 -1
  169. package/template/src/pages/Home.svelte +4 -18
  170. package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
  171. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
@@ -2,13 +2,12 @@
2
2
  import { tick } from 'svelte';
3
3
  import { navigate } from '../core/routing/router';
4
4
  import Button from '../../system/components/Button.svelte';
5
- import Callout from '../../system/components/Callout.svelte';
6
5
  import TabBar from '../../system/components/TabBar.svelte';
7
6
  import SourcePane from './SourcePane.svelte';
8
- import TreeNodeCard from './TreeNodeCard.svelte';
7
+ import TreeCanvas from './TreeCanvas.svelte';
9
8
  import { SKILL_DOC, skillDocs } from './skillSources';
10
9
  import { skillTrees } from './skillTrees';
11
- import type { Edge, LineRange, Selection, TreeNode } from './types';
10
+ import type { LineRange, Selection } from './types';
12
11
 
13
12
  // `/skills#set-type` opens that skill, so a link can hand someone one tree
14
13
  // rather than the atlas front door.
@@ -42,16 +41,6 @@
42
41
  sourcePane?.scrollTo({ top: 0 });
43
42
  }
44
43
 
45
- let rows = $derived.by(() => {
46
- const byRow = new Map<number, TreeNode[]>();
47
- for (const node of tree.nodes) {
48
- const group = byRow.get(node.row);
49
- if (group) group.push(node);
50
- else byRow.set(node.row, [node]);
51
- }
52
- return [...byRow.entries()].sort((a, b) => a[0] - b[0]).map(([, group]) => group);
53
- });
54
-
55
44
  function selectTarget(key: string, label: string, range: LineRange) {
56
45
  selection = { key, label, lines: range };
57
46
  // Every range cites SKILL.md, so a step selected while a reference is open
@@ -104,339 +93,11 @@
104
93
  sourcePane?.scrollTo({ top: 0 });
105
94
  }
106
95
 
107
- // ---- wires -------------------------------------------------------------
108
- // Connectors are measured from the laid-out cards, so they survive wrapping,
109
- // theme changes, and the geometry skill moving every radius and padding.
110
- let canvas: HTMLElement | undefined = $state();
111
96
  let treePane: HTMLElement | undefined = $state();
112
97
  let sourcePane: HTMLElement | undefined = $state();
113
-
114
- interface Wire {
115
- d: string;
116
- back: boolean;
117
- lit: boolean;
118
- /** Trunks and buses carry no arrowhead; only the segment that arrives does. */
119
- arrow: boolean;
120
- }
121
-
122
- interface Label {
123
- x: number;
124
- y: number;
125
- text: string;
126
- lit: boolean;
127
- /** Lane labels run along the lane; branch labels sit across the drop. */
128
- rotate: boolean;
129
- }
130
-
131
- interface Box {
132
- left: number;
133
- right: number;
134
- top: number;
135
- bottom: number;
136
- midX: number;
137
- midY: number;
138
- }
139
-
140
- interface Drawing {
141
- wires: Wire[];
142
- labels: Label[];
143
- }
144
-
145
- let wires: Wire[] = $state([]);
146
- let labels: Label[] = $state([]);
147
- let canvasSize = $state({ w: 0, h: 0 });
148
-
149
- /** Clearance between a card border and the wire, so the arrowhead reads as
150
- * an arrival rather than as part of the card outline. */
151
- const STANDOFF = 6;
152
- const CORNER = 12;
153
- /** How far outside the cards a lane sits, and how far one lane nests
154
- * inside the next when two share a stretch of gutter. */
155
- const LANE_INSET = 40;
156
- const LANE_STEP = 16;
157
-
158
- /** Cards laid out in one row share a top to within a pixel; cards pushed onto
159
- * a second wrapped line do not, and a single bus cannot serve both. */
160
- function aligned(values: number[]): boolean {
161
- return Math.max(...values) - Math.min(...values) < 4;
162
- }
163
-
164
- function push<T>(map: Map<string, T[]>, key: string, value: T) {
165
- const list = map.get(key);
166
- if (list) list.push(value);
167
- else map.set(key, [value]);
168
- }
169
-
170
- function label(out: Drawing, text: string | undefined, x: number, y: number, lit: boolean, rotate = false) {
171
- if (text) out.labels.push({ x, y, text, lit, rotate });
172
- }
173
-
174
- /** One trunk down from the parent, one bus across, one drop into each child —
175
- * so a split reads as a single decision rather than as N crossing curves.
176
- * Each answer sits on its own drop. */
177
- function fanOut(out: Drawing, a: Box, kids: Box[], edges: Edge[], lit: (i: number) => boolean) {
178
- const top = kids[0].top;
179
- const busY = (a.bottom + top) / 2;
180
- const xs = kids.map((k) => k.midX);
181
- const r = Math.max(
182
- 0,
183
- Math.min(CORNER, (Math.max(...xs) - Math.min(...xs)) / 2, (busY - a.bottom) / 2, (top - busY) / 2),
184
- );
185
- const turns = xs.map((x) => (x < a.midX ? x + r : x > a.midX ? x - r : x));
186
- const busLeft = Math.min(...turns, a.midX);
187
- const busRight = Math.max(...turns, a.midX);
188
- const anyLit = kids.some((_, i) => lit(i));
189
-
190
- out.wires.push({
191
- back: false,
192
- arrow: false,
193
- lit: anyLit,
194
- d: `M ${a.midX} ${a.bottom + STANDOFF} L ${a.midX} ${busY} M ${busLeft} ${busY} L ${busRight} ${busY}`,
195
- });
196
- kids.forEach((k, i) => {
197
- const x = k.midX;
198
- const turn = turns[i];
199
- out.wires.push({
200
- back: false,
201
- arrow: true,
202
- lit: lit(i),
203
- d:
204
- turn === x
205
- ? `M ${x} ${busY} L ${x} ${top - STANDOFF}`
206
- : `M ${turn} ${busY} Q ${x} ${busY}, ${x} ${busY + r} L ${x} ${top - STANDOFF}`,
207
- });
208
- label(out, edges[i].label, x, (busY + r + top - STANDOFF) / 2, lit(i));
209
- });
210
- }
211
-
212
- /** The mirror of `fanOut`: risers up to a shared bus, one trunk into the child,
213
- * so several outcomes converging land as one arrow instead of a pile. */
214
- function fanIn(out: Drawing, parents: Box[], b: Box, edges: Edge[], lit: (i: number) => boolean) {
215
- const bottom = parents[0].bottom;
216
- const busY = (bottom + b.top) / 2;
217
- const xs = parents.map((p) => p.midX);
218
- const r = Math.max(
219
- 0,
220
- Math.min(CORNER, (Math.max(...xs) - Math.min(...xs)) / 2, (busY - bottom) / 2, (b.top - busY) / 2),
221
- );
222
- const turns = xs.map((x) => (x < b.midX ? x + r : x > b.midX ? x - r : x));
223
- const anyLit = parents.some((_, i) => lit(i));
224
-
225
- parents.forEach((p, i) => {
226
- const x = p.midX;
227
- const turn = turns[i];
228
- out.wires.push({
229
- back: false,
230
- arrow: false,
231
- lit: lit(i),
232
- d:
233
- turn === x
234
- ? `M ${x} ${bottom + STANDOFF} L ${x} ${busY}`
235
- : `M ${x} ${bottom + STANDOFF} L ${x} ${busY - r} Q ${x} ${busY}, ${turn} ${busY}`,
236
- });
237
- label(out, edges[i].label, x, (bottom + STANDOFF + busY - r) / 2, lit(i));
238
- });
239
- out.wires.push({
240
- back: false,
241
- arrow: true,
242
- lit: anyLit,
243
- d:
244
- `M ${Math.min(...turns, b.midX)} ${busY} L ${Math.max(...turns, b.midX)} ${busY}` +
245
- ` M ${b.midX} ${busY} L ${b.midX} ${b.top - STANDOFF}`,
246
- });
247
- }
248
-
249
- /** A plain parent-to-child curve, and the fallback wherever a bus would have
250
- * to cross a card: children wrapped onto a second line, or parents staggered
251
- * across rows. */
252
- function curve(out: Drawing, a: Box, b: Box, edge: Edge, lit: boolean) {
253
- const y1 = a.bottom + STANDOFF;
254
- const y2 = b.top - STANDOFF;
255
- const bend = Math.max(16, (y2 - y1) * 0.55);
256
- out.wires.push({
257
- back: false,
258
- arrow: true,
259
- lit,
260
- d: `M ${a.midX} ${y1} C ${a.midX} ${y1 + bend}, ${b.midX} ${y2 - bend}, ${b.midX} ${y2}`,
261
- });
262
- label(out, edge.label, (a.midX + b.midX) / 2, (y1 + y2) / 2, lit);
263
- }
264
-
265
- /** Every card the span passes between, not just the two it joins. */
266
- function crossed(a: Box, b: Box, all: Box[]): Box[] {
267
- const top = Math.min(a.midY, b.midY);
268
- const bottom = Math.max(a.midY, b.midY);
269
- return all.filter((x) => x !== a && x !== b && x.bottom > top && x.top < bottom);
270
- }
271
-
272
- /** On the spine: no other card shares its row. A wrapped branch row also
273
- * "crosses" cards on its way down, and those stay curves. */
274
- function alone(box: Box, all: Box[]): boolean {
275
- return all.every((x) => x === box || !aligned([x.top, box.top]));
276
- }
277
-
278
- /** Re-run loops ride a lane in the left gutter the centred spine opens up,
279
- * rather than bulging around the card they leave. */
280
- function backEdge(out: Drawing, a: Box, b: Box, lane: number, edge: Edge, lit: boolean) {
281
- const dir = Math.sign(b.midY - a.midY) || -1;
282
- const r = Math.max(
283
- 0,
284
- Math.min(CORNER, (a.left - STANDOFF - lane) / 2, Math.abs(a.midY - b.midY) / 2),
285
- );
286
- out.wires.push({
287
- back: true,
288
- arrow: true,
289
- lit,
290
- d:
291
- `M ${a.left - STANDOFF} ${a.midY} L ${lane + r} ${a.midY}` +
292
- ` Q ${lane} ${a.midY}, ${lane} ${a.midY + dir * r}` +
293
- ` L ${lane} ${b.midY - dir * r} Q ${lane} ${b.midY}, ${lane + r} ${b.midY}` +
294
- ` L ${b.left - STANDOFF} ${b.midY}`,
295
- });
296
- label(out, edge.label ?? 're-run', lane, (a.midY + b.midY) / 2, lit, true);
297
- }
298
-
299
- /** A branch that skips a row rides the right gutter, the mirror of a loop,
300
- * so a "no" never has to be drawn through the card the "yes" leads to. */
301
- function skipEdge(out: Drawing, a: Box, b: Box, lane: number, edge: Edge, lit: boolean) {
302
- const r = Math.max(
303
- 0,
304
- Math.min(CORNER, (lane - a.right - STANDOFF) / 2, Math.abs(a.midY - b.midY) / 2),
305
- );
306
- out.wires.push({
307
- back: false,
308
- arrow: true,
309
- lit,
310
- d:
311
- `M ${a.right + STANDOFF} ${a.midY} L ${lane - r} ${a.midY}` +
312
- ` Q ${lane} ${a.midY}, ${lane} ${a.midY + r}` +
313
- ` L ${lane} ${b.midY - r} Q ${lane} ${b.midY}, ${lane - r} ${b.midY}` +
314
- ` L ${b.right + STANDOFF} ${b.midY}`,
315
- });
316
- label(out, edge.label, lane, (a.midY + b.midY) / 2, lit, true);
317
- }
318
-
319
- function drawWires() {
320
- if (!canvas) return;
321
- const base = canvas.getBoundingClientRect();
322
- canvasSize = { w: base.width, h: base.height };
323
- const selectedNode = selection?.key.split(':')[0] ?? null;
324
- const lit = (...ids: string[]) => ids.includes(selectedNode ?? '');
325
-
326
- const box = (id: string): Box | null => {
327
- const el = canvas?.querySelector(`[data-node="${id}"]`);
328
- if (!el) return null;
329
- const r = el.getBoundingClientRect();
330
- return {
331
- left: r.left - base.left,
332
- right: r.right - base.left,
333
- top: r.top - base.top,
334
- bottom: r.bottom - base.top,
335
- midX: r.left + r.width / 2 - base.left,
336
- midY: r.top + r.height / 2 - base.top,
337
- };
338
- };
339
-
340
- const boxes = new Map<string, Box>();
341
- for (const node of tree.nodes) {
342
- const b = box(node.id);
343
- if (b) boxes.set(node.id, b);
344
- }
345
- const all = [...boxes.values()];
346
-
347
- const edges = tree.edges as Edge[];
348
- const children = new Map<string, Edge[]>();
349
- const parents = new Map<string, Edge[]>();
350
- for (const edge of edges) {
351
- if (edge.back) continue;
352
- push(children, edge.from, edge);
353
- push(parents, edge.to, edge);
354
- }
355
-
356
- const out: Drawing = { wires: [], labels: [] };
357
- const claimed = new Set<Edge>();
358
-
359
- for (const [from, kidEdges] of children) {
360
- const a = boxes.get(from);
361
- const kids = kidEdges.map((e) => boxes.get(e.to));
362
- if (!a || kidEdges.length < 2 || kids.some((k) => !k)) continue;
363
- const kept = kids as Box[];
364
- if (!aligned(kept.map((k) => k.top))) continue;
365
- kidEdges.forEach((e) => claimed.add(e));
366
- fanOut(out, a, kept, kidEdges, (i) => lit(from, kidEdges[i].to));
367
- }
368
-
369
- for (const [to, parentEdges] of parents) {
370
- const open = parentEdges.filter((e) => !claimed.has(e));
371
- const b = boxes.get(to);
372
- const ups = open.map((e) => boxes.get(e.from));
373
- if (!b || open.length < 2 || ups.some((u) => !u)) continue;
374
- const kept = ups as Box[];
375
- if (!aligned(kept.map((u) => u.bottom))) continue;
376
- open.forEach((e) => claimed.add(e));
377
- fanIn(out, kept, b, open, (i) => lit(open[i].from, to));
378
- }
379
-
380
- // Longest skip first, so a long one always rides outside the skips it spans.
381
- const skips: { edge: Edge; a: Box; b: Box; over: Box[] }[] = [];
382
- for (const edge of edges) {
383
- if (edge.back || claimed.has(edge)) continue;
384
- const a = boxes.get(edge.from);
385
- const b = boxes.get(edge.to);
386
- if (!a || !b) continue;
387
- const over = alone(a, all) && alone(b, all) ? crossed(a, b, all) : [];
388
- if (over.length > 0) skips.push({ edge, a, b, over });
389
- else curve(out, a, b, edge, lit(edge.from, edge.to));
390
- }
391
- skips.sort((x, y) => Math.abs(y.a.midY - y.b.midY) - Math.abs(x.a.midY - x.b.midY));
392
- let outer = -Infinity;
393
- for (const { edge, a, b, over } of skips) {
394
- const lane = Math.max(
395
- ...over.map((x) => x.right),
396
- a.right,
397
- b.right,
398
- ) + LANE_INSET;
399
- const placed = Math.max(lane, outer + LANE_STEP);
400
- outer = placed;
401
- skipEdge(out, a, b, placed, edge, lit(edge.from, edge.to));
402
- }
403
-
404
- // Shortest loop first, so a long one always nests outside the loops it spans.
405
- const loops = edges
406
- .filter((e) => e.back)
407
- .map((edge) => ({ edge, a: boxes.get(edge.from), b: boxes.get(edge.to) }))
408
- .filter((e): e is { edge: Edge; a: Box; b: Box } => !!e.a && !!e.b)
409
- .sort((x, y) => Math.abs(x.a.midY - x.b.midY) - Math.abs(y.a.midY - y.b.midY));
410
-
411
- let inner = Infinity;
412
- for (const { edge, a, b } of loops) {
413
- const clear = Math.min(...crossed(a, b, all).map((x) => x.left), a.left, b.left) - LANE_INSET;
414
- const lane = Math.max(4, Math.min(clear, inner - LANE_STEP));
415
- inner = lane;
416
- backEdge(out, a, b, lane, edge, lit(edge.from, edge.to));
417
- }
418
-
419
- wires = out.wires;
420
- labels = out.labels;
421
- }
422
-
423
- $effect(() => {
424
- // Re-read on tab change and on selection (which only relights existing wires).
425
- void tree;
426
- void selection;
427
- const frame = requestAnimationFrame(drawWires);
428
- return () => cancelAnimationFrame(frame);
429
- });
430
-
431
- $effect(() => {
432
- if (!canvas) return;
433
- const observer = new ResizeObserver(() => drawWires());
434
- observer.observe(canvas);
435
- return () => observer.disconnect();
436
- });
437
98
  </script>
438
99
 
439
- <div class="atlas">
100
+ <div class="skill-atlas">
440
101
  <header class="masthead">
441
102
  <div class="masthead-top">
442
103
  <h1>Skill Atlas</h1>
@@ -444,15 +105,12 @@
444
105
  Back to Demo
445
106
  </Button>
446
107
  </div>
447
- <p class="standfirst">
448
- The eight live-tokens authoring skills as decision trees. Select a step and its lines light up in
449
- the skill on the right; select a line number and the step that owns it lights up on the left.
450
- </p>
451
- <div class="tabs">
452
- <TabBar {tabs} selectedTab={active} ontabChange={changeTab} />
453
- </div>
454
108
  </header>
455
109
 
110
+ <div class="tabs">
111
+ <TabBar {tabs} selectedTab={active} ontabChange={changeTab} />
112
+ </div>
113
+
456
114
  <div class="split">
457
115
  <section class="pane" aria-label="{tree.id} decision tree">
458
116
  <div class="pane-head">
@@ -460,72 +118,9 @@
460
118
  <span class="pane-note">{tree.nodes.length} steps</span>
461
119
  </div>
462
120
  <div class="pane-body" bind:this={treePane}>
463
- <div class="intro">
464
- <Callout variant="info" label="The skill in one line">{tree.tagline}</Callout>
465
- </div>
466
-
467
- <div class="canvas" bind:this={canvas}>
468
- <svg
469
- class="wires"
470
- viewBox="0 0 {canvasSize.w} {canvasSize.h}"
471
- width={canvasSize.w}
472
- height={canvasSize.h}
473
- aria-hidden="true"
474
- >
475
- <defs>
476
- <marker
477
- id="atlas-arrow"
478
- viewBox="0 0 10 10"
479
- refX="9"
480
- refY="5"
481
- markerWidth="9"
482
- markerHeight="9"
483
- markerUnits="userSpaceOnUse"
484
- orient="auto"
485
- >
486
- <path d="M 0 1 L 9 5 L 0 9 z" />
487
- </marker>
488
- </defs>
489
-
490
- {#each wires as wire, i (i)}
491
- <path
492
- d={wire.d}
493
- class:back={wire.back}
494
- class:lit={wire.lit}
495
- marker-end={wire.arrow ? 'url(#atlas-arrow)' : undefined}
496
- />
497
- {/each}
498
-
499
- {#each labels as label, i (i)}
500
- <text
501
- class="wire-label"
502
- class:lit={label.lit}
503
- x={label.x}
504
- y={label.y}
505
- transform={label.rotate ? `rotate(-90 ${label.x} ${label.y})` : undefined}
506
- text-anchor="middle"
507
- dominant-baseline="central"
508
- >
509
- {label.text}
510
- </text>
511
- {/each}
512
- </svg>
513
-
514
- <div class="flow">
515
- {#each rows as group, i (i)}
516
- <div class="row" data-count={group.length} style="--n: {group.length}">
517
- {#each group as node (node.id)}
518
- <TreeNodeCard
519
- {node}
520
- selected={selection?.key ?? null}
521
- onselect={selectTarget}
522
- onopen={openDoc}
523
- />
524
- {/each}
525
- </div>
526
- {/each}
527
- </div>
528
- </div>
121
+ <h2 class="tagline">{tree.tagline}</h2>
122
+
123
+ <TreeCanvas {tree} selected={selection?.key ?? null} onselect={selectTarget} onopen={openDoc} />
529
124
  </div>
530
125
  </section>
531
126
 
@@ -548,33 +143,22 @@
548
143
  </div>
549
144
  </section>
550
145
  </div>
551
-
552
- <footer class="statusbar">
553
- {#if selection}
554
- <span class="status-label">{selection.label}</span>
555
- <span class="status-range">
556
- SKILL.md {selection.lines[0] === selection.lines[1]
557
- ? `line ${selection.lines[0]}`
558
- : `lines ${selection.lines[0]}–${selection.lines[1]}`}
559
- </span>
560
- {:else}
561
- <span class="status-range">Select a step to map it onto the skill.</span>
562
- {/if}
563
- </footer>
564
146
  </div>
565
147
 
566
148
  <style>
567
149
  /* A two-pane tool rather than a reading column. Nothing here sits at a page
568
- column position, so the grid would only be capping the working surface;
569
- the standfirst keeps its own measure instead. */
570
- .atlas {
150
+ column position, so the grid would only be capping the working surface. */
151
+ .skill-atlas {
571
152
  display: flex;
572
153
  flex-direction: column;
573
154
  padding: var(--space-48) var(--space-32) var(--space-32);
574
155
  }
575
156
 
576
157
  .masthead {
577
- margin-bottom: var(--space-40);
158
+ /* Replaces the 0.67em the user agent put on the h1, which the row's own
159
+ spacing now carries so the heading can sit at margin zero. */
160
+ margin-top: var(--space-24);
161
+ margin-bottom: var(--space-32);
578
162
  }
579
163
 
580
164
  .masthead-top {
@@ -584,9 +168,9 @@
584
168
  and would cover a button parked there. */
585
169
  gap: var(--space-24);
586
170
  flex-wrap: wrap;
587
- /* The row carries the heading's own bottom margin; a margin left on the h1
588
- would centre the button against the margin box, not the title. */
589
- margin-bottom: var(--space-12);
171
+ /* The row carries the heading's own margins; align-items centres the margin
172
+ box, so a margin left on the h1 offsets the title against the button by
173
+ half of it. */
590
174
  }
591
175
 
592
176
  .masthead-top h1 {
@@ -596,21 +180,20 @@
596
180
  line-height: var(--heading-xl-line-height);
597
181
  letter-spacing: var(--heading-xl-letter-spacing);
598
182
  color: var(--text-primary);
599
- margin-bottom: 0;
600
- }
601
-
602
- .standfirst {
603
- max-width: 68ch;
604
- margin: 0 0 var(--space-32);
605
- font-family: var(--body-md-font-family);
606
- font-size: var(--body-md-font-size);
607
- font-weight: var(--body-md-font-weight);
608
- line-height: var(--body-md-line-height);
609
- letter-spacing: var(--body-md-letter-spacing);
610
- color: var(--text-secondary);
183
+ margin: 0;
611
184
  }
612
185
 
186
+ /* Below the desktop lock the page scrolls, and the strip pins at the top;
187
+ it sits outside the masthead so the title can scroll away without it. The
188
+ gap below is padding: a margin would leave a slit the panes show through
189
+ once the strip is pinned. */
613
190
  .tabs {
191
+ position: sticky;
192
+ top: 0;
193
+ z-index: 1;
194
+ padding-bottom: var(--space-40);
195
+ background: var(--page-bg);
196
+ background-attachment: var(--page-bg-attachment, fixed);
614
197
  overflow-x: auto;
615
198
  }
616
199
 
@@ -627,13 +210,6 @@
627
210
  gap: var(--space-32);
628
211
  }
629
212
 
630
- @media (min-width: 64rem) {
631
- .split {
632
- grid-template-columns: 3fr 2fr;
633
- align-items: start;
634
- }
635
- }
636
-
637
213
  /* The reference tabs only exist on the source pane, so a body of its own
638
214
  fixed height would leave the two panes ending at different places. */
639
215
  .pane {
@@ -700,132 +276,58 @@
700
276
  scroll-behavior: smooth;
701
277
  }
702
278
 
703
- .intro {
279
+ .tagline {
280
+ text-align: center;
281
+ margin: 0;
704
282
  padding: var(--space-24) var(--space-24) 0;
283
+ font-family: var(--heading-xl-font-family);
284
+ font-size: var(--heading-xl-font-size);
285
+ font-weight: var(--heading-xl-font-weight);
286
+ line-height: var(--heading-xl-line-height);
287
+ letter-spacing: var(--heading-xl-letter-spacing);
288
+ color: var(--text-primary);
705
289
  }
706
290
 
707
- /* The side padding is the lanes: re-run loops on the left, row-skipping
708
- branches on the right. A branch row runs the full width of the flow, so
709
- without reserved room a lane would have nowhere to go but over the cards
710
- it is routing around. */
711
- .canvas {
712
- position: relative;
713
- padding: var(--space-32) var(--space-64) var(--space-64);
714
- }
715
-
716
- .wires {
717
- position: absolute;
718
- inset: 0;
719
- pointer-events: none;
720
- }
721
-
722
- .wires > path {
723
- fill: none;
724
- stroke: var(--border-neutral-medium);
725
- stroke-width: var(--border-width-2);
726
- stroke-linecap: round;
727
- }
728
-
729
- .wires > path.lit {
730
- stroke: var(--text-accent);
731
- stroke-width: var(--border-width-3);
732
- }
733
-
734
- /* A re-run loop is a route, not a failure — danger red overstated it. */
735
- .wires > path.back {
736
- stroke: var(--border-accent-medium);
737
- stroke-dasharray: 6 8;
738
- }
739
-
740
- .wires > path.back.lit {
741
- stroke: var(--text-accent);
742
- }
743
-
744
- .wires marker path {
745
- fill: context-stroke;
746
- }
747
-
748
- /* The halo breaks the wire so the answer reads on its own ground. */
749
- .wire-label {
750
- font-family: var(--code-font-family);
751
- font-size: var(--font-size-sm);
752
- fill: var(--text-tertiary);
753
- stroke: var(--surface-neutral-lowest);
754
- stroke-width: calc(var(--border-width-2) * 3);
755
- stroke-linejoin: round;
756
- paint-order: stroke;
757
- }
758
-
759
- .wire-label.lit {
760
- fill: var(--text-accent);
761
- }
762
-
763
- .flow {
764
- /* Nearly every row holds one node, so the pane's full width would only
765
- stretch a card that has nothing to say at its right edge. Capping the
766
- track turns the column into a spine and gives the wires somewhere to
767
- travel. */
768
- --node-w: 30rem;
769
-
770
- position: relative;
771
- display: flex;
772
- flex-direction: column;
773
- gap: var(--space-40);
774
- }
775
-
776
- /* Sibling branch rows share a column count so a family and its follow-up
777
- question stay in the same column when they wrap. */
778
- .row {
779
- /* A fan only earns a trunk-and-bus while it fits on one line, so branches
780
- narrow enough to stay unwrapped are worth more than uniform columns. */
781
- --track: 9rem;
782
-
783
- display: grid;
784
- grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--track)), 1fr));
785
- gap: var(--space-24);
786
- align-items: start;
787
- /* The auto margins that centre the row also switch off the flex stretch,
788
- so the width has to be asked for rather than inherited. */
789
- width: 100%;
790
- max-width: calc(var(--n) * var(--node-w) + (var(--n) - 1) * var(--space-24));
791
- margin-inline: auto;
792
- }
793
-
794
- /* A flat step would spend the same gap on "next" as on "the tree splits
795
- here". The extra margin lets a branch announce itself before it is read. */
796
- .row:not([data-count='1']) {
797
- margin-block: var(--space-48);
798
- }
291
+ /* Desktop: the surface locks to the viewport and each pane scrolls on its
292
+ own, so nothing ever slides under the tab strip. Placed after the base
293
+ .pane rule: its 82vh would otherwise win on source order. The router wrapper
294
+ assumes window-scroll pages (min-height + a 12rem bottom pad); the doubled
295
+ .lt-app outranks its scoped rule on a specificity tie. The child
296
+ combinator keeps the demo home out: it has an .atlas of its own. */
297
+ @media (min-width: 64rem) {
298
+ :global(.lt-app.lt-app:has(> .skill-atlas)) {
299
+ height: 100vh;
300
+ min-height: 0;
301
+ padding-bottom: 0;
302
+ overflow: hidden;
303
+ }
799
304
 
800
- /* Past four, one line would shave the cards past reading; wrap them in pairs
801
- and let the connectors fall back to curves. */
802
- .row[data-count='5'],
803
- .row[data-count='6'],
804
- .row[data-count='7'] {
805
- --track: 17rem;
806
- }
305
+ /* border-box: with no global reset the frame is content-box, so its own
306
+ padding would push the panes' bottom edge past the wrapper's clip. */
307
+ .skill-atlas {
308
+ box-sizing: border-box;
309
+ height: 100%;
310
+ overflow: hidden;
311
+ }
807
312
 
808
- .statusbar {
809
- display: flex;
810
- align-items: baseline;
811
- gap: var(--space-16);
812
- flex-wrap: wrap;
813
- margin-top: var(--space-24);
814
- padding-top: var(--space-16);
815
- border-top: var(--border-width-1) solid var(--border-neutral-faint);
816
- }
313
+ .masthead,
314
+ .tabs {
315
+ flex: 0 0 auto;
316
+ }
817
317
 
818
- .status-label {
819
- font-family: var(--body-md-font-family);
820
- font-size: var(--font-size-md);
821
- font-weight: var(--font-weight-bold);
822
- color: var(--text-primary);
823
- }
318
+ /* minmax(0, 1fr) pins the row to the remaining height; an auto row would
319
+ grow to the panes' content and break their scroll. */
320
+ .split {
321
+ flex: 1 1 auto;
322
+ min-height: 0;
323
+ grid-template-columns: 3fr 2fr;
324
+ grid-template-rows: minmax(0, 1fr);
325
+ }
824
326
 
825
- .status-range {
826
- font-family: var(--code-font-family);
827
- font-size: var(--font-size-md);
828
- color: var(--text-tertiary);
327
+ .pane {
328
+ box-sizing: border-box;
329
+ height: 100%;
330
+ }
829
331
  }
830
332
 
831
333
  @media (prefers-reduced-motion: reduce) {