@picked/svelte 0.0.1-dev.1788925875333 → 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,526 @@
1
+ <script lang="ts">
2
+ /**
3
+ * @file The Answers tab: every question a journey can ask, and the answer given — the Developer
4
+ * group's Responses screen in a reviewer's vocabulary.
5
+ *
6
+ * Four differences from that screen, all deliberate:
7
+ * 1. Rows are titled by the QUESTION the visitor saw (`field.label`), not `field.key`.
8
+ * 2. An object answer (an address, a chosen plan) is read-only with a Copy button. A reviewer
9
+ * hand-editing a resolved object is how a record ends up pointing at the wrong thing.
10
+ * 3. An edit reports the SDK's own validation message on the row that caused it, and keeps the
11
+ * draft so the reviewer can correct it.
12
+ * 4. Only fields a screen asks are listed (`asked`). What code commits behind the scenes is not
13
+ * an answer anyone gave.
14
+ */
15
+ import type { Field, FieldValue, Picked } from '@picked/client'
16
+ import { Badge, Banner, Button, Card, CopyButton, EmptyState, Stack, Text } from '../../ui'
17
+ import { describeAnswer, systemPayload } from '../../derive'
18
+ import type { AnswerView, SafetyPosture } from '../../derive'
19
+
20
+ interface AnswersPanelProps {
21
+ client: Picked
22
+ posture: SafetyPosture
23
+ /** Every field a journey's screens ask — the rest is committed by code, not answered. */
24
+ asked: ReadonlySet<string>
25
+ /** A button offered after an edit, such as re-running a fetch the answers feed. */
26
+ followUp: { label: string; run: () => void } | null
27
+ }
28
+
29
+ const { client, posture, asked, followUp }: AnswersPanelProps = $props()
30
+
31
+ type Filter = 'answered' | 'unanswered' | 'all'
32
+
33
+ interface AnswerRow {
34
+ field: Field
35
+ view: AnswerView
36
+ value: unknown
37
+ }
38
+
39
+ // SDK stores read straight off the prop: effector stores meet Svelte's store contract, so `$`
40
+ // subscribes, and `$derived` keeps it on whichever client the panel is given.
41
+ const fields = $derived(client.fields.listStore)
42
+ const responsesStore = $derived(client.responses.getAllStore)
43
+ // `unknown`, not `FieldValue`: the store's index signature types an ABSENT key as a PRESENT value,
44
+ // so a `=== undefined` guard would read as dead code while being the case that actually happens.
45
+ const responses: Readonly<Record<string, unknown>> = $derived($responsesStore)
46
+
47
+ let filter = $state<Filter>('answered')
48
+ let editingKey = $state<string | null>(null)
49
+ let draft = $state('')
50
+ let draftMultiple = $state<string[]>([])
51
+ let rowError = $state<string | null>(null)
52
+ let confirmingReset = $state(false)
53
+ let edited = $state(false)
54
+
55
+ /** Why a write is withheld, as the withheld control's tooltip. */
56
+ const withheld = $derived(posture.canEditAnswers ? undefined : (posture.reason ?? undefined))
57
+
58
+ const rows = $derived<AnswerRow[]>(
59
+ $fields
60
+ .filter((field) => asked.has(field.key))
61
+ .map((field) => ({
62
+ field,
63
+ value: responses[field.key],
64
+ view: describeAnswer(responses[field.key], field),
65
+ }))
66
+ .sort((a, b) => a.view.title.localeCompare(b.view.title)),
67
+ )
68
+
69
+ const answeredCount = $derived(rows.filter((row) => row.view.answered).length)
70
+ const visibleRows = $derived(
71
+ rows.filter((row) => {
72
+ if (filter === 'answered') return row.view.answered
73
+ if (filter === 'unanswered') return !row.view.answered
74
+ return true
75
+ }),
76
+ )
77
+
78
+ const filters = $derived<{ value: Filter; label: string }[]>([
79
+ { value: 'answered', label: `Answered (${answeredCount})` },
80
+ { value: 'unanswered', label: `Not answered (${rows.length - answeredCount})` },
81
+ { value: 'all', label: 'All' },
82
+ ])
83
+
84
+ /**
85
+ * Opens a row's editor, seeded with the answer it holds now.
86
+ *
87
+ * @param {AnswerRow} row - The row to edit.
88
+ * @example
89
+ * startEdit(row) // the row shows its options, or a text box, with Save and Cancel
90
+ */
91
+ function startEdit(row: AnswerRow): void {
92
+ editingKey = row.field.key
93
+ rowError = null
94
+ const current = row.value
95
+
96
+ if (row.field.multiple) {
97
+ draftMultiple = Array.isArray(current) ? current.map((entry) => String(entry)) : []
98
+ return
99
+ }
100
+
101
+ draft = current === null || current === undefined ? '' : String(current)
102
+ }
103
+
104
+ /**
105
+ * Closes the open editor and drops its draft.
106
+ *
107
+ * @example
108
+ * cancelEdit()
109
+ */
110
+ function cancelEdit(): void {
111
+ editingKey = null
112
+ draft = ''
113
+ draftMultiple = []
114
+ rowError = null
115
+ }
116
+
117
+ /**
118
+ * The value to commit, in the field's own type — the panel never asks a reviewer to type JSON.
119
+ *
120
+ * The mixed return type IS the contract: a field's committed value is whatever that field
121
+ * declares, and collapsing it to one type here would commit a string where the workspace declared
122
+ * a number. A number that does not parse to a finite value is committed as typed, so the field's
123
+ * own validation names the problem.
124
+ *
125
+ * @param {Field} field - The field being edited.
126
+ * @returns {FieldValue} The draft as the value to commit.
127
+ * @example
128
+ * draftValue(ageField) // 42, when the draft reads '42'
129
+ */
130
+ function draftValue(field: Field): FieldValue {
131
+ if (field.multiple) return [...draftMultiple]
132
+ if (draft === '') return null
133
+ if (field.type === 'number') {
134
+ const parsed = Number(draft)
135
+ return Number.isFinite(parsed) ? parsed : draft
136
+ }
137
+
138
+ if (field.type === 'boolean') return draft === 'true'
139
+ return draft
140
+ }
141
+
142
+ /**
143
+ * Commits the draft. A rejection shows the SDK's first validation message on the row and keeps
144
+ * the draft; a success closes the editor.
145
+ *
146
+ * @param {Field} field - The field being edited.
147
+ * @returns {Promise<void>} Settles once the commit has answered.
148
+ * @example
149
+ * await save(postcodeField)
150
+ */
151
+ async function save(field: Field): Promise<void> {
152
+ const result = await client.responses.commit({ [field.key]: draftValue(field) })
153
+ const message = result.errors.at(0)?.userMessage
154
+ if (message !== undefined) {
155
+ rowError = message
156
+ return
157
+ }
158
+
159
+ edited = true
160
+ cancelEdit()
161
+ }
162
+
163
+ /**
164
+ * Removes one answer.
165
+ *
166
+ * @param {Field} field - The field whose answer goes.
167
+ * @example
168
+ * clear(postcodeField)
169
+ */
170
+ function clear(field: Field): void {
171
+ client.responses.clear(field.key)
172
+ edited = true
173
+ if (editingKey === field.key) cancelEdit()
174
+ }
175
+
176
+ /**
177
+ * Removes every answer, once the reviewer has confirmed it.
178
+ *
179
+ * @example
180
+ * reset()
181
+ */
182
+ function reset(): void {
183
+ client.responses.clearAll()
184
+ confirmingReset = false
185
+ edited = false
186
+ cancelEdit()
187
+ }
188
+
189
+ /**
190
+ * Single-select: the clicked option becomes the whole draft.
191
+ *
192
+ * @param {string} value - The option's value, as text.
193
+ * @example
194
+ * selectOption('WA')
195
+ */
196
+ function selectOption(value: string): void {
197
+ draft = value
198
+ }
199
+
200
+ /**
201
+ * Multi-select: adds the option to the draft, or takes it out.
202
+ *
203
+ * @param {string} value - The option's value, as text.
204
+ * @example
205
+ * toggleOption('gas')
206
+ */
207
+ function toggleOption(value: string): void {
208
+ draftMultiple = draftMultiple.includes(value)
209
+ ? draftMultiple.filter((entry) => entry !== value)
210
+ : [...draftMultiple, value]
211
+ }
212
+
213
+ /**
214
+ * Runs the host's follow-up, which brings what the old answers produced up to date.
215
+ *
216
+ * @example
217
+ * runFollowUp()
218
+ */
219
+ function runFollowUp(): void {
220
+ followUp?.run()
221
+ edited = false
222
+ }
223
+ </script>
224
+
225
+ <Stack direction="vertical" gap="medium">
226
+ {#if posture.readOnly && posture.reason !== null}
227
+ <Banner status="warning" testId="devtools-answers-readonly">{posture.reason}</Banner>
228
+ {/if}
229
+
230
+ <div class="toolbar-row">
231
+ <div class="filters" role="group" aria-label="Show">
232
+ {#each filters as option (option.value)}
233
+ <button
234
+ type="button"
235
+ class="filter"
236
+ class:selected={filter === option.value}
237
+ aria-pressed={filter === option.value}
238
+ onclick={() => (filter = option.value)}
239
+ >
240
+ {option.label}
241
+ </button>
242
+ {/each}
243
+ </div>
244
+ <Button
245
+ variant="ghost"
246
+ size="small"
247
+ disabled={!posture.canEditAnswers || rows.length === 0}
248
+ title={withheld}
249
+ onclick={() => (confirmingReset = true)}
250
+ testId="devtools-answers-reset"
251
+ >
252
+ Start over
253
+ </Button>
254
+ </div>
255
+
256
+ {#if confirmingReset}
257
+ <Banner status="warning" title="Clear every answer?">
258
+ <Stack direction="vertical" gap="small">
259
+ <Text size="small">
260
+ This empties every answer in this session. Nothing about a real customer changes.
261
+ </Text>
262
+ <Stack direction="horizontal" gap="small">
263
+ <Button variant="destructive" size="small" onclick={reset}>Clear everything</Button>
264
+ <Button variant="secondary" size="small" onclick={() => (confirmingReset = false)}>
265
+ Keep my answers
266
+ </Button>
267
+ </Stack>
268
+ </Stack>
269
+ </Banner>
270
+ {/if}
271
+
272
+ {#if edited && followUp !== null}
273
+ <!-- An edited answer changes nothing already worked out from the old one until that runs
274
+ again. Saying so beats a reviewer reporting the stale screen as a bug. -->
275
+ <Banner status="info" testId="devtools-answers-follow-up">
276
+ <Stack direction="horizontal" gap="small" justify="center" align="space-between">
277
+ <Text size="small">What is on screen was worked out before this change.</Text>
278
+ <Button
279
+ variant="secondary"
280
+ size="small"
281
+ onclick={runFollowUp}
282
+ testId="devtools-answers-follow-up-run"
283
+ >
284
+ {followUp.label}
285
+ </Button>
286
+ </Stack>
287
+ </Banner>
288
+ {/if}
289
+
290
+ <Card sectioned>
291
+ {#if visibleRows.length === 0}
292
+ <EmptyState
293
+ message="Nothing to show here"
294
+ hint={filter === 'answered'
295
+ ? 'No questions have been answered yet. Start the journey on the page.'
296
+ : 'Every question this journey asks already has an answer.'}
297
+ />
298
+ {:else}
299
+ <ul class="rows" data-testid="devtools-answers">
300
+ {#each visibleRows as row (row.field.key)}
301
+ <li class="row" data-testid="devtools-answers-row" data-field-key={row.field.key}>
302
+ <div class="row-head">
303
+ <div class="row-title">
304
+ <Text size="medium" weight="medium">{row.view.title}</Text>
305
+ <!-- The title is the field's authored `label`, which is usually the question the
306
+ visitor read, but not always: a label can be its step's heading. So when the
307
+ short noun says something the title does not, it rides the second line ahead
308
+ of the key. -->
309
+ <span class="row-meta">
310
+ {#if row.view.noun !== row.view.title}
311
+ <span>{row.view.noun}</span>
312
+ {/if}
313
+ <span class="row-key">{row.field.key}</span>
314
+ </span>
315
+ </div>
316
+ {#if row.view.editable}
317
+ {#if editingKey === row.field.key}
318
+ <span class="row-actions">
319
+ <Button variant="primary" size="small" onclick={() => void save(row.field)}>
320
+ Save
321
+ </Button>
322
+ <Button variant="ghost" size="small" onclick={cancelEdit}>Cancel</Button>
323
+ </span>
324
+ {:else}
325
+ <span class="row-actions">
326
+ <Button
327
+ variant="secondary"
328
+ size="small"
329
+ disabled={!posture.canEditAnswers}
330
+ title={withheld}
331
+ onclick={() => startEdit(row)}
332
+ >
333
+ {row.view.answered ? 'Change' : 'Answer'}
334
+ </Button>
335
+ {#if row.view.answered}
336
+ <Button
337
+ variant="ghost"
338
+ size="small"
339
+ disabled={!posture.canEditAnswers}
340
+ title={withheld}
341
+ onclick={() => clear(row.field)}
342
+ >
343
+ Clear
344
+ </Button>
345
+ {/if}
346
+ </span>
347
+ {/if}
348
+ {:else}
349
+ <Badge status="default" size="small">Change it on its screen</Badge>
350
+ {/if}
351
+ </div>
352
+
353
+ {#if editingKey === row.field.key}
354
+ <div class="editor">
355
+ {#if row.view.kind === 'options' && row.field.options}
356
+ {#each row.field.options as option (option.id)}
357
+ <label class="option-label">
358
+ <input
359
+ type={row.field.multiple ? 'checkbox' : 'radio'}
360
+ name="answer-{row.field.key}"
361
+ value={String(option.value)}
362
+ checked={row.field.multiple
363
+ ? draftMultiple.includes(String(option.value))
364
+ : draft === String(option.value)}
365
+ onchange={() =>
366
+ row.field.multiple
367
+ ? toggleOption(String(option.value))
368
+ : selectOption(String(option.value))}
369
+ />
370
+ <Text size="small" inline>{option.label}</Text>
371
+ </label>
372
+ {/each}
373
+ {:else if row.view.kind === 'boolean'}
374
+ {#each [{ value: 'true', label: 'Yes' }, { value: 'false', label: 'No' }] as option (option.value)}
375
+ <label class="option-label">
376
+ <input
377
+ type="radio"
378
+ name="answer-{row.field.key}"
379
+ value={option.value}
380
+ checked={draft === option.value}
381
+ onchange={() => selectOption(option.value)}
382
+ />
383
+ <Text size="small" inline>{option.label}</Text>
384
+ </label>
385
+ {/each}
386
+ {:else}
387
+ <input
388
+ class="inline-input"
389
+ type={row.view.kind === 'number' ? 'number' : 'text'}
390
+ bind:value={draft}
391
+ placeholder={row.field.placeholder ?? 'Type an answer'}
392
+ aria-label={row.view.title}
393
+ />
394
+ {/if}
395
+
396
+ {#if rowError !== null}
397
+ <Text size="small" color="error">{rowError}</Text>
398
+ {/if}
399
+ </div>
400
+ {:else if row.view.kind === 'system' && row.view.answered}
401
+ <div class="system-value">
402
+ <Text size="small" color="subdued">{row.view.display}</Text>
403
+ <CopyButton text={systemPayload(row.value)} label="Copy" showLabel />
404
+ </div>
405
+ {:else if row.view.answered && row.view.display !== null}
406
+ <Text size="small" color="subdued">{row.view.display}</Text>
407
+ {:else}
408
+ <Text size="small" color="subdued">Not answered yet</Text>
409
+ {/if}
410
+ </li>
411
+ {/each}
412
+ </ul>
413
+ {/if}
414
+ </Card>
415
+ </Stack>
416
+
417
+ <style>
418
+ .toolbar-row {
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: space-between;
422
+ gap: var(--space-2);
423
+ flex-wrap: wrap;
424
+ }
425
+
426
+ .filters {
427
+ display: flex;
428
+ gap: var(--space-1);
429
+ flex-wrap: wrap;
430
+ }
431
+
432
+ .filter {
433
+ border: 1px solid var(--color-gray-200);
434
+ border-radius: var(--radius-full);
435
+ background: var(--color-white);
436
+ color: var(--color-gray-600);
437
+ font-family: inherit;
438
+ font-size: var(--text-xs);
439
+ padding: var(--space-1) var(--space-2-5);
440
+ cursor: pointer;
441
+ }
442
+
443
+ .filter:hover {
444
+ border-color: var(--color-gray-300);
445
+ color: var(--color-gray-900);
446
+ }
447
+
448
+ .filter.selected {
449
+ background: var(--color-blue-600);
450
+ border-color: var(--color-blue-600);
451
+ color: var(--color-white);
452
+ }
453
+
454
+ .filter:focus-visible {
455
+ outline: var(--ring-width) solid var(--ring-color);
456
+ outline-offset: var(--ring-offset);
457
+ }
458
+
459
+ .rows {
460
+ list-style: none;
461
+ margin: 0;
462
+ padding: 0;
463
+ }
464
+
465
+ .row {
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: var(--space-1);
469
+ padding: var(--space-3);
470
+ border-bottom: 1px solid var(--color-gray-200);
471
+ }
472
+
473
+ .row:last-child {
474
+ border-bottom: none;
475
+ }
476
+
477
+ .row-head {
478
+ display: flex;
479
+ align-items: flex-start;
480
+ justify-content: space-between;
481
+ gap: var(--space-2);
482
+ }
483
+
484
+ .row-title {
485
+ min-width: 0;
486
+ display: flex;
487
+ flex-direction: column;
488
+ gap: 1px;
489
+ }
490
+
491
+ /* The key is a developer's handle, not the reviewer's — present, dimmed, never the headline. */
492
+ .row-meta {
493
+ display: flex;
494
+ align-items: baseline;
495
+ gap: var(--space-1-5);
496
+ font-size: 10px;
497
+ color: var(--color-gray-500);
498
+ min-width: 0;
499
+ flex-wrap: wrap;
500
+ }
501
+
502
+ .row-key {
503
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
504
+ overflow-wrap: anywhere;
505
+ }
506
+
507
+ .row-actions {
508
+ display: inline-flex;
509
+ gap: var(--space-1);
510
+ flex-shrink: 0;
511
+ }
512
+
513
+ .editor {
514
+ display: flex;
515
+ flex-direction: column;
516
+ gap: var(--space-2);
517
+ padding-top: var(--space-1);
518
+ }
519
+
520
+ .system-value {
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: space-between;
524
+ gap: var(--space-2);
525
+ }
526
+ </style>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ /** @file One line of guide text, with each `**control name**` in bold, built without raw HTML. */
3
+ import { emphasis } from '../../derive'
4
+
5
+ interface GuideLineProps {
6
+ text: string
7
+ }
8
+
9
+ const { text }: GuideLineProps = $props()
10
+ </script>
11
+
12
+ {#each emphasis(text) as run, index (index)}
13
+ {#if run.bold}<strong>{run.text}</strong>{:else}{run.text}{/if}
14
+ {/each}
15
+
16
+ <style>
17
+ strong {
18
+ font-weight: var(--font-semibold);
19
+ }
20
+ </style>
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ /**
3
+ * @file The How to use tab: each section's steps and notes, then the common problems and their
4
+ * fixes. The shell decides which sections a build shows; this lays them out and turns a section's
5
+ * tab into an "Open …" button.
6
+ */
7
+ import { Button, Card, Heading, Stack } from '../../ui'
8
+ import type { GuideFix, GuideSection } from '../../derive'
9
+ import GuideLine from './GuideLine.svelte'
10
+
11
+ interface GuidePanelProps {
12
+ sections: readonly GuideSection[]
13
+ fixes: readonly GuideFix[]
14
+ /** Each listed tab's name, by tab id. A section whose tab is missing here offers no button. */
15
+ tabLabels: Readonly<Record<string, string>>
16
+ onOpenTab: (id: string) => void
17
+ }
18
+
19
+ const { sections, fixes, tabLabels, onOpenTab }: GuidePanelProps = $props()
20
+
21
+ /**
22
+ * The "Open …" button a section offers, or null when its tab is not listed in this panel.
23
+ *
24
+ * @param {GuideSection} section - The section to read.
25
+ * @returns {{ id: string; label: string } | null} The tab to open and its name.
26
+ * @example
27
+ * openTarget({ ...section, tab: 'answers' }) // { id: 'answers', label: 'Answers' }
28
+ */
29
+ function openTarget(section: GuideSection): { id: string; label: string } | null {
30
+ const id = section.tab
31
+ if (id === null || !Object.hasOwn(tabLabels, id)) return null
32
+ return { id, label: tabLabels[id] }
33
+ }
34
+ </script>
35
+
36
+ <div class="guide" data-testid="devtools-guide">
37
+ {#each sections as section (section.id)}
38
+ {@const target = openTarget(section)}
39
+ <section data-testid="devtools-guide-{section.id}">
40
+ <Card>
41
+ <Stack direction="vertical" gap="small">
42
+ <Stack direction="horizontal" gap="small" align="space-between" justify="center">
43
+ <Heading level={3} size="small">{section.title}</Heading>
44
+ {#if target !== null}
45
+ <Button variant="secondary" size="small" onclick={() => onOpenTab(target.id)}>
46
+ Open {target.label}
47
+ </Button>
48
+ {/if}
49
+ </Stack>
50
+ {#if section.steps.length > 0}
51
+ <ol class="steps">
52
+ {#each section.steps as step, index (index)}
53
+ <li><GuideLine text={step} /></li>
54
+ {/each}
55
+ </ol>
56
+ {/if}
57
+ {#each section.notes as note, index (index)}
58
+ <p class="note"><GuideLine text={note} /></p>
59
+ {/each}
60
+ </Stack>
61
+ </Card>
62
+ </section>
63
+ {/each}
64
+
65
+ {#if fixes.length > 0}
66
+ <section data-testid="devtools-guide-fixes">
67
+ <Card>
68
+ <Stack direction="vertical" gap="small">
69
+ <Heading level={3} size="small">If something goes wrong</Heading>
70
+ <dl class="fixes">
71
+ {#each fixes as entry (entry.problem)}
72
+ <dt>{entry.problem}</dt>
73
+ <dd><GuideLine text={entry.fix} /></dd>
74
+ {/each}
75
+ </dl>
76
+ </Stack>
77
+ </Card>
78
+ </section>
79
+ {/if}
80
+ </div>
81
+
82
+ <style>
83
+ .guide {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: var(--space-4);
87
+ max-width: 52rem;
88
+ }
89
+
90
+ /* A host page's base styles may strip list markers, and the numbers are the point of a step list. */
91
+ .steps {
92
+ list-style: decimal;
93
+ margin: 0;
94
+ padding-left: var(--space-6);
95
+ font-size: var(--text-sm);
96
+ line-height: 1.5;
97
+ color: var(--color-gray-900);
98
+ }
99
+
100
+ .steps li + li {
101
+ margin-top: var(--space-1);
102
+ }
103
+
104
+ .steps li::marker {
105
+ color: var(--color-gray-500);
106
+ }
107
+
108
+ .note {
109
+ margin: 0;
110
+ font-size: var(--text-sm);
111
+ line-height: 1.5;
112
+ color: var(--color-gray-700);
113
+ }
114
+
115
+ .fixes {
116
+ display: grid;
117
+ grid-template-columns: minmax(10rem, 1fr) 2fr;
118
+ gap: var(--space-2) var(--space-4);
119
+ margin: 0;
120
+ font-size: var(--text-sm);
121
+ line-height: 1.5;
122
+ }
123
+
124
+ .fixes dt {
125
+ font-weight: var(--font-medium);
126
+ color: var(--color-gray-900);
127
+ }
128
+
129
+ .fixes dd {
130
+ margin: 0;
131
+ color: var(--color-gray-700);
132
+ }
133
+ </style>
@@ -0,0 +1,6 @@
1
+ /** @file The review tabs' panels: How to use, This session, Answers, Journey map and Variations. */
2
+ export { default as GuidePanel } from './guide/GuidePanel.svelte';
3
+ export { default as SessionPanel } from './session/SessionPanel.svelte';
4
+ export { default as AnswersPanel } from './answers/AnswersPanel.svelte';
5
+ export { default as JourneyPanel } from './journey/JourneyPanel.svelte';
6
+ export { default as VariationsPanel } from './variations/VariationsPanel.svelte';
@@ -0,0 +1 @@
1
+ import e from"./guide/GuidePanel.svelte";import t from"./session/SessionPanel.svelte";import n from"./answers/AnswersPanel.svelte";import r from"./journey/JourneyPanel.svelte";import i from"./variations/VariationsPanel.svelte";export{n as AnswersPanel,e as GuidePanel,r as JourneyPanel,t as SessionPanel,i as VariationsPanel};