@picked/svelte 0.0.1-dev.1789541668638 → 0.0.1-dev.1793753513002

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 (118) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +51 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/_virtual/_vite/preload-helper.js +1 -1
  5. package/dist/components/index.js +1 -1
  6. package/dist/experience/ExperienceHost.svelte +29 -13
  7. package/dist/experience/ExperienceSlot.svelte +21 -3
  8. package/dist/experience/ExperienceWrapper.svelte +23 -0
  9. package/dist/form/index.js +1 -1
  10. package/dist/hooks/action.d.ts +47 -18
  11. package/dist/hooks/action.js +1 -1
  12. package/dist/hooks/index.d.ts +2 -2
  13. package/dist/hooks/index.js +1 -1
  14. package/dist/hooks/step.d.ts +14 -0
  15. package/dist/hooks/step.js +1 -1
  16. package/dist/plugins/devtools/DevToolsExperience.svelte +545 -203
  17. package/dist/plugins/devtools/components/AdaptedToolHost.svelte +2 -2
  18. package/dist/plugins/devtools/constants.d.ts +5 -0
  19. package/dist/plugins/devtools/constants.js +1 -0
  20. package/dist/plugins/devtools/customTools.d.ts +42 -6
  21. package/dist/plugins/devtools/customTools.js +1 -1
  22. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +112 -0
  23. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -0
  24. package/dist/plugins/devtools/derive/answer-display/index.d.ts +3 -0
  25. package/dist/plugins/devtools/derive/answer-display/index.js +1 -0
  26. package/dist/plugins/devtools/derive/answer-history/answer-history.d.ts +76 -0
  27. package/dist/plugins/devtools/derive/answer-history/answer-history.js +1 -0
  28. package/dist/plugins/devtools/derive/answer-history/index.d.ts +3 -0
  29. package/dist/plugins/devtools/derive/answer-history/index.js +1 -0
  30. package/dist/plugins/devtools/derive/guide-sections/guide-sections.d.ts +67 -0
  31. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -0
  32. package/dist/plugins/devtools/derive/guide-sections/index.d.ts +3 -0
  33. package/dist/plugins/devtools/derive/guide-sections/index.js +1 -0
  34. package/dist/plugins/devtools/derive/index.d.ts +28 -0
  35. package/dist/plugins/devtools/derive/index.js +1 -0
  36. package/dist/plugins/devtools/derive/journey-outline/index.d.ts +3 -0
  37. package/dist/plugins/devtools/derive/journey-outline/index.js +1 -0
  38. package/dist/plugins/devtools/derive/journey-outline/journey-outline.d.ts +86 -0
  39. package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -0
  40. package/dist/plugins/devtools/derive/journey-pick/index.d.ts +3 -0
  41. package/dist/plugins/devtools/derive/journey-pick/index.js +1 -0
  42. package/dist/plugins/devtools/derive/journey-pick/journey-pick.d.ts +73 -0
  43. package/dist/plugins/devtools/derive/journey-pick/journey-pick.js +1 -0
  44. package/dist/plugins/devtools/derive/journey-timeline/index.d.ts +3 -0
  45. package/dist/plugins/devtools/derive/journey-timeline/index.js +1 -0
  46. package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.d.ts +70 -0
  47. package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.js +1 -0
  48. package/dist/plugins/devtools/derive/journey-tree/index.d.ts +3 -0
  49. package/dist/plugins/devtools/derive/journey-tree/index.js +1 -0
  50. package/dist/plugins/devtools/derive/journey-tree/journey-tree.d.ts +191 -0
  51. package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -0
  52. package/dist/plugins/devtools/derive/plain-language/index.d.ts +3 -0
  53. package/dist/plugins/devtools/derive/plain-language/index.js +1 -0
  54. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +73 -0
  55. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -0
  56. package/dist/plugins/devtools/derive/plain-logic/index.d.ts +3 -0
  57. package/dist/plugins/devtools/derive/plain-logic/index.js +1 -0
  58. package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +48 -0
  59. package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -0
  60. package/dist/plugins/devtools/derive/safety-posture/index.d.ts +3 -0
  61. package/dist/plugins/devtools/derive/safety-posture/index.js +1 -0
  62. package/dist/plugins/devtools/derive/safety-posture/safety-posture.d.ts +43 -0
  63. package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -0
  64. package/dist/plugins/devtools/derive/visited-steps/index.d.ts +3 -0
  65. package/dist/plugins/devtools/derive/visited-steps/index.js +1 -0
  66. package/dist/plugins/devtools/derive/visited-steps/visited-steps.d.ts +35 -0
  67. package/dist/plugins/devtools/derive/visited-steps/visited-steps.js +1 -0
  68. package/dist/plugins/devtools/derive/walk-route/index.d.ts +3 -0
  69. package/dist/plugins/devtools/derive/walk-route/index.js +1 -0
  70. package/dist/plugins/devtools/derive/walk-route/walk-route.d.ts +22 -0
  71. package/dist/plugins/devtools/derive/walk-route/walk-route.js +1 -0
  72. package/dist/plugins/devtools/index.d.ts +5 -2
  73. package/dist/plugins/devtools/index.js +1 -1
  74. package/dist/plugins/devtools/launcher/Launcher.svelte +150 -0
  75. package/dist/plugins/devtools/launcher/launcher.d.ts +36 -0
  76. package/dist/plugins/devtools/launcher/launcher.js +1 -0
  77. package/dist/plugins/devtools/options.d.ts +106 -0
  78. package/dist/plugins/devtools/options.js +1 -0
  79. package/dist/plugins/devtools/plugin.d.ts +8 -8
  80. package/dist/plugins/devtools/plugin.js +1 -1
  81. package/dist/plugins/devtools/posture-context.d.ts +15 -0
  82. package/dist/plugins/devtools/posture-context.js +1 -0
  83. package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +526 -0
  84. package/dist/plugins/devtools/review/guide/GuideLine.svelte +20 -0
  85. package/dist/plugins/devtools/review/guide/GuidePanel.svelte +133 -0
  86. package/dist/plugins/devtools/review/index.d.ts +6 -0
  87. package/dist/plugins/devtools/review/index.js +1 -0
  88. package/dist/plugins/devtools/review/journey/JourneyMap.svelte +1076 -0
  89. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +584 -0
  90. package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +750 -0
  91. package/dist/plugins/devtools/review/journey/TimelineMarker.svelte +59 -0
  92. package/dist/plugins/devtools/review/session/SessionPanel.svelte +214 -0
  93. package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +279 -0
  94. package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +6 -0
  95. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +3 -3
  96. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +6 -0
  97. package/dist/plugins/devtools/session/index.d.ts +3 -0
  98. package/dist/plugins/devtools/session/index.js +1 -0
  99. package/dist/plugins/devtools/session/session.d.ts +90 -0
  100. package/dist/plugins/devtools/session/session.js +1 -0
  101. package/dist/plugins/devtools/session-access.d.ts +32 -0
  102. package/dist/plugins/devtools/session-access.js +1 -0
  103. package/dist/plugins/devtools/sidebar/Sidebar.svelte +105 -0
  104. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +117 -0
  105. package/dist/plugins/devtools/sidebar/nav.d.ts +105 -0
  106. package/dist/plugins/devtools/sidebar/nav.js +1 -0
  107. package/dist/plugins/devtools/ui/actions/Button.svelte +10 -1
  108. package/dist/plugins/devtools/ui/actions/CopyButton.svelte +13 -1
  109. package/dist/plugins/devtools/ui/data/Badge.svelte +3 -2
  110. package/dist/plugins/devtools/ui/data/EmptyState.svelte +16 -1
  111. package/dist/plugins/devtools/ui/feedback/Banner.svelte +3 -2
  112. package/dist/plugins/framework/index.js +1 -1
  113. package/dist/render/browser.js +1 -1
  114. package/package.json +23 -12
  115. package/dist/form/__tests__/CreateFormHarness.svelte +0 -30
  116. package/dist/hooks/events.d.ts +0 -2
  117. package/dist/hooks/events.js +0 -1
  118. package/dist/vite-env.d.js +0 -0
@@ -0,0 +1,750 @@
1
+ <script lang="ts">
2
+ /**
3
+ * @file The journey drawn as a TIMELINE — `git log --graph` for a flow. One row per node, top to
4
+ * bottom; branches run in lanes beside the rows and MERGE back into the node they meet, so a
5
+ * screen many branches reach is one row with lines converging on it, not a tree full of dashed
6
+ * jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling only.
7
+ *
8
+ * A LOOP is the one thing a git graph never has. It stays off the drawing and rides on its row as
9
+ * a "↩ back to …" note, and the details pane can take the reviewer to its target.
10
+ *
11
+ * COLOUR IS THE BRANCH, as in a git tool: every line and dot takes its branch's colour, so the
12
+ * eye can follow one route down the lanes. That frees the node KIND to be a SHAPE — a circle for
13
+ * a screen, a diamond for a decision, a square for another journey. Actions are not drawn at all;
14
+ * where a branch ends on one, the row names it.
15
+ *
16
+ * ROWS ARE HTML, LINES ARE SVG, the same split as the map: each row is a real `<button>` the full
17
+ * width of the timeline, and the graph column is painted over its left edge with
18
+ * `pointer-events: none`, so clicking a dot or its label selects the same row.
19
+ */
20
+ import { tick } from 'svelte'
21
+ import { COLOURS, ROW_HEIGHT } from '../../derive'
22
+ import type { TimelineLayout, TimelineNode, TreeNodeKind } from '../../derive'
23
+ import TimelineMarker from './TimelineMarker.svelte'
24
+
25
+ interface JourneyTimelineProps {
26
+ layout: TimelineLayout
27
+ /** Whether a screen may be jumped to at all — the posture, and whether this journey is open. */
28
+ canNavigate: boolean
29
+ onJump: (stepKey: string) => void
30
+ }
31
+
32
+ const { layout, canNavigate, onJump }: JourneyTimelineProps = $props()
33
+
34
+ const KIND_LABELS: Record<TreeNodeKind, string> = {
35
+ start: 'Start',
36
+ screen: 'Screen',
37
+ decision: 'Decision',
38
+ system: 'Behind the scenes',
39
+ journey: 'Opens another journey',
40
+ }
41
+
42
+ /** Marker size per kind — the start a little larger, as on the map. */
43
+ const RADIUS: Record<TreeNodeKind, number> = {
44
+ start: 7,
45
+ screen: 5.5,
46
+ decision: 6.5,
47
+ system: 4.5,
48
+ journey: 5,
49
+ }
50
+
51
+ /** One colour per branch, in turn — `COLOURS` long, and distinct from its neighbours. */
52
+ const PALETTE = [
53
+ '#2563eb',
54
+ '#db2777',
55
+ '#059669',
56
+ '#ea580c',
57
+ '#7c3aed',
58
+ '#0891b2',
59
+ '#ca8a04',
60
+ '#dc2626',
61
+ ]
62
+
63
+ // The palette and the layout must agree on how many colours there are, or a fork repeats one.
64
+ if (PALETTE.length !== COLOURS) throw new Error('The timeline palette must hold COLOURS colours.')
65
+
66
+ let scroller: HTMLDivElement | undefined = $state()
67
+ let selected = $state<string | null>(null)
68
+
69
+ const byId = $derived(new Map(layout.nodes.map((node) => [node.id, node])))
70
+ const selectedNode = $derived(selected === null ? null : (byId.get(selected) ?? null))
71
+
72
+ /**
73
+ * A branch's colour.
74
+ *
75
+ * @param {number} colour - The branch's index into the palette.
76
+ * @returns {string} The colour, as a CSS hex value.
77
+ * @example
78
+ * paint(9) // the palette's second colour
79
+ */
80
+ function paint(colour: number): string {
81
+ return PALETTE.at(colour % PALETTE.length) ?? '#94a3b8'
82
+ }
83
+
84
+ /**
85
+ * Whether a row can take the visitor back to its screen.
86
+ *
87
+ * @param {TimelineNode} node - The row to test.
88
+ * @returns {boolean} True for a visited screen in the open journey, when the posture allows it.
89
+ * @example
90
+ * canJump(node)
91
+ */
92
+ function canJump(node: TimelineNode): boolean {
93
+ return (
94
+ node.kind === 'screen' && node.state === 'visited' && node.stepKey !== null && canNavigate
95
+ )
96
+ }
97
+
98
+ /**
99
+ * A node's name, for a loop note or a "Leads to" link.
100
+ *
101
+ * @param {string} id - The node's id.
102
+ * @returns {string} The node's label.
103
+ * @example
104
+ * labelOf(next.to) // 'Contact details'
105
+ */
106
+ function labelOf(id: string): string {
107
+ return byId.get(id)?.label ?? 'an unnamed node'
108
+ }
109
+
110
+ /**
111
+ * Brings a row to the middle of the timeline.
112
+ *
113
+ * @param {TimelineNode} node - The row to show.
114
+ * @param {boolean} smooth - Whether to scroll smoothly.
115
+ * @example
116
+ * reveal(node, true)
117
+ */
118
+ function reveal(node: TimelineNode, smooth: boolean): void {
119
+ if (scroller === undefined) return
120
+ const top = node.row * ROW_HEIGHT + ROW_HEIGHT / 2 - scroller.clientHeight / 2
121
+ scroller.scrollTo({ top: Math.max(0, top), behavior: smooth ? 'smooth' : 'auto' })
122
+ }
123
+
124
+ /**
125
+ * Shows a row's details; a second click on the same row hides them.
126
+ *
127
+ * @param {TimelineNode} node - The row clicked.
128
+ * @example
129
+ * select(node)
130
+ */
131
+ function select(node: TimelineNode): void {
132
+ selected = selected === node.id ? null : node.id
133
+ }
134
+
135
+ /**
136
+ * From the details pane: follows a route to where it lands, and focuses that row.
137
+ *
138
+ * @param {string} id - The row to go to.
139
+ * @returns {Promise<void>} Settles once the row has focus.
140
+ * @example
141
+ * await goTo(next.to)
142
+ */
143
+ async function goTo(id: string): Promise<void> {
144
+ const node = byId.get(id)
145
+ if (node === undefined) return
146
+ selected = id
147
+ reveal(node, true)
148
+ await tick()
149
+ scroller?.querySelector<HTMLButtonElement>(`[data-row="${CSS.escape(id)}"]`)?.focus({
150
+ preventScroll: true,
151
+ })
152
+ }
153
+
154
+ /**
155
+ * Moves the open journey back to a row's screen.
156
+ *
157
+ * @param {TimelineNode} node - A visited screen.
158
+ * @example
159
+ * goBack(node)
160
+ */
161
+ function goBack(node: TimelineNode): void {
162
+ if (canJump(node) && node.stepKey !== null) onJump(node.stepKey)
163
+ }
164
+
165
+ /**
166
+ * Clears the selection on Escape, and only that — the panel itself stays open.
167
+ *
168
+ * @param {KeyboardEvent} event - The key event.
169
+ * @example
170
+ * onKeydown(event)
171
+ */
172
+ function onKeydown(event: KeyboardEvent): void {
173
+ if (event.key !== 'Escape' || selected === null) return
174
+ event.stopPropagation()
175
+ selected = null
176
+ }
177
+
178
+ /**
179
+ * Whether a line lights up: either end is the row the reviewer is looking at.
180
+ *
181
+ * @param {string} from - The line's upper row.
182
+ * @param {string} to - The line's lower row.
183
+ * @returns {boolean} True when either end is selected.
184
+ * @example
185
+ * active(edge.from, edge.to)
186
+ */
187
+ function active(from: string, to: string): boolean {
188
+ return selected !== null && (selected === from || selected === to)
189
+ }
190
+
191
+ // Scroll to where the visitor is whenever a DIFFERENT timeline arrives — keyed on its shape, as
192
+ // the map is, so a batch of fields landing does not yank the list from under the reviewer.
193
+ const shape = $derived(
194
+ [
195
+ layout.nodes.length,
196
+ layout.lanes,
197
+ layout.nodes.find((node) => node.state === 'current')?.id ?? '',
198
+ ].join(':'),
199
+ )
200
+ let revealed = ''
201
+
202
+ $effect(() => {
203
+ if (shape === revealed || scroller === undefined) return
204
+ revealed = shape
205
+ const here = layout.nodes.find((node) => node.state === 'current')
206
+ if (here !== undefined) reveal(here, false)
207
+ else scroller.scrollTo({ top: 0 })
208
+ })
209
+ </script>
210
+
211
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
212
+ <div class="timeline" onkeydown={onKeydown}>
213
+ <div class="body">
214
+ <div class="scroller" bind:this={scroller} data-testid="devtools-journey-timeline">
215
+ <div
216
+ class="canvas"
217
+ style:--height="{layout.height}px"
218
+ style:--graph="{layout.width}px"
219
+ style:--row="{ROW_HEIGHT}px"
220
+ >
221
+ {#each layout.nodes as node (node.id)}
222
+ {@const loops = node.next.filter((next) => next.loop)}
223
+ <button
224
+ type="button"
225
+ class="row kind-{node.kind} state-{node.state}"
226
+ class:selected={selected === node.id}
227
+ style:--top="{node.row * ROW_HEIGHT}px"
228
+ data-row={node.id}
229
+ data-testid={node.kind === 'screen' ? 'devtools-journey-screen' : undefined}
230
+ onclick={() => select(node)}
231
+ >
232
+ <span class="label">
233
+ {#if node.kind === 'decision'}<span class="if">If</span>{/if}
234
+ {node.label}
235
+ </span>
236
+ {#if node.state === 'current'}
237
+ <span class="here">You are here</span>
238
+ {/if}
239
+ {#each loops as loop (loop.to)}
240
+ <span class="loop" title="Loops back to {labelOf(loop.to)}"
241
+ >↩ {loop.label === null ? '' : `${loop.label}: `}back to {labelOf(loop.to)}</span
242
+ >
243
+ {/each}
244
+ {#each node.ends as end (end.key)}
245
+ <span class="end" title="Hands off to {end.label}, behind the scenes"
246
+ >⇥ {end.armLabel === null ? '' : `${end.armLabel}: `}{end.label}</span
247
+ >
248
+ {/each}
249
+ </button>
250
+ {/each}
251
+
252
+ <svg class="graph" width={layout.width} height={layout.height} aria-hidden="true">
253
+ {#each layout.edges as edge (edge.id)}
254
+ <path
255
+ class="edge"
256
+ class:travelled={edge.travelled}
257
+ class:active={active(edge.from, edge.to)}
258
+ stroke={paint(edge.colour)}
259
+ d={edge.path}
260
+ />
261
+ {/each}
262
+ {#each layout.nodes as node (node.id)}
263
+ {#if node.state === 'current'}
264
+ <circle class="halo" cx={node.x} cy={node.y} r={RADIUS[node.kind] + 6} />
265
+ {/if}
266
+ <TimelineMarker
267
+ kind={node.kind}
268
+ x={node.x}
269
+ y={node.y}
270
+ r={RADIUS[node.kind]}
271
+ fill={paint(node.colour)}
272
+ state={node.state}
273
+ selected={selected === node.id}
274
+ />
275
+ {/each}
276
+ </svg>
277
+ </div>
278
+ </div>
279
+
280
+ <aside class="details" aria-live="polite" data-testid="devtools-journey-details">
281
+ {#if selectedNode === null}
282
+ <p class="hint">
283
+ Pick a row to see what it does and where it leads. Branches run down the lanes on the left
284
+ and merge back where they meet.
285
+ </p>
286
+ {:else}
287
+ <div class="head">
288
+ <svg class="key-dot" width="14" height="14" aria-hidden="true">
289
+ <TimelineMarker
290
+ kind={selectedNode.kind}
291
+ x={7}
292
+ y={7}
293
+ r={5.5}
294
+ fill={paint(selectedNode.colour)}
295
+ />
296
+ </svg>
297
+ <span class="kind">{KIND_LABELS[selectedNode.kind]}</span>
298
+ <button type="button" class="close" aria-label="Close" onclick={() => (selected = null)}
299
+ >×</button
300
+ >
301
+ </div>
302
+
303
+ <p class="title">{selectedNode.label}</p>
304
+
305
+ {#if selectedNode.state === 'current'}
306
+ <p class="state">You are here</p>
307
+ {:else if selectedNode.state === 'visited'}
308
+ <p class="state">On the route taken this session</p>
309
+ {/if}
310
+
311
+ {#if selectedNode.detail !== null}
312
+ <code class="key">{selectedNode.detail}</code>
313
+ {/if}
314
+
315
+ {#if selectedNode.next.length > 0 || selectedNode.ends.length > 0}
316
+ <div class="next">
317
+ <span class="next-title">Leads to</span>
318
+ <ul>
319
+ {#each selectedNode.next as next (next.to)}
320
+ <li>
321
+ {#if next.label !== null}
322
+ <span class="arm arm-{next.arm ?? 'none'}">{next.label}</span>
323
+ {/if}
324
+ <button type="button" class="next-link" onclick={() => void goTo(next.to)}>
325
+ {next.loop ? '↩ back to ' : ''}{labelOf(next.to)}
326
+ </button>
327
+ </li>
328
+ {/each}
329
+ {#each selectedNode.ends as end (end.key)}
330
+ <li>
331
+ {#if end.armLabel !== null}
332
+ <span class="arm arm-{end.arm ?? 'none'}">{end.armLabel}</span>
333
+ {/if}
334
+ <span class="end-text">Ends: {end.label} (behind the scenes)</span>
335
+ </li>
336
+ {/each}
337
+ </ul>
338
+ </div>
339
+ {:else}
340
+ <p class="text">The journey ends here.</p>
341
+ {/if}
342
+
343
+ {#if selectedNode.rule !== null}
344
+ <!-- The sentence is lossy by design; the verbatim rule is what makes that safe. -->
345
+ <pre class="rule">{selectedNode.rule}</pre>
346
+ {/if}
347
+
348
+ {#if canJump(selectedNode)}
349
+ <button type="button" class="action" onclick={() => goBack(selectedNode)}>
350
+ Go back to this screen
351
+ </button>
352
+ {/if}
353
+ {/if}
354
+ </aside>
355
+ </div>
356
+
357
+ <p class="legend">
358
+ <span class="key-swatch kind-start"></span> Start
359
+ <span class="key-swatch kind-screen"></span> Screen
360
+ <span class="key-swatch kind-decision"></span> Decision
361
+ <span class="key-swatch kind-journey"></span> Opens another journey
362
+ <span class="key-swatch seen"></span> Seen
363
+ <span class="key-swatch current"></span> You are here
364
+ <span
365
+ >· each colour is one branch · a decision's Yes runs straight on · ↩ loops back · ⇥ hands off</span
366
+ >
367
+ </p>
368
+ </div>
369
+
370
+ <style>
371
+ .timeline {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: var(--space-2);
375
+ }
376
+
377
+ .body {
378
+ display: grid;
379
+ grid-template-columns: minmax(0, 1fr) 320px;
380
+ gap: var(--space-3);
381
+ }
382
+
383
+ @media (max-width: 760px) {
384
+ .body {
385
+ grid-template-columns: minmax(0, 1fr);
386
+ }
387
+ }
388
+
389
+ /* The same height as the map's viewport, so switching views never moves the page. */
390
+ .scroller,
391
+ .details {
392
+ box-sizing: border-box;
393
+ height: max(420px, calc(90vh - 223px));
394
+ border: 1px solid var(--color-gray-200);
395
+ border-radius: var(--radius-lg);
396
+ background-color: var(--color-white);
397
+ overflow-y: auto;
398
+ scrollbar-width: thin;
399
+ }
400
+
401
+ .canvas {
402
+ position: relative;
403
+ height: var(--height);
404
+ min-width: 100%;
405
+ }
406
+
407
+ .graph {
408
+ position: absolute;
409
+ top: 0;
410
+ left: 0;
411
+ pointer-events: none;
412
+ overflow: visible;
413
+ }
414
+
415
+ .row {
416
+ position: absolute;
417
+ top: var(--top);
418
+ left: 0;
419
+ right: 0;
420
+ height: var(--row);
421
+ display: flex;
422
+ align-items: center;
423
+ gap: var(--space-2);
424
+ padding: 0 var(--space-3) 0 calc(var(--graph) + var(--space-1));
425
+ font-family: inherit;
426
+ text-align: left;
427
+ background: none;
428
+ border: none;
429
+ cursor: pointer;
430
+ }
431
+
432
+ .row:hover {
433
+ background-color: var(--color-gray-50);
434
+ }
435
+
436
+ .row.selected {
437
+ background-color: #eff6ff;
438
+ }
439
+
440
+ .row:focus-visible {
441
+ outline: var(--ring-width) solid var(--ring-color);
442
+ outline-offset: -2px;
443
+ }
444
+
445
+ .label {
446
+ min-width: 0;
447
+ overflow: hidden;
448
+ text-overflow: ellipsis;
449
+ white-space: nowrap;
450
+ font-size: var(--text-sm);
451
+ color: var(--color-gray-900);
452
+ }
453
+
454
+ .kind-decision .label {
455
+ color: var(--color-gray-700);
456
+ }
457
+
458
+ .kind-start .label {
459
+ font-weight: var(--font-semibold);
460
+ }
461
+
462
+ .if {
463
+ font-weight: var(--font-semibold);
464
+ color: #b45309;
465
+ }
466
+
467
+ .state-visited .label,
468
+ .state-current .label {
469
+ color: var(--color-blue-700);
470
+ }
471
+
472
+ .here {
473
+ flex-shrink: 0;
474
+ padding: 1px var(--space-1-5);
475
+ font-size: 10px;
476
+ font-weight: var(--font-medium);
477
+ color: var(--color-blue-700);
478
+ background-color: #dbeafe;
479
+ border-radius: var(--radius-full);
480
+ }
481
+
482
+ .end {
483
+ flex-shrink: 0;
484
+ max-width: 40%;
485
+ overflow: hidden;
486
+ text-overflow: ellipsis;
487
+ white-space: nowrap;
488
+ padding: 1px var(--space-1-5);
489
+ font-size: 10px;
490
+ color: var(--color-gray-600);
491
+ background-color: var(--color-gray-100);
492
+ border-radius: var(--radius-full);
493
+ }
494
+
495
+ .end-text {
496
+ color: var(--color-gray-600);
497
+ }
498
+
499
+ .loop {
500
+ flex-shrink: 0;
501
+ max-width: 40%;
502
+ overflow: hidden;
503
+ text-overflow: ellipsis;
504
+ white-space: nowrap;
505
+ padding: 1px var(--space-1-5);
506
+ font-size: 10px;
507
+ color: #0369a1;
508
+ background-color: #e0f2fe;
509
+ border-radius: var(--radius-full);
510
+ }
511
+
512
+ /* The stroke is the branch's colour, set on the element; only the weight is styled here. */
513
+ .edge {
514
+ fill: none;
515
+ stroke-width: 2;
516
+ opacity: 0.85;
517
+ }
518
+
519
+ /* The visitor's route: the same branch colours, drawn heavier and at full strength. */
520
+ .edge.travelled {
521
+ stroke-width: 4;
522
+ opacity: 1;
523
+ }
524
+
525
+ .edge.active {
526
+ stroke-width: 3.5;
527
+ filter: drop-shadow(0 0 2px rgb(37 99 235 / 45%));
528
+ }
529
+
530
+ .halo {
531
+ fill: #bfdbfe;
532
+ }
533
+
534
+ .details {
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: var(--space-2);
538
+ padding: var(--space-3);
539
+ }
540
+
541
+ .hint,
542
+ .text {
543
+ margin: 0;
544
+ font-size: var(--text-xs);
545
+ color: var(--color-gray-500);
546
+ }
547
+
548
+ .head {
549
+ display: flex;
550
+ align-items: center;
551
+ gap: var(--space-1-5);
552
+ }
553
+
554
+ .kind {
555
+ flex: 1;
556
+ font-size: var(--text-xs);
557
+ font-weight: var(--font-medium);
558
+ color: var(--color-gray-500);
559
+ text-transform: uppercase;
560
+ letter-spacing: 0.04em;
561
+ }
562
+
563
+ .close {
564
+ width: 24px;
565
+ height: 24px;
566
+ padding: 0;
567
+ font-family: inherit;
568
+ font-size: var(--text-base);
569
+ line-height: 1;
570
+ color: var(--color-gray-500);
571
+ background: none;
572
+ border: none;
573
+ border-radius: var(--radius-md);
574
+ cursor: pointer;
575
+ }
576
+
577
+ .close:hover {
578
+ background-color: var(--color-gray-100);
579
+ color: var(--color-gray-900);
580
+ }
581
+
582
+ .close:focus-visible,
583
+ .action:focus-visible,
584
+ .next-link:focus-visible {
585
+ outline: var(--ring-width) solid var(--ring-color);
586
+ outline-offset: var(--ring-offset);
587
+ }
588
+
589
+ .title {
590
+ margin: 0;
591
+ font-size: var(--text-sm);
592
+ font-weight: var(--font-semibold);
593
+ color: var(--color-gray-900);
594
+ overflow-wrap: anywhere;
595
+ }
596
+
597
+ .state {
598
+ margin: 0;
599
+ font-size: var(--text-xs);
600
+ font-weight: var(--font-medium);
601
+ color: var(--color-blue-600);
602
+ }
603
+
604
+ .key,
605
+ .rule {
606
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
607
+ font-size: 10px;
608
+ color: var(--color-gray-500);
609
+ }
610
+
611
+ .key {
612
+ align-self: flex-start;
613
+ padding: 1px var(--space-1);
614
+ background-color: var(--color-gray-100);
615
+ border-radius: var(--radius-sm);
616
+ overflow-wrap: anywhere;
617
+ }
618
+
619
+ .next {
620
+ display: flex;
621
+ flex-direction: column;
622
+ gap: var(--space-1);
623
+ }
624
+
625
+ .next-title {
626
+ font-size: var(--text-xs);
627
+ font-weight: var(--font-medium);
628
+ color: var(--color-gray-500);
629
+ }
630
+
631
+ .next ul {
632
+ display: flex;
633
+ flex-direction: column;
634
+ gap: var(--space-1);
635
+ margin: 0;
636
+ padding: 0;
637
+ list-style: none;
638
+ }
639
+
640
+ .next li {
641
+ display: flex;
642
+ align-items: baseline;
643
+ gap: var(--space-1-5);
644
+ font-size: var(--text-xs);
645
+ }
646
+
647
+ .arm {
648
+ flex-shrink: 0;
649
+ font-weight: 700;
650
+ }
651
+
652
+ .arm.arm-yes {
653
+ color: #10b981;
654
+ }
655
+
656
+ .arm.arm-no {
657
+ color: var(--color-red-500);
658
+ }
659
+
660
+ .next-link {
661
+ padding: 0;
662
+ font-family: inherit;
663
+ font-size: var(--text-xs);
664
+ text-align: left;
665
+ color: var(--color-blue-600);
666
+ background: none;
667
+ border: none;
668
+ cursor: pointer;
669
+ overflow-wrap: anywhere;
670
+ }
671
+
672
+ .next-link:hover {
673
+ text-decoration: underline;
674
+ }
675
+
676
+ .rule {
677
+ max-height: 180px;
678
+ margin: 0;
679
+ padding: var(--space-2);
680
+ overflow: auto;
681
+ background-color: var(--color-gray-50);
682
+ border: 1px solid var(--color-gray-200);
683
+ border-radius: var(--radius-md);
684
+ white-space: pre-wrap;
685
+ }
686
+
687
+ .action {
688
+ align-self: flex-start;
689
+ padding: var(--space-1) var(--space-3);
690
+ font-family: inherit;
691
+ font-size: var(--text-xs);
692
+ font-weight: var(--font-medium);
693
+ color: var(--color-white);
694
+ background-color: var(--color-blue-600);
695
+ border: none;
696
+ border-radius: var(--radius-md);
697
+ cursor: pointer;
698
+ }
699
+
700
+ .action:hover {
701
+ background-color: var(--color-blue-700);
702
+ }
703
+
704
+ .legend {
705
+ display: flex;
706
+ align-items: center;
707
+ flex-wrap: wrap;
708
+ gap: var(--space-1-5);
709
+ margin: 0;
710
+ font-size: var(--text-xs);
711
+ color: var(--color-gray-500);
712
+ }
713
+
714
+ /* Shapes only — the colours belong to branches, so the key is in a neutral grey. */
715
+ .key-swatch {
716
+ width: 10px;
717
+ height: 10px;
718
+ margin-left: var(--space-1);
719
+ border-radius: var(--radius-full);
720
+ background-color: #64748b;
721
+ }
722
+
723
+ .key-swatch.kind-start {
724
+ width: 13px;
725
+ height: 13px;
726
+ }
727
+
728
+ .key-swatch.kind-decision {
729
+ width: 9px;
730
+ height: 9px;
731
+ border-radius: 0;
732
+ transform: rotate(45deg);
733
+ }
734
+
735
+ .key-swatch.kind-journey {
736
+ border-radius: 2px;
737
+ }
738
+
739
+ .key-swatch.seen {
740
+ background-color: var(--color-white);
741
+ box-shadow: 0 0 0 2px #1e3a8a;
742
+ }
743
+
744
+ .key-swatch.current {
745
+ background-color: var(--color-white);
746
+ box-shadow:
747
+ 0 0 0 2px #1e3a8a,
748
+ 0 0 0 5px #bfdbfe;
749
+ }
750
+ </style>