@picked/svelte 0.0.1-dev.1793772853474 → 0.0.1-dev.1793835766402

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 (62) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/plugins/devtools/DevToolsExperience.svelte +169 -84
  3. package/dist/plugins/devtools/components/AdaptedToolHost.svelte +9 -3
  4. package/dist/plugins/devtools/customTools.d.ts +16 -7
  5. package/dist/plugins/devtools/customTools.js +1 -1
  6. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +1 -1
  7. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
  8. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
  9. package/dist/plugins/devtools/derive/index.d.ts +3 -3
  10. package/dist/plugins/devtools/derive/index.js +1 -1
  11. package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -1
  12. package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
  13. package/dist/plugins/devtools/derive/persona-builder/persona-builder.js +1 -1
  14. package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
  15. package/dist/plugins/devtools/derive/personas/index.js +1 -1
  16. package/dist/plugins/devtools/derive/personas/personas.d.ts +89 -8
  17. package/dist/plugins/devtools/derive/personas/personas.js +1 -1
  18. package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
  19. package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
  20. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +20 -25
  21. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
  22. package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
  23. package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
  24. package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -1
  25. package/dist/plugins/devtools/index.d.ts +1 -1
  26. package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
  27. package/dist/plugins/devtools/options.d.ts +27 -24
  28. package/dist/plugins/devtools/options.js +1 -1
  29. package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
  30. package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
  31. package/dist/plugins/devtools/review/index.d.ts +2 -2
  32. package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
  33. package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
  34. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +70 -24
  35. package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
  36. package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +15 -46
  37. package/dist/plugins/devtools/review/personas/PersonaCard.svelte +227 -44
  38. package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +191 -78
  39. package/dist/plugins/devtools/review/personas/load-persona.d.ts +43 -22
  40. package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
  41. package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
  42. package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
  43. package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
  44. package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
  45. package/dist/plugins/devtools/review/session/SessionPanel.svelte +11 -11
  46. package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
  47. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +11 -25
  48. package/dist/plugins/devtools/sidebar/Sidebar.svelte +2 -33
  49. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +5 -42
  50. package/dist/plugins/devtools/sidebar/nav.d.ts +26 -28
  51. package/dist/plugins/devtools/sidebar/nav.js +1 -1
  52. package/package.json +2 -2
  53. package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
  54. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
  55. package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
  56. package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
  57. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
  58. package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
  59. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
  60. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
  61. package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
  62. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
@@ -1,21 +1,29 @@
1
1
  <script lang="ts">
2
2
  /**
3
3
  * @file The Personas & Scenarios tab: customers and situations to try the journey as. The site's
4
- * own come from the plugin's `personas` option. A reviewer makes theirs in this browser, either by
5
- * answering the journey's questions here, screen by screen, or from where they are on the page.
6
- * A persona keeps the answers a customer gives on fields they can see, the screens moved on from,
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.
4
+ * own come from the plugin's `personas` option, under their headings. A reviewer makes theirs in
5
+ * this browser, either by answering the journey's questions here, screen by screen, or from where
6
+ * they are on the page, and can edit any persona; an edit of the site's own stays in this
7
+ * browser. A persona keeps the answers a customer gives on fields they can see, the screens moved
8
+ * on from, the screen it stops on and the page it starts on.
9
+ *
10
+ * Walk through reloads that page clean and pre-fills every step with the persona's responses, so
11
+ * the reviewer moves through it themselves. Jump to step walks the persona to a step on its
12
+ * route first. Site tools placed here follow, each a section under its label.
9
13
  */
10
14
  import type { FieldValue, Picked } from '@picked/client'
11
15
  import { getStepFields } from '@picked/client/form'
16
+ import { SvelteMap } from 'svelte/reactivity'
12
17
  import { Banner, Button, Card, EmptyState, RadioGroup, Stack, Text, TextField } from '../../ui'
