@picked/svelte 0.0.1-dev.1793772853474 → 0.0.1-dev.1793835766402
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +169 -84
- 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/index.d.ts +3 -3
- package/dist/plugins/devtools/derive/index.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/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/personas/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +89 -8
- 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 +20 -25
- 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/index.d.ts +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
- package/dist/plugins/devtools/options.d.ts +27 -24
- 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/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +70 -24
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +15 -46
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +227 -44
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +191 -78
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +43 -22
- 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/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/sidebar/Sidebar.svelte +2 -33
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +5 -42
- package/dist/plugins/devtools/sidebar/nav.d.ts +26 -28
- 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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A persona's route: the screens its responses take it through, worked out over the real
|
|
3
|
+
* flow graph and the workspace's own rules without touching the live session. Jump to step lists
|
|
4
|
+
* them, and the panel's builder draws them.
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldValue, Picked } from '@picked/client';
|
|
7
|
+
import { evaluateRuleLogic } from '@picked/client';
|
|
8
|
+
import { type BuilderWalk, type Persona, type PersonaLanding } from '../../derive';
|
|
9
|
+
/** The rule type the client evaluates. */
|
|
10
|
+
export type Rule = Parameters<typeof evaluateRuleLogic>[1];
|
|
11
|
+
/** The parts of a page's address a rule on `location` reads. */
|
|
12
|
+
export interface EntryLocation {
|
|
13
|
+
hash: string;
|
|
14
|
+
host: string;
|
|
15
|
+
hostname: string;
|
|
16
|
+
href: string;
|
|
17
|
+
origin: string;
|
|
18
|
+
pathname: string;
|
|
19
|
+
port: string;
|
|
20
|
+
protocol: string;
|
|
21
|
+
search: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The entry page's address, as a rule on `location` reads it.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} entryUrl - The entry page: a path on this site, with its query.
|
|
27
|
+
* @param {string} origin - This site's origin.
|
|
28
|
+
* @returns {EntryLocation} The address.
|
|
29
|
+
* @example entryLocation('/sale?utm=spring', 'https://shop.example').pathname // '/sale'
|
|
30
|
+
*/
|
|
31
|
+
export declare function entryLocation(entryUrl: string, origin: string): EntryLocation;
|
|
32
|
+
/**
|
|
33
|
+
* Evaluates rules against draft responses, on an entry page.
|
|
34
|
+
*
|
|
35
|
+
* @param {Picked} client - The client.
|
|
36
|
+
* @param {EntryLocation} location - The entry page's address.
|
|
37
|
+
* @returns {(rule: Rule, responses: Readonly<Record<string, FieldValue>>) => Promise<boolean>} The evaluator.
|
|
38
|
+
* @example await ruleEvaluator(client, location)(condition.logic, { purpose: 'home' })
|
|
39
|
+
*/
|
|
40
|
+
export declare function ruleEvaluator(client: Picked, location: EntryLocation): (rule: Rule, responses: Readonly<Record<string, FieldValue>>) => Promise<boolean>;
|
|
41
|
+
/** What a route reads beyond the persona. */
|
|
42
|
+
export interface RouteSettings {
|
|
43
|
+
/** Every key a journey's screens ask. */
|
|
44
|
+
asked: ReadonlySet<string>;
|
|
45
|
+
codeOwned: ReadonlySet<string>;
|
|
46
|
+
selfAdvancing: ReadonlySet<string>;
|
|
47
|
+
/** Actions that open another journey, by action key. */
|
|
48
|
+
handoffs: Readonly<Record<string, string>>;
|
|
49
|
+
/** Experience keys never walked. */
|
|
50
|
+
hiddenKeys: ReadonlySet<string>;
|
|
51
|
+
/** Responses every walk gives when a step asks for them, over the persona's own. */
|
|
52
|
+
responses: Readonly<Record<string, FieldValue>>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The route a persona's responses take through the experiences on this page.
|
|
56
|
+
*
|
|
57
|
+
* @param {Picked} client - The client.
|
|
58
|
+
* @param {Persona} persona - The persona.
|
|
59
|
+
* @param {RouteSettings} settings - What the walk reads beyond the persona.
|
|
60
|
+
* @param {{ origin: string; pathname: string; search: string }} here - This page's address, for a persona with no entry page.
|
|
61
|
+
* @param {string} here.origin - Its origin.
|
|
62
|
+
* @param {string} here.pathname - Its path.
|
|
63
|
+
* @param {string} here.search - Its query.
|
|
64
|
+
* @returns {Promise<BuilderWalk | null>} The walk, or `null` for a persona with no journey.
|
|
65
|
+
* @example await personaRoute(client, persona, settings, window.location)
|
|
66
|
+
*/
|
|
67
|
+
export declare function personaRoute(client: Picked, persona: Persona, settings: RouteSettings, here: {
|
|
68
|
+
origin: string;
|
|
69
|
+
pathname: string;
|
|
70
|
+
search: string;
|
|
71
|
+
}): Promise<BuilderWalk | null>;
|
|
72
|
+
/** A screen on a persona's route. */
|
|
73
|
+
export interface RouteScreen extends PersonaLanding {
|
|
74
|
+
stepKey: string;
|
|
75
|
+
/** `true` on the screen the persona stops on. */
|
|
76
|
+
isStop: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The screens on a route, in order, marking the persona's own stop: the screen it names, else
|
|
80
|
+
* where its responses run out.
|
|
81
|
+
*
|
|
82
|
+
* @param {BuilderWalk} walk - The route.
|
|
83
|
+
* @param {PersonaLanding | null} landing - Where the persona stops.
|
|
84
|
+
* @returns {RouteScreen[]} The screens.
|
|
85
|
+
* @example routeScreens(walk, persona.landing)
|
|
86
|
+
*/
|
|
87
|
+
export declare function routeScreens(walk: BuilderWalk, landing: PersonaLanding | null): RouteScreen[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{startOf as e}from"../../derive/personas/personas.js";import{builderGraph as t,responseKeysOf as n,walkDraft as r}from"../../derive/persona-builder/persona-builder.js";import"../../derive/index.js";import{evaluateRuleLogic as i}from"@picked/client";function a(e,t){let n=new URL(e,t);return{hash:n.hash,host:n.host,hostname:n.hostname,href:n.href,origin:n.origin,pathname:n.pathname,port:n.port,protocol:n.protocol,search:n.search}}function o(e,t){return async(n,r)=>!!await i(e,n,{responses:{...r},location:{...t}})}async function s(i,s,c,l){let u=e(s);if(u===null)return null;let d=new Map(i.fields.listStore.getState().map(e=>[e.key,e])),f=a(s.entryUrl??`${l.pathname}${l.search}`,l.origin);return r({...t(i.experiences.listStore.getState(),c.hiddenKeys),start:u,answers:{...s.responses,...c.responses},choices:s.choices,asked:c.asked,codeOwned:c.codeOwned,selfAdvancing:c.selfAdvancing,handoffs:c.handoffs,field:e=>d.get(e),evaluate:o(i,f),readsOf:n})}function c(e,t){let n=t?.stepKey==null?e.landing:t;return e.rows.flatMap(e=>e.kind===`screen`?[{experienceKey:e.experienceKey,experienceName:e.experienceName,stepKey:e.stepKey,stepName:e.stepName,isStop:n?.experienceKey===e.experienceKey&&n.stepKey===e.stepKey}]:[])}export{a as entryLocation,s as personaRoute,c as routeScreens,o as ruleEvaluator};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A persona session: the tab walks through the experience as one persona. Every field a
|
|
3
|
+
* screen asks shows the persona's response as its default, flagged `prefilled`, so the reviewer
|
|
4
|
+
* moves on with Continue or changes a value as a visitor would. Nothing is committed for them.
|
|
5
|
+
*
|
|
6
|
+
* The session lasts for the tab, in `sessionStorage`, so a reload keeps it, until the reviewer
|
|
7
|
+
* stops it. A shared link starts one with the `picked-persona` query parameter.
|
|
8
|
+
*/
|
|
9
|
+
import type { FieldValue } from '@picked/client';
|
|
10
|
+
/** The `sessionStorage` key that names the persona a tab walks through as. */
|
|
11
|
+
export declare const ACTIVE_PERSONA_KEY = "picked-devtools-persona-session";
|
|
12
|
+
/** The query parameter a shared link names a persona with. */
|
|
13
|
+
export declare const PERSONA_LINK_PARAM = "picked-persona";
|
|
14
|
+
/**
|
|
15
|
+
* The persona this tab walks through as.
|
|
16
|
+
*
|
|
17
|
+
* @param {Pick<Storage, 'getItem'>} storage - The tab's `sessionStorage`.
|
|
18
|
+
* @returns {string | null} The persona's id, or `null` outside a persona session.
|
|
19
|
+
* @example readActivePersona(sessionStorage)
|
|
20
|
+
*/
|
|
21
|
+
export declare function readActivePersona(storage: Pick<Storage, 'getItem'>): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Notes the persona this tab walks through as, or ends the session.
|
|
24
|
+
*
|
|
25
|
+
* @param {Pick<Storage, 'setItem' | 'removeItem'>} storage - The tab's `sessionStorage`.
|
|
26
|
+
* @param {string | null} id - The persona's id; `null` ends the session.
|
|
27
|
+
* @example writeActivePersona(sessionStorage, null)
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeActivePersona(storage: Pick<Storage, 'setItem' | 'removeItem'>, id: string | null): void;
|
|
30
|
+
/**
|
|
31
|
+
* The link that starts a walk-through as a site persona on its entry page.
|
|
32
|
+
*
|
|
33
|
+
* @param {{ origin: string; pathname: string; search: string }} here - This page's address.
|
|
34
|
+
* @param {string} here.origin - Its origin.
|
|
35
|
+
* @param {string} here.pathname - Its path.
|
|
36
|
+
* @param {string} here.search - Its query.
|
|
37
|
+
* @param {string | null} entryUrl - The persona's entry page.
|
|
38
|
+
* @param {string} id - The persona's id.
|
|
39
|
+
* @returns {string} The full URL.
|
|
40
|
+
* @example personaLink(window.location, '/sale', 'mover') // 'https://shop.example/sale?picked-persona=mover'
|
|
41
|
+
*/
|
|
42
|
+
export declare function personaLink(here: {
|
|
43
|
+
origin: string;
|
|
44
|
+
pathname: string;
|
|
45
|
+
search: string;
|
|
46
|
+
}, entryUrl: string | null, id: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Takes the persona a shared link names off the address bar, so a reload does not start it again.
|
|
49
|
+
*
|
|
50
|
+
* @param {{ href: string }} location - The page's location.
|
|
51
|
+
* @param {string} location.href - Its URL.
|
|
52
|
+
* @param {(url: string) => void} replace - Replaces the address without a reload.
|
|
53
|
+
* @returns {string | null} The persona's id, or `null` when the page was not opened from a link.
|
|
54
|
+
* @example takePersonaLink(window.location, (url) => history.replaceState(history.state, '', url))
|
|
55
|
+
*/
|
|
56
|
+
export declare function takePersonaLink(location: {
|
|
57
|
+
href: string;
|
|
58
|
+
}, replace: (url: string) => void): string | null;
|
|
59
|
+
/** A field as a walk-through reads and pre-fills it. */
|
|
60
|
+
export interface PrefillField {
|
|
61
|
+
key: string;
|
|
62
|
+
defaultValue?: FieldValue;
|
|
63
|
+
prefilled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/** A store a walk-through reads, and hears change. */
|
|
66
|
+
export interface ReadableStore<T> {
|
|
67
|
+
getState: () => T;
|
|
68
|
+
watch: (watcher: (state: T) => unknown) => () => void;
|
|
69
|
+
}
|
|
70
|
+
/** The part of the client a walk-through pre-fills. */
|
|
71
|
+
export interface PrefillClient {
|
|
72
|
+
fields: {
|
|
73
|
+
listStore: ReadableStore<readonly PrefillField[]>;
|
|
74
|
+
update: (key: string, updates: {
|
|
75
|
+
defaultValue?: FieldValue;
|
|
76
|
+
prefilled?: boolean;
|
|
77
|
+
}) => void;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** A running pre-fill. */
|
|
81
|
+
export interface Prefill {
|
|
82
|
+
/** Puts back each field's own default and stops pre-filling. */
|
|
83
|
+
stop: () => void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pre-fills every field a screen asks with the persona's response, now and as fields arrive.
|
|
87
|
+
*
|
|
88
|
+
* @param {PrefillClient} client - The client.
|
|
89
|
+
* @param {Readonly<Record<string, FieldValue>>} answers - The persona's responses.
|
|
90
|
+
* @param {ReadableStore<ReadonlySet<string>>} asked - The keys the experiences' screens ask.
|
|
91
|
+
* @returns {Prefill} Stops the pre-fill.
|
|
92
|
+
* @example const prefill = prefillPersona(client, persona.responses, session.askedStore)
|
|
93
|
+
*/
|
|
94
|
+
export declare function prefillPersona(client: PrefillClient, answers: Readonly<Record<string, FieldValue>>, asked: ReadableStore<ReadonlySet<string>>): Prefill;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{loadUrl as e}from"./load-persona.js";var t=`picked-devtools-persona-session`,n=`picked-persona`;function r(e){try{let n=e.getItem(t);return n!==null&&n.length>0?n:null}catch{return null}}function i(e,n){try{n===null?e.removeItem(t):e.setItem(t,n)}catch{}}function a(t,r,i){let a=new URL(e(r,t),t.origin);return a.searchParams.set(n,i),a.toString()}function o(e,t){let r=new URL(e.href),i=r.searchParams.get(n);return i===null||i.length===0?null:(r.searchParams.delete(n),t(r.toString()),i)}function s(e,t,n){let r=new Map,i=()=>{let i=n.getState();for(let n of e.fields.listStore.getState())n.prefilled!==!0&&i.has(n.key)&&Object.hasOwn(t,n.key)&&(r.has(n.key)||r.set(n.key,{defaultValue:n.defaultValue,prefilled:n.prefilled}),e.fields.update(n.key,{defaultValue:t[n.key],prefilled:!0}))},a=[e.fields.listStore.watch(i),n.watch(i)];return{stop:()=>{for(let e of a)e();for(let[t,n]of r)e.fields.update(t,n);r.clear()}}}export{t as ACTIVE_PERSONA_KEY,n as PERSONA_LINK_PARAM,a as personaLink,s as prefillPersona,r as readActivePersona,o as takePersonaLink,i as writeActivePersona};
|
|
@@ -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>
|
|
@@ -2,11 +2,9 @@
|
|
|
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).
|
|
6
5
|
-->
|
|
7
6
|
<script lang="ts">
|
|
8
|
-
/** @file The devtools sidebar:
|
|
9
|
-
import { DEVELOPER_LABEL } from '../derive/plain-language'
|
|
7
|
+
/** @file The devtools sidebar: one row per tab. */
|
|
10
8
|
import type { NavModel } from './nav'
|
|
11
9
|
import { nextFocusIndex } from './nav'
|
|
12
10
|
import SidebarRow from './SidebarRow.svelte'
|
|
@@ -15,13 +13,10 @@
|
|
|
15
13
|
nav: NavModel
|
|
16
14
|
/** The selected tab's id. */
|
|
17
15
|
selected: string
|
|
18
|
-
/** Whether the Developer group lists its screens. */
|
|
19
|
-
developerOpen: boolean
|
|
20
16
|
onSelect: (id: string) => void
|
|
21
|
-
onToggleDeveloper: () => void
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
const { nav, selected,
|
|
19
|
+
const { nav, selected, onSelect }: Props = $props()
|
|
25
20
|
|
|
26
21
|
let list: HTMLElement | undefined = $state()
|
|
27
22
|
|
|
@@ -52,30 +47,10 @@
|
|
|
52
47
|
<SidebarRow
|
|
53
48
|
label={item.label}
|
|
54
49
|
testId="devtools-nav-{item.id}"
|
|
55
|
-
depth={0}
|
|
56
50
|
selected={selected === item.id}
|
|
57
51
|
onPress={() => onSelect(item.id)}
|
|
58
52
|
/>
|
|
59
53
|
{/each}
|
|
60
|
-
<li class="divider" role="presentation"></li>
|
|
61
|
-
<SidebarRow
|
|
62
|
-
label={DEVELOPER_LABEL}
|
|
63
|
-
testId="devtools-nav-developer"
|
|
64
|
-
depth={0}
|
|
65
|
-
expanded={developerOpen}
|
|
66
|
-
onPress={onToggleDeveloper}
|
|
67
|
-
/>
|
|
68
|
-
{#if developerOpen}
|
|
69
|
-
{#each nav.developer as item (item.id)}
|
|
70
|
-
<SidebarRow
|
|
71
|
-
label={item.label}
|
|
72
|
-
testId="devtools-nav-{item.id}"
|
|
73
|
-
depth={2}
|
|
74
|
-
selected={selected === item.id}
|
|
75
|
-
onPress={() => onSelect(item.id)}
|
|
76
|
-
/>
|
|
77
|
-
{/each}
|
|
78
|
-
{/if}
|
|
79
54
|
</ul>
|
|
80
55
|
</nav>
|
|
81
56
|
|
|
@@ -96,10 +71,4 @@
|
|
|
96
71
|
flex-direction: column;
|
|
97
72
|
gap: 1px;
|
|
98
73
|
}
|
|
99
|
-
|
|
100
|
-
.divider {
|
|
101
|
-
height: 1px;
|
|
102
|
-
margin: var(--space-2) 0;
|
|
103
|
-
background-color: var(--color-gray-200);
|
|
104
|
-
}
|
|
105
74
|
</style>
|
|
@@ -1,45 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
/** @file One row of the devtools sidebar: a tab
|
|
2
|
+
/** @file One row of the devtools sidebar: a tab. */
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
label: string
|
|
6
6
|
testId: string
|
|
7
|
-
/** Indent depth: 0 for a top-level row, 2 for a screen inside the Developer group. */
|
|
8
|
-
depth: 0 | 2
|
|
9
7
|
onPress: () => void
|
|
10
|
-
/**
|
|
8
|
+
/** Whether it is the selected tab. */
|
|
11
9
|
selected?: boolean
|
|
12
|
-
/** A disclosure row: whether its group is open. Renders the chevron. */
|
|
13
|
-
expanded?: boolean
|
|
14
10
|
}
|
|
15
11
|
|
|
16
|
-
const { label, testId,
|
|
12
|
+
const { label, testId, onPress, selected = false }: Props = $props()
|
|
17
13
|
</script>
|
|
18
14
|
|
|
19
15
|
<li>
|
|
20
16
|
<button
|
|
21
17
|
type="button"
|
|
22
|
-
class="row
|
|
18
|
+
class="row"
|
|
23
19
|
class:active={selected}
|
|
24
20
|
aria-current={selected ? 'page' : undefined}
|
|
25
|
-
aria-expanded={expanded}
|
|
26
21
|
data-nav-row
|
|
27
22
|
data-testid={testId}
|
|
28
23
|
onclick={onPress}
|
|
29
24
|
>
|
|
30
|
-
{#if expanded !== undefined}
|
|
31
|
-
<span class="icon" class:open={expanded} aria-hidden="true">
|
|
32
|
-
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
33
|
-
<path
|
|
34
|
-
d="M6 4l4 4-4 4"
|
|
35
|
-
stroke="currentColor"
|
|
36
|
-
stroke-width="1.5"
|
|
37
|
-
stroke-linecap="round"
|
|
38
|
-
stroke-linejoin="round"
|
|
39
|
-
/>
|
|
40
|
-
</svg>
|
|
41
|
-
</span>
|
|
42
|
-
{/if}
|
|
43
25
|
<span class="label">{label}</span>
|
|
44
26
|
</button>
|
|
45
27
|
</li>
|
|
@@ -66,10 +48,6 @@
|
|
|
66
48
|
color 0.15s ease;
|
|
67
49
|
}
|
|
68
50
|
|
|
69
|
-
.row.depth-2 {
|
|
70
|
-
padding-left: var(--space-5);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
51
|
.row:hover {
|
|
74
52
|
color: var(--color-gray-900);
|
|
75
53
|
background-color: color-mix(in srgb, currentColor 5%, transparent);
|
|
@@ -86,20 +64,6 @@
|
|
|
86
64
|
outline-offset: -2px;
|
|
87
65
|
}
|
|
88
66
|
|
|
89
|
-
.icon {
|
|
90
|
-
display: flex;
|
|
91
|
-
flex-shrink: 0;
|
|
92
|
-
align-items: center;
|
|
93
|
-
justify-content: center;
|
|
94
|
-
width: 16px;
|
|
95
|
-
height: 16px;
|
|
96
|
-
transition: transform 0.15s ease;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.icon.open {
|
|
100
|
-
transform: rotate(90deg);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
67
|
.label {
|
|
104
68
|
flex: 1;
|
|
105
69
|
min-width: 0;
|
|
@@ -109,8 +73,7 @@
|
|
|
109
73
|
}
|
|
110
74
|
|
|
111
75
|
@media (prefers-reduced-motion: reduce) {
|
|
112
|
-
.row
|
|
113
|
-
.icon {
|
|
76
|
+
.row {
|
|
114
77
|
transition: none;
|
|
115
78
|
}
|
|
116
79
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import type { CustomToolIcon } from '../customTools';
|
|
3
3
|
import type { StorageGetter } from '../launcher/launcher';
|
|
4
4
|
/**
|
|
5
|
-
* What a sidebar entry opens: a review tab, a
|
|
6
|
-
*
|
|
5
|
+
* What a sidebar entry opens: a review tab, a custom tool, the Custom Tools page listed while no
|
|
6
|
+
* unplaced tool is registered, or one of the SDK's own screens.
|
|
7
7
|
*/
|
|
8
|
-
export type NavKind = 'review' | 'tool' | 'custom-tools' | '
|
|
8
|
+
export type NavKind = 'review' | 'tool' | 'custom-tools' | 'screen';
|
|
9
9
|
/** One entry of the sidebar: a row, or an icon at its foot. */
|
|
10
10
|
export interface NavItem {
|
|
11
11
|
/** Unique across the whole sidebar. */
|
|
@@ -15,27 +15,29 @@ export interface NavItem {
|
|
|
15
15
|
/** The glyph of an icon at the sidebar's foot. */
|
|
16
16
|
icon?: CustomToolIcon;
|
|
17
17
|
}
|
|
18
|
-
/** The sidebar's entries:
|
|
18
|
+
/** The sidebar's entries: its rows, and the icons at its foot. */
|
|
19
19
|
export interface NavModel {
|
|
20
20
|
primary: readonly NavItem[];
|
|
21
|
-
developer: readonly NavItem[];
|
|
22
21
|
footer: readonly NavItem[];
|
|
23
22
|
}
|
|
24
23
|
/** `sessionStorage` key holding the selected tab's id. */
|
|
25
24
|
export declare const TAB_STORAGE_KEY = "picked-devtools-tab";
|
|
26
|
-
/**
|
|
27
|
-
export declare const
|
|
25
|
+
/** The label of the Custom Tools row. */
|
|
26
|
+
export declare const CUSTOM_TOOLS_LABEL = "Custom Tools";
|
|
28
27
|
/** The tab a first open lands on, and where a vanished tab falls back to. */
|
|
29
28
|
export declare const DEFAULT_TAB = "guide";
|
|
30
|
-
/** The id of the Custom Tools
|
|
29
|
+
/** The id of the Custom Tools row, listed while no unplaced tool is registered; it links the how-to page. */
|
|
31
30
|
export declare const CUSTOM_TOOLS_TAB = "custom-tools";
|
|
32
|
-
/**
|
|
33
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The SDK's own screens, in the order the sidebar lists them after the review tabs and the site's
|
|
33
|
+
* tools. Experiences, Feature flags and Responses are review tabs, so they are not here.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SCREENS: readonly {
|
|
34
36
|
id: string;
|
|
35
37
|
label: string;
|
|
36
38
|
}[];
|
|
37
39
|
/**
|
|
38
|
-
* The sidebar id of a
|
|
40
|
+
* The sidebar id of a custom tool.
|
|
39
41
|
*
|
|
40
42
|
* @param {string} toolId - The tool's registered id.
|
|
41
43
|
* @returns {string} Its tab id, prefixed so it never collides with a review tab.
|
|
@@ -43,22 +45,27 @@ export declare const DEVELOPER_SCREENS: readonly {
|
|
|
43
45
|
*/
|
|
44
46
|
export declare function toolTabId(toolId: string): string;
|
|
45
47
|
/**
|
|
46
|
-
* The sidebar id of
|
|
48
|
+
* The sidebar id of one of the SDK's own screens.
|
|
47
49
|
*
|
|
48
|
-
* @param {string} screenId - The screen's id in {@link
|
|
50
|
+
* @param {string} screenId - The screen's id in {@link SCREENS}.
|
|
49
51
|
* @returns {string} Its tab id, prefixed so it never collides with a review tab.
|
|
50
|
-
* @example
|
|
52
|
+
* @example screenTabId('events') // 'screen:events'
|
|
51
53
|
*/
|
|
52
|
-
export declare function
|
|
54
|
+
export declare function screenTabId(screenId: string): string;
|
|
53
55
|
/** What decides the sidebar's rows. */
|
|
54
56
|
export interface NavInput {
|
|
55
|
-
/** Whether the workspace publishes a feature, which lists
|
|
57
|
+
/** Whether the workspace publishes a feature, which lists Feature flags. */
|
|
56
58
|
variations: boolean;
|
|
57
59
|
/** The review-placed custom tools, in registration order, each a tab of its own. */
|
|
58
60
|
tools: readonly {
|
|
59
61
|
id: string;
|
|
60
62
|
label: string;
|
|
61
63
|
}[];
|
|
64
|
+
/** The unplaced custom tools, in registration order, each a top-level row after the review tools. */
|
|
65
|
+
customTools: readonly {
|
|
66
|
+
id: string;
|
|
67
|
+
label: string;
|
|
68
|
+
}[];
|
|
62
69
|
/** The footer-placed custom tools, in registration order, each an icon at the sidebar's foot. */
|
|
63
70
|
footerTools: readonly {
|
|
64
71
|
id: string;
|
|
@@ -67,11 +74,11 @@ export interface NavInput {
|
|
|
67
74
|
}[];
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
70
|
-
* Builds the sidebar's entries.
|
|
77
|
+
* Builds the sidebar's entries: the review tabs, the site's tools, then the SDK's own screens.
|
|
71
78
|
*
|
|
72
79
|
* @param {NavInput} input - What this session lists.
|
|
73
|
-
* @returns {NavModel} The
|
|
74
|
-
* @example buildNav({ variations: true, tools: [], footerTools: [] })
|
|
80
|
+
* @returns {NavModel} The rows and the foot's icons.
|
|
81
|
+
* @example buildNav({ variations: true, tools: [], customTools: [], footerTools: [] })
|
|
75
82
|
*/
|
|
76
83
|
export declare function buildNav(input: NavInput): NavModel;
|
|
77
84
|
/**
|
|
@@ -83,15 +90,6 @@ export declare function buildNav(input: NavInput): NavModel;
|
|
|
83
90
|
* @example resolveTab('journey', nav) // 'journey'
|
|
84
91
|
*/
|
|
85
92
|
export declare function resolveTab(stored: string | null, nav: NavModel): string;
|
|
86
|
-
/**
|
|
87
|
-
* Whether a tab sits inside the Developer group.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} tab - A tab id.
|
|
90
|
-
* @param {NavModel} nav - The sidebar's rows.
|
|
91
|
-
* @returns {boolean} `true` for a developer screen.
|
|
92
|
-
* @example isInDeveloper('dev:events', nav) // true
|
|
93
|
-
*/
|
|
94
|
-
export declare function isInDeveloper(tab: string, nav: NavModel): boolean;
|
|
95
93
|
/**
|
|
96
94
|
* Where a key moves focus among the sidebar's visible rows.
|
|
97
95
|
*
|