@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.
- 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 +1076 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +584 -0
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +750 -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,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};
|