13
18
  import {
19
+ applyEdits,
14
20
  buildPersona,
15
21
  customerAnswers,
16
22
  deslug,
17
23
  entryUrlOf,
24
+ groupPersonas,
18
25
  isBlank,
26
+ matchesSearch,
19
27
  passedScreens,
20
28
  readSavedPersonas,
21
29
  writeSavedPersonas,
@@ -27,8 +35,12 @@
27
35
  type RouteEntry,
28
36
  type SafetyPosture,
29
37
  } from '../../derive'
38
+ import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
39
+ import type { CustomTool } from '../../customTools'
30
40
  import type { JourneyView } from '../../session'
31
41
  import { loadUrl, startPersonaLoad } from './load-persona'
42
+ import { personaLink } from './persona-session'
43
+ import { personaRoute, routeScreens, type RouteScreen } from './persona-route'
32
44
  import PersonaBuilder from './PersonaBuilder.svelte'
33
45
  import PersonaCard from './PersonaCard.svelte'
34
46
 
@@ -47,12 +59,22 @@
47
59
  hiddenKeys: ReadonlySet<string>
48
60
  /** Keys a screen carries only for its code. */
49
61
  codeOwned: ReadonlySet<string>
50
- /** Screens that move on by themselves, and actions that open another journey. */
62
+ /**
63
+ * Screens that move on by themselves, responses every walk gives, and actions that open
64
+ * another journey.
65
+ */
51
66
  personaWalk: {
52
67
  selfAdvancingSteps: ReadonlySet<string>
68
+ responses: Readonly<Record<string, FieldValue>>
53
69
  handoffs: Readonly<Record<string, string>>
54
70
  }
55
- /** How the last load's walk ended; `null` when no load ran on this page. */
71
+ /** The site's tools placed in this tab. */
72
+ tools?: readonly CustomTool[]
73
+ /** The persona this tab walks through as; `null` outside a persona session. */
74
+ activePersona: Persona | null
75
+ /** Ends the persona session. */
76
+ onStop: () => void
77
+ /** How the last jump's walk ended; `null` when no jump ran on this page. */
56
78
  walkNotice: Notice | null
57
79
  onWalkNoticeDismiss: () => void
58
80
  /** The storage saved personas live in. */
@@ -83,6 +105,9 @@
83
105
  hiddenKeys,
84
106
  codeOwned,
85
107
  personaWalk,
108
+ tools = [],
109
+ activePersona,
110
+ onStop,
86
111
  walkNotice,
87
112
  onWalkNoticeDismiss,
88
113
  getStorage = () => window.localStorage,
@@ -123,26 +148,65 @@
123
148
  let buildChoices = $state.raw<Record<string, boolean>>({})
124
149
  let buildWalk = $state.raw<BuilderWalk | null>(null)
125
150
  let notice = $state<Notice | null>(null)
126
- let loadingId = $state<string | null>(null)
151
+ let startingId = $state<string | null>(null)
152
+ let search = $state('')
127
153
  /** The answers Save would keep; `null` while they are read. */
128
154
  let captured = $state<Record<string, FieldValue> | null>(null)
129
155
 
130
- const isEmpty = $derived(personas.length === 0 && saved.length === 0)
156
+ const listed = $derived(applyEdits(personas, saved))
157
+ const siteGroups = $derived(
158
+ groupPersonas(
159
+ listed.site.filter((persona) => matchesSearch(persona, search)),
160
+ 'For this site',
161
+ ),
162
+ )
163
+ const own = $derived(listed.own.filter((persona) => matchesSearch(persona, search)))
164
+ /** Every heading in the list: the site's groups, then this browser's own. */
165
+ const sections = $derived(
166
+ own.length > 0 ? [...siteGroups, { title: 'Saved in this browser', personas: own }] : siteGroups,
167
+ )
168
+ const isEmpty = $derived(personas.length === 0 && saved.length === 0 && tools.length === 0)
169
+ /** Enough personas that a reviewer needs to search them. */
170
+ const searchable = $derived(personas.length + listed.own.length > 6)
131
171
  const postureReason = $derived(posture.canEditAnswers ? undefined : (posture.reason ?? undefined))
