@picked/svelte 0.0.1-dev.1793769511104 → 0.0.1-dev.1793776638554
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/dist/.tsbuildinfo +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +46 -15
- package/dist/plugins/devtools/components/AdaptedToolHost.svelte +9 -3
- package/dist/plugins/devtools/customTools.d.ts +16 -7
- package/dist/plugins/devtools/customTools.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -1
- package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
- package/dist/plugins/devtools/derive/persona-builder/persona-builder.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +8 -1
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +20 -23
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
- package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -1
- package/dist/plugins/devtools/options.d.ts +22 -21
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
- package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
- package/dist/plugins/devtools/review/index.d.ts +2 -2
- package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +20 -20
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +12 -12
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +7 -2
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +77 -15
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +2 -2
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/session/SessionPanel.svelte +11 -11
- package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
- package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +14 -22
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -2
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +3 -3
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +38 -4
- package/dist/plugins/devtools/sidebar/nav.d.ts +32 -10
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @file The
|
|
4
|
-
* experience tree without the node ids or the raw JSON Logic. It opens on the
|
|
5
|
-
* of the reviewer, and the picker reaches every other experience in the workspace.
|
|
3
|
+
* @file The Flow tab: any experience's flow in plain English — the Developer group's
|
|
4
|
+
* experience tree without the node ids or the raw JSON Logic. It opens on the experience in
|
|
5
|
+
* front of the reviewer, and the picker reaches every other experience in the workspace.
|
|
6
6
|
*
|
|
7
|
-
* Navigation is BACKWARDS ONLY, and only in the
|
|
8
|
-
* a plain `goToStepByKey`: the flow has been through every action in between, so returning to
|
|
9
|
-
* is safe. A
|
|
10
|
-
* make it renderable, and the
|
|
11
|
-
*
|
|
7
|
+
* Navigation is BACKWARDS ONLY, and only in the experience that is OPEN. A step already visited
|
|
8
|
+
* is a plain `goToStepByKey`: the flow has been through every action in between, so returning to
|
|
9
|
+
* it is safe. A step AHEAD is rendered but not clickable — jumping forward skips the actions that
|
|
10
|
+
* make it renderable, and the step it lands on can render an error state no visitor could reach.
|
|
11
|
+
* Forward motion belongs to the experience itself, or to a custom tool that replays one.
|
|
12
12
|
*
|
|
13
13
|
* THREE VIEWS, ONE GRAPH. The outline answers "where am I, and why did it go there"; the map
|
|
14
14
|
* answers "what shape is this, and where does each branch end"; the timeline answers "in what
|
|
15
15
|
* order, and where do the branches meet again". All three read the same `treeStore` through the
|
|
16
|
-
* same vocabulary modules, so a
|
|
16
|
+
* same vocabulary modules, so a step is named identically in each and they cannot drift.
|
|
17
17
|
*/
|
|
18
18
|
import type { Picked } from '@picked/client'
|
|
19
19
|
import { Badge, Banner, Button, Card, EmptyState } from '../../ui'
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
* @param {OutlineRow} row - The row to explain.
|
|
156
156
|
* @returns {string | undefined} The reason, or undefined when there is nothing to explain.
|
|
157
157
|
* @example
|
|
158
|
-
* jumpReason(aheadRow) // "
|
|
158
|
+
* jumpReason(aheadRow) // "Your responses don't reach this step yet."
|
|
159
159
|
*/
|
|
160
160
|
function jumpReason(row: OutlineRow): string | undefined {
|
|
161
161
|
if (row.kind !== 'screen') return undefined
|
|
162
162
|
if (posture.reason !== null) return posture.reason
|
|
163
|
-
if (!isOpen) return "This
|
|
164
|
-
if (row.state === 'current') return "You're on this
|
|
165
|
-
if (row.state === 'ahead') return "
|
|
163
|
+
if (!isOpen) return "This experience isn't open, so there is nowhere to go back to."
|
|
164
|
+
if (row.state === 'current') return "You're on this step."
|
|
165
|
+
if (row.state === 'ahead') return "Your responses don't reach this step yet."
|
|
166
166
|
return undefined
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -219,13 +219,13 @@
|
|
|
219
219
|
<div class="journey">
|
|
220
220
|
{#if selectedKey === null}
|
|
221
221
|
<EmptyState
|
|
222
|
-
message="No
|
|
222
|
+
message="No experiences yet"
|
|
223
223
|
hint="The workspace has not loaded its experiences. Reload the page if this does not change."
|
|
224
224
|
/>
|
|
225
225
|
{:else}
|
|
226
226
|
<!-- One row: how to look on the left, what to look at on the right. -->
|
|
227
227
|
<div class="controls">
|
|
228
|
-
<div class="views" role="group" aria-label="
|
|
228
|
+
<div class="views" role="group" aria-label="Flow view">
|
|
229
229
|
<button
|
|
230
230
|
type="button"
|
|
231
231
|
class="view"
|
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
<div class="picker">
|
|
262
262
|
{#if followed !== null && !isOpen}
|
|
263
263
|
<Button variant="ghost" size="small" onclick={() => (picked = null)}>
|
|
264
|
-
Back to the open
|
|
264
|
+
Back to the open experience
|
|
265
265
|
</Button>
|
|
266
266
|
{/if}
|
|
267
267
|
<select
|
|
268
268
|
class="picker-select"
|
|
269
|
-
aria-label="
|
|
269
|
+
aria-label="Experience"
|
|
270
270
|
value={selectedKey}
|
|
271
271
|
onchange={pick}
|
|
272
272
|
data-testid="devtools-journey-picker"
|
|
@@ -287,8 +287,8 @@
|
|
|
287
287
|
{#if rows.length === 0}
|
|
288
288
|
<Card sectioned>
|
|
289
289
|
<EmptyState
|
|
290
|
-
message="This
|
|
291
|
-
hint="Its flow has not loaded, or it is built entirely from
|
|
290
|
+
message="This experience has no steps yet"
|
|
291
|
+
hint="Its flow has not loaded, or it is built entirely from actions."
|
|
292
292
|
/>
|
|
293
293
|
</Card>
|
|
294
294
|
{:else if layout !== null}
|
|
@@ -343,7 +343,7 @@
|
|
|
343
343
|
{:else if row.kind === 'arm'}
|
|
344
344
|
<span class="arm">{row.label}</span>
|
|
345
345
|
{:else if row.kind === 'end'}
|
|
346
|
-
<span class="end-row">⇥ Ends: {row.label} (
|
|
346
|
+
<span class="end-row">⇥ Ends: {row.label} (action)</span>
|
|
347
347
|
{:else if row.kind === 'journey'}
|
|
348
348
|
<span class="journey-row">
|
|
349
349
|
<span class="marker muted" aria-hidden="true"></span>
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @file
|
|
4
|
-
* bottom; branches run in lanes beside the rows and MERGE back into the node they
|
|
5
|
-
*
|
|
6
|
-
* jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling
|
|
3
|
+
* @file An experience's flow drawn as a TIMELINE — `git log --graph` for a flow. One row per
|
|
4
|
+
* node, top to bottom; branches run in lanes beside the rows and MERGE back into the node they
|
|
5
|
+
* meet, so a step many branches reach is one row with lines converging on it, not a tree full of
|
|
6
|
+
* dashed jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling
|
|
7
|
+
* only.
|
|
7
8
|
*
|
|
8
9
|
* A LOOP is the one thing a git graph never has. It stays off the drawing and rides on its row as
|
|
9
10
|
* a "↩ back to …" note, and the details pane can take the reviewer to its target.
|
|
10
11
|
*
|
|
11
12
|
* COLOUR IS THE BRANCH, as in a git tool: every line and dot takes its branch's colour, so the
|
|
12
13
|
* eye can follow one route down the lanes. That frees the node KIND to be a SHAPE — a circle for
|
|
13
|
-
* a
|
|
14
|
-
* where a branch ends on one, the row names it.
|
|
14
|
+
* a step, a diamond for a condition, a square for another experience. Actions are not drawn at
|
|
15
|
+
* all; where a branch ends on one, the row names it.
|
|
15
16
|
*
|
|
16
17
|
* ROWS ARE HTML, LINES ARE SVG, the same split as the map: each row is a real `<button>` the full
|
|
17
18
|
* width of the timeline, and the graph column is painted over its left edge with
|
|
18
19
|
* `pointer-events: none`, so clicking a dot or its label selects the same row.
|
|
19
20
|
*/
|
|
20
21
|
import { tick } from 'svelte'
|
|
21
|
-
import { COLOURS, ROW_HEIGHT } from '../../derive'
|
|
22
|
+
import { COLOURS, NODE_LABELS, ROW_HEIGHT } from '../../derive'
|
|
22
23
|
import type { TimelineLayout, TimelineNode, TreeNodeKind } from '../../derive'
|
|
23
24
|
import TimelineMarker from './TimelineMarker.svelte'
|
|
24
25
|
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
const { layout, canNavigate, onJump }: JourneyTimelineProps = $props()
|
|
33
34
|
|
|
34
35
|
const KIND_LABELS: Record<TreeNodeKind, string> = {
|
|
35
|
-
start:
|
|
36
|
-
screen:
|
|
37
|
-
decision:
|
|
38
|
-
system:
|
|
39
|
-
journey:
|
|
36
|
+
start: NODE_LABELS.origin,
|
|
37
|
+
screen: NODE_LABELS.step,
|
|
38
|
+
decision: NODE_LABELS.condition,
|
|
39
|
+
system: NODE_LABELS.action,
|
|
40
|
+
journey: NODE_LABELS.experience,
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/** Marker size per kind — the start a little larger, as on the map. */
|
|
@@ -242,7 +243,7 @@
|
|
|
242
243
|
>
|
|
243
244
|
{/each}
|
|
244
245
|
{#each node.ends as end (end.key)}
|
|
245
|
-
<span class="end" title="Hands off to {end.label}
|
|
246
|
+
<span class="end" title="Hands off to the {end.label} action"
|
|
246
247
|
>⇥ {end.armLabel === null ? '' : `${end.armLabel}: `}{end.label}</span
|
|
247
248
|
>
|
|
248
249
|
{/each}
|
|
@@ -331,13 +332,13 @@
|
|
|
331
332
|
{#if end.armLabel !== null}
|
|
332
333
|
<span class="arm arm-{end.arm ?? 'none'}">{end.armLabel}</span>
|
|
333
334
|
{/if}
|
|
334
|
-
<span class="end-text">Ends: {end.label} (
|
|
335
|
+
<span class="end-text">Ends: {end.label} (action)</span>
|
|
335
336
|
</li>
|
|
336
337
|
{/each}
|
|
337
338
|
</ul>
|
|
338
339
|
</div>
|
|
339
340
|
{:else}
|
|
340
|
-
<p class="text">The
|
|
341
|
+
<p class="text">The experience ends here.</p>
|
|
341
342
|
{/if}
|
|
342
343
|
|
|
343
344
|
{#if selectedNode.rule !== null}
|
|
@@ -347,7 +348,7 @@
|
|
|
347
348
|
|
|
348
349
|
{#if canJump(selectedNode)}
|
|
349
350
|
<button type="button" class="action" onclick={() => goBack(selectedNode)}>
|
|
350
|
-
Go back to this
|
|
351
|
+
Go back to this step
|
|
351
352
|
</button>
|
|
352
353
|
{/if}
|
|
353
354
|
{/if}
|
|
@@ -355,14 +356,14 @@
|
|
|
355
356
|
</div>
|
|
356
357
|
|
|
357
358
|
<p class="legend">
|
|
358
|
-
<span class="key-swatch kind-start"></span>
|
|
359
|
-
<span class="key-swatch kind-screen"></span>
|
|
360
|
-
<span class="key-swatch kind-decision"></span>
|
|
361
|
-
<span class="key-swatch kind-journey"></span>
|
|
359
|
+
<span class="key-swatch kind-start"></span> {KIND_LABELS.start}
|
|
360
|
+
<span class="key-swatch kind-screen"></span> {KIND_LABELS.screen}
|
|
361
|
+
<span class="key-swatch kind-decision"></span> {KIND_LABELS.decision}
|
|
362
|
+
<span class="key-swatch kind-journey"></span> {KIND_LABELS.journey}
|
|
362
363
|
<span class="key-swatch seen"></span> Seen
|
|
363
364
|
<span class="key-swatch current"></span> You are here
|
|
364
365
|
<span
|
|
365
|
-
>· each colour is one branch · a
|
|
366
|
+
>· each colour is one branch · a condition's Yes runs straight on · ↩ loops back · ⇥ hands off</span
|
|
366
367
|
>
|
|
367
368
|
</p>
|
|
368
369
|
</div>
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
if (typeof result === 'string' || result === false) {
|
|
216
216
|
errors = {
|
|
217
217
|
...errors,
|
|
218
|
-
[field.key]: typeof result === 'string' ? result : 'This
|
|
218
|
+
[field.key]: typeof result === 'string' ? result : 'This response is not accepted.',
|
|
219
219
|
}
|
|
220
220
|
return
|
|
221
221
|
}
|
|
@@ -245,13 +245,13 @@
|
|
|
245
245
|
function endText(row: Extract<BuilderRow, { kind: 'end' }>): string {
|
|
246
246
|
switch (row.reason) {
|
|
247
247
|
case 'loop':
|
|
248
|
-
return `The
|
|
248
|
+
return `The flow goes back to “${row.name ?? 'an earlier step'}” here.`
|
|
249
249
|
case 'handoff':
|
|
250
|
-
return `${row.name ?? 'Another
|
|
250
|
+
return `${row.name ?? 'Another experience'} takes over here, outside this build.`
|
|
251
251
|
case 'limit':
|
|
252
|
-
return 'The
|
|
252
|
+
return 'The flow is too long to walk here, so it stops.'
|
|
253
253
|
case 'end':
|
|
254
|
-
return 'The
|
|
254
|
+
return 'The experience ends here.'
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -274,18 +274,18 @@
|
|
|
274
274
|
|
|
275
275
|
<div class="builder" data-testid="devtools-persona-builder">
|
|
276
276
|
{#if journeyOptions.length === 0}
|
|
277
|
-
<Text size="small" color="subdued">This page has no
|
|
277
|
+
<Text size="small" color="subdued">This page has no experiences to build a persona in.</Text>
|
|
278
278
|
{:else}
|
|
279
279
|
<Select
|
|
280
280
|
label="Starts in"
|
|
281
281
|
name="devtools-builder-start"
|
|
282
282
|
options={journeyOptions}
|
|
283
283
|
value={start}
|
|
284
|
-
placeholder="Pick
|
|
284
|
+
placeholder="Pick an experience"
|
|
285
285
|
onchange={onStartChange}
|
|
286
286
|
/>
|
|
287
287
|
<Text size="small" color="subdued">
|
|
288
|
-
|
|
288
|
+
Complete each step; the next one appears below. Where the site's own code decides the route,
|
|
289
289
|
pick the outcome to follow. The real outcome decides when the persona loads.
|
|
290
290
|
</Text>
|
|
291
291
|
|
|
@@ -351,21 +351,21 @@
|
|
|
351
351
|
</div>
|
|
352
352
|
{:else if kind === 'system'}
|
|
353
353
|
<Text size="small" color="subdued">
|
|
354
|
-
This is
|
|
355
|
-
|
|
354
|
+
This is set on its own step. The persona stops here, so load it and set it
|
|
355
|
+
there.
|
|
356
356
|
</Text>
|
|
357
357
|
{/if}
|
|
358
358
|
</div>
|
|
359
359
|
{/if}
|
|
360
360
|
{:else}
|
|
361
361
|
<Text size="small" color="subdued">
|
|
362
|
-
Nothing to
|
|
362
|
+
Nothing to fill in here. The visitor continues.
|
|
363
363
|
</Text>
|
|
364
364
|
{/each}
|
|
365
365
|
</li>
|
|
366
366
|
{:else if row.kind === 'choice'}
|
|
367
367
|
<li class="decision" data-testid="devtools-builder-decision">
|
|
368
|
-
<Text size="small">The site decides: {row.question}</Text>
|
|
368
|
+
<Text size="small">The site's code decides: {row.question}</Text>
|
|
369
369
|
<div class="choices">
|
|
370
370
|
<ChoiceButton
|
|
371
371
|
label="Yes"
|
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|
/** True while this persona is loading. */
|
|
11
11
|
loading: boolean
|
|
12
12
|
onLoad: () => void
|
|
13
|
+
/** Opens it in the form; absent for a persona the site defines. */
|
|
14
|
+
onEdit?: () => void
|
|
13
15
|
/** Deletes it; absent for a persona the site defines. */
|
|
14
16
|
onDelete?: () => void
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
const { persona, loadReason, loading, onLoad, onDelete }: PersonaCardProps = $props()
|
|
19
|
+
const { persona, loadReason, loading, onLoad, onEdit, onDelete }: PersonaCardProps = $props()
|
|
18
20
|
|
|
19
21
|
let confirming = $state(false)
|
|
20
22
|
|
|
21
23
|
const answers = $derived(Object.keys(persona.responses).length)
|
|
22
|
-
const answerLabel = $derived(answers === 1 ? '1
|
|
24
|
+
const answerLabel = $derived(answers === 1 ? '1 response' : `${answers} responses`)
|
|
23
25
|
const summary = $derived(
|
|
24
26
|
persona.load === null
|
|
25
27
|
? `${answerLabel} · ${describeLanding(persona.landing)}`
|
|
@@ -52,6 +54,9 @@
|
|
|
52
54
|
>
|
|
53
55
|
Load
|
|
54
56
|
</Button>
|
|
57
|
+
{#if onEdit}
|
|
58
|
+
<Button variant="ghost" size="small" onclick={onEdit}>Edit</Button>
|
|
59
|
+
{/if}
|
|
55
60
|
{#if onDelete}
|
|
56
61
|
<Button variant="ghost" size="small" onclick={() => (confirming = true)}>Delete</Button>
|
|
57
62
|
{/if}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* answering the journey's questions here, screen by screen, or from where they are on the page.
|
|
6
6
|
* A persona keeps the answers a customer gives on fields they can see, the screens moved on from,
|
|
7
7
|
* the screen it stops on and the page it starts on. Loading one reloads that page clean and walks
|
|
8
|
-
* the journey with its answers, screen by screen, up to that screen.
|
|
8
|
+
* the journey with its answers, screen by screen, up to that screen. Site tools placed here
|
|
9
|
+
* follow, each a section under its label.
|
|
9
10
|
*/
|
|
10
11
|
import type { FieldValue, Picked } from '@picked/client'
|
|
11
12
|
import { getStepFields } from '@picked/client/form'
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
type RouteEntry,
|
|
28
29
|
type SafetyPosture,
|
|
29
30
|
} from '../../derive'
|
|
31
|
+
import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
|
|
32
|
+
import type { CustomTool } from '../../customTools'
|
|
30
33
|
import type { JourneyView } from '../../session'
|
|
31
34
|
import { loadUrl, startPersonaLoad } from './load-persona'
|
|
32
35
|
import PersonaBuilder from './PersonaBuilder.svelte'
|
|
@@ -52,6 +55,8 @@
|
|
|
52
55
|
selfAdvancingSteps: ReadonlySet<string>
|
|
53
56
|
handoffs: Readonly<Record<string, string>>
|
|
54
57
|
}
|
|
58
|
+
/** The site's tools placed in this tab. */
|
|
59
|
+
tools?: readonly CustomTool[]
|
|
55
60
|
/** How the last load's walk ended; `null` when no load ran on this page. */
|
|
56
61
|
walkNotice: Notice | null
|
|
57
62
|
onWalkNoticeDismiss: () => void
|
|
@@ -83,6 +88,7 @@
|
|
|
83
88
|
hiddenKeys,
|
|
84
89
|
codeOwned,
|
|
85
90
|
personaWalk,
|
|
91
|
+
tools = [],
|
|
86
92
|
walkNotice,
|
|
87
93
|
onWalkNoticeDismiss,
|
|
88
94
|
getStorage = () => window.localStorage,
|
|
@@ -108,6 +114,8 @@
|
|
|
108
114
|
|
|
109
115
|
let saved = $state<Persona[]>(readSaved())
|
|
110
116
|
let creating = $state(false)
|
|
117
|
+
/** The saved persona the form edits; `null` while it makes a new one. */
|
|
118
|
+
let editingId = $state<string | null>(null)
|
|
111
119
|
/** How a new persona is made: answered here, or taken from where the reviewer is. */
|
|
112
120
|
let mode = $state<'build' | 'capture'>('build')
|
|
113
121
|
let name = $state('')
|
|
@@ -125,7 +133,7 @@
|
|
|
125
133
|
/** The answers Save would keep; `null` while they are read. */
|
|
126
134
|
let captured = $state<Record<string, FieldValue> | null>(null)
|
|
127
135
|
|
|
128
|
-
const isEmpty = $derived(personas.length === 0 && saved.length === 0)
|
|
136
|
+
const isEmpty = $derived(personas.length === 0 && saved.length === 0 && tools.length === 0)
|
|
129
137
|
const postureReason = $derived(posture.canEditAnswers ? undefined : (posture.reason ?? undefined))
|
|
130
138
|
|
|
131
139
|
/**
|
|
@@ -154,19 +162,19 @@
|
|
|
154
162
|
const buildSummary = $derived.by(() => {
|
|
155
163
|
if (buildWalk === null) return 'Working out the route…'
|
|
156
164
|
const count = Object.keys(buildWalk.answers).length
|
|
157
|
-
const answers = count === 1 ? '1
|
|
165
|
+
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
158
166
|
const stop = buildWalk.landing
|
|
159
167
|
if (stop === null) return `Saves ${answers}.`
|
|
160
168
|
const screen = stop.stepName ?? deslug(stop.stepKey ?? '')
|
|
161
|
-
return `Saves ${answers}. Loading it walks the
|
|
169
|
+
return `Saves ${answers}. Loading it walks the experience to ${screen}.`
|
|
162
170
|
})
|
|
163
171
|
|
|
164
172
|
const captureSummary = $derived.by(() => {
|
|
165
|
-
if (captured === null) return 'Reading your
|
|
173
|
+
if (captured === null) return 'Reading your responses…'
|
|
166
174
|
const count = Object.keys(captured).length
|
|
167
|
-
const answers = count === 1 ? '1
|
|
175
|
+
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
168
176
|
if (landing === null)
|
|
169
|
-
return `Saves ${answers}. No
|
|
177
|
+
return `Saves ${answers}. No experience is open, so it loads the responses only.`
|
|
170
178
|
const journeyName = landing.experienceName ?? deslug(landing.experienceKey)
|
|
171
179
|
if (landing.stepKey === null) return `Saves ${answers}. Loading it walks ${journeyName}.`
|
|
172
180
|
const screen = landing.stepName ?? deslug(landing.stepKey)
|
|
@@ -240,6 +248,7 @@
|
|
|
240
248
|
const responses = client.responses.getAllStore.getState()
|
|
241
249
|
const answeredHere = [...asked].some((key) => !isBlank(responses[key]))
|
|
242
250
|
creating = true
|
|
251
|
+
editingId = null
|
|
243
252
|
mode = answeredHere ? 'capture' : 'build'
|
|
244
253
|
name = ''
|
|
245
254
|
description = ''
|
|
@@ -254,6 +263,42 @@
|
|
|
254
263
|
buildWalk = null
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Opens the form on a saved persona, to answer its questions again here. The walk starts in its
|
|
268
|
+
* first journey with its answers and its picks for the site's own decisions, so changing an
|
|
269
|
+
* answer re-routes the screens after it.
|
|
270
|
+
*
|
|
271
|
+
* @param {Persona} persona - The persona to edit.
|
|
272
|
+
* @example startEdit(persona)
|
|
273
|
+
*/
|
|
274
|
+
function startEdit(persona: Persona): void {
|
|
275
|
+
creating = true
|
|
276
|
+
editingId = persona.id
|
|
277
|
+
mode = 'build'
|
|
278
|
+
name = persona.name
|
|
279
|
+
description = persona.description ?? ''
|
|
280
|
+
entryText = persona.entryUrl ?? `${window.location.pathname}${window.location.search}`
|
|
281
|
+
nameError = null
|
|
282
|
+
entryError = null
|
|
283
|
+
notice = null
|
|
284
|
+
captured = null
|
|
285
|
+
buildStart =
|
|
286
|
+
persona.passed.at(0)?.experienceKey ?? persona.landing?.experienceKey ?? defaultStart()
|
|
287
|
+
buildAnswers = { ...persona.responses }
|
|
288
|
+
buildChoices = { ...persona.choices }
|
|
289
|
+
buildWalk = null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Closes the form without saving.
|
|
294
|
+
*
|
|
295
|
+
* @example cancel()
|
|
296
|
+
*/
|
|
297
|
+
function cancel(): void {
|
|
298
|
+
creating = false
|
|
299
|
+
editingId = null
|
|
300
|
+
}
|
|
301
|
+
|
|
257
302
|
/**
|
|
258
303
|
* What Save keeps, from the build or from where the reviewer is.
|
|
259
304
|
*
|
|
@@ -262,7 +307,7 @@
|
|
|
262
307
|
* @example await capture('/compare')
|
|
263
308
|
*/
|
|
264
309
|
async function capture(entryUrl: string | null): Promise<PersonaCapture | null> {
|
|
265
|
-
const id = newId()
|
|
310
|
+
const id = editingId ?? newId()
|
|
266
311
|
const savedAt = new Date().toISOString()
|
|
267
312
|
if (mode === 'build') {
|
|
268
313
|
if (buildWalk === null) return null
|
|
@@ -270,6 +315,7 @@
|
|
|
270
315
|
responses: buildWalk.answers,
|
|
271
316
|
journey: buildWalk.landing,
|
|
272
317
|
passed: buildWalk.passed,
|
|
318
|
+
choices: buildChoices,
|
|
273
319
|
entryUrl: entryUrl ?? '',
|
|
274
320
|
id,
|
|
275
321
|
savedAt,
|
|
@@ -287,7 +333,8 @@
|
|
|
287
333
|
}
|
|
288
334
|
|
|
289
335
|
/**
|
|
290
|
-
* Saves
|
|
336
|
+
* Saves the form's persona in this browser: a new one at the end of the list, an edited one in
|
|
337
|
+
* its place.
|
|
291
338
|
*
|
|
292
339
|
* @returns {Promise<void>} Settles once it is saved or refused.
|
|
293
340
|
* @example await save()
|
|
@@ -307,11 +354,15 @@
|
|
|
307
354
|
return
|
|
308
355
|
}
|
|
309
356
|
|
|
310
|
-
|
|
357
|
+
const edited = editingId !== null
|
|
358
|
+
saved = edited
|
|
359
|
+
? saved.map((entry) => (entry.id === result.persona.id ? result.persona : entry))
|
|
360
|
+
: [...saved, result.persona]
|
|
311
361
|
creating = false
|
|
362
|
+
editingId = null
|
|
312
363
|
const stored = writeSavedPersonas(getStorage, client.projectId, saved)
|
|
313
364
|
notice = stored
|
|
314
|
-
? { status: 'success', text:
|
|
365
|
+
? { status: 'success', text: `${edited ? 'Updated' : 'Saved'} ${result.persona.name}.` }
|
|
315
366
|
: {
|
|
316
367
|
status: 'warning',
|
|
317
368
|
text: `${result.persona.name} is here until you reload. This browser would not let it be saved.`,
|
|
@@ -387,14 +438,14 @@
|
|
|
387
438
|
<Card>
|
|
388
439
|
<div class="form">
|
|
389
440
|
<Stack direction="vertical" gap="medium">
|
|
390
|
-
<Text weight="semibold">New persona</Text>
|
|
441
|
+
<Text weight="semibold">{editingId === null ? 'New persona' : 'Edit persona'}</Text>
|
|
391
442
|
<RadioGroup
|
|
392
443
|
label="Make it by"
|
|
393
444
|
name="persona-mode"
|
|
394
445
|
orientation="horizontal"
|
|
395
446
|
value={mode}
|
|
396
447
|
options={[
|
|
397
|
-
{ value: 'build', label: '
|
|
448
|
+
{ value: 'build', label: 'Filling in the fields here' },
|
|
398
449
|
{ value: 'capture', label: 'Using where I am now' },
|
|
399
450
|
]}
|
|
400
451
|
onchange={(value) => (mode = value === 'capture' ? 'capture' : 'build')}
|
|
@@ -452,7 +503,7 @@
|
|
|
452
503
|
</Text>
|
|
453
504
|
<div class="form-actions">
|
|
454
505
|
<Button variant="primary" size="small" onclick={() => void save()}>Save</Button>
|
|
455
|
-
<Button variant="ghost" size="small" onclick={
|
|
506
|
+
<Button variant="ghost" size="small" onclick={cancel}>Cancel</Button>
|
|
456
507
|
</div>
|
|
457
508
|
</Stack>
|
|
458
509
|
</div>
|
|
@@ -461,7 +512,7 @@
|
|
|
461
512
|
<div class="empty">
|
|
462
513
|
<EmptyState
|
|
463
514
|
message="No personas or scenarios yet"
|
|
464
|
-
hint="Walk the
|
|
515
|
+
hint="Walk the experience the way a visitor would, then save it here to load it again in one click."
|
|
465
516
|
/>
|
|
466
517
|
<Button variant="primary" onclick={startCreate}>Create</Button>
|
|
467
518
|
</div>
|
|
@@ -494,11 +545,22 @@
|
|
|
494
545
|
loadReason={loadReason(persona)}
|
|
495
546
|
loading={loadingId === persona.id}
|
|
496
547
|
onLoad={() => void load(persona)}
|
|
548
|
+
onEdit={() => startEdit(persona)}
|
|
497
549
|
onDelete={() => remove(persona)}
|
|
498
550
|
/>
|
|
499
551
|
{/each}
|
|
500
552
|
</section>
|
|
501
553
|
{/if}
|
|
554
|
+
|
|
555
|
+
{#each tools as tool (tool.id)}
|
|
556
|
+
<section class="group" aria-label={tool.label} data-testid="devtools-persona-tool-{tool.id}">
|
|
557
|
+
<h3 class="group-title">{tool.label}</h3>
|
|
558
|
+
{#if tool.description}
|
|
559
|
+
<Text size="small" color="subdued">{tool.description}</Text>
|
|
560
|
+
{/if}
|
|
561
|
+
<AdaptedToolHost {tool} flush />
|
|
562
|
+
</section>
|
|
563
|
+
{/each}
|
|
502
564
|
</div>
|
|
503
565
|
|
|
504
566
|
<style>
|
|
@@ -83,8 +83,8 @@ export interface WalkSettings {
|
|
|
83
83
|
openOrder: () => readonly string[];
|
|
84
84
|
/** Step keys that move on by themselves, such as a loader. The walk waits on them. */
|
|
85
85
|
selfAdvancing: ReadonlySet<string>;
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/** Responses every walk gives when a step asks for their fields, over the persona's own. */
|
|
87
|
+
responses: Readonly<Record<string, FieldValue>>;
|
|
88
88
|
/**
|
|
89
89
|
* The keys a screen asks: its fields that show now, minus the code-owned ones.
|
|
90
90
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{pickJourney as n}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as r}from"@picked/client/form";var i=`picked-devtools-persona-load`,a=30,o={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function s(e){return`${e.experienceKey}/${e.stepKey}`}async function c(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(a)}return null}function l(e,r,i){let a=e.experienceInstances.listStore.getState().filter(e=>r.has(e.experienceKey)),o=n(a,{exclude:i.exclude,precedence:i.precedence,openOrder:i.openOrder()}),s=o?.currentStepStore.getState();return o===void 0||s==null||o.currentNodeStore.getState()?.type!==`step`||o.stateStore.getState()!==`ready`?null:{instance:o,screen:{experienceKey:o.experienceKey,stepKey:s.key},fieldKeys:s.fieldKeys,stop:{experienceName:o.experienceName,stepName:t(s.name,s.key)}}}async function u(e,t,n){await e.openExperienceByKey(t),await c(()=>e.experienceInstances.listStore.getState().some(e=>e.experienceKey===t&&e.isOpenStore.getState())?!0:null,n.openTries,n)===null&&await e.launchExperienceByKey(t,{bypassCondition:!0,initialiseOpen:!0})}function d(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function f(e,t,n){let i=e.fields.listStore.getState(),a=t.flatMap(e=>i.filter(t=>t.key===e));return(await r(e,a)).map(e=>e.key).filter(e=>!n.has(e))}function p(e,t,n){try{n.storage.setItem(i,t.id)}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function m(e,t){let n=new URL(e??`${t.pathname}${t.search}`,`https://entry.invalid`);for(let[e,r]of new URLSearchParams(t.search))e.startsWith(`picked-`)&&!n.searchParams.has(e)&&n.searchParams.append(e,r);return`${n.pathname}${n.search}`}function h(e){try{let t=e.getItem(i);return e.removeItem(i),t!==null&&t.length>0?t:null}catch{return null}}function g(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function _(e,t,n){let r=!1,i=0;return await c(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):s(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function v(e,t){let n=await e.settings.askedOn(t.fieldKeys),r=n.filter(t=>Object.hasOwn(e.answers,t));if(r.length>0){let{errors:n}=await e.client.responses.commit(d(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(g(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=s(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await _(e,i,e.timing.selfAdvanceTries)?null:{end:`stuck`,at:t.stop}:(n.length>0?r.length===n.length:e.passed.has(i))?(t.instance.goToNextStep(),await _(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function y(e,t,n,r=o){let{landing:i}=t,a=t.passed.at(0)?.experienceKey??i?.experienceKey;if(a===void 0)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let d=new Set(t.passed.map(e=>e.experienceKey));i!==null&&d.add(i.experienceKey);let f={client:e,settings:n,timing:r,landing:i,passed:new Set(t.passed.map(s)),answers:{...t.responses,...n.
|
|
1
|
+
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{pickJourney as n}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as r}from"@picked/client/form";var i=`picked-devtools-persona-load`,a=30,o={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function s(e){return`${e.experienceKey}/${e.stepKey}`}async function c(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(a)}return null}function l(e,r,i){let a=e.experienceInstances.listStore.getState().filter(e=>r.has(e.experienceKey)),o=n(a,{exclude:i.exclude,precedence:i.precedence,openOrder:i.openOrder()}),s=o?.currentStepStore.getState();return o===void 0||s==null||o.currentNodeStore.getState()?.type!==`step`||o.stateStore.getState()!==`ready`?null:{instance:o,screen:{experienceKey:o.experienceKey,stepKey:s.key},fieldKeys:s.fieldKeys,stop:{experienceName:o.experienceName,stepName:t(s.name,s.key)}}}async function u(e,t,n){await e.openExperienceByKey(t),await c(()=>e.experienceInstances.listStore.getState().some(e=>e.experienceKey===t&&e.isOpenStore.getState())?!0:null,n.openTries,n)===null&&await e.launchExperienceByKey(t,{bypassCondition:!0,initialiseOpen:!0})}function d(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function f(e,t,n){let i=e.fields.listStore.getState(),a=t.flatMap(e=>i.filter(t=>t.key===e));return(await r(e,a)).map(e=>e.key).filter(e=>!n.has(e))}function p(e,t,n){try{n.storage.setItem(i,t.id)}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function m(e,t){let n=new URL(e??`${t.pathname}${t.search}`,`https://entry.invalid`);for(let[e,r]of new URLSearchParams(t.search))e.startsWith(`picked-`)&&!n.searchParams.has(e)&&n.searchParams.append(e,r);return`${n.pathname}${n.search}`}function h(e){try{let t=e.getItem(i);return e.removeItem(i),t!==null&&t.length>0?t:null}catch{return null}}function g(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function _(e,t,n){let r=!1,i=0;return await c(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):s(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function v(e,t){let n=await e.settings.askedOn(t.fieldKeys),r=n.filter(t=>Object.hasOwn(e.answers,t));if(r.length>0){let{errors:n}=await e.client.responses.commit(d(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(g(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=s(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await _(e,i,e.timing.selfAdvanceTries)?null:{end:`stuck`,at:t.stop}:(n.length>0?r.length===n.length:e.passed.has(i))?(t.instance.goToNextStep(),await _(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function y(e,t,n,r=o){let{landing:i}=t,a=t.passed.at(0)?.experienceKey??i?.experienceKey;if(a===void 0)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let d=new Set(t.passed.map(e=>e.experienceKey));i!==null&&d.add(i.experienceKey);let f={client:e,settings:n,timing:r,landing:i,passed:new Set(t.passed.map(s)),answers:{...t.responses,...n.responses},resting:()=>l(e,d,n)};await c(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await c(f.resting,r.startTries,r)===null&&await u(e,a,r);for(let e=0;e<r.maxHops;e++){let e=await c(f.resting,r.startTries,r);if(e===null)return{end:`no-journey`,at:null};let t=await v(f,e);if(t!==null)return t}return{end:`hop-limit`,at:null}}function b(t,n){let{name:r}=t,i=n.at===null?``:`“${n.at.stepName}”`;switch(n.end){case`arrived`:return{status:`success`,text:`Loaded ${r} on ${i}.`};case`answers-only`:return{status:`success`,text:`Loaded ${r}’s responses.`};case`needs-answer`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. ${r} has no response for a field there.`};case`invalid`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. A response there no longer passes its check.`};case`stuck`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. The experience did not move on from there.`};case`no-journey`:{let n=t.passed.at(0)?.experienceKey??t.landing?.experienceKey;return{status:`error`,text:`${r} did not load: ${t.landing?.experienceName??(n===void 0?`its experience`:e(n))} didn't open on this page.`}}case`hop-limit`:return{status:`warning`,text:`Stopped loading ${r} after too many steps. The flow may be going round in a loop.`}}}export{i as PENDING_LOAD_KEY,m as loadUrl,f as screenQuestions,p as startPersonaLoad,h as takePendingLoad,b as walkNotice,y as walkPersona};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
journey: JourneyView | null
|
|
17
17
|
/** Every screen submitted since the session record started, oldest first. */
|
|
18
18
|
history: readonly HistoryEntry[]
|
|
19
|
-
/** Opens the
|
|
19
|
+
/** Opens the Responses tab, so the response summary leads somewhere it can be acted on. */
|
|
20
20
|
onShowAnswers: () => void
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -65,18 +65,18 @@
|
|
|
65
65
|
{#if journey === null}
|
|
66
66
|
<Text size="medium">Nothing is open yet.</Text>
|
|
67
67
|
<Text size="small" color="subdued">
|
|
68
|
-
Start
|
|
68
|
+
Start an experience on the page, and this will follow you.
|
|
69
69
|
</Text>
|
|
70
70
|
{:else}
|
|
71
71
|
<Stack direction="vertical" gap="none">
|
|
72
|
-
<Text size="large" weight="semibold">{journey.stepName ?? 'Moving between
|
|
72
|
+
<Text size="large" weight="semibold">{journey.stepName ?? 'Moving between steps'}</Text>
|
|
73
73
|
<Text size="small" color="subdued">in {journey.experienceName}</Text>
|
|
74
74
|
</Stack>
|
|
75
75
|
{#if journey.inTransit}
|
|
76
76
|
<!-- The engine is transiting a condition or action node, so the recorded step is the one
|
|
77
77
|
just LEFT. Saying so stops a "stuck on loading" report being filed against it. -->
|
|
78
78
|
<Text size="small" color="subdued">
|
|
79
|
-
Still working — the next
|
|
79
|
+
Still working — the next step has not settled yet.
|
|
80
80
|
</Text>
|
|
81
81
|
{/if}
|
|
82
82
|
{/if}
|
|
@@ -111,24 +111,24 @@
|
|
|
111
111
|
<Card>
|
|
112
112
|
<Stack direction="vertical" gap="small">
|
|
113
113
|
<Stack direction="horizontal" gap="small" align="space-between" justify="center">
|
|
114
|
-
<Text size="caption" color="subdued" weight="semibold">
|
|
114
|
+
<Text size="caption" color="subdued" weight="semibold">YOUR RESPONSES</Text>
|
|
115
115
|
{#if rows.length > 0}
|
|
116
|
-
<Button variant="ghost" size="small" onclick={onShowAnswers}>Change
|
|
116
|
+
<Button variant="ghost" size="small" onclick={onShowAnswers}>Change a response</Button>
|
|
117
117
|
{/if}
|
|
118
118
|
</Stack>
|
|
119
119
|
|
|
120
120
|
{#if rows.length === 0}
|
|
121
121
|
<EmptyState
|
|
122
|
-
message="
|
|
123
|
-
hint="Each
|
|
122
|
+
message="No responses yet"
|
|
123
|
+
hint="Each step you submit is listed here, in order. Responses from before this page loaded are on the Responses tab."
|
|
124
124
|
/>
|
|
125
125
|
{:else}
|
|
126
126
|
<table class="history" data-testid="devtools-session-answers">
|
|
127
127
|
<thead>
|
|
128
128
|
<tr>
|
|
129
|
-
<th scope="col">
|
|
130
|
-
<th scope="col">
|
|
131
|
-
<th scope="col">
|
|
129
|
+
<th scope="col">Step</th>
|
|
130
|
+
<th scope="col">Field</th>
|
|
131
|
+
<th scope="col">Response</th>
|
|
132
132
|
</tr>
|
|
133
133
|
</thead>
|
|
134
134
|
<tbody>
|