@picked/svelte 0.0.1-dev.1793776638554 → 0.0.1-dev.1793923852884

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 (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/experience/set-picked-context.js +1 -1
  3. package/dist/plugins/devtools/DevToolsExperience.svelte +155 -80
  4. package/dist/plugins/devtools/customTools.d.ts +2 -2
  5. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +14 -0
  6. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
  7. package/dist/plugins/devtools/derive/answer-display/index.d.ts +2 -2
  8. package/dist/plugins/devtools/derive/answer-display/index.js +1 -1
  9. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
  10. package/dist/plugins/devtools/derive/index.d.ts +5 -5
  11. package/dist/plugins/devtools/derive/index.js +1 -1
  12. package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
  13. package/dist/plugins/devtools/derive/personas/index.js +1 -1
  14. package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
  15. package/dist/plugins/devtools/derive/personas/personas.js +1 -1
  16. package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
  17. package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
  18. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +5 -5
  19. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
  20. package/dist/plugins/devtools/index.d.ts +1 -1
  21. package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
  22. package/dist/plugins/devtools/options.d.ts +6 -4
  23. package/dist/plugins/devtools/options.js +1 -1
  24. package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
  25. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
  26. package/dist/plugins/devtools/review/personas/ChoiceButton.svelte +40 -6
  27. package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +69 -45
  28. package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
  29. package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
  30. package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
  31. package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
  32. package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
  33. package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
  34. package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
  35. package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
  36. package/dist/plugins/devtools/sidebar/Sidebar.svelte +14 -48
  37. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +2 -2
  38. package/dist/plugins/devtools/sidebar/nav.d.ts +26 -39
  39. package/dist/plugins/devtools/sidebar/nav.js +1 -1
  40. package/package.json +2 -2
  41. package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
  42. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
  43. package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
  44. package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
  45. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
  46. package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
  47. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
  48. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
  49. package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
  50. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
@@ -10,7 +10,7 @@
10
10
  import { Select, Text, TextField } from '../../ui'
11
11
  import {
12
12
  builderGraph,
13
- kindOf,
13
+ inputStyleOf,
14
14
  questionOf,
15
15
  responseKeysOf,
16
16
  screenName,
@@ -19,9 +19,7 @@
19
19
  type BuilderWalk,
20
20
  } from '../../derive'
21
21
  import ChoiceButton from './ChoiceButton.svelte'
22
-
23
- /** The rule type the client evaluates. */
24
- type Rule = Parameters<typeof evaluateRuleLogic>[1]
22
+ import { entryLocation, ruleEvaluator, type Rule } from './persona-route'
25
23
 
26
24
  interface PersonaBuilderProps {
27
25
  client: Picked
@@ -73,37 +71,8 @@
73
71
  [...graph.journeys.values()].map((journey) => ({ value: journey.key, label: journey.name })),
74
72
  )
75
73
 
76
- /** The entry page's address, as a rule on `location` reads it. */
77
- const location = $derived.by(() => {
78
- const url = new URL(entryUrl, window.location.origin)
79
- return {
80
- hash: url.hash,
81
- host: url.host,
82
- hostname: url.hostname,
83
- href: url.href,
84
- origin: url.origin,
85
- pathname: url.pathname,
86
- port: url.port,
87
- protocol: url.protocol,
88
- search: url.search,
89
- }
90
- })
91
-
92
- /**
93
- * Evaluates a rule against draft answers, on the entry page.
94
- *
95
- * @param {Rule} rule - The rule.
96
- * @param {Readonly<Record<string, FieldValue>>} responses - The answers it reads.
97
- * @returns {Promise<boolean>} Whether it holds.
98
- * @example await evaluate(condition.logic, { purpose: 'home' })
99
- */
100
- async function evaluate(
101
- rule: Rule,
102
- responses: Readonly<Record<string, FieldValue>>,
103
- ): Promise<boolean> {
104
- const result = await evaluateRuleLogic(client, rule, { responses: { ...responses }, location })
105
- return Boolean(result)
106
- }
74
+ /** Evaluates a rule against draft answers, on the entry page. */
75
+ const evaluate = $derived(ruleEvaluator(client, entryLocation(entryUrl, window.location.origin)))
107
76
 
108
77
  let walk = $state.raw<BuilderWalk | null>(null)
109
78
 
@@ -305,13 +274,18 @@
305
274
  {#each row.fields as key (key)}
306
275
  {@const field = fields.get(key)}
307
276
  {#if field !== undefined}
308
- {@const kind = kindOf(field)}
309
- <div class="question" data-testid="devtools-builder-question-{key}">
310
- {#if kind === 'text' || kind === 'number'}
277
+ {@const positions = row.options[key] ?? []}
278
+ {@const style = inputStyleOf(field, positions.length)}
279
+ <div
280
+ class="question"
281
+ data-testid="devtools-builder-question-{key}"
282
+ data-input-style={style}
283
+ >
284
+ {#if style === 'text' || style === 'number'}
311
285
  <TextField
312
286
  label={questionOf(field)}
313
287
  name="devtools-builder-{key}"
314
- type={kind}
288
+ type={style}
315
289
  placeholder={field.placeholder}
316
290
  value={typedText(answers[key])}
317
291
  error={errors[key]}
@@ -320,13 +294,15 @@
320
294
  {:else}
321
295
  <Text size="small" weight="medium">{questionOf(field)}</Text>
322
296
  {/if}
323
- {#if kind === 'options'}
324
- <div class="choices">
325
- {#each row.options[key] ?? [] as position (position)}
297
+ {#if style === 'tiles' || style === 'pills'}
298
+ <div class="choices {style}">
299
+ {#each positions as position (position)}
326
300
  {@const option = field.options?.at(position)}
327
301
  {#if option !== undefined}
328
302
  <ChoiceButton
329
303
  label={option.label}
304
+ detail={option.description}
305
+ variant={style === 'tiles' ? 'tile' : 'pill'}
330
306
  selected={isChosen(field, option.value)}
331
307
  testId="devtools-builder-choice-{key}-{position}"
332
308
  onclick={() => choose(field, option.value)}
@@ -334,22 +310,41 @@
334
310
  {/if}
335
311
  {/each}
336
312
  </div>
337
- {:else if kind === 'boolean'}
338
- <div class="choices">
313
+ {:else if style === 'checkboxes'}
314
+ <div class="checkboxes">
315
+ {#each positions as position (position)}
316
+ {@const option = field.options?.at(position)}
317
+ {#if option !== undefined}
318
+ <label class="checkbox">
319
+ <input
320
+ type="checkbox"
321
+ checked={isChosen(field, option.value)}
322
+ data-testid="devtools-builder-choice-{key}-{position}"
323
+ onchange={() => choose(field, option.value)}
324
+ />
325
+ <span>{option.label}</span>
326
+ </label>
327
+ {/if}
328
+ {/each}
329
+ </div>
330
+ {:else if style === 'yes-no'}
331
+ <div class="choices tiles">
339
332
  <ChoiceButton
340
333
  label="Yes"
334
+ variant="tile"
341
335
  selected={answers[key] === true}
342
336
  testId="devtools-builder-choice-{key}-yes"
343
337
  onclick={() => setAnswer(key, true)}
344
338
  />
345
339
  <ChoiceButton
346
340
  label="No"
341
+ variant="tile"
347
342
  selected={answers[key] === false}
348
343
  testId="devtools-builder-choice-{key}-no"
349
344
  onclick={() => setAnswer(key, false)}
350
345
  />
351
346
  </div>
352
- {:else if kind === 'system'}
347
+ {:else if style === 'system'}
353
348
  <Text size="small" color="subdued">
354
349
  This is set on its own step. The persona stops here, so load it and set it
355
350
  there.
@@ -447,4 +442,33 @@
447
442
  flex-wrap: wrap;
448
443
  gap: var(--space-1);
449
444
  }
445
+
446
+ .choices.tiles {
447
+ display: grid;
448
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
449
+ gap: var(--space-2);
450
+ }
451
+
452
+ .checkboxes {
453
+ display: grid;
454
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
455
+ gap: var(--space-1) var(--space-3);
456
+ }
457
+
458
+ .checkbox {
459
+ display: flex;
460
+ align-items: center;
461
+ gap: var(--space-2);
462
+ padding: var(--space-1) 0;
463
+ font-size: var(--text-sm);
464
+ color: var(--color-gray-900);
465
+ cursor: pointer;
466
+ }
467
+
468
+ .checkbox input {
469
+ width: 1rem;
470
+ height: 1rem;
471
+ margin: 0;
472
+ accent-color: var(--color-blue-600);
473
+ }
450
474
  </style>
@@ -1,72 +1,207 @@
1
1
  <script lang="ts">
2
- /** @file One persona in the Personas & Scenarios tab: who it is, where it lands, Load and Delete. */
3
- import { Button, Card, Text } from '../../ui'
4
- import { describeLanding, type Persona } from '../../derive'
2
+ /**
3
+ * @file One persona in the Personas & Scenarios tab: who it is and where it lands, with Walk
4
+ * through, Jump to step, Edit, and Delete or Reset. Jump to step lists the screens on the
5
+ * persona's route; picking one walks the persona there.
6
+ */
7
+ import { Badge, Button, Card, CopyButton, Text } from '../../ui'
8
+ import { describeLanding, deslug, type Persona } from '../../derive'
9
+ import type { RouteScreen } from './persona-route'
5
10
 
6
11
  interface PersonaCardProps {
7
12
  persona: Persona
8
- /** Why Load is withheld; `undefined` while it is allowed. */
9
- loadReason: string | undefined
10
- /** True while this persona is loading. */
11
- loading: boolean
12
- onLoad: () => void
13
- /** Opens it in the form; absent for a persona the site defines. */
14
- onEdit?: () => void
15
- /** Deletes it; absent for a persona the site defines. */
13
+ /** Why starting it is withheld; `undefined` while it is allowed. */
14
+ startReason: string | undefined
15
+ /** True while this persona is starting. */
16
+ starting: boolean
17
+ /** True while the tab walks through as this persona. */
18
+ active: boolean
19
+ onWalkThrough: () => void
20
+ /** Works out the screens on its route; `null` when it has no journey. */
21
+ route: () => Promise<RouteScreen[] | null>
22
+ /**
23
+ * Walks the persona to a screen on its route.
24
+ *
25
+ * @param screen - The screen.
26
+ */
27
+ onJump: (screen: RouteScreen) => void
28
+ /** Opens it in the form. */
29
+ onEdit: () => void
30
+ /** Deletes a persona saved in this browser; absent for the site's own. */
16
31
  onDelete?: () => void
32
+ /** Drops this browser's edit of a site persona; absent otherwise. */
33
+ onReset?: () => void
34
+ /** The link that starts a walk-through as it in another browser; absent for a saved persona. */
35
+ link?: string
17
36
  }
18
37
 
19
- const { persona, loadReason, loading, onLoad, onEdit, onDelete }: PersonaCardProps = $props()
38
+ const {
39
+ persona,
40
+ startReason,
41
+ starting,
42
+ active,
43
+ onWalkThrough,
44
+ route,
45
+ onJump,
46
+ onEdit,
47
+ onDelete,
48
+ onReset,
49
+ link,
50
+ }: PersonaCardProps = $props()
20
51
 
21
52
  let confirming = $state(false)
53
+ let jumpOpen = $state(false)
54
+ /** The route's screens; `undefined` while they are worked out. */
55
+ let screens = $state.raw<RouteScreen[] | null | undefined>(undefined)
22
56
 
23
57
  const answers = $derived(Object.keys(persona.responses).length)
24
58
  const answerLabel = $derived(answers === 1 ? '1 response' : `${answers} responses`)
25
59
  const summary = $derived(
26
- persona.load === null
27
- ? `${answerLabel} · ${describeLanding(persona.landing)}`
28
- : 'Set up by this site',
60
+ [answerLabel, describeLanding(persona.landing), persona.edited ? 'Edited in this browser' : '']
61
+ .filter((part) => part.length > 0)
62
+ .join(' · '),
29
63
  )
64
+ const removeLabel = $derived(onReset ? 'Reset' : 'Delete')
65
+ const removeQuestion = $derived(
66
+ onReset ? `Undo your edits to ${persona.name}?` : `Delete ${persona.name}?`,
67
+ )
68
+
69
+ /**
70
+ * Opens or closes the list of screens to jump to, working the route out on first open.
71
+ *
72
+ * @returns {Promise<void>} Settles once the route is worked out.
73
+ * @example await toggleJump()
74
+ */
75
+ async function toggleJump(): Promise<void> {
76
+ jumpOpen = !jumpOpen
77
+ if (!jumpOpen) return
78
+ screens = undefined
79
+ try {
80
+ screens = await route()
81
+ } catch {
82
+ screens = null
83
+ }
84
+ }
85
+
86
+ /**
87
+ * A screen's name in the list.
88
+ *
89
+ * @param {RouteScreen} screen - The screen.
90
+ * @returns {string} Its name.
91
+ * @example screenLabel(screen) // 'Postcode'
92
+ */
93
+ function screenLabel(screen: RouteScreen): string {
94
+ return screen.stepName ?? deslug(screen.stepKey)
95
+ }
30
96
  </script>
31
97
 
32
98
  <Card>
33
99
  <div class="persona" data-testid="devtools-persona" data-persona-id={persona.id}>
34
- <div class="details">
35
- <Text weight="semibold">{persona.name}</Text>
36
- {#if persona.description}
37
- <Text size="small">{persona.description}</Text>
38
- {/if}
39
- <Text size="caption" color="subdued">{summary}</Text>
40
- </div>
41
- <div class="actions">
42
- {#if confirming && onDelete}
43
- <Text size="small">Delete {persona.name}?</Text>
44
- <Button variant="destructive" size="small" onclick={onDelete}>Yes, delete</Button>
45
- <Button variant="ghost" size="small" onclick={() => (confirming = false)}>Keep</Button>
46
- {:else}
47
- <Button
48
- variant="primary"
49
- size="small"
50
- disabled={loadReason !== undefined}
51
- title={loadReason}
52
- {loading}
53
- onclick={onLoad}
54
- >
55
- Load
56
- </Button>
57
- {#if onEdit}
58
- <Button variant="ghost" size="small" onclick={onEdit}>Edit</Button>
100
+ <div class="head">
101
+ <div class="details">
102
+ <Text weight="semibold">{persona.name}</Text>
103
+ {#if persona.description}
104
+ <Text size="small">{persona.description}</Text>
59
105
  {/if}
60
- {#if onDelete}
61
- <Button variant="ghost" size="small" onclick={() => (confirming = true)}>Delete</Button>
106
+ <Text size="caption" color="subdued">{summary}</Text>
107
+ {#if active}
108
+ <span><Badge status="info" size="small">Walking through now</Badge></span>
62
109
  {/if}
63
- {/if}
110
+ </div>
111
+ <div class="actions">
112
+ {#if confirming && (onDelete || onReset)}
113
+ <Text size="small">{removeQuestion}</Text>
114
+ <Button
115
+ variant="destructive"
116
+ size="small"
117
+ onclick={() => {
118
+ confirming = false
119
+ ;(onReset ?? onDelete)?.()
120
+ }}
121
+ >
122
+ Yes, {removeLabel.toLowerCase()}
123
+ </Button>
124
+ <Button variant="ghost" size="small" onclick={() => (confirming = false)}>Keep</Button>
125
+ {:else}
126
+ <Button
127
+ variant="primary"
128
+ size="small"
129
+ disabled={startReason !== undefined}
130
+ title={startReason}
131
+ loading={starting}
132
+ onclick={onWalkThrough}
133
+ >
134
+ Walk through
135
+ </Button>
136
+ <Button
137
+ variant="secondary"
138
+ size="small"
139
+ disabled={startReason !== undefined}
140
+ title={startReason}
141
+ onclick={() => void toggleJump()}
142
+ >
143
+ Jump to step
144
+ </Button>
145
+ <Button variant="ghost" size="small" onclick={onEdit}>Edit</Button>
146
+ {#if link}
147
+ <CopyButton
148
+ text={link}
149
+ label="Copy a link that walks through as {persona.name}"
150
+ testId="devtools-persona-link"
151
+ />
152
+ {/if}
153
+ {#if onDelete || onReset}
154
+ <Button variant="ghost" size="small" onclick={() => (confirming = true)}>
155
+ {removeLabel}
156
+ </Button>
157
+ {/if}
158
+ {/if}
159
+ </div>
64
160
  </div>
161
+
162
+ {#if jumpOpen}
163
+ <div class="jump" data-testid="devtools-persona-jump">
164
+ {#if screens === undefined}
165
+ <Text size="small" color="subdued">Working out the route…</Text>
166
+ {:else if screens === null || screens.length === 0}
167
+ <Text size="small" color="subdued">No steps to jump to from this page.</Text>
168
+ {:else}
169
+ <Text size="small" color="subdued">
170
+ Pick a step. The page reloads and walks there as {persona.name}.
171
+ </Text>
172
+ <ol class="steps">
173
+ {#each screens as screen (`${screen.experienceKey}/${screen.stepKey}`)}
174
+ <li>
175
+ <button
176
+ type="button"
177
+ class="step"
178
+ data-testid="devtools-persona-jump-step"
179
+ disabled={startReason !== undefined}
180
+ onclick={() => onJump(screen)}
181
+ >
182
+ <span class="step-name">{screenLabel(screen)}</span>
183
+ <span class="step-journey">{screen.experienceName}</span>
184
+ {#if screen.isStop}
185
+ <Badge size="small">Where it stops</Badge>
186
+ {/if}
187
+ </button>
188
+ </li>
189
+ {/each}
190
+ </ol>
191
+ {/if}
192
+ </div>
193
+ {/if}
65
194
  </div>
66
195
  </Card>
67
196
 
68
197
  <style>
69
198
  .persona {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: var(--space-3);
202
+ }
203
+
204
+ .head {
70
205
  display: flex;
71
206
  flex-wrap: wrap;
72
207
  align-items: center;
@@ -88,4 +223,52 @@
88
223
  align-items: center;
89
224
  gap: var(--space-2);
90
225
  }
226
+
227
+ .jump {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: var(--space-2);
231
+ padding-top: var(--space-3);
232
+ border-top: 1px solid var(--color-gray-200);
233
+ }
234
+
235
+ .steps {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: var(--space-1);
239
+ margin: 0;
240
+ padding-left: var(--space-5);
241
+ }
242
+
243
+ .step {
244
+ display: flex;
245
+ flex-wrap: wrap;
246
+ align-items: center;
247
+ gap: var(--space-2);
248
+ width: 100%;
249
+ padding: var(--space-1-5) var(--space-2);
250
+ border: 0;
251
+ border-radius: var(--radius-sm);
252
+ background: transparent;
253
+ color: var(--color-gray-900);
254
+ font: inherit;
255
+ font-size: var(--text-sm);
256
+ text-align: left;
257
+ cursor: pointer;
258
+ }
259
+
260
+ .step:hover:not(:disabled),
261
+ .step:focus-visible {
262
+ background: var(--color-gray-100);
263
+ }
264
+
265
+ .step:disabled {
266
+ cursor: not-allowed;
267
+ opacity: 0.6;
268
+ }
269
+
270
+ .step-journey {
271
+ color: var(--color-gray-500);
272
+ font-size: var(--text-xs);
273
+ }
91
274
  </style>