@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +46 -13
- package/dist/plugins/devtools/components/AdaptedToolHost.svelte +9 -3
- package/dist/plugins/devtools/customTools.d.ts +16 -7
- package/dist/plugins/devtools/customTools.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -1
- package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
- package/dist/plugins/devtools/derive/persona-builder/persona-builder.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +1 -1
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +20 -23
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
- package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -1
- package/dist/plugins/devtools/options.d.ts +22 -21
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
- package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
- package/dist/plugins/devtools/review/index.d.ts +2 -2
- package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +20 -20
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +12 -12
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +1 -1
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +25 -9
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +2 -2
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/session/SessionPanel.svelte +11 -11
- package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
- package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +11 -25
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -2
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +3 -3
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +38 -4
- package/dist/plugins/devtools/sidebar/nav.d.ts +32 -8
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @file
|
|
4
|
-
* bottom; branches run in lanes beside the rows and MERGE back into the node they
|
|
5
|
-
*
|
|
6
|
-
* jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling
|
|
3
|
+
* @file An experience's flow drawn as a TIMELINE — `git log --graph` for a flow. One row per
|
|
4
|
+
* node, top to bottom; branches run in lanes beside the rows and MERGE back into the node they
|
|
5
|
+
* meet, so a step many branches reach is one row with lines converging on it, not a tree full of
|
|
6
|
+
* dashed jumps. `derive/journey-timeline` owns the layout; this file owns selection and scrolling
|
|
7
|
+
* only.
|
|
7
8
|
*
|
|
8
9
|
* A LOOP is the one thing a git graph never has. It stays off the drawing and rides on its row as
|
|
9
10
|
* a "↩ back to …" note, and the details pane can take the reviewer to its target.
|
|
10
11
|
*
|
|
11
12
|
* COLOUR IS THE BRANCH, as in a git tool: every line and dot takes its branch's colour, so the
|
|
12
13
|
* eye can follow one route down the lanes. That frees the node KIND to be a SHAPE — a circle for
|
|
13
|
-
* a
|
|
14
|
-
* where a branch ends on one, the row names it.
|
|
14
|
+
* a step, a diamond for a condition, a square for another experience. Actions are not drawn at
|
|
15
|
+
* all; where a branch ends on one, the row names it.
|
|
15
16
|
*
|
|
16
17
|
* ROWS ARE HTML, LINES ARE SVG, the same split as the map: each row is a real `<button>` the full
|
|
17
18
|
* width of the timeline, and the graph column is painted over its left edge with
|
|
18
19
|
* `pointer-events: none`, so clicking a dot or its label selects the same row.
|
|
19
20
|
*/
|
|
20
21
|
import { tick } from 'svelte'
|
|
21
|
-
import { COLOURS, ROW_HEIGHT } from '../../derive'
|
|
22
|
+
import { COLOURS, NODE_LABELS, ROW_HEIGHT } from '../../derive'
|
|
22
23
|
import type { TimelineLayout, TimelineNode, TreeNodeKind } from '../../derive'
|
|
23
24
|
import TimelineMarker from './TimelineMarker.svelte'
|
|
24
25
|
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
const { layout, canNavigate, onJump }: JourneyTimelineProps = $props()
|
|
33
34
|
|
|
34
35
|
const KIND_LABELS: Record<TreeNodeKind, string> = {
|
|
35
|
-
start:
|
|
36
|
-
screen:
|
|
37
|
-
decision:
|
|
38
|
-
system:
|
|
39
|
-
journey:
|
|
36
|
+
start: NODE_LABELS.origin,
|
|
37
|
+
screen: NODE_LABELS.step,
|
|
38
|
+
decision: NODE_LABELS.condition,
|
|
39
|
+
system: NODE_LABELS.action,
|
|
40
|
+
journey: NODE_LABELS.experience,
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/** Marker size per kind — the start a little larger, as on the map. */
|
|
@@ -242,7 +243,7 @@
|
|
|
242
243
|
>
|
|
243
244
|
{/each}
|
|
244
245
|
{#each node.ends as end (end.key)}
|
|
245
|
-
<span class="end" title="Hands off to {end.label}
|
|
246
|
+
<span class="end" title="Hands off to the {end.label} action"
|
|
246
247
|
>⇥ {end.armLabel === null ? '' : `${end.armLabel}: `}{end.label}</span
|
|
247
248
|
>
|
|
248
249
|
{/each}
|
|
@@ -331,13 +332,13 @@
|
|
|
331
332
|
{#if end.armLabel !== null}
|
|
332
333
|
<span class="arm arm-{end.arm ?? 'none'}">{end.armLabel}</span>
|
|
333
334
|
{/if}
|
|
334
|
-
<span class="end-text">Ends: {end.label} (
|
|
335
|
+
<span class="end-text">Ends: {end.label} (action)</span>
|
|
335
336
|
</li>
|
|
336
337
|
{/each}
|
|
337
338
|
</ul>
|
|
338
339
|
</div>
|
|
339
340
|
{:else}
|
|
340
|
-
<p class="text">The
|
|
341
|
+
<p class="text">The experience ends here.</p>
|
|
341
342
|
{/if}
|
|
342
343
|
|
|
343
344
|
{#if selectedNode.rule !== null}
|
|
@@ -347,7 +348,7 @@
|
|
|
347
348
|
|
|
348
349
|
{#if canJump(selectedNode)}
|
|
349
350
|
<button type="button" class="action" onclick={() => goBack(selectedNode)}>
|
|
350
|
-
Go back to this
|
|
351
|
+
Go back to this step
|
|
351
352
|
</button>
|
|
352
353
|
{/if}
|
|
353
354
|
{/if}
|
|
@@ -355,14 +356,14 @@
|
|
|
355
356
|
</div>
|
|
356
357
|
|
|
357
358
|
<p class="legend">
|
|
358
|
-
<span class="key-swatch kind-start"></span>
|
|
359
|
-
<span class="key-swatch kind-screen"></span>
|
|
360
|
-
<span class="key-swatch kind-decision"></span>
|
|
361
|
-
<span class="key-swatch kind-journey"></span>
|
|
359
|
+
<span class="key-swatch kind-start"></span> {KIND_LABELS.start}
|
|
360
|
+
<span class="key-swatch kind-screen"></span> {KIND_LABELS.screen}
|
|
361
|
+
<span class="key-swatch kind-decision"></span> {KIND_LABELS.decision}
|
|
362
|
+
<span class="key-swatch kind-journey"></span> {KIND_LABELS.journey}
|
|
362
363
|
<span class="key-swatch seen"></span> Seen
|
|
363
364
|
<span class="key-swatch current"></span> You are here
|
|
364
365
|
<span
|
|
365
|
-
>· each colour is one branch · a
|
|
366
|
+
>· each colour is one branch · a condition's Yes runs straight on · ↩ loops back · ⇥ hands off</span
|
|
366
367
|
>
|
|
367
368
|
</p>
|
|
368
369
|
</div>
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
if (typeof result === 'string' || result === false) {
|
|
216
216
|
errors = {
|
|
217
217
|
...errors,
|
|
218
|
-
[field.key]: typeof result === 'string' ? result : 'This
|
|
218
|
+
[field.key]: typeof result === 'string' ? result : 'This response is not accepted.',
|
|
219
219
|
}
|
|
220
220
|
return
|
|
221
221
|
}
|
|
@@ -245,13 +245,13 @@
|
|
|
245
245
|
function endText(row: Extract<BuilderRow, { kind: 'end' }>): string {
|
|
246
246
|
switch (row.reason) {
|
|
247
247
|
case 'loop':
|
|
248
|
-
return `The
|
|
248
|
+
return `The flow goes back to “${row.name ?? 'an earlier step'}” here.`
|
|
249
249
|
case 'handoff':
|
|
250
|
-
return `${row.name ?? 'Another
|
|
250
|
+
return `${row.name ?? 'Another experience'} takes over here, outside this build.`
|
|
251
251
|
case 'limit':
|
|
252
|
-
return 'The
|
|
252
|
+
return 'The flow is too long to walk here, so it stops.'
|
|
253
253
|
case 'end':
|
|
254
|
-
return 'The
|
|
254
|
+
return 'The experience ends here.'
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -274,18 +274,18 @@
|
|
|
274
274
|
|
|
275
275
|
<div class="builder" data-testid="devtools-persona-builder">
|
|
276
276
|
{#if journeyOptions.length === 0}
|
|
277
|
-
<Text size="small" color="subdued">This page has no
|
|
277
|
+
<Text size="small" color="subdued">This page has no experiences to build a persona in.</Text>
|
|
278
278
|
{:else}
|
|
279
279
|
<Select
|
|
280
280
|
label="Starts in"
|
|
281
281
|
name="devtools-builder-start"
|
|
282
282
|
options={journeyOptions}
|
|
283
283
|
value={start}
|
|
284
|
-
placeholder="Pick
|
|
284
|
+
placeholder="Pick an experience"
|
|
285
285
|
onchange={onStartChange}
|
|
286
286
|
/>
|
|
287
287
|
<Text size="small" color="subdued">
|
|
288
|
-
|
|
288
|
+
Complete each step; the next one appears below. Where the site's own code decides the route,
|
|
289
289
|
pick the outcome to follow. The real outcome decides when the persona loads.
|
|
290
290
|
</Text>
|
|
291
291
|
|
|
@@ -351,21 +351,21 @@
|
|
|
351
351
|
</div>
|
|
352
352
|
{:else if kind === 'system'}
|
|
353
353
|
<Text size="small" color="subdued">
|
|
354
|
-
This is
|
|
355
|
-
|
|
354
|
+
This is set on its own step. The persona stops here, so load it and set it
|
|
355
|
+
there.
|
|
356
356
|
</Text>
|
|
357
357
|
{/if}
|
|
358
358
|
</div>
|
|
359
359
|
{/if}
|
|
360
360
|
{:else}
|
|
361
361
|
<Text size="small" color="subdued">
|
|
362
|
-
Nothing to
|
|
362
|
+
Nothing to fill in here. The visitor continues.
|
|
363
363
|
</Text>
|
|
364
364
|
{/each}
|
|
365
365
|
</li>
|
|
366
366
|
{:else if row.kind === 'choice'}
|
|
367
367
|
<li class="decision" data-testid="devtools-builder-decision">
|
|
368
|
-
<Text size="small">The site decides: {row.question}</Text>
|
|
368
|
+
<Text size="small">The site's code decides: {row.question}</Text>
|
|
369
369
|
<div class="choices">
|
|
370
370
|
<ChoiceButton
|
|
371
371
|
label="Yes"
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
173
|
+
if (captured === null) return 'Reading your responses…'
|
|
168
174
|
const count = Object.keys(captured).length
|
|
169
|
-
const answers = count === 1 ? '1
|
|
175
|
+
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
170
176
|
if (landing === null)
|
|
171
|
-
return `Saves ${answers}. No
|
|
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: '
|
|
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
|
|
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
|
-
/**
|
|
87
|
-
|
|
86
|
+
/** Responses every walk gives when a step asks for their fields, over the persona's own. */
|
|
87
|
+
responses: Readonly<Record<string, FieldValue>>;
|
|
88
88
|
/**
|
|
89
89
|
* The keys a screen asks: its fields that show now, minus the code-owned ones.
|
|
90
90
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{pickJourney as n}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as r}from"@picked/client/form";var i=`picked-devtools-persona-load`,a=30,o={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function s(e){return`${e.experienceKey}/${e.stepKey}`}async function c(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(a)}return null}function l(e,r,i){let a=e.experienceInstances.listStore.getState().filter(e=>r.has(e.experienceKey)),o=n(a,{exclude:i.exclude,precedence:i.precedence,openOrder:i.openOrder()}),s=o?.currentStepStore.getState();return o===void 0||s==null||o.currentNodeStore.getState()?.type!==`step`||o.stateStore.getState()!==`ready`?null:{instance:o,screen:{experienceKey:o.experienceKey,stepKey:s.key},fieldKeys:s.fieldKeys,stop:{experienceName:o.experienceName,stepName:t(s.name,s.key)}}}async function u(e,t,n){await e.openExperienceByKey(t),await c(()=>e.experienceInstances.listStore.getState().some(e=>e.experienceKey===t&&e.isOpenStore.getState())?!0:null,n.openTries,n)===null&&await e.launchExperienceByKey(t,{bypassCondition:!0,initialiseOpen:!0})}function d(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function f(e,t,n){let i=e.fields.listStore.getState(),a=t.flatMap(e=>i.filter(t=>t.key===e));return(await r(e,a)).map(e=>e.key).filter(e=>!n.has(e))}function p(e,t,n){try{n.storage.setItem(i,t.id)}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function m(e,t){let n=new URL(e??`${t.pathname}${t.search}`,`https://entry.invalid`);for(let[e,r]of new URLSearchParams(t.search))e.startsWith(`picked-`)&&!n.searchParams.has(e)&&n.searchParams.append(e,r);return`${n.pathname}${n.search}`}function h(e){try{let t=e.getItem(i);return e.removeItem(i),t!==null&&t.length>0?t:null}catch{return null}}function g(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function _(e,t,n){let r=!1,i=0;return await c(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):s(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function v(e,t){let n=await e.settings.askedOn(t.fieldKeys),r=n.filter(t=>Object.hasOwn(e.answers,t));if(r.length>0){let{errors:n}=await e.client.responses.commit(d(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(g(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=s(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await _(e,i,e.timing.selfAdvanceTries)?null:{end:`stuck`,at:t.stop}:(n.length>0?r.length===n.length:e.passed.has(i))?(t.instance.goToNextStep(),await _(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function y(e,t,n,r=o){let{landing:i}=t,a=t.passed.at(0)?.experienceKey??i?.experienceKey;if(a===void 0)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let d=new Set(t.passed.map(e=>e.experienceKey));i!==null&&d.add(i.experienceKey);let f={client:e,settings:n,timing:r,landing:i,passed:new Set(t.passed.map(s)),answers:{...t.responses,...n.
|
|
1
|
+
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{pickJourney as n}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as r}from"@picked/client/form";var i=`picked-devtools-persona-load`,a=30,o={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function s(e){return`${e.experienceKey}/${e.stepKey}`}async function c(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(a)}return null}function l(e,r,i){let a=e.experienceInstances.listStore.getState().filter(e=>r.has(e.experienceKey)),o=n(a,{exclude:i.exclude,precedence:i.precedence,openOrder:i.openOrder()}),s=o?.currentStepStore.getState();return o===void 0||s==null||o.currentNodeStore.getState()?.type!==`step`||o.stateStore.getState()!==`ready`?null:{instance:o,screen:{experienceKey:o.experienceKey,stepKey:s.key},fieldKeys:s.fieldKeys,stop:{experienceName:o.experienceName,stepName:t(s.name,s.key)}}}async function u(e,t,n){await e.openExperienceByKey(t),await c(()=>e.experienceInstances.listStore.getState().some(e=>e.experienceKey===t&&e.isOpenStore.getState())?!0:null,n.openTries,n)===null&&await e.launchExperienceByKey(t,{bypassCondition:!0,initialiseOpen:!0})}function d(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function f(e,t,n){let i=e.fields.listStore.getState(),a=t.flatMap(e=>i.filter(t=>t.key===e));return(await r(e,a)).map(e=>e.key).filter(e=>!n.has(e))}function p(e,t,n){try{n.storage.setItem(i,t.id)}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function m(e,t){let n=new URL(e??`${t.pathname}${t.search}`,`https://entry.invalid`);for(let[e,r]of new URLSearchParams(t.search))e.startsWith(`picked-`)&&!n.searchParams.has(e)&&n.searchParams.append(e,r);return`${n.pathname}${n.search}`}function h(e){try{let t=e.getItem(i);return e.removeItem(i),t!==null&&t.length>0?t:null}catch{return null}}function g(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function _(e,t,n){let r=!1,i=0;return await c(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):s(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function v(e,t){let n=await e.settings.askedOn(t.fieldKeys),r=n.filter(t=>Object.hasOwn(e.answers,t));if(r.length>0){let{errors:n}=await e.client.responses.commit(d(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(g(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=s(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await _(e,i,e.timing.selfAdvanceTries)?null:{end:`stuck`,at:t.stop}:(n.length>0?r.length===n.length:e.passed.has(i))?(t.instance.goToNextStep(),await _(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function y(e,t,n,r=o){let{landing:i}=t,a=t.passed.at(0)?.experienceKey??i?.experienceKey;if(a===void 0)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let d=new Set(t.passed.map(e=>e.experienceKey));i!==null&&d.add(i.experienceKey);let f={client:e,settings:n,timing:r,landing:i,passed:new Set(t.passed.map(s)),answers:{...t.responses,...n.responses},resting:()=>l(e,d,n)};await c(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await c(f.resting,r.startTries,r)===null&&await u(e,a,r);for(let e=0;e<r.maxHops;e++){let e=await c(f.resting,r.startTries,r);if(e===null)return{end:`no-journey`,at:null};let t=await v(f,e);if(t!==null)return t}return{end:`hop-limit`,at:null}}function b(t,n){let{name:r}=t,i=n.at===null?``:`“${n.at.stepName}”`;switch(n.end){case`arrived`:return{status:`success`,text:`Loaded ${r} on ${i}.`};case`answers-only`:return{status:`success`,text:`Loaded ${r}’s responses.`};case`needs-answer`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. ${r} has no response for a field there.`};case`invalid`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. A response there no longer passes its check.`};case`stuck`:return{status:`warning`,text:`Loaded ${r} as far as ${i}. The experience did not move on from there.`};case`no-journey`:{let n=t.passed.at(0)?.experienceKey??t.landing?.experienceKey;return{status:`error`,text:`${r} did not load: ${t.landing?.experienceName??(n===void 0?`its experience`:e(n))} didn't open on this page.`}}case`hop-limit`:return{status:`warning`,text:`Stopped loading ${r} after too many steps. The flow may be going round in a loop.`}}}export{i as PENDING_LOAD_KEY,m as loadUrl,f as screenQuestions,p as startPersonaLoad,h as takePendingLoad,b as walkNotice,y as walkPersona};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
journey: JourneyView | null
|
|
17
17
|
/** Every screen submitted since the session record started, oldest first. */
|
|
18
18
|
history: readonly HistoryEntry[]
|
|
19
|
-
/** Opens the
|
|
19
|
+
/** Opens the Responses tab, so the response summary leads somewhere it can be acted on. */
|
|
20
20
|
onShowAnswers: () => void
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -65,18 +65,18 @@
|
|
|
65
65
|
{#if journey === null}
|
|
66
66
|
<Text size="medium">Nothing is open yet.</Text>
|
|
67
67
|
<Text size="small" color="subdued">
|
|
68
|
-
Start
|
|
68
|
+
Start an experience on the page, and this will follow you.
|
|
69
69
|
</Text>
|
|
70
70
|
{:else}
|
|
71
71
|
<Stack direction="vertical" gap="none">
|
|
72
|
-
<Text size="large" weight="semibold">{journey.stepName ?? 'Moving between
|
|
72
|
+
<Text size="large" weight="semibold">{journey.stepName ?? 'Moving between steps'}</Text>
|
|
73
73
|
<Text size="small" color="subdued">in {journey.experienceName}</Text>
|
|
74
74
|
</Stack>
|
|
75
75
|
{#if journey.inTransit}
|
|
76
76
|
<!-- The engine is transiting a condition or action node, so the recorded step is the one
|
|
77
77
|
just LEFT. Saying so stops a "stuck on loading" report being filed against it. -->
|
|
78
78
|
<Text size="small" color="subdued">
|
|
79
|
-
Still working — the next
|
|
79
|
+
Still working — the next step has not settled yet.
|
|
80
80
|
</Text>
|
|
81
81
|
{/if}
|
|
82
82
|
{/if}
|
|
@@ -111,24 +111,24 @@
|
|
|
111
111
|
<Card>
|
|
112
112
|
<Stack direction="vertical" gap="small">
|
|
113
113
|
<Stack direction="horizontal" gap="small" align="space-between" justify="center">
|
|
114
|
-
<Text size="caption" color="subdued" weight="semibold">
|
|
114
|
+
<Text size="caption" color="subdued" weight="semibold">YOUR RESPONSES</Text>
|
|
115
115
|
{#if rows.length > 0}
|
|
116
|
-
<Button variant="ghost" size="small" onclick={onShowAnswers}>Change
|
|
116
|
+
<Button variant="ghost" size="small" onclick={onShowAnswers}>Change a response</Button>
|
|
117
117
|
{/if}
|
|
118
118
|
</Stack>
|
|
119
119
|
|
|
120
120
|
{#if rows.length === 0}
|
|
121
121
|
<EmptyState
|
|
122
|
-
message="
|
|
123
|
-
hint="Each
|
|
122
|
+
message="No responses yet"
|
|
123
|
+
hint="Each step you submit is listed here, in order. Responses from before this page loaded are on the Responses tab."
|
|
124
124
|
/>
|
|
125
125
|
{:else}
|
|
126
126
|
<table class="history" data-testid="devtools-session-answers">
|
|
127
127
|
<thead>
|
|
128
128
|
<tr>
|
|
129
|
-
<th scope="col">
|
|
130
|
-
<th scope="col">
|
|
131
|
-
<th scope="col">
|
|
129
|
+
<th scope="col">Step</th>
|
|
130
|
+
<th scope="col">Field</th>
|
|
131
|
+
<th scope="col">Response</th>
|
|
132
132
|
</tr>
|
|
133
133
|
</thead>
|
|
134
134
|
<tbody>
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @file The
|
|
4
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
162
|
-
hint="When
|
|
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
|
|
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="
|
|
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
|
|
202
|
-
aria-label="
|
|
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
|
|
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
|
|
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
|
|
4
|
-
*
|
|
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
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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">
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
|
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}
|