@picked/svelte 0.0.1-dev.1793772853474 → 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.
Files changed (40) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/plugins/devtools/DevToolsExperience.svelte +46 -13
  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/journey-outline/journey-outline.js +1 -1
  10. package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
  11. package/dist/plugins/devtools/derive/persona-builder/persona-builder.js +1 -1
  12. package/dist/plugins/devtools/derive/personas/personas.d.ts +1 -1
  13. package/dist/plugins/devtools/derive/personas/personas.js +1 -1
  14. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +20 -23
  15. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
  16. package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
  17. package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
  18. package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -1
  19. package/dist/plugins/devtools/options.d.ts +22 -21
  20. package/dist/plugins/devtools/options.js +1 -1
  21. package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
  22. package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
  23. package/dist/plugins/devtools/review/index.d.ts +2 -2
  24. package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
  25. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +20 -20
  26. package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
  27. package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +12 -12
  28. package/dist/plugins/devtools/review/personas/PersonaCard.svelte +1 -1
  29. package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +25 -9
  30. package/dist/plugins/devtools/review/personas/load-persona.d.ts +2 -2
  31. package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
  32. package/dist/plugins/devtools/review/session/SessionPanel.svelte +11 -11
  33. package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
  34. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +11 -25
  35. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -2
  36. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +3 -3
  37. package/dist/plugins/devtools/sidebar/Sidebar.svelte +38 -4
  38. package/dist/plugins/devtools/sidebar/nav.d.ts +32 -8
  39. package/dist/plugins/devtools/sidebar/nav.js +1 -1
  40. package/package.json +2 -2
