@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
@@ -0,0 +1,263 @@
1
+ <script lang="ts">
2
+ import TreeNodeCard from './TreeNodeCard.svelte';
3
+ import { mergeParallelEdges, withoutChipAnswers } from './edges';
4
+ import { routeWires, type Box, type Label, type Wire } from './wireLayout';
5
+ import type { Edge, LineRange, SkillTree, TreeNode } from './types';
6
+
7
+ interface Props {
8
+ tree: SkillTree;
9
+ /** The selection key: a node id, or `node:chip`. */
10
+ selected: string | null;
11
+ onselect: (key: string, label: string, lines: LineRange) => void;
12
+ onopen: (doc: string) => void;
13
+ }
14
+
15
+ let { tree, selected, onselect, onopen }: Props = $props();
16
+
17
+ let edges = $derived(mergeParallelEdges(withoutChipAnswers(tree.edges, tree.nodes)));
18
+
19
+ /** The cards of each row, with the tallest stacked answer leaving it so the
20
+ * row can reserve room for the label under its fan-out. */
21
+ let rows = $derived.by(() => {
22
+ const byRow = new Map<number, TreeNode[]>();
23
+ for (const node of tree.nodes) {
24
+ const group = byRow.get(node.row);
25
+ if (group) group.push(node);
26
+ else byRow.set(node.row, [node]);
27
+ }
28
+ return [...byRow.entries()]
29
+ .sort((a, b) => a[0] - b[0])
30
+ .map(([, nodes]) => ({
31
+ nodes,
32
+ answerLines: Math.max(
33
+ 0,
34
+ ...edges
35
+ .filter((edge) => nodes.some((node) => node.id === edge.from))
36
+ .map((edge) => (edge.label?.split('\n').length ?? 1) - 1),
37
+ ),
38
+ }));
39
+ });
40
+
41
+ // Connectors are measured from the laid-out cards, so they survive wrapping,
42
+ // theme changes, and the geometry skill moving every radius and padding.
43
+ let canvas: HTMLElement | undefined = $state();
44
+ let wires: Wire[] = $state([]);
45
+ let labels: Label[] = $state([]);
46
+ let canvasSize = $state({ w: 0, h: 0 });
47
+
48
+ function drawWires() {
49
+ if (!canvas) return;
50
+ const base = canvas.getBoundingClientRect();
51
+ canvasSize = { w: base.width, h: base.height };
52
+ const [selectedNode = '', chipIndex] = selected?.split(':') ?? [];
53
+ const chip = chipIndex === undefined
54
+ ? undefined
55
+ : tree.nodes.find((node) => node.id === selectedNode)?.chips?.[Number(chipIndex)]?.label;
56
+ // A selected badge lights only the wire that carries its answer; a selected
57
+ // card lights every wire that touches it.
58
+ const lit = (edge: Edge) => {
59
+ if (chip === undefined) return edge.from === selectedNode || edge.to === selectedNode;
60
+ return edge.from === selectedNode && (edge.answers?.includes(chip) ?? true);
61
+ };
62
+
63
+ const boxes = new Map<string, Box>();
64
+ for (const node of tree.nodes) {
65
+ const el = canvas.querySelector(`[data-node="${node.id}"]`);
66
+ if (!el) continue;
67
+ const r = el.getBoundingClientRect();
68
+ boxes.set(node.id, {
69
+ left: r.left - base.left,
70
+ right: r.right - base.left,
71
+ top: r.top - base.top,
72
+ bottom: r.bottom - base.top,
73
+ midX: r.left + r.width / 2 - base.left,
74
+ midY: r.top + r.height / 2 - base.top,
75
+ });
76
+ }
77
+
78
+ const drawing = routeWires(boxes, edges, lit);
79
+ // Lit last, so the selected wire paints over the ones it crosses.
80
+ wires = [...drawing.wires].sort((a, b) => Number(a.lit) - Number(b.lit));
81
+ labels = [...drawing.labels].sort((a, b) => Number(a.lit) - Number(b.lit));
82
+ }
83
+
84
+ $effect(() => {
85
+ // Re-read on tab change and on selection (which only relights existing wires).
86
+ void tree;
87
+ void selected;
88
+ const frame = requestAnimationFrame(drawWires);
89
+ return () => cancelAnimationFrame(frame);
90
+ });
91
+
92
+ $effect(() => {
93
+ if (!canvas) return;
94
+ const observer = new ResizeObserver(() => drawWires());
95
+ observer.observe(canvas);
96
+ return () => observer.disconnect();
97
+ });
98
+ </script>
99
+
100
+ <div class="canvas" bind:this={canvas}>
101
+ <svg
102
+ class="wires"
103
+ viewBox="0 0 {canvasSize.w} {canvasSize.h}"
104
+ width={canvasSize.w}
105
+ height={canvasSize.h}
106
+ aria-hidden="true"
107
+ >
108
+ <defs>
109
+ <marker
110
+ id="atlas-arrow"
111
+ viewBox="0 0 10 10"
112
+ refX="9"
113
+ refY="5"
114
+ markerWidth="9"
115
+ markerHeight="9"
116
+ markerUnits="userSpaceOnUse"
117
+ orient="auto"
118
+ >
119
+ <path d="M 0 1 L 9 5 L 0 9 z" />
120
+ </marker>
121
+ </defs>
122
+
123
+ {#each wires as wire, i (i)}
124
+ <path
125
+ d={wire.d}
126
+ class:back={wire.back}
127
+ class:lit={wire.lit}
128
+ marker-end={wire.arrow ? 'url(#atlas-arrow)' : undefined}
129
+ />
130
+ {/each}
131
+
132
+ {#each labels as label, i (i)}
133
+ <text
134
+ class="wire-label"
135
+ class:lit={label.lit}
136
+ x={label.x}
137
+ y={label.y}
138
+ transform={label.rotate ? `rotate(-90 ${label.x} ${label.y})` : undefined}
139
+ text-anchor="middle"
140
+ dominant-baseline="central"
141
+ >
142
+ {#each label.lines as answer, j (j)}
143
+ <tspan x={label.x} dy={j === 0 ? `${-(label.lines.length - 1) * 0.6}em` : '1.2em'}>{answer}</tspan>
144
+ {/each}
145
+ </text>
146
+ {/each}
147
+ </svg>
148
+
149
+ <div class="flow">
150
+ {#each rows as { nodes, answerLines }, i (i)}
151
+ <div class="row" data-count={nodes.length} style="--n: {nodes.length}; --answer-lines: {answerLines}">
152
+ {#each nodes as node (node.id)}
153
+ <TreeNodeCard {node} {selected} {onselect} {onopen} />
154
+ {/each}
155
+ </div>
156
+ {/each}
157
+ </div>
158
+ </div>
159
+
160
+ <style>
161
+ /* The side padding is the lanes: re-run loops on the left, row-skipping
162
+ branches on the right. A branch row runs the full width of the flow, so
163
+ without reserved room a lane would have nowhere to go but over the cards
164
+ it is routing around. */
165
+ .canvas {
166
+ position: relative;
167
+ padding: var(--space-32) var(--space-64) var(--space-64);
168
+ }
169
+
170
+ .wires {
171
+ position: absolute;
172
+ inset: 0;
173
+ pointer-events: none;
174
+ }
175
+
176
+ .wires > path {
177
+ fill: none;
178
+ stroke: var(--border-neutral-medium);
179
+ stroke-width: var(--border-width-2);
180
+ stroke-linecap: round;
181
+ }
182
+
183
+ .wires > path.lit {
184
+ stroke: var(--text-accent);
185
+ stroke-width: var(--border-width-3);
186
+ }
187
+
188
+ /* A re-run loop is a route, not a failure — danger red overstated it. */
189
+ .wires > path.back {
190
+ stroke: var(--border-accent-medium);
191
+ stroke-dasharray: 6 8;
192
+ }
193
+
194
+ .wires > path.back.lit {
195
+ stroke: var(--text-accent);
196
+ }
197
+
198
+ .wires marker path {
199
+ fill: context-stroke;
200
+ }
201
+
202
+ /* The halo breaks the wire so the answer reads on its own ground. */
203
+ .wire-label {
204
+ font-family: var(--code-font-family);
205
+ font-size: var(--font-size-sm);
206
+ fill: var(--text-tertiary);
207
+ stroke: var(--surface-neutral-lowest);
208
+ stroke-width: calc(var(--border-width-2) * 3);
209
+ stroke-linejoin: round;
210
+ paint-order: stroke;
211
+ }
212
+
213
+ .wire-label.lit {
214
+ fill: var(--text-accent);
215
+ }
216
+
217
+ .flow {
218
+ /* Nearly every row holds one node, so the pane's full width would only
219
+ stretch a card that has nothing to say at its right edge. Capping the
220
+ track turns the column into a spine and gives the wires somewhere to
221
+ travel. */
222
+ --node-w: 24rem;
223
+
224
+ position: relative;
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: var(--space-24);
228
+ }
229
+
230
+ /* Sibling branch rows share a column count so a family and its follow-up
231
+ question stay in the same column when they wrap. */
232
+ .row {
233
+ /* A fan only earns a trunk-and-bus while it fits on one line, so branches
234
+ narrow enough to stay unwrapped are worth more than uniform columns. */
235
+ --track: 9rem;
236
+
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--track)), 1fr));
239
+ gap: var(--space-16);
240
+ align-items: start;
241
+ /* The auto margins that centre the row also switch off the flex stretch,
242
+ so the width has to be asked for rather than inherited. */
243
+ width: 100%;
244
+ max-width: calc(var(--n) * var(--node-w) + (var(--n) - 1) * var(--space-16));
245
+ margin-inline: auto;
246
+ margin-bottom: calc(var(--answer-lines) * var(--space-20));
247
+ }
248
+
249
+ /* A flat step would spend the same gap on "next" as on "the tree splits
250
+ here". The extra margin lets a branch announce itself before it is read. */
251
+ .row:not([data-count='1']) {
252
+ margin-top: var(--space-32);
253
+ margin-bottom: calc(var(--space-32) + var(--answer-lines) * var(--space-20));
254
+ }
255
+
256
+ /* Past four, one line would shave the cards past reading; wrap them in pairs
257
+ and let the connectors fall back to curves. */
258
+ .row[data-count='5'],
259
+ .row[data-count='6'],
260
+ .row[data-count='7'] {
261
+ --track: 14rem;
262
+ }
263
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
+ import Badge from '../../system/components/Badge.svelte';
2
3
  import Button from '../../system/components/Button.svelte';
3
4
  import Card from '../../system/components/Card.svelte';
4
- import CodeSnippet from '../../system/components/CodeSnippet.svelte';
5
+ import Tooltip from '../../system/components/Tooltip.svelte';
5
6
  import type { LineRange, NodeKind, TreeNode } from './types';
6
7
 
7
8
  interface Props {
@@ -18,21 +19,36 @@
18
19
  step: 'step',
19
20
  decide: 'decision',
20
21
  cli: 'command',
21
- hand: 'hands off',
22
+ hand: 'handoff',
22
23
  gate: 'failure',
23
24
  ok: 'pass',
24
25
  ref: 'reference',
25
- ask: 'question',
26
- chipset: 'reference set',
27
- done: 'verify',
26
+ chipset: 'step',
27
+ done: 'complete',
28
+ };
29
+
30
+ /** The structural rule that puts a node in each kind, as the audit enforces it. */
31
+ const KIND_MEANING: Record<NodeKind, string> = {
32
+ trigger: 'The request that starts the skill. Each chart has one, and it summarises what the skill is for.',
33
+ step: 'One action the skill takes. It has a single continuation.',
34
+ decide: 'A branch on a fact the skill reads from the request, the report, or a file. Each wire carries the answer that selects it, and there are at least two.',
35
+ cli: 'A CLI command the skill runs. The wires carry its exit outcomes, or a single continuation when it cannot fail.',
36
+ hand: 'The chart ends by invoking the named skill.',
37
+ gate: 'A check that failed. Its one wire returns up the chart to the command that runs again.',
38
+ ok: 'A check that passed, so the chart continues.',
39
+ ref: 'A step that reads a references document. The link opens it beside the source.',
40
+ chipset: 'One action the skill takes. It has a single continuation.',
41
+ done: 'The chart ends.',
28
42
  };
29
43
 
30
44
  let kindLabel = $derived(node.tag ?? KIND_LABEL[node.kind]);
45
+ let kindMeaning = $derived(node.tag ? undefined : KIND_MEANING[node.kind]);
46
+ let meaningOpen = $state(false);
31
47
  let cardSelected = $derived(selected === node.id);
32
48
  let range = $derived(node.lines ? rangeLabel(node.lines) : '');
33
49
 
34
50
  function rangeLabel([a, b]: LineRange): string {
35
- return a === b ? `line ${a}` : `lines ${a}–${b}`;
51
+ return a === b ? `Ln ${a}` : `Ln ${a}–${b}`;
36
52
  }
37
53
 
38
54
  function selectSelf() {
@@ -51,13 +67,25 @@
51
67
  ></button>
52
68
  {/if}
53
69
 
54
- <Card title={node.title} prose={false}>
55
- <div class="meta">
56
- <h3 class="kind">
57
- {#if node.n}<span class="step-n">{node.n}</span>{/if}{kindLabel}
58
- </h3>
59
- {#if range}<span class="range">{range}</span>{/if}
60
- </div>
70
+ <Card title={node.title} size="compact" prose={false}>
71
+ {#snippet aside()}
72
+ <div class="meta">
73
+ <Tooltip text={kindMeaning} position="bottom" open={meaningOpen}>
74
+ <button
75
+ type="button"
76
+ class="kind"
77
+ aria-pressed={meaningOpen}
78
+ aria-label="{kindLabel}. What this kind of node means"
79
+ onclick={() => (meaningOpen = !meaningOpen)}
80
+ >
81
+ <Badge variant="neutral" size="small">
82
+ {kindLabel}
83
+ </Badge>
84
+ </button>
85
+ </Tooltip>
86
+ {#if range}<span class="range">{range}</span>{/if}
87
+ </div>
88
+ {/snippet}
61
89
 
62
90
  {#if node.desc}<p class="desc">{node.desc}</p>{/if}
63
91
 
@@ -67,17 +95,14 @@
67
95
  </button>
68
96
  {/if}
69
97
 
70
- {#if node.command}
71
- <div class="command"><CodeSnippet code={node.command} /></div>
72
- {/if}
73
-
74
98
  {#if node.chips}
75
99
  <ul class="chips">
76
100
  {#each node.chips as chip, i (chip.label)}
77
101
  {@const key = `${node.id}:${i}`}
78
- <li>
102
+ <li class:picked={selected === key}>
79
103
  <Button
80
- variant={selected === key ? 'secondary' : 'outline'}
104
+ variant="outline"
105
+ size="small"
81
106
  onclick={() => onselect(key, chip.label, chip.lines)}
82
107
  >
83
108
  {chip.label}
@@ -93,6 +118,9 @@
93
118
  .shell {
94
119
  position: relative;
95
120
  border-radius: var(--card-default-radius);
121
+ /* Without the blur (below), the shipped 70% lets the wires read through
122
+ the body. */
123
+ --card-default-surface: color-mix(in srgb, var(--surface-neutral-lower) 80%, transparent);
96
124
  }
97
125
 
98
126
  /* A transparent overlay makes the whole card the click target without
@@ -110,11 +138,27 @@
110
138
 
111
139
  /* The package Card clips its title to one line; a node title is the step
112
140
  itself, so it has to read whole. */
113
- .shell :global(.card-title) {
141
+ .shell :global(.card.compact .card-title) {
142
+ font-weight: var(--font-weight-bold);
143
+ line-height: var(--line-height-tight);
114
144
  white-space: normal;
115
145
  overflow-wrap: anywhere;
116
146
  }
117
147
 
148
+ /* The package Card clips to its corners and its backdrop blur makes it a
149
+ stacking context, which would both clip the kind tooltip and keep the
150
+ chips under the hit overlay. The header takes the rounding instead. */
151
+ .shell :global(.card.compact) {
152
+ overflow: visible;
153
+ backdrop-filter: none;
154
+ }
155
+
156
+ .shell :global(.card.compact .card-header) {
157
+ align-items: flex-start;
158
+ border-radius: calc(var(--card-default-radius) - var(--card-default-border-width))
159
+ calc(var(--card-default-radius) - var(--card-default-border-width)) 0 0;
160
+ }
161
+
118
162
  .shell:has(.hit:hover) {
119
163
  outline: var(--border-width-1) solid var(--border-accent-medium);
120
164
  outline-offset: var(--space-2);
@@ -125,23 +169,30 @@
125
169
  outline-offset: var(--space-2);
126
170
  }
127
171
 
128
- /* Chips, the copy button and the reference link sit above the overlay so they
129
- stay clickable. */
172
+ /* Chips and the reference link sit above the overlay so they stay clickable. */
130
173
  .chips,
131
- .command,
132
- .doclink {
174
+ .doclink,
175
+ .meta :global(.tooltip-wrapper) {
133
176
  position: relative;
134
177
  z-index: 2;
135
178
  }
136
179
 
180
+ /* A shown tooltip hangs over the row below, whose chips also sit at
181
+ z-index 2 and come later in the DOM. Hover reveals it without the open
182
+ class. */
183
+ .meta :global(.tooltip-wrapper:hover),
184
+ .meta :global(.tooltip-wrapper.open) {
185
+ z-index: var(--z-tooltip);
186
+ }
187
+
137
188
  .doclink {
138
189
  max-width: 100%;
139
- padding: var(--space-4) var(--space-8);
190
+ padding: var(--space-2) var(--space-6);
140
191
  border: var(--border-width-1) solid var(--border-accent-subtle);
141
192
  border-radius: var(--radius-sm);
142
193
  background: var(--surface-accent-lower);
143
194
  font-family: var(--code-font-family);
144
- font-size: var(--font-size-sm);
195
+ font-size: var(--font-size-xs);
145
196
  color: var(--text-accent);
146
197
  overflow-wrap: break-word;
147
198
  text-align: left;
@@ -152,55 +203,87 @@
152
203
  background: var(--surface-accent-low);
153
204
  }
154
205
 
206
+ /* The title's half-leading pushes its glyphs below the header padding;
207
+ the badge gets the same offset so the two tops line up. */
155
208
  .meta {
156
209
  display: flex;
157
- align-items: center;
158
- gap: var(--space-12);
159
- flex-wrap: wrap;
160
- margin-bottom: var(--space-12);
210
+ flex-direction: column;
211
+ align-items: flex-end;
212
+ gap: var(--space-2);
213
+ padding-top: calc(var(--font-size-md) * (var(--line-height-tight) - 1) / 2);
214
+ font-size: var(--font-size-xs);
161
215
  }
162
216
 
163
217
  .kind {
218
+ --badge-neutral-text-font-size: 1em;
219
+ --badge-neutral-padding: var(--space-2);
220
+ --badge-neutral-padding-right: var(--space-6);
221
+ --badge-neutral-padding-left: var(--space-6);
222
+ display: inline-flex;
164
223
  margin: 0;
165
- font-family: var(--heading-sm-font-family);
166
- font-size: var(--heading-sm-font-size);
167
- font-weight: var(--heading-sm-font-weight);
168
- line-height: var(--heading-sm-line-height);
169
- letter-spacing: var(--heading-sm-letter-spacing);
170
- color: var(--text-secondary);
171
- }
172
-
173
- .step-n {
174
- margin-right: var(--space-8);
175
- font-weight: var(--font-weight-bold);
224
+ padding: 0;
225
+ font-size: inherit;
226
+ border: none;
227
+ background: transparent;
228
+ cursor: pointer;
176
229
  }
177
230
 
178
231
  .range {
179
232
  font-family: var(--code-font-family);
180
- font-size: var(--font-size-md);
233
+ font-size: inherit;
181
234
  color: var(--text-tertiary);
182
235
  }
183
236
 
237
+ /* The package Tooltip is a one-line hint; a kind's meaning is a sentence
238
+ or two, so it wraps and hangs from the badge's right edge, which sits at
239
+ the card's edge. */
240
+ .meta :global(.tooltip.bottom) {
241
+ right: 0;
242
+ left: auto;
243
+ width: max-content;
244
+ max-width: 36ch;
245
+ transform: none;
246
+ white-space: normal;
247
+ text-align: left;
248
+ }
249
+
250
+ .meta :global(.tooltip.bottom)::after {
251
+ right: var(--space-16);
252
+ left: auto;
253
+ transform: none;
254
+ }
255
+
184
256
  .desc {
185
257
  margin: 0;
186
- font-family: var(--body-md-font-family);
187
- font-size: var(--body-md-font-size);
188
- font-weight: var(--body-md-font-weight);
189
- line-height: var(--body-md-line-height);
190
- letter-spacing: var(--body-md-letter-spacing);
258
+ font-family: var(--body-sm-font-family);
259
+ font-size: var(--body-sm-font-size);
260
+ font-weight: var(--body-sm-font-weight);
261
+ line-height: var(--body-sm-line-height);
262
+ letter-spacing: var(--body-sm-letter-spacing);
191
263
  color: var(--text-secondary);
192
- }
193
-
194
- .command {
195
- margin-top: var(--space-16);
264
+ white-space: pre-line;
196
265
  }
197
266
 
198
267
  .chips {
199
268
  display: flex;
200
269
  flex-wrap: wrap;
201
- gap: var(--space-12);
202
- margin: var(--space-20) 0 0;
270
+ gap: var(--space-8);
271
+ margin: var(--space-12) 0 0;
203
272
  padding: 0;
204
273
  list-style: none;
205
274
  }
275
+
276
+ /* The selected badge takes the lit wire's colour, and its focus ring does
277
+ too, so a click never shows the browser's blue ring over it. */
278
+ .chips li.picked {
279
+ --button-outline-border: var(--text-accent);
280
+ --button-outline-hover-border: var(--text-accent);
281
+ --button-outline-text: var(--text-accent);
282
+ --button-outline-surface: var(--surface-accent-lower);
283
+ }
284
+
285
+ .chips :global(button:focus-visible) {
286
+ outline: var(--border-width-2) solid var(--text-accent);
287
+ outline-offset: var(--space-2);
288
+ }
206
289
  </style>
@@ -0,0 +1,31 @@
1
+ import type { Edge, TreeNode } from './types';
2
+
3
+ /** An answer the card already shows as a chip says nothing more on the wire. It
4
+ * moves to `answers`, so selecting the chip still lights its wire. */
5
+ export function withoutChipAnswers(edges: Edge[], nodes: TreeNode[]): Edge[] {
6
+ const chips = new Map(nodes.map((node) => [node.id, new Set((node.chips ?? []).map((chip) => chip.label))]));
7
+ return edges.map((edge) => {
8
+ if (!edge.label || !chips.get(edge.from)?.has(edge.label)) return edge;
9
+ const { label, ...bare } = edge;
10
+ return { ...bare, answers: [label] };
11
+ });
12
+ }
13
+
14
+ /** Several answers can lead to the same next step. Draw their shared wire once. */
15
+ export function mergeParallelEdges(edges: Edge[]): Edge[] {
16
+ const groups = new Map<string, Edge>();
17
+ for (const edge of edges) {
18
+ const key = JSON.stringify([edge.from, edge.to, Boolean(edge.back)]);
19
+ const existing = groups.get(key);
20
+ if (!existing) {
21
+ groups.set(key, { ...edge });
22
+ } else {
23
+ if (edge.label) {
24
+ const labels = new Set([...(existing.label?.split('\n') ?? []), edge.label]);
25
+ existing.label = [...labels].join('\n');
26
+ }
27
+ if (edge.answers) existing.answers = [...new Set([...(existing.answers ?? []), ...edge.answers])];
28
+ }
29
+ }
30
+ return [...groups.values()];
31
+ }