@picked/svelte 0.0.1-dev.1793776638554 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +145 -93
- package/dist/plugins/devtools/customTools.d.ts +2 -2
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/index.d.ts +3 -3
- package/dist/plugins/devtools/derive/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/personas/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +3 -5
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/index.d.ts +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
- package/dist/plugins/devtools/options.d.ts +6 -4
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +3 -34
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
- package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
- package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
- package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +3 -68
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +5 -42
- package/dist/plugins/devtools/sidebar/nav.d.ts +18 -44
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
- package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
- package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
- package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
|
@@ -53,20 +53,18 @@ export declare const TAB_LABELS: {
|
|
|
53
53
|
readonly guide: 'How to use';
|
|
54
54
|
readonly session: 'This session';
|
|
55
55
|
readonly answers: 'Responses';
|
|
56
|
-
readonly journey: '
|
|
56
|
+
readonly journey: 'Experiences';
|
|
57
57
|
readonly personas: 'Personas & Scenarios';
|
|
58
58
|
readonly variations: 'Feature flags';
|
|
59
59
|
};
|
|
60
60
|
/** The id of one of the review tools' own tabs. */
|
|
61
61
|
export type ReviewTabId = keyof typeof TAB_LABELS;
|
|
62
|
-
/** The name of the sidebar section that holds the developer tools, apart from the review tabs. */
|
|
63
|
-
export declare const DEVELOPER_LABEL = "Developer";
|
|
64
62
|
/** The one-sentence purpose each tab leads with. Kept together so the tone stays consistent. */
|
|
65
63
|
export declare const TAB_INTROS: {
|
|
66
64
|
readonly guide: 'How to use each tab. Open a tab straight from its section.';
|
|
67
65
|
readonly session: 'Where you are right now, and your responses on each step. Start here if something looks wrong.';
|
|
68
66
|
readonly answers: 'Every field this experience can ask for, and the responses given. Change one to see what the visitor would see.';
|
|
69
|
-
readonly journey: 'Every
|
|
70
|
-
readonly personas: 'Visitors and situations to try this experience as.
|
|
67
|
+
readonly journey: 'Every experience on this site: the route each can take, with yours marked, and where and when it shows. Go back to any step you have already visited.';
|
|
68
|
+
readonly personas: 'Visitors and situations to try this experience as. Walk through as one, or jump to a step on its route.';
|
|
71
69
|
readonly variations: 'The feature flags running on this site. Force a variant to review it, then copy the link to show someone else exactly what you saw.';
|
|
72
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e={origin:`Origin`,step:`Step`,condition:`Condition`,action:`Action`,experience:`Opens another experience`};function t(e){let t=e.replace(/[-_]+/g,` `).trim();return t.length===0?e:t.charAt(0).toUpperCase()+t.slice(1)}function n(e,n){let r=e?.trim();return r!==void 0&&r.length>0?r:n!==void 0&&n.length>0?t(n):`Untitled step`}function r(e){return e!==void 0&&e.length>0?`Viewing unpublished changes`:`Viewing published content`}var i={guide:`How to use`,session:`This session`,answers:`Responses`,journey:`
|
|
1
|
+
var e={origin:`Origin`,step:`Step`,condition:`Condition`,action:`Action`,experience:`Opens another experience`};function t(e){let t=e.replace(/[-_]+/g,` `).trim();return t.length===0?e:t.charAt(0).toUpperCase()+t.slice(1)}function n(e,n){let r=e?.trim();return r!==void 0&&r.length>0?r:n!==void 0&&n.length>0?t(n):`Untitled step`}function r(e){return e!==void 0&&e.length>0?`Viewing unpublished changes`:`Viewing published content`}var i={guide:`How to use`,session:`This session`,answers:`Responses`,journey:`Experiences`,personas:`Personas & Scenarios`,variations:`Feature flags`},a={guide:`How to use each tab. Open a tab straight from its section.`,session:`Where you are right now, and your responses on each step. Start here if something looks wrong.`,answers:`Every field this experience can ask for, and the responses given. Change one to see what the visitor would see.`,journey:`Every experience on this site: the route each can take, with yours marked, and where and when it shows. Go back to any step you have already visited.`,personas:`Visitors and situations to try this experience as. Walk through as one, or jump to a step on its route.`,variations:`The feature flags running on this site. Force a variant to review it, then copy the link to show someone else exactly what you saw.`};export{e as NODE_LABELS,a as TAB_INTROS,i as TAB_LABELS,r as configSourceLabel,t as deslug,n as screenName};
|
|
@@ -5,4 +5,4 @@ export type * from './plugin';
|
|
|
5
5
|
export type * from './options';
|
|
6
6
|
export type { CustomTool, CustomToolGuide, CustomToolIcon, CustomToolPlacement, } from './customTools';
|
|
7
7
|
export type { DevtoolsSession } from './session-access';
|
|
8
|
-
export type { PersonaDefinition,
|
|
8
|
+
export type { PersonaDefinition, PersonaSetup } from './derive/personas';
|
|
@@ -11,13 +11,20 @@
|
|
|
11
11
|
autoHide: boolean
|
|
12
12
|
/** The tag beside the button: a preview session, a preview whose workspace is gone, or none. */
|
|
13
13
|
preview: 'preview' | 'unavailable' | null
|
|
14
|
+
/** The persona the tab walks through as, named in the tag; `null` outside a persona session. */
|
|
15
|
+
walkingAs?: string | null
|
|
14
16
|
/** Opens the panel. */
|
|
15
17
|
onOpen: () => void
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const { position, autoHide, preview, onOpen }: Props = $props()
|
|
20
|
+
const { position, autoHide, preview, walkingAs = null, onOpen }: Props = $props()
|
|
19
21
|
|
|
20
22
|
const edges = $derived(launcherEdges(position))
|
|
23
|
+
const tag = $derived(
|
|
24
|
+
[preview === 'preview' ? 'Preview mode' : '', walkingAs === null ? '' : `As ${walkingAs}`]
|
|
25
|
+
.filter((part) => part.length > 0)
|
|
26
|
+
.join(' · '),
|
|
27
|
+
)
|
|
21
28
|
</script>
|
|
22
29
|
|
|
23
30
|
<div
|
|
@@ -33,12 +40,14 @@
|
|
|
33
40
|
aria-live="polite"
|
|
34
41
|
title="This preview link's workspace no longer exists. The published site is shown."
|
|
35
42
|
>Preview unavailable — showing published</span>
|
|
36
|
-
{:else if
|
|
37
|
-
<span class="preview-tag" role="status" aria-live="polite"
|
|
43
|
+
{:else if tag.length > 0}
|
|
44
|
+
<span class="preview-tag" role="status" aria-live="polite" data-testid="devtools-launcher-tag">
|
|
45
|
+
{tag}
|
|
46
|
+
</span>
|
|
38
47
|
{/if}
|
|
39
48
|
<button
|
|
40
49
|
class="floating-button"
|
|
41
|
-
class:has-preview-tag={preview
|
|
50
|
+
class:has-preview-tag={preview === 'unavailable' || tag.length > 0}
|
|
42
51
|
onclick={onOpen}
|
|
43
52
|
aria-label="Open Picked Devtools"
|
|
44
53
|
title="Open Picked Devtools"
|
|
@@ -97,10 +97,11 @@ export interface DevtoolsPanelOptions {
|
|
|
97
97
|
/** The floating button's corner and auto-hide. */
|
|
98
98
|
launcher?: LauncherOptions;
|
|
99
99
|
/**
|
|
100
|
-
* Visitors and situations every viewer can load from the Personas & Scenarios tab
|
|
101
|
-
*
|
|
100
|
+
* Visitors and situations every viewer can load from the Personas & Scenarios tab, or a function
|
|
101
|
+
* that loads them, such as a dynamic import a production build can drop. Reviewers also save
|
|
102
|
+
* their own there, in their browser.
|
|
102
103
|
*/
|
|
103
|
-
personas?: readonly PersonaDefinition[];
|
|
104
|
+
personas?: readonly PersonaDefinition[] | (() => Promise<readonly PersonaDefinition[]>);
|
|
104
105
|
/** How loading a persona walks an experience. */
|
|
105
106
|
personaWalk?: PersonaWalkOptions;
|
|
106
107
|
}
|
|
@@ -118,7 +119,8 @@ export interface ResolvedDevtoolsOptions {
|
|
|
118
119
|
fixes: DevtoolsGuideFix[];
|
|
119
120
|
};
|
|
120
121
|
launcher: Required<LauncherOptions>;
|
|
121
|
-
personas
|
|
122
|
+
/** The site's personas, loaded on the first call. */
|
|
123
|
+
personas: () => Promise<Persona[]>;
|
|
122
124
|
personaWalk: {
|
|
123
125
|
selfAdvancingSteps: ReadonlySet<string>;
|
|
124
126
|
responses: Readonly<Record<string, FieldValue>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEVTOOLS_EXPERIENCE_KEY as e,DEVTOOLS_PLUGIN_NAME as t}from"./constants.js";import{readAnswers as n,sitePersonas as r}from"./derive/personas/personas.js";var i=[`top-left`,`top-right`,`bottom-left`,`bottom-right`];function a(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function o(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function s(e){if(!a(e))return{};let t={};for(let[n,r]of Object.entries(e))typeof r==`string`&&(t[n]=r);return t}function c(e){return i.some(t=>t===e)}function l(e){if(!a(e))return null;let{id:t,title:n,steps:r,notes:i}=e;return typeof t!=`string`||typeof n!=`string`||!Array.isArray(r)?null:{id:t,title:n,steps:o(r),notes:o(i)}}function u(e){if(!a(e))return null;let{problem:t,fix:n}=e;return typeof t!=`string`||typeof n!=`string`?null:{problem:t,fix:n}}function d(e){if(!a(e))return null;let{label:t,run:n}=e;return typeof t!=`string`||typeof n!=`function`?null:{label:t,run:e=>{Reflect.apply(n,void 0,[e])}}}function f(
|
|
1
|
+
import{DEVTOOLS_EXPERIENCE_KEY as e,DEVTOOLS_PLUGIN_NAME as t}from"./constants.js";import{readAnswers as n,sitePersonas as r}from"./derive/personas/personas.js";var i=[`top-left`,`top-right`,`bottom-left`,`bottom-right`];function a(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function o(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function s(e){if(!a(e))return{};let t={};for(let[n,r]of Object.entries(e))typeof r==`string`&&(t[n]=r);return t}function c(e){return i.some(t=>t===e)}function l(e){if(!a(e))return null;let{id:t,title:n,steps:r,notes:i}=e;return typeof t!=`string`||typeof n!=`string`||!Array.isArray(r)?null:{id:t,title:n,steps:o(r),notes:o(i)}}function u(e){if(!a(e))return null;let{problem:t,fix:n}=e;return typeof t!=`string`||typeof n!=`string`?null:{problem:t,fix:n}}function d(e){if(!a(e))return null;let{label:t,run:n}=e;return typeof t!=`string`||typeof n!=`function`?null:{label:t,run:e=>{Reflect.apply(n,void 0,[e])}}}function f(e){if(typeof e!=`function`){let t=r(e);return()=>Promise.resolve(t)}let t=null;return()=>(t??=Promise.resolve().then(()=>Reflect.apply(e,void 0,[])).then(e=>r(e)).catch(()=>[]),t)}function p(r){let i=r.getPluginConfig(t).options,p=a(i)?i:{},m=a(p.guide)?p.guide:{},h=a(p.launcher)?p.launcher:{},g=a(p.personaWalk)?p.personaWalk:{},_=p.environmentLabel;return{readOnly:p.readOnly===!0,environmentLabel:typeof _==`string`&&_.length>0?_:void 0,hiddenExperienceKeys:new Set([e,...o(p.hiddenExperienceKeys)]),experiencePrecedence:o(p.experiencePrecedence),codeOwnedFields:new Set(o(p.codeOwnedFields)),responseFollowUp:d(p.responseFollowUp),guide:{sections:(Array.isArray(m.sections)?m.sections:[]).map(l).filter(e=>e!==null),fixes:(Array.isArray(m.fixes)?m.fixes:[]).map(u).filter(e=>e!==null)},launcher:{position:c(h.position)?h.position:`bottom-right`,autoHide:typeof h.autoHide!=`boolean`||h.autoHide},personas:f(p.personas),personaWalk:{selfAdvancingSteps:new Set(o(g.selfAdvancingSteps)),responses:n(g.responses),handoffs:s(g.handoffs)}}}export{e as DEVTOOLS_EXPERIENCE_KEY,i as LAUNCHER_POSITIONS,p as readDevtoolsOptions};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @file The Experiences tab's Details view: what one experience is, where on the page it shows,
|
|
4
|
+
* when it starts, and its instances on this page.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExperienceStore, Picked } from '@picked/client'
|
|
7
|
+
import { Card, CodeBlock, Text } from '../../ui'
|
|
8
|
+
import { describeRule, type FieldLookup } from '../../derive'
|
|
9
|
+
import ExperienceInstancesTable from '../../components/ExperienceInstancesTable.svelte'
|
|
10
|
+
|
|
11
|
+
interface ExperienceDetailsProps {
|
|
12
|
+
client: Picked
|
|
13
|
+
experience: ExperienceStore
|
|
14
|
+
/** Names a field the condition reads. */
|
|
15
|
+
fieldLookup: FieldLookup
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { client, experience, fieldLookup }: ExperienceDetailsProps = $props()
|
|
19
|
+
|
|
20
|
+
const config = $derived(experience.configStore)
|
|
21
|
+
const component = $derived(experience.componentStore)
|
|
22
|
+
const instancesStore = $derived(client.experienceInstances.listStore)
|
|
23
|
+
const instances = $derived(
|
|
24
|
+
$instancesStore.filter((instance) => instance.experienceKey === experience.key),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
/** How a placement puts the experience on the page, by placement. */
|
|
28
|
+
const PLACEMENTS = {
|
|
29
|
+
replace: 'In place of',
|
|
30
|
+
append: 'At the end of',
|
|
31
|
+
prepend: 'At the start of',
|
|
32
|
+
before: 'Before',
|
|
33
|
+
after: 'After',
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
/** The condition's verbatim rule is shown while this is `true`. */
|
|
37
|
+
let showRule = $state(false)
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="details" data-testid="devtools-experience-details">
|
|
41
|
+
<Card sectioned>
|
|
42
|
+
<dl class="facts">
|
|
43
|
+
<dt>Name</dt>
|
|
44
|
+
<dd>{experience.name}</dd>
|
|
45
|
+
<dt>Key</dt>
|
|
46
|
+
<dd><code>{experience.key}</code></dd>
|
|
47
|
+
<dt>Component</dt>
|
|
48
|
+
<dd>{$component?.name ?? 'None'}</dd>
|
|
49
|
+
<dt>Shows on the page</dt>
|
|
50
|
+
<dd>
|
|
51
|
+
{#if $config.targetSelectors.length === 0}
|
|
52
|
+
Nowhere by itself. Code, or another experience, opens it.
|
|
53
|
+
{:else}
|
|
54
|
+
<ul class="targets">
|
|
55
|
+
{#each $config.targetSelectors as target (`${target.selector}/${target.placement}`)}
|
|
56
|
+
<li>{PLACEMENTS[target.placement]} <code>{target.selector}</code></li>
|
|
57
|
+
{/each}
|
|
58
|
+
</ul>
|
|
59
|
+
{/if}
|
|
60
|
+
</dd>
|
|
61
|
+
<dt>Starts when</dt>
|
|
62
|
+
<dd>
|
|
63
|
+
<span data-testid="devtools-experience-condition">
|
|
64
|
+
{describeRule($config.decisionLogic, fieldLookup)}
|
|
65
|
+
</span>
|
|
66
|
+
<button type="button" class="disclosure" onclick={() => (showRule = !showRule)}>
|
|
67
|
+
{showRule ? 'Hide rule' : 'Show rule'}
|
|
68
|
+
</button>
|
|
69
|
+
{#if showRule}
|
|
70
|
+
<CodeBlock code={JSON.stringify($config.decisionLogic ?? null, null, 2)} />
|
|
71
|
+
{/if}
|
|
72
|
+
</dd>
|
|
73
|
+
</dl>
|
|
74
|
+
</Card>
|
|
75
|
+
|
|
76
|
+
<section class="instances" aria-label="On this page">
|
|
77
|
+
<Text weight="semibold">On this page</Text>
|
|
78
|
+
<ExperienceInstancesTable experienceInstances={instances} />
|
|
79
|
+
</section>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.details {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
gap: var(--space-4);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.facts {
|
|
90
|
+
display: grid;
|
|
91
|
+
grid-template-columns: max-content 1fr;
|
|
92
|
+
gap: var(--space-2) var(--space-4);
|
|
93
|
+
margin: 0;
|
|
94
|
+
padding: var(--space-4);
|
|
95
|
+
font-size: var(--text-sm);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.facts dt {
|
|
99
|
+
color: var(--color-gray-500);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.facts dd {
|
|
103
|
+
margin: 0;
|
|
104
|
+
min-width: 0;
|
|
105
|
+
color: var(--color-gray-900);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.targets {
|
|
109
|
+
margin: 0;
|
|
110
|
+
padding-left: var(--space-4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.disclosure {
|
|
114
|
+
margin-left: var(--space-2);
|
|
115
|
+
padding: 0;
|
|
116
|
+
border: 0;
|
|
117
|
+
background: none;
|
|
118
|
+
color: var(--color-blue-600);
|
|
119
|
+
font: inherit;
|
|
120
|
+
font-size: var(--text-xs);
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.instances {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
gap: var(--space-2);
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @file The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* @file The Experiences tab: any experience's flow in plain English, without node ids or raw
|
|
4
|
+
* JSON Logic, and its details. It opens on the experience in front of the reviewer, the picker
|
|
5
|
+
* reaches every other experience in the workspace, and Launch experience opens the one picked.
|
|
6
6
|
*
|
|
7
7
|
* Navigation is BACKWARDS ONLY, and only in the experience that is OPEN. A step already visited
|
|
8
8
|
* is a plain `goToStepByKey`: the flow has been through every action in between, so returning to
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
* THREE VIEWS, ONE GRAPH. The outline answers "where am I, and why did it go there"; the map
|
|
14
14
|
* answers "what shape is this, and where does each branch end"; the timeline answers "in what
|
|
15
15
|
* order, and where do the branches meet again". All three read the same `treeStore` through the
|
|
16
|
-
* same vocabulary modules, so a step is named identically in each and they cannot drift.
|
|
16
|
+
* same vocabulary modules, so a step is named identically in each and they cannot drift. The
|
|
17
|
+
* fourth view, Details, says where the experience shows, when it starts, and its instances.
|
|
17
18
|
*/
|
|
18
19
|
import type { Picked } from '@picked/client'
|
|
19
20
|
import { Badge, Banner, Button, Card, EmptyState } from '../../ui'
|
|
20
21
|
import { buildOutline, buildTimeline, buildTree } from '../../derive'
|
|
21
22
|
import type { FieldLookup, OutlineRow, SafetyPosture, Trails } from '../../derive'
|
|
22
23
|
import type { JourneyView } from '../../session'
|
|
24
|
+
import ExperienceDetails from './ExperienceDetails.svelte'
|
|
23
25
|
import JourneyMap from './JourneyMap.svelte'
|
|
24
26
|
import JourneyTimeline from './JourneyTimeline.svelte'
|
|
25
27
|
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
// The union goes on `$state`, not on the `let`. With the annotation alone TypeScript narrows the
|
|
63
65
|
// variable to its initial literal and the `view === 'map'` below reads as a comparison that can
|
|
64
66
|
// never hold — `$derived` is an expression in this same scope, so nothing widens it back.
|
|
65
|
-
let view = $state<'outline' | 'map' | 'timeline'>('map')
|
|
67
|
+
let view = $state<'outline' | 'map' | 'timeline' | 'details'>('map')
|
|
66
68
|
|
|
67
69
|
/** The open journey, unless it is one the tab never follows. */
|
|
68
70
|
const followed = $derived(
|
|
@@ -98,11 +100,12 @@
|
|
|
98
100
|
//
|
|
99
101
|
// It reads the shared `listStore` rather than `experiences.getByKey`, which mints a NEW derived
|
|
100
102
|
// store per call and never releases it.
|
|
101
|
-
const
|
|
103
|
+
const selectedExperience = $derived(
|
|
102
104
|
selectedKey === null
|
|
103
105
|
? undefined
|
|
104
|
-
: $experiences.find((experience) => experience.key === selectedKey)
|
|
106
|
+
: $experiences.find((experience) => experience.key === selectedKey),
|
|
105
107
|
)
|
|
108
|
+
const treeStore = $derived(selectedExperience?.treeStore)
|
|
106
109
|
const graph: FlowGraph = $derived($treeStore ?? EMPTY_GRAPH)
|
|
107
110
|
|
|
108
111
|
// Fields arrive asynchronously and AFTER the graph does. Read reactively, so the views re-render
|
|
@@ -204,6 +207,17 @@
|
|
|
204
207
|
picked = key === followed?.experienceKey ? null : key
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Opens the experience picked in the picker, past its own condition.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* launch()
|
|
215
|
+
*/
|
|
216
|
+
function launch(): void {
|
|
217
|
+
if (selectedKey === null) return
|
|
218
|
+
void client.experiences.open({ key: selectedKey, options: { bypassCondition: true } })
|
|
219
|
+
}
|
|
220
|
+
|
|
207
221
|
/**
|
|
208
222
|
* Shows a decision's verbatim rule, or hides it again.
|
|
209
223
|
*
|
|
@@ -223,6 +237,19 @@
|
|
|
223
237
|
hint="The workspace has not loaded its experiences. Reload the page if this does not change."
|
|
224
238
|
/>
|
|
225
239
|
{:else}
|
|
240
|
+
<div class="launch">
|
|
241
|
+
<Button
|
|
242
|
+
variant="primary"
|
|
243
|
+
size="small"
|
|
244
|
+
disabled={!posture.canNavigate}
|
|
245
|
+
title={posture.canNavigate ? undefined : (posture.reason ?? undefined)}
|
|
246
|
+
testId="devtools-journey-launch"
|
|
247
|
+
onclick={launch}
|
|
248
|
+
>
|
|
249
|
+
Launch experience
|
|
250
|
+
</Button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
226
253
|
<!-- One row: how to look on the left, what to look at on the right. -->
|
|
227
254
|
<div class="controls">
|
|
228
255
|
<div class="views" role="group" aria-label="Flow view">
|
|
@@ -256,6 +283,16 @@
|
|
|
256
283
|
>
|
|
257
284
|
Timeline
|
|
258
285
|
</button>
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
class="view"
|
|
289
|
+
class:selected={view === 'details'}
|
|
290
|
+
aria-pressed={view === 'details'}
|
|
291
|
+
data-testid="devtools-journey-view-details"
|
|
292
|
+
onclick={() => (view = 'details')}
|
|
293
|
+
>
|
|
294
|
+
Details
|
|
295
|
+
</button>
|
|
259
296
|
</div>
|
|
260
297
|
|
|
261
298
|
<div class="picker">
|
|
@@ -284,7 +321,11 @@
|
|
|
284
321
|
<Banner status="warning" testId="devtools-journey-readonly">{posture.reason}</Banner>
|
|
285
322
|
{/if}
|
|
286
323
|
|
|
287
|
-
{#if
|
|
324
|
+
{#if view === 'details'}
|
|
325
|
+
{#if selectedExperience !== undefined}
|
|
326
|
+
<ExperienceDetails {client} experience={selectedExperience} {fieldLookup} />
|
|
327
|
+
{/if}
|
|
328
|
+
{:else if rows.length === 0}
|
|
288
329
|
<Card sectioned>
|
|
289
330
|
<EmptyState
|
|
290
331
|
message="This experience has no steps yet"
|
|
@@ -380,6 +421,11 @@
|
|
|
380
421
|
flex-direction: column;
|
|
381
422
|
}
|
|
382
423
|
|
|
424
|
+
.launch {
|
|
425
|
+
display: flex;
|
|
426
|
+
justify-content: flex-end;
|
|
427
|
+
}
|
|
428
|
+
|
|
383
429
|
.controls {
|
|
384
430
|
display: flex;
|
|
385
431
|
align-items: center;
|
|
@@ -19,9 +19,7 @@
|
|
|
19
19
|
type BuilderWalk,
|
|
20
20
|
} from '../../derive'
|
|
21
21
|
import ChoiceButton from './ChoiceButton.svelte'
|
|
22
|
-
|
|
23
|
-
/** The rule type the client evaluates. */
|
|
24
|
-
type Rule = Parameters<typeof evaluateRuleLogic>[1]
|
|
22
|
+
import { entryLocation, ruleEvaluator, type Rule } from './persona-route'
|
|
25
23
|
|
|
26
24
|
interface PersonaBuilderProps {
|
|
27
25
|
client: Picked
|
|
@@ -73,37 +71,8 @@
|
|
|
73
71
|
[...graph.journeys.values()].map((journey) => ({ value: journey.key, label: journey.name })),
|
|
74
72
|
)
|
|
75
73
|
|
|
76
|
-
/**
|
|
77
|
-
const
|
|
78
|
-
const url = new URL(entryUrl, window.location.origin)
|
|
79
|
-
return {
|
|
80
|
-
hash: url.hash,
|
|
81
|
-
host: url.host,
|
|
82
|
-
hostname: url.hostname,
|
|
83
|
-
href: url.href,
|
|
84
|
-
origin: url.origin,
|
|
85
|
-
pathname: url.pathname,
|
|
86
|
-
port: url.port,
|
|
87
|
-
protocol: url.protocol,
|
|
88
|
-
search: url.search,
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Evaluates a rule against draft answers, on the entry page.
|
|
94
|
-
*
|
|
95
|
-
* @param {Rule} rule - The rule.
|
|
96
|
-
* @param {Readonly<Record<string, FieldValue>>} responses - The answers it reads.
|
|
97
|
-
* @returns {Promise<boolean>} Whether it holds.
|
|
98
|
-
* @example await evaluate(condition.logic, { purpose: 'home' })
|
|
99
|
-
*/
|
|
100
|
-
async function evaluate(
|
|
101
|
-
rule: Rule,
|
|
102
|
-
responses: Readonly<Record<string, FieldValue>>,
|
|
103
|
-
): Promise<boolean> {
|
|
104
|
-
const result = await evaluateRuleLogic(client, rule, { responses: { ...responses }, location })
|
|
105
|
-
return Boolean(result)
|
|
106
|
-
}
|
|
74
|
+
/** Evaluates a rule against draft answers, on the entry page. */
|
|
75
|
+
const evaluate = $derived(ruleEvaluator(client, entryLocation(entryUrl, window.location.origin)))
|
|
107
76
|
|
|
108
77
|
let walk = $state.raw<BuilderWalk | null>(null)
|
|
109
78
|
|