@@ -1,24 +1,25 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * @file The journey drawn as a TIMELINE — `git log --graph` for a flow. One row per node, top to
4
- * bottom; branches run in lanes beside the rows and MERGE back into the node they meet, so a
5
- * screen many branches reach is one row with lines converging on it, not a tree full of dashed
6
- * jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling only.
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 screen, a diamond for a decision, a square for another journey. Actions are not drawn at all;
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: 'Start',
36
- screen: 'Screen',
37
- decision: 'Decision',
38
- system: 'Behind the scenes',
39
- journey: 'Opens another 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}, behind the scenes"
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} (behind the scenes)</span>
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 journey ends here.</p>
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 screen
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> Start
359
- <span class="key-swatch kind-screen"></span> Screen
360
- <span class="key-swatch kind-decision"></span> Decision
361
- <span class="key-swatch kind-journey"></span> Opens another journey
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 decision's Yes runs straight on · ↩ loops back · ⇥ hands off</span
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 answer is not accepted.',
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 journey goes back to “${row.name ?? 'an earlier screen'}” here.`
248
+ return `The flow goes back to “${row.name ?? 'an earlier step'}” here.`
249
249
  case 'handoff':
250
- return `${row.name ?? 'Another journey'} takes over here, outside this build.`
250
+ return `${row.name ?? 'Another experience'} takes over here, outside this build.`
251
251
  case 'limit':
252
- return 'The journey is too long to walk here, so it stops.'
252
+ return 'The flow is too long to walk here, so it stops.'
253
253
  case 'end':
254
- return 'The journey ends here.'
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 journeys to build a persona in.</Text>
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 a journey"
284
+ placeholder="Pick an experience"
285
285
  onchange={onStartChange}
286
286
  />
287
287
  <Text size="small" color="subdued">
288
- Answer each screen; the next one appears below. Where the site's own code decides the route,
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 answered on its own screen. The persona stops here, so load it and
355
- answer it there.
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 answer here. The customer continues.
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"
@@ -21,7 +21,7 @@
21
21
  let confirming = $state(false)
22
22
 
23
23
  const answers = $derived(Object.keys(persona.responses).length)
24
- const answerLabel = $derived(answers === 1 ? '1 answer' : `${answers} answers`)
24
+ const answerLabel = $derived(answers === 1 ? '1 response' : `${answers} responses`)
25
25
  const summary = $derived(
26
26
  persona.load === null
27
27
  ? `${answerLabel} · ${describeLanding(persona.landing)}`
@@ -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,
@@ -127,7 +133,7 @@
127
133
  /** The answers Save would keep; `null` while they are read. */
128
134
  let captured = $state<Record<string, FieldValue> | null>(null)
129
135
 
130
- const isEmpty = $derived(personas.length === 0 && saved.length === 0)
136
+ const isEmpty = $derived(personas.length === 0 && saved.length === 0 && tools.length === 0)
131
137
  const postureReason = $derived(posture.canEditAnswers ? undefined : (posture.reason ?? undefined))
132
138
 
133
139
  /**
@@ -156,19 +162,19 @@
156
162
  const buildSummary = $derived.by(() => {
157
163
  if (buildWalk === null) return 'Working out the route…'
158
164
  const count = Object.keys(buildWalk.answers).length
159
- const answers = count === 1 ? '1 answer' : `${count} answers`
165
+ const answers = count === 1 ? '1 response' : `${count} responses`
160
166
  const stop = buildWalk.landing
161
167
  if (stop === null) return `Saves ${answers}.`
162
168
  const screen = stop.stepName ?? deslug(stop.stepKey ?? '')
163
- return `Saves ${answers}. Loading it walks the journey to ${screen}.`
169
+ return `Saves ${answers}. Loading it walks the experience to ${screen}.`
164
170
  })
165
171
 
166
172
  const captureSummary = $derived.by(() => {
167
- if (captured === null) return 'Reading your answers…'
173
+ if (captured === null) return 'Reading your responses…'
168
174
  const count = Object.keys(captured).length
169
- const answers = count === 1 ? '1 answer' : `${count} answers`
175
+ const answers = count === 1 ? '1 response' : `${count} responses`
170
176
  if (landing === null)
171
- return `Saves ${answers}. No journey is open, so it loads the answers only.`
177
+ return `Saves ${answers}. No experience is open, so it loads the responses only.`
172
178
  const journeyName = landing.experienceName ?? deslug(landing.experienceKey)
173
179
  if (landing.stepKey === null) return `Saves ${answers}. Loading it walks ${journeyName}.`
174
180
  const screen = landing.stepName ?? deslug(landing.stepKey)
@@ -439,7 +445,7 @@
439
445
  orientation="horizontal"
440
446
  value={mode}
441
447
  options={[
442
- { value: 'build', label: 'Answering the questions here' },
448
+ { value: 'build', label: 'Filling in the fields here' },
443
449
  { value: 'capture', label: 'Using where I am now' },
444
450
  ]}
445
451
  onchange={(value) => (mode = value === 'capture' ? 'capture' : 'build')}
@@ -506,7 +512,7 @@
506
512
  <div class="empty">
507
513
  <EmptyState
508
514
  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."
515
+ hint="Walk the experience the way a visitor would, then save it here to load it again in one click."
510
516
  />
511
517
  <Button variant="primary" onclick={startCreate}>Create</Button>
512
518
  </div>
@@ -545,6 +551,16 @@
545
551
  {/each}
546
552
  </section>
547
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}
548
564
  </div>
549
565
 
550
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
- /** 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
  *
@@ -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{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 Answers tab, so the answer summary leads somewhere it can be acted on. */
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 a journey on the page, and this will follow you.
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 screens'}</Text>
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 screen has not settled yet.
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">WHAT YOU ANSWERED</Text>
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 an answer</Button>
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="Nothing answered yet"
123
- hint="Each screen you answer is listed here, in order. Answers from before this page loaded are on the Answers tab."
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">Screen</th>
130
- <th scope="col">Question</th>
131
- <th scope="col">Answer</th>
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>
@@ -1,12 +1,11 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * @file The Variations tab: features as named variations, aimed at someone deciding whether a
4
- * variation looks right rather than someone debugging bucketing.
3
+ * @file The Feature flags tab: each feature flag by name, aimed at someone deciding whether a
4
+ * variant looks right.
5
5
  *
6
6
  * The mechanism is the SDK's: `?picked-feature=<id>:<value>` forces an arm on the next page load.
7
- * What changes is the framing — a variation is NAMED, its arms are radio buttons rather than an
8
- * include checkbox plus a value dropdown, and the payoff is a link the reviewer can send to
9
- * someone else so both people look at the same thing.
7
+ * A feature flag is NAMED, its variants are radio buttons, and the payoff is a link the reviewer
8
+ * can send to someone else so both people look at the same thing.
10
9
  */
11
10
  import type { FeatureBucket, Picked } from '@picked/client'
12
11
  import { Badge, Button, Card, CodeBlock, CopyButton, EmptyState, Stack, Text } from '../../ui'
@@ -158,8 +157,8 @@
158
157
  <Stack direction="vertical" gap="medium">
159
158
  {#if $features.length === 0}
160
159
  <EmptyState
161
- message="No variations are running on this site"
162
- hint="When an A/B test or a switched-on feature is published, it appears here and you can force either version."
160
+ message="No feature flags are running on this site"
161
+ hint="When a feature flag is published, it appears here and you can force any of its variants."
163
162
  />
164
163
  {:else}
165
164
  <Card sectioned>
@@ -174,13 +173,13 @@
174
173
  <Badge status="default" size="small">Now: {armLabel(feature, served)}</Badge>
175
174
  {:else}
176
175
  <Button variant="ghost" size="small" onclick={() => resolveBucket(feature)}>
177
- Check which version
176
+ Check which variant
178
177
  </Button>
179
178
  {/if}
180
179
  </div>
181
180
 
182
181
  {#if arms}
183
- <div class="arms" role="group" aria-label="Version of {feature.name}">
182
+ <div class="arms" role="group" aria-label="Variant of {feature.name}">
184
183
  {#each arms as arm, index (index)}
185
184
  <label class="option-label">
186
185
  <input
@@ -198,8 +197,8 @@
198
197
  class="inline-input"
199
198
  type="text"
200
199
  value={chosen[feature.id] ?? ''}
201
- placeholder="Type the version to force"
202
- aria-label="Version of {feature.name}"
200
+ placeholder="Type the variant to force"
201
+ aria-label="Variant of {feature.name}"
203
202
  oninput={(event) => choose(feature.id, event.currentTarget.value)}
204
203
  />
205
204
  {/if}
@@ -221,14 +220,14 @@
221
220
  <Stack direction="vertical" gap="small">
222
221
  {#if chosenCount === 0}
223
222
  <Text size="small" color="subdued">
224
- Pick a version above. The page reloads showing it, and the link below reproduces exactly
223
+ Pick a variant above. The page reloads showing it, and the link below reproduces exactly
225
224
  what you saw for anyone you send it to.
226
225
  </Text>
227
226
  {:else}
228
227
  <CodeBlock code={shareUrl} />
229
228
  <Stack direction="horizontal" gap="small">
230
229
  <Button variant="primary" size="small" onclick={apply} testId="devtools-variations-apply">
231
- Show me this version
230
+ Show me this variant
232
231
  </Button>
233
232
  <CopyButton
234
233
  text={shareUrl}
@@ -1,35 +1,21 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * @file The Custom Tools tab: every custom tool placed neither `'review'` nor `'footer'`, as
4
- * nested tabs. With none, it says what a custom tool is and links the how-to page.
3
+ * @file The Custom Tools page, listed while no tool sits in that group: it says what a custom
4
+ * tool is and links the how-to page.
5
5
  */
6
6
  import { getPicked } from '../../../../hooks'
7
- import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
8
- import Tabs from '../../components/Tabs.svelte'
9
- import { developerTools, readCustomTools } from '../../customTools'
10
7
  import { Button, EmptyState, Stack } from '../../ui'
11
8
 
12
9
  const client = getPicked()
13
10
  const docsUrl = `${client.documentationSiteUrl}/how-to/add-a-tool-to-the-devtools`
14
- const tools = developerTools(readCustomTools(client))
15
- const tabs = tools.map((tool) => ({
16
- id: tool.id,
17
- label: tool.label,
18
- component: AdaptedToolHost,
19
- props: { tool },
20
- }))
21
11
  </script>
22
12
 
23
- {#if tabs.length > 0}
24
- <Tabs {tabs} />
25
- {:else}
26
- <Stack direction="vertical" gap="medium" padding="medium" justify="center">
27
- <EmptyState
28
- message="No custom tools yet"
29
- hint="A developer can add tools for this site here, such as test customers or a way to request a change."
30
- />
31
- <Button variant="secondary" size="small" href={docsUrl} target="_blank">
32
- How to add a custom tool
33
- </Button>
34
- </Stack>
35
- {/if}
13
+ <Stack direction="vertical" gap="medium" padding="medium" justify="center">
14
+ <EmptyState
15
+ message="No custom tools yet"
16
+ hint="A developer can add tools for this site here, such as test accounts or a way to request a change."
17
+ />
18
+ <Button variant="secondary" size="small" href={docsUrl} target="_blank">
19
+ How to add a custom tool
20
+ </Button>
21
+ </Stack>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- /** @file Detail screen for a single experience, showing its config, target selectors, and decision logic. */
2
+ /** @file Detail screen for a single experience, showing its config, target selectors, and condition. */
3
3
  import type { ExperienceStore } from '@picked/client'
4
4
  import { getPicked } from '../../../../hooks'
5
5
  import { Button, Card, Text, CodeBlock, Stack } from '../../ui'
@@ -36,7 +36,7 @@
36
36
  </div>
37
37
 
38
38
  <div>
39
- <Text weight="semibold">Decision logic</Text>
39
+ <Text weight="semibold">Condition</Text>
40
40
  <CodeBlock code={JSON.stringify($config.decisionLogic, null, 2)} />
41
41
  </div>
42
42
  </Stack>
@@ -132,7 +132,7 @@
132
132
 
133
133
  const columns = [
134
134
  { key: 'include', header: 'Include' },
135
- { key: 'feature', header: 'Feature' },
135
+ { key: 'feature', header: 'Feature flag' },
136
136
  { key: 'currentValue', header: 'Current Value' },
137
137
  { key: 'overrideValue', header: 'Override Value' },
138
138
  ]
@@ -140,8 +140,8 @@
140
140
 
141
141
  <Stack direction="vertical" gap="medium" padding="medium">
142
142
  <Text size="caption" color="subdued">
143
- Override feature flags via URL parameters. Select features below, choose values, then copy the
144
- generated URL. Recipients will see the overridden values on page load.
143
+ Override feature flags via URL parameters. Select feature flags below, choose values, then copy
144
+ the generated URL. Recipients will see the overridden values on page load.
145
145
  </Text>
146
146
 
147
147
  <Card sectioned>
@@ -2,13 +2,16 @@
2
2
  @paired-component apps/dashboard/src/shared/ui/navigation/sidebar-row-classes.ts
3
3
  @design-parity A row mirrors the Dashboard sidebar row: 28px high, text-sm, rounded-md, a 10%
4
4
  current-colour fill when selected and 5% on hover, a label that fades out instead of wrapping.
5
- The Developer row mirrors the Dashboard's folding nav group (sidebar-nav-group.tsx).
5
+ The Custom Tools and Developer rows mirror the Dashboard's folding nav group (sidebar-nav-group.tsx).
6
6
  -->
7
7
  <script lang="ts">
8
- /** @file The devtools sidebar: the main tabs, then the folding Developer group. */
8
+ /**
9
+ * @file The devtools sidebar: the main tabs, the folding Custom Tools group, then the folding
10
+ * Developer group.
11
+ */
9
12
  import { DEVELOPER_LABEL } from '../derive/plain-language'
10
13
  import type { NavModel } from './nav'
11
- import { nextFocusIndex } from './nav'
14
+ import { CUSTOM_TOOLS_GROUP, CUSTOM_TOOLS_LABEL, nextFocusIndex } from './nav'
12
15
  import SidebarRow from './SidebarRow.svelte'
13
16
 
14
17
  interface Props {
@@ -17,11 +20,22 @@
17
20
  selected: string
18
21
  /** Whether the Developer group lists its screens. */
19
22
  developerOpen: boolean
23
+ /** Whether the Custom Tools group lists its tools. */
24
+ customOpen: boolean
20
25
  onSelect: (id: string) => void
21
26
  onToggleDeveloper: () => void
27
+ onToggleCustom: () => void
22
28
  }
23
29
 
24
- const { nav, selected, developerOpen, onSelect, onToggleDeveloper }: Props = $props()
30
+ const {
31
+ nav,
32
+ selected,
33
+ developerOpen,
34
+ customOpen,
35
+ onSelect,
36
+ onToggleDeveloper,
37
+ onToggleCustom,
38
+ }: Props = $props()
25
39
 
26
40
  let list: HTMLElement | undefined = $state()
27
41
 
@@ -57,6 +71,26 @@
57
71
  onPress={() => onSelect(item.id)}
58
72
  />
59
73
  {/each}
74
+ {#if nav.custom.length > 0}
75
+ <SidebarRow
76
+ label={CUSTOM_TOOLS_LABEL}
77
+ testId="devtools-nav-{CUSTOM_TOOLS_GROUP}"
78
+ depth={0}
79
+ expanded={customOpen}
80
+ onPress={onToggleCustom}
81
+ />
82
+ {#if customOpen}
83
+ {#each nav.custom as item (item.id)}
84
+ <SidebarRow
85
+ label={item.label}
86
+ testId="devtools-nav-{item.id}"
87
+ depth={2}
88
+ selected={selected === item.id}
89
+ onPress={() => onSelect(item.id)}
90
+ />
91
+ {/each}
92
+ {/if}
93
+ {/if}
60
94
  <li class="divider" role="presentation"></li>
61
95
  <SidebarRow
62
96
  label={DEVELOPER_LABEL}