@picked/svelte 0.0.1-dev.1789541668638 → 0.0.1-dev.1793756239746
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.
- package/LICENSE +1 -0
- package/README.md +51 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/_virtual/_vite/preload-helper.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/experience/ExperienceHost.svelte +29 -13
- package/dist/experience/ExperienceSlot.svelte +21 -3
- package/dist/experience/ExperienceWrapper.svelte +23 -0
- package/dist/form/index.js +1 -1
- package/dist/hooks/action.d.ts +47 -18
- package/dist/hooks/action.js +1 -1
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/step.d.ts +14 -0
- package/dist/hooks/step.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +545 -203
- package/dist/plugins/devtools/components/AdaptedToolHost.svelte +2 -2
- package/dist/plugins/devtools/constants.d.ts +5 -0
- package/dist/plugins/devtools/constants.js +1 -0
- package/dist/plugins/devtools/customTools.d.ts +42 -6
- package/dist/plugins/devtools/customTools.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +112 -0
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -0
- package/dist/plugins/devtools/derive/answer-display/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/answer-display/index.js +1 -0
- package/dist/plugins/devtools/derive/answer-history/answer-history.d.ts +76 -0
- package/dist/plugins/devtools/derive/answer-history/answer-history.js +1 -0
- package/dist/plugins/devtools/derive/answer-history/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/answer-history/index.js +1 -0
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.d.ts +67 -0
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -0
- package/dist/plugins/devtools/derive/guide-sections/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/guide-sections/index.js +1 -0
- package/dist/plugins/devtools/derive/index.d.ts +28 -0
- package/dist/plugins/devtools/derive/index.js +1 -0
- package/dist/plugins/devtools/derive/journey-outline/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/journey-outline/index.js +1 -0
- package/dist/plugins/devtools/derive/journey-outline/journey-outline.d.ts +86 -0
- package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -0
- package/dist/plugins/devtools/derive/journey-pick/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/journey-pick/index.js +1 -0
- package/dist/plugins/devtools/derive/journey-pick/journey-pick.d.ts +73 -0
- package/dist/plugins/devtools/derive/journey-pick/journey-pick.js +1 -0
- package/dist/plugins/devtools/derive/journey-timeline/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/journey-timeline/index.js +1 -0
- package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.d.ts +70 -0
- package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.js +1 -0
- package/dist/plugins/devtools/derive/journey-tree/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/journey-tree/index.js +1 -0
- package/dist/plugins/devtools/derive/journey-tree/journey-tree.d.ts +191 -0
- package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -0
- package/dist/plugins/devtools/derive/plain-language/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/plain-language/index.js +1 -0
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +73 -0
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -0
- package/dist/plugins/devtools/derive/plain-logic/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/plain-logic/index.js +1 -0
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +48 -0
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -0
- package/dist/plugins/devtools/derive/safety-posture/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/safety-posture/index.js +1 -0
- package/dist/plugins/devtools/derive/safety-posture/safety-posture.d.ts +43 -0
- package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -0
- package/dist/plugins/devtools/derive/visited-steps/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/visited-steps/index.js +1 -0
- package/dist/plugins/devtools/derive/visited-steps/visited-steps.d.ts +35 -0
- package/dist/plugins/devtools/derive/visited-steps/visited-steps.js +1 -0
- package/dist/plugins/devtools/derive/walk-route/index.d.ts +3 -0
- package/dist/plugins/devtools/derive/walk-route/index.js +1 -0
- package/dist/plugins/devtools/derive/walk-route/walk-route.d.ts +22 -0
- package/dist/plugins/devtools/derive/walk-route/walk-route.js +1 -0
- package/dist/plugins/devtools/index.d.ts +5 -2
- package/dist/plugins/devtools/index.js +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +150 -0
- package/dist/plugins/devtools/launcher/launcher.d.ts +36 -0
- package/dist/plugins/devtools/launcher/launcher.js +1 -0
- package/dist/plugins/devtools/options.d.ts +106 -0
- package/dist/plugins/devtools/options.js +1 -0
- package/dist/plugins/devtools/plugin.d.ts +8 -8
- package/dist/plugins/devtools/plugin.js +1 -1
- package/dist/plugins/devtools/posture-context.d.ts +15 -0
- package/dist/plugins/devtools/posture-context.js +1 -0
- package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +526 -0
- package/dist/plugins/devtools/review/guide/GuideLine.svelte +20 -0
- package/dist/plugins/devtools/review/guide/GuidePanel.svelte +133 -0
- package/dist/plugins/devtools/review/index.d.ts +6 -0
- package/dist/plugins/devtools/review/index.js +1 -0
- package/dist/plugins/devtools/review/journey/JourneyMap.svelte +1078 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +608 -0
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +756 -0
- package/dist/plugins/devtools/review/journey/TimelineMarker.svelte +59 -0
- package/dist/plugins/devtools/review/session/SessionPanel.svelte +214 -0
- package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +279 -0
- package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +6 -0
- package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +3 -3
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +6 -0
- package/dist/plugins/devtools/session/index.d.ts +3 -0
- package/dist/plugins/devtools/session/index.js +1 -0
- package/dist/plugins/devtools/session/session.d.ts +90 -0
- package/dist/plugins/devtools/session/session.js +1 -0
- package/dist/plugins/devtools/session-access.d.ts +32 -0
- package/dist/plugins/devtools/session-access.js +1 -0
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +105 -0
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +117 -0
- package/dist/plugins/devtools/sidebar/nav.d.ts +105 -0
- package/dist/plugins/devtools/sidebar/nav.js +1 -0
- package/dist/plugins/devtools/ui/actions/Button.svelte +10 -1
- package/dist/plugins/devtools/ui/actions/CopyButton.svelte +13 -1
- package/dist/plugins/devtools/ui/data/Badge.svelte +3 -2
- package/dist/plugins/devtools/ui/data/EmptyState.svelte +16 -1
- package/dist/plugins/devtools/ui/feedback/Banner.svelte +3 -2
- package/dist/plugins/framework/index.js +1 -1
- package/dist/render/browser.js +1 -1
- package/package.json +23 -12
- package/dist/form/__tests__/CreateFormHarness.svelte +0 -30
- package/dist/hooks/events.d.ts +0 -2
- package/dist/hooks/events.js +0 -1
- package/dist/vite-env.d.js +0 -0
|
@@ -0,0 +1,756 @@
|
|
|
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
|
+
flex: 1;
|
|
373
|
+
min-height: 0;
|
|
374
|
+
display: flex;
|
|
375
|
+
flex-direction: column;
|
|
376
|
+
gap: var(--space-2);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Takes the height the legend leaves in the frame, as the map's viewport does. */
|
|
380
|
+
.body {
|
|
381
|
+
flex: 1;
|
|
382
|
+
min-height: 0;
|
|
383
|
+
display: grid;
|
|
384
|
+
grid-template-columns: minmax(0, 1fr) 320px;
|
|
385
|
+
grid-template-rows: minmax(0, 1fr);
|
|
386
|
+
gap: var(--space-3);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@media (max-width: 760px) {
|
|
390
|
+
.body {
|
|
391
|
+
grid-template-columns: minmax(0, 1fr);
|
|
392
|
+
grid-template-rows: minmax(0, 2fr) minmax(0, 1fr);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.scroller,
|
|
397
|
+
.details {
|
|
398
|
+
box-sizing: border-box;
|
|
399
|
+
min-height: 0;
|
|
400
|
+
border: 1px solid var(--color-gray-200);
|
|
401
|
+
border-radius: var(--radius-lg);
|
|
402
|
+
background-color: var(--color-white);
|
|
403
|
+
overflow-y: auto;
|
|
404
|
+
scrollbar-width: thin;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.canvas {
|
|
408
|
+
position: relative;
|
|
409
|
+
height: var(--height);
|
|
410
|
+
min-width: 100%;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.graph {
|
|
414
|
+
position: absolute;
|
|
415
|
+
top: 0;
|
|
416
|
+
left: 0;
|
|
417
|
+
pointer-events: none;
|
|
418
|
+
overflow: visible;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.row {
|
|
422
|
+
position: absolute;
|
|
423
|
+
top: var(--top);
|
|
424
|
+
left: 0;
|
|
425
|
+
right: 0;
|
|
426
|
+
height: var(--row);
|
|
427
|
+
display: flex;
|
|
428
|
+
align-items: center;
|
|
429
|
+
gap: var(--space-2);
|
|
430
|
+
padding: 0 var(--space-3) 0 calc(var(--graph) + var(--space-1));
|
|
431
|
+
font-family: inherit;
|
|
432
|
+
text-align: left;
|
|
433
|
+
background: none;
|
|
434
|
+
border: none;
|
|
435
|
+
cursor: pointer;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.row:hover {
|
|
439
|
+
background-color: var(--color-gray-50);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.row.selected {
|
|
443
|
+
background-color: #eff6ff;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.row:focus-visible {
|
|
447
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
448
|
+
outline-offset: -2px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.label {
|
|
452
|
+
min-width: 0;
|
|
453
|
+
overflow: hidden;
|
|
454
|
+
text-overflow: ellipsis;
|
|
455
|
+
white-space: nowrap;
|
|
456
|
+
font-size: var(--text-sm);
|
|
457
|
+
color: var(--color-gray-900);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.kind-decision .label {
|
|
461
|
+
color: var(--color-gray-700);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.kind-start .label {
|
|
465
|
+
font-weight: var(--font-semibold);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.if {
|
|
469
|
+
font-weight: var(--font-semibold);
|
|
470
|
+
color: #b45309;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.state-visited .label,
|
|
474
|
+
.state-current .label {
|
|
475
|
+
color: var(--color-blue-700);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.here {
|
|
479
|
+
flex-shrink: 0;
|
|
480
|
+
padding: 1px var(--space-1-5);
|
|
481
|
+
font-size: 10px;
|
|
482
|
+
font-weight: var(--font-medium);
|
|
483
|
+
color: var(--color-blue-700);
|
|
484
|
+
background-color: #dbeafe;
|
|
485
|
+
border-radius: var(--radius-full);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.end {
|
|
489
|
+
flex-shrink: 0;
|
|
490
|
+
max-width: 40%;
|
|
491
|
+
overflow: hidden;
|
|
492
|
+
text-overflow: ellipsis;
|
|
493
|
+
white-space: nowrap;
|
|
494
|
+
padding: 1px var(--space-1-5);
|
|
495
|
+
font-size: 10px;
|
|
496
|
+
color: var(--color-gray-600);
|
|
497
|
+
background-color: var(--color-gray-100);
|
|
498
|
+
border-radius: var(--radius-full);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.end-text {
|
|
502
|
+
color: var(--color-gray-600);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.loop {
|
|
506
|
+
flex-shrink: 0;
|
|
507
|
+
max-width: 40%;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
text-overflow: ellipsis;
|
|
510
|
+
white-space: nowrap;
|
|
511
|
+
padding: 1px var(--space-1-5);
|
|
512
|
+
font-size: 10px;
|
|
513
|
+
color: #0369a1;
|
|
514
|
+
background-color: #e0f2fe;
|
|
515
|
+
border-radius: var(--radius-full);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* The stroke is the branch's colour, set on the element; only the weight is styled here. */
|
|
519
|
+
.edge {
|
|
520
|
+
fill: none;
|
|
521
|
+
stroke-width: 2;
|
|
522
|
+
opacity: 0.85;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* The visitor's route: the same branch colours, drawn heavier and at full strength. */
|
|
526
|
+
.edge.travelled {
|
|
527
|
+
stroke-width: 4;
|
|
528
|
+
opacity: 1;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.edge.active {
|
|
532
|
+
stroke-width: 3.5;
|
|
533
|
+
filter: drop-shadow(0 0 2px rgb(37 99 235 / 45%));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.halo {
|
|
537
|
+
fill: #bfdbfe;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.details {
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
gap: var(--space-2);
|
|
544
|
+
padding: var(--space-3);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.hint,
|
|
548
|
+
.text {
|
|
549
|
+
margin: 0;
|
|
550
|
+
font-size: var(--text-xs);
|
|
551
|
+
color: var(--color-gray-500);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.head {
|
|
555
|
+
display: flex;
|
|
556
|
+
align-items: center;
|
|
557
|
+
gap: var(--space-1-5);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.kind {
|
|
561
|
+
flex: 1;
|
|
562
|
+
font-size: var(--text-xs);
|
|
563
|
+
font-weight: var(--font-medium);
|
|
564
|
+
color: var(--color-gray-500);
|
|
565
|
+
text-transform: uppercase;
|
|
566
|
+
letter-spacing: 0.04em;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.close {
|
|
570
|
+
width: 24px;
|
|
571
|
+
height: 24px;
|
|
572
|
+
padding: 0;
|
|
573
|
+
font-family: inherit;
|
|
574
|
+
font-size: var(--text-base);
|
|
575
|
+
line-height: 1;
|
|
576
|
+
color: var(--color-gray-500);
|
|
577
|
+
background: none;
|
|
578
|
+
border: none;
|
|
579
|
+
border-radius: var(--radius-md);
|
|
580
|
+
cursor: pointer;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.close:hover {
|
|
584
|
+
background-color: var(--color-gray-100);
|
|
585
|
+
color: var(--color-gray-900);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.close:focus-visible,
|
|
589
|
+
.action:focus-visible,
|
|
590
|
+
.next-link:focus-visible {
|
|
591
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
592
|
+
outline-offset: var(--ring-offset);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.title {
|
|
596
|
+
margin: 0;
|
|
597
|
+
font-size: var(--text-sm);
|
|
598
|
+
font-weight: var(--font-semibold);
|
|
599
|
+
color: var(--color-gray-900);
|
|
600
|
+
overflow-wrap: anywhere;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.state {
|
|
604
|
+
margin: 0;
|
|
605
|
+
font-size: var(--text-xs);
|
|
606
|
+
font-weight: var(--font-medium);
|
|
607
|
+
color: var(--color-blue-600);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.key,
|
|
611
|
+
.rule {
|
|
612
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
613
|
+
font-size: 10px;
|
|
614
|
+
color: var(--color-gray-500);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.key {
|
|
618
|
+
align-self: flex-start;
|
|
619
|
+
padding: 1px var(--space-1);
|
|
620
|
+
background-color: var(--color-gray-100);
|
|
621
|
+
border-radius: var(--radius-sm);
|
|
622
|
+
overflow-wrap: anywhere;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.next {
|
|
626
|
+
display: flex;
|
|
627
|
+
flex-direction: column;
|
|
628
|
+
gap: var(--space-1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.next-title {
|
|
632
|
+
font-size: var(--text-xs);
|
|
633
|
+
font-weight: var(--font-medium);
|
|
634
|
+
color: var(--color-gray-500);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.next ul {
|
|
638
|
+
display: flex;
|
|
639
|
+
flex-direction: column;
|
|
640
|
+
gap: var(--space-1);
|
|
641
|
+
margin: 0;
|
|
642
|
+
padding: 0;
|
|
643
|
+
list-style: none;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.next li {
|
|
647
|
+
display: flex;
|
|
648
|
+
align-items: baseline;
|
|
649
|
+
gap: var(--space-1-5);
|
|
650
|
+
font-size: var(--text-xs);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.arm {
|
|
654
|
+
flex-shrink: 0;
|
|
655
|
+
font-weight: 700;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.arm.arm-yes {
|
|
659
|
+
color: #10b981;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.arm.arm-no {
|
|
663
|
+
color: var(--color-red-500);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.next-link {
|
|
667
|
+
padding: 0;
|
|
668
|
+
font-family: inherit;
|
|
669
|
+
font-size: var(--text-xs);
|
|
670
|
+
text-align: left;
|
|
671
|
+
color: var(--color-blue-600);
|
|
672
|
+
background: none;
|
|
673
|
+
border: none;
|
|
674
|
+
cursor: pointer;
|
|
675
|
+
overflow-wrap: anywhere;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.next-link:hover {
|
|
679
|
+
text-decoration: underline;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.rule {
|
|
683
|
+
max-height: 180px;
|
|
684
|
+
margin: 0;
|
|
685
|
+
padding: var(--space-2);
|
|
686
|
+
overflow: auto;
|
|
687
|
+
background-color: var(--color-gray-50);
|
|
688
|
+
border: 1px solid var(--color-gray-200);
|
|
689
|
+
border-radius: var(--radius-md);
|
|
690
|
+
white-space: pre-wrap;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.action {
|
|
694
|
+
align-self: flex-start;
|
|
695
|
+
padding: var(--space-1) var(--space-3);
|
|
696
|
+
font-family: inherit;
|
|
697
|
+
font-size: var(--text-xs);
|
|
698
|
+
font-weight: var(--font-medium);
|
|
699
|
+
color: var(--color-white);
|
|
700
|
+
background-color: var(--color-blue-600);
|
|
701
|
+
border: none;
|
|
702
|
+
border-radius: var(--radius-md);
|
|
703
|
+
cursor: pointer;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.action:hover {
|
|
707
|
+
background-color: var(--color-blue-700);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.legend {
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
flex-wrap: wrap;
|
|
714
|
+
gap: var(--space-1-5);
|
|
715
|
+
margin: 0;
|
|
716
|
+
font-size: var(--text-xs);
|
|
717
|
+
color: var(--color-gray-500);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* Shapes only — the colours belong to branches, so the key is in a neutral grey. */
|
|
721
|
+
.key-swatch {
|
|
722
|
+
width: 10px;
|
|
723
|
+
height: 10px;
|
|
724
|
+
margin-left: var(--space-1);
|
|
725
|
+
border-radius: var(--radius-full);
|
|
726
|
+
background-color: #64748b;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.key-swatch.kind-start {
|
|
730
|
+
width: 13px;
|
|
731
|
+
height: 13px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.key-swatch.kind-decision {
|
|
735
|
+
width: 9px;
|
|
736
|
+
height: 9px;
|
|
737
|
+
border-radius: 0;
|
|
738
|
+
transform: rotate(45deg);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.key-swatch.kind-journey {
|
|
742
|
+
border-radius: 2px;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.key-swatch.seen {
|
|
746
|
+
background-color: var(--color-white);
|
|
747
|
+
box-shadow: 0 0 0 2px #1e3a8a;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.key-swatch.current {
|
|
751
|
+
background-color: var(--color-white);
|
|
752
|
+
box-shadow:
|
|
753
|
+
0 0 0 2px #1e3a8a,
|
|
754
|
+
0 0 0 5px #bfdbfe;
|
|
755
|
+
}
|
|
756
|
+
</style>
|