172
+ const routeSettings = $derived({
173
+ asked,
174
+ codeOwned,
175
+ selfAdvancing: personaWalk.selfAdvancingSteps,
176
+ handoffs: personaWalk.handoffs,
177
+ hiddenKeys,
178
+ responses: personaWalk.responses,
179
+ })
180
+ /** Each persona's route as last worked out, so a jump moves on from its screens. */
181
+ const routes = new SvelteMap<string, BuilderWalk>()
132
182
 
133
183
  /**
134
- * Why a persona's Load is withheld: the session's posture, or another persona loading.
184
+ * Why starting a persona is withheld: the session's posture, or another persona starting.
135
185
  *
136
186
  * @param {Persona} persona - The persona on the card.
137
- * @returns {string | undefined} The reason, or `undefined` while Load is allowed.
138
- * @example loadReason(persona)
187
+ * @returns {string | undefined} The reason, or `undefined` while it can start.
188
+ * @example startReason(persona)
139
189
  */
140
- function loadReason(persona: Persona): string | undefined {
190
+ function startReason(persona: Persona): string | undefined {
141
191
  if (postureReason !== undefined) return postureReason
142
- return loadingId !== null && loadingId !== persona.id
143
- ? 'Another persona is loading.'
192
+ return startingId !== null && startingId !== persona.id
193
+ ? 'Another persona is starting.'
144
194
  : undefined
145
195
  }
196
+
197
+ /**
198
+ * The screens a persona's responses take it through.
199
+ *
200
+ * @param {Persona} persona - The persona.
201
+ * @returns {Promise<RouteScreen[] | null>} The screens, or `null` when it has no journey.
202
+ * @example await routeOf(persona)
203
+ */
204
+ async function routeOf(persona: Persona): Promise<RouteScreen[] | null> {
205
+ const walk = await personaRoute(client, persona, routeSettings, window.location)
206
+ if (walk === null) return null
207
+ routes.set(persona.id, walk)
208
+ return routeScreens(walk, persona.landing)
209
+ }
146
210
  const landing = $derived<PersonaLanding | null>(
147
211
  journey === null
148
212
  ? null
@@ -156,23 +220,23 @@
156
220
  const buildSummary = $derived.by(() => {
157
221
  if (buildWalk === null) return 'Working out the route…'
158
222
  const count = Object.keys(buildWalk.answers).length
159
- const answers = count === 1 ? '1 answer' : `${count} answers`
223
+ const answers = count === 1 ? '1 response' : `${count} responses`
160
224
  const stop = buildWalk.landing
161
225
  if (stop === null) return `Saves ${answers}.`
162
226
  const screen = stop.stepName ?? deslug(stop.stepKey ?? '')
163
- return `Saves ${answers}. Loading it walks the journey to ${screen}.`
227
+ return `Saves ${answers}. It stops at ${screen}.`
164
228
  })
165
229
 
166
230
  const captureSummary = $derived.by(() => {
167
- if (captured === null) return 'Reading your answers…'
231
+ if (captured === null) return 'Reading your responses…'
168
232
  const count = Object.keys(captured).length
169
- const answers = count === 1 ? '1 answer' : `${count} answers`
233
+ const answers = count === 1 ? '1 response' : `${count} responses`
170
234
  if (landing === null)
171
- return `Saves ${answers}. No journey is open, so it loads the answers only.`
235
+ return `Saves ${answers}. No experience is open, so it keeps the responses only.`
172
236
  const journeyName = landing.experienceName ?? deslug(landing.experienceKey)
173
- if (landing.stepKey === null) return `Saves ${answers}. Loading it walks ${journeyName}.`
237
+ if (landing.stepKey === null) return `Saves ${answers}. It starts in ${journeyName}.`
174
238
  const screen = landing.stepName ?? deslug(landing.stepKey)
175
- return `Saves ${answers}. Loading it walks ${journeyName} to ${screen}.`
239
+ return `Saves ${answers}. It stops at ${screen} in ${journeyName}.`
176
240
  })
177
241
 
178
242
  /**
@@ -349,7 +413,7 @@
349
413
  }
350
414
 
351
415
  const edited = editingId !== null
352
- saved = edited
416
+ saved = saved.some((entry) => entry.id === result.persona.id)
353
417
  ? saved.map((entry) => (entry.id === result.persona.id ? result.persona : entry))
354
418
  : [...saved, result.persona]
355
419
  creating = false
@@ -364,56 +428,69 @@
364
428
  }
365
429
 
366
430
  /**
367
- * Deletes a saved persona.
431
+ * Deletes a saved persona, or drops this browser's edit of a site persona.
368
432
  *
369
- * @param {Persona} persona - The persona to delete.
433
+ * @param {Persona} persona - The persona.
370
434
  * @example remove(persona)
371
435
  */
372
436
  function remove(persona: Persona): void {
373
437
  saved = saved.filter((entry) => entry.id !== persona.id)
374
438
  writeSavedPersonas(getStorage, client.projectId, saved)
375
- notice = { status: 'success', text: `Deleted ${persona.name}.` }
439
+ notice = {
440
+ status: 'success',
441
+ text: persona.edited ? `Put ${persona.name} back as the site has it.` : `Deleted ${persona.name}.`,
442
+ }
376
443
  }
377
444
 
378
445
  /**
379
- * Loads a persona: the site's own loader when it has one, else a clean reload that walks its
380
- * journey on the persona's page, keeping this page's query.
446
+ * Starts a persona session: a clean reload of the persona's page that pre-fills every step, after
447
+ * walking to `stop` when one is picked.
381
448
  *
382
- * @param {Persona} persona - The persona to load.
383
- * @returns {Promise<void>} Settles once it has loaded, failed, or the reload is under way.
384
- * @example await load(persona)
449
+ * @param {Persona} persona - The persona.
450
+ * @param {RouteScreen | null} stop - The step to walk to; `null` for a walk-through from the start.
451
+ * @example start(persona, null)
385
452
  */
386
- async function load(persona: Persona): Promise<void> {
387
- loadingId = persona.id
453
+ function start(persona: Persona, stop: RouteScreen | null): void {
454
+ startingId = persona.id
388
455
  notice = null
389
456
  onWalkNoticeDismiss()
390
- try {
391
- if (persona.load === null) {
392
- const started = startPersonaLoad(client, persona, {
393
- storage: getSessionStorage(),
394
- url: loadUrl(persona.entryUrl, window.location),
395
- navigate,
396
- })
397
- if (!started) {
398
- notice = { status: 'error', text: 'This browser would not let the load start.' }
399
- loadingId = null
400
- }
401
-
402
- return
403
- }
404
-
405
- await persona.load(client)
406
- notice = { status: 'success', text: `Loaded ${persona.name}.` }
407
- loadingId = null
408
- } catch (error) {
409
- const detail = error instanceof Error ? ` ${error.message}` : ''
410
- notice = { status: 'error', text: `${persona.name} did not load.${detail}` }
411
- loadingId = null
457
+ const started = startPersonaLoad(
458
+ client,
459
+ {
460
+ id: persona.id,
461
+ stop:
462
+ stop === null
463
+ ? null
464
+ : {
465
+ experienceKey: stop.experienceKey,
466
+ experienceName: stop.experienceName,
467
+ stepKey: stop.stepKey,
468
+ stepName: stop.stepName,
469
+ },
470
+ passed: stop === null ? [] : (routes.get(persona.id)?.passed ?? []),
471
+ },
472
+ { storage: getSessionStorage(), url: loadUrl(persona.entryUrl, window.location), navigate },
473
+ )
474
+ if (!started) {
475
+ notice = { status: 'error', text: 'This browser would not let the persona start.' }
476
+ startingId = null
412
477
  }
413
478
  }
414
479
  </script>
415
480
 
416
481
  <div class="personas" data-testid="devtools-personas">
482
+ {#if activePersona}
483
+ <Banner status="info" testId="devtools-persona-session">
484
+ <div class="session">
485
+ <span>
486
+ Walking through as <strong>{activePersona.name}</strong>. Each step shows their
487
+ responses. Click Continue, or change a response first.
488
+ </span>
489
+ <Button variant="secondary" size="small" onclick={onStop}>Stop</Button>
490
+ </div>
491
+ </Banner>
492
+ {/if}
493
+
417
494
  {#if walkNotice}
418
495
  <Banner
419
496
  status={walkNotice.status}
@@ -433,13 +510,18 @@
433
510
  <div class="form">
434
511
  <Stack direction="vertical" gap="medium">
435
512
  <Text weight="semibold">{editingId === null ? 'New persona' : 'Edit persona'}</Text>
513
+ {#if personas.some((persona) => persona.id === editingId)}
514
+ <Text size="small" color="subdued">
515
+ Your edits stay in this browser. Reset puts it back as the site has it.
516
+ </Text>
517
+ {/if}
436
518
  <RadioGroup
437
519
  label="Make it by"
438
520
  name="persona-mode"
439
521
  orientation="horizontal"
440
522
  value={mode}
441
523
  options={[
442
- { value: 'build', label: 'Answering the questions here' },
524
+ { value: 'build', label: 'Filling in the fields here' },
443
525
  { value: 'capture', label: 'Using where I am now' },
444
526
  ]}
445
527
  onchange={(value) => (mode = value === 'capture' ? 'capture' : 'build')}
@@ -506,45 +588,63 @@
506
588
  <div class="empty">
507
589
  <EmptyState
508
590
  message="No personas or scenarios yet"
509
- hint="Walk the journey the way a customer would, then save it here to load it again in one click."
591
+ hint="Walk the experience the way a visitor would, then save it here to load it again in one click."
510
592
  />
511
593
  <Button variant="primary" onclick={startCreate}>Create</Button>
512
594
  </div>
513
595
  {:else}
514
596
  <div class="toolbar">
597
+ {#if searchable}
598
+ <div class="search">
599
+ <TextField
600
+ label="Search"
601
+ name="persona-search"
602
+ placeholder="Search, such as moving or gas"
603
+ value={search}
604
+ oninput={(value) => (search = value)}
605
+ />
606
+ </div>
607
+ {/if}
515
608
  <Button variant="secondary" size="small" onclick={startCreate}>Create</Button>
516
609
  </div>
517
610
  {/if}
518
611
 
519
- {#if personas.length > 0}
520
- <section class="group" aria-label="For this site">
521
- <h3 class="group-title">For this site</h3>
522
- {#each personas as persona (persona.id)}
612
+ {#each sections as section (section.title)}
613
+ <section class="group" aria-label={section.title}>
614
+ <h3 class="group-title">{section.title}</h3>
615
+ {#each section.personas as persona (persona.id)}
523
616
  <PersonaCard
524
617
  {persona}
525
- loadReason={loadReason(persona)}
526
- loading={loadingId === persona.id}
527
- onLoad={() => void load(persona)}
618
+ startReason={startReason(persona)}
619
+ starting={startingId === persona.id}
620
+ active={activePersona?.id === persona.id}
621
+ onWalkThrough={() => start(persona, null)}
622
+ route={() => routeOf(persona)}
623
+ onJump={(screen) => start(persona, screen)}
624
+ onEdit={() => startEdit(persona)}
625
+ onDelete={persona.source === 'saved' ? () => remove(persona) : undefined}
626
+ onReset={persona.edited ? () => remove(persona) : undefined}
627
+ link={persona.source === 'site'
628
+ ? personaLink(window.location, persona.entryUrl, persona.id)
629
+ : undefined}
528
630
  />
529
631
  {/each}
530
632
  </section>
633
+ {/each}
634
+
635
+ {#if search.length > 0 && sections.length === 0}
636
+ <Text size="small" color="subdued">No persona or scenario matches “{search}”.</Text>
531
637
  {/if}
532
638
 
533
- {#if saved.length > 0}
534
- <section class="group" aria-label="Saved in this browser">
535
- <h3 class="group-title">Saved in this browser</h3>
536
- {#each saved as persona (persona.id)}
537
- <PersonaCard
538
- {persona}
539
- loadReason={loadReason(persona)}
540
- loading={loadingId === persona.id}
541
- onLoad={() => void load(persona)}
542
- onEdit={() => startEdit(persona)}
543
- onDelete={() => remove(persona)}
544
- />
545
- {/each}
639
+ {#each tools as tool (tool.id)}
640
+ <section class="group" aria-label={tool.label} data-testid="devtools-persona-tool-{tool.id}">
641
+ <h3 class="group-title">{tool.label}</h3>
642
+ {#if tool.description}
643
+ <Text size="small" color="subdued">{tool.description}</Text>
644
+ {/if}
645
+ <AdaptedToolHost {tool} flush />
546
646
  </section>
547
- {/if}
647
+ {/each}
548
648
  </div>
549
649
 
550
650
  <style>
@@ -572,9 +672,22 @@
572
672
  }
573
673
 
574
674
  .toolbar {
675
+ align-items: flex-end;
575
676
  justify-content: flex-end;
576
677
  }
577
678
 
679
+ .search {
680
+ flex: 1 1 auto;
681
+ }
682
+
683
+ .session {
684
+ display: flex;
685
+ flex-wrap: wrap;
686
+ align-items: center;
687
+ justify-content: space-between;
688
+ gap: var(--space-2);
689
+ }
690
+
578
691
  .group {
579
692
  display: flex;
580
693
  flex-direction: column;
@@ -1,20 +1,20 @@
1
1
  /**
2
- * @file Loads a persona the way a customer arrives: the page reloads clean, then the persona's
3
- * journey is walked from its first screen. Each screen gets its own answers as the walk reaches it,
4
- * then Continue, so every condition and action between the screens runs for real. A host persona
5
- * with its own `load` skips this.
2
+ * @file Starts a persona the way a customer arrives: the page reloads clean on the persona's entry
3
+ * page. A jump then walks the persona's journey from its first screen to the screen picked. Each
4
+ * screen gets its own answers as the walk reaches it, then Continue, so every condition and action
5
+ * between the screens runs for real.
6
6
  *
7
- * Load has two halves on either side of the reload. The first, {@link startPersonaLoad}, clears the
8
- * answers and the journey history the SDK keeps, notes the persona in `sessionStorage` and
9
- * reloads. The second, {@link walkPersona}, runs on the next page, once the devtools mount there
10
- * and pick up the note.
7
+ * A start has two halves on either side of the reload. The first, {@link startPersonaLoad}, clears
8
+ * the answers and the journey history the SDK keeps, notes the persona in `sessionStorage` and
9
+ * reloads. The second runs on the next page, once the devtools mount there and pick up the note:
10
+ * {@link walkPersona} for a jump, {@link openPersonaJourney} for a walk-through.
11
11
  *
12
- * The walk stops at the persona's screen, or at the first screen it cannot finish: a question with
13
- * no answer, an answer that fails its check, a screen that asks nothing and was not moved on from
14
- * when the persona was saved, or a Continue that lands nowhere.
12
+ * The walk stops at the screen picked, or at the first screen it cannot finish: a question with no
13
+ * answer, an answer that fails its check, a screen that asks nothing and is not on the persona's
14
+ * route, or a Continue that lands nowhere.
15
15
  */
16
16
  import type { FieldValue, Picked } from '@picked/client';
17
- import { type Persona } from '../../derive';
17
+ import { type Persona, type PersonaLanding, type PersonaScreen } from '../../derive';
18
18
  /** The part of an experience instance a walk reads and moves. */
19
19
  export interface WalkInstance {
20
20
  id: string;
@@ -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
- /** Answers every walk gives when a screen asks them, over the persona's own. */
87
- answers: Readonly<Record<string, FieldValue>>;
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
  *
@@ -142,6 +142,15 @@ export interface WalkOutcome {
142
142
  }
143
143
  /** The `sessionStorage` key that carries a load across the reload. */
144
144
  export declare const PENDING_LOAD_KEY = "picked-devtools-persona-load";
145
+ /** A persona start, carried across its reload. */
146
+ export interface PendingLoad {
147
+ /** The persona's id. */
148
+ id: string;
149
+ /** The screen a jump walks to; `null` for a walk-through, which walks nowhere. */
150
+ stop: PersonaLanding | null;
151
+ /** The screens the walk moves on from on its way to `stop`, in order. */
152
+ passed: PersonaScreen[];
153
+ }
145
154
  /** Where a load reloads, and how. */
146
155
  export interface PersonaReload {
147
156
  /** The tab's `sessionStorage`, which carries the load across the reload. */
@@ -166,16 +175,16 @@ export interface PersonaReload {
166
175
  */
167
176
  export declare function screenQuestions(client: Picked, fieldKeys: readonly string[], codeOwned: ReadonlySet<string>): Promise<string[]>;
168
177
  /**
169
- * Clears what the session holds and reloads, so the next page can walk the persona from the start.
170
- * The answers go, and so does the journey history, which a host may keep across reloads.
178
+ * Clears what the session holds and reloads, so the next page can start the persona clean. The
179
+ * answers go, and so does the journey history, which a host may keep across reloads.
171
180
  *
172
181
  * @param {ResetClient} client - The client.
173
- * @param {Persona} persona - The persona to load.
182
+ * @param {PendingLoad} load - The persona, and the screen a jump walks to.
174
183
  * @param {PersonaReload} reload - Where the load reloads, and how.
175
184
  * @returns {boolean} `false` when the load could not be noted, so nothing was cleared.
176
- * @example startPersonaLoad(client, persona, { storage: sessionStorage, url: '/', navigate })
185
+ * @example startPersonaLoad(client, { id: persona.id, stop: null, passed: [] }, reload)
177
186
  */
178
- export declare function startPersonaLoad(client: ResetClient, persona: Persona, reload: PersonaReload): boolean;
187
+ export declare function startPersonaLoad(client: ResetClient, load: PendingLoad, reload: PersonaReload): boolean;
179
188
  /**
180
189
  * The URL a load goes to: the persona's entry page, or this page when it has none. This page's
181
190
  * `picked-` parameters (a preview token, a devtools switch) ride along where the entry URL does not
@@ -193,13 +202,13 @@ export declare function loadUrl(entryUrl: string | null, here: {
193
202
  search: string;
194
203
  }): string;
195
204
  /**
196
- * Takes the persona a reload was for off `sessionStorage`, so it loads once.
205
+ * Takes the start a reload was for off `sessionStorage`, so it runs once.
197
206
  *
198
207
  * @param {Pick<Storage, 'getItem' | 'removeItem'>} storage - The tab's `sessionStorage`.
199
- * @returns {string | null} The persona's id, or `null` when no load is waiting.
208
+ * @returns {PendingLoad | null} The start, or `null` when none is waiting.
200
209
  * @example takePendingLoad(sessionStorage)
201
210
  */
202
- export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'removeItem'>): string | null;
211
+ export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'removeItem'>): PendingLoad | null;
203
212
  /**
204
213
  * Walks a persona's journey from its first screen: each screen gets the answers it asks, then
205
214
  * Continue, until the persona's screen or the first screen the walk cannot finish.
@@ -212,6 +221,18 @@ export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'remo
212
221
  * @example await walkPersona(client, persona, settings)
213
222
  */
214
223
  export declare function walkPersona(client: WalkClient, persona: Persona, settings: WalkSettings, timing?: WalkTiming): Promise<WalkOutcome>;
224
+ /**
225
+ * Gets a walk-through going once the page has started: the page's own experience when one is
226
+ * open, else the persona's first one.
227
+ *
228
+ * @param {WalkClient} client - The client, on a page that has just loaded.
229
+ * @param {Persona} persona - The persona.
230
+ * @param {WalkSettings} settings - Which experiences the devtools never follow.
231
+ * @param {WalkTiming} [timing] - How long to wait; tests pass a short one.
232
+ * @returns {Promise<boolean>} `false` when no experience is open at the end.
233
+ * @example await openPersonaJourney(client, persona, settings)
234
+ */
235
+ export declare function openPersonaJourney(client: WalkClient, persona: Persona, settings: Pick<WalkSettings, 'exclude'>, timing?: WalkTiming): Promise<boolean>;
215
236
  /**
216
237
  * What the reviewer reads once a walk ends.
217
238
  *
@@ -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.answers},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 answers.`};case`needs-answer`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. ${r} has no answer for that screen.`};case`invalid`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. An answer there no longer passes its check.`};case`stuck`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. The journey 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 journey`:e(n))} didn't open on this page.`}}case`hop-limit`:return{status:`warning`,text:`Stopped loading ${r} after too many screens. The journey 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};
1
+ import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{readLanding as n,readPassed as r,startOf as i}from"../../derive/personas/personas.js";import{pickJourney as a}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as o}from"@picked/client/form";var s=`picked-devtools-persona-load`,c=30,l={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function u(e){return`${e.experienceKey}/${e.stepKey}`}async function d(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(c)}return null}function f(e,n,r){let i=e.experienceInstances.listStore.getState().filter(e=>n.has(e.experienceKey)),o=a(i,{exclude:r.exclude,precedence:r.precedence,openOrder:r.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 p(e,t,n){await e.openExperienceByKey(t),await d(()=>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 m(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function h(e,t,n){let r=e.fields.listStore.getState(),i=t.flatMap(e=>r.filter(t=>t.key===e));return(await o(e,i)).map(e=>e.key).filter(e=>!n.has(e))}function g(e,t,n){try{n.storage.setItem(s,JSON.stringify(t))}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function _(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 v(e){let t;try{t=JSON.parse(e)}catch{return null}if(typeof t!=`object`||!t||Array.isArray(t))return null;let i=Reflect.get(t,`id`);return typeof i!=`string`||i.length===0?null:{id:i,stop:n(Reflect.get(t,`stop`)),passed:r(Reflect.get(t,`passed`))}}function y(e){try{let t=e.getItem(s);return e.removeItem(s),t===null?null:v(t)}catch{return null}}function b(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function x(e,t,n){let r=!1,i=0;return await d(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):u(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function S(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(m(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(b(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=u(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await x(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 x(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function C(e,t,n,r=l){let{landing:a}=t,o=i(t);if(o===null)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let s=new Set([o,...t.passed.map(e=>e.experienceKey)]);a!==null&&s.add(a.experienceKey);let c={client:e,settings:n,timing:r,landing:a,passed:new Set(t.passed.map(u)),answers:{...t.responses,...n.responses},resting:()=>f(e,s,n)};await d(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await d(c.resting,r.startTries,r)===null&&await p(e,o,r);for(let e=0;e<r.maxHops;e++){let e=await d(c.resting,r.startTries,r);if(e===null)return{end:`no-journey`,at:null};let t=await S(c,e);if(t!==null)return t}return{end:`hop-limit`,at:null}}async function w(e,t,n,r=l){let a=()=>e.experienceInstances.listStore.getState().some(e=>!n.exclude.has(e.experienceKey)&&e.isOpenStore.getState())?!0:null;if(await d(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await d(a,r.startTries,r)!==null)return!0;let o=i(t);return o!==null&&(await p(e,o,r),await d(a,r.openTries,r)!==null)}function T(t,n){let{name:r}=t,a=n.at===null?``:`“${n.at.stepName}”`;switch(n.end){case`arrived`:return{status:`success`,text:`Jumped to ${a} as ${r}.`};case`answers-only`:return{status:`success`,text:`Loaded ${r}’s responses.`};case`needs-answer`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. ${r} has no response for a field there.`};case`invalid`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. A response there no longer passes its check.`};case`stuck`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. The experience did not move on from there.`};case`no-journey`:{let n=i(t);return{status:`error`,text:`${r} did not load: ${t.landing?.experienceName??(n===null?`its experience`:e(n))} didn't open on this page.`}}case`hop-limit`:return{status:`warning`,text:`Stopped walking as ${r} after too many steps. The flow may be going round in a loop.`}}}export{s as PENDING_LOAD_KEY,_ as loadUrl,w as openPersonaJourney,h as screenQuestions,g as startPersonaLoad,y as takePendingLoad,T as walkNotice,C as walkPersona};