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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/plugins/devtools/DevToolsExperience.svelte +169 -84
  3. package/dist/plugins/devtools/components/AdaptedToolHost.svelte +9 -3
  4. package/dist/plugins/devtools/customTools.d.ts +16 -7
  5. package/dist/plugins/devtools/customTools.js +1 -1
  6. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +1 -1
  7. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
  8. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
  9. package/dist/plugins/devtools/derive/index.d.ts +3 -3
  10. package/dist/plugins/devtools/derive/index.js +1 -1
  11. package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -1
  12. package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
  13. package/dist/plugins/devtools/derive/persona-builder/persona-builder.js +1 -1
  14. package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
  15. package/dist/plugins/devtools/derive/personas/index.js +1 -1
  16. package/dist/plugins/devtools/derive/personas/personas.d.ts +89 -8
  17. package/dist/plugins/devtools/derive/personas/personas.js +1 -1
  18. package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
  19. package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
  20. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +20 -25
  21. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
  22. package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
  23. package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
  24. package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -1
  25. package/dist/plugins/devtools/index.d.ts +1 -1
  26. package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
  27. package/dist/plugins/devtools/options.d.ts +27 -24
  28. package/dist/plugins/devtools/options.js +1 -1
  29. package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
  30. package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
  31. package/dist/plugins/devtools/review/index.d.ts +2 -2
  32. package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
  33. package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
  34. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +70 -24
  35. package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
  36. package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +15 -46
  37. package/dist/plugins/devtools/review/personas/PersonaCard.svelte +227 -44
  38. package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +191 -78
  39. package/dist/plugins/devtools/review/personas/load-persona.d.ts +43 -22
  40. package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
  41. package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
  42. package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
  43. package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
  44. package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
  45. package/dist/plugins/devtools/review/session/SessionPanel.svelte +11 -11
  46. package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
  47. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +11 -25
  48. package/dist/plugins/devtools/sidebar/Sidebar.svelte +2 -33
  49. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +5 -42
  50. package/dist/plugins/devtools/sidebar/nav.d.ts +26 -28
  51. package/dist/plugins/devtools/sidebar/nav.js +1 -1
  52. package/package.json +2 -2
  53. package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
  54. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
  55. package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
  56. package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
  57. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
  58. package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
  59. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
  60. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
  61. package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
  62. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
@@ -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 Answers tab, so the answer summary leads somewhere it can be acted on. */
19
+ /** Opens the Responses tab, so the response summary leads somewhere it can be acted on. */
20
20
  onShowAnswers: () => void
21
21
  }
22
22
 
@@ -65,18 +65,18 @@
65
65
  {#if journey === null}
66
66
  <Text size="medium">Nothing is open yet.</Text>
67
67
  <Text size="small" color="subdued">
68
- Start a journey on the page, and this will follow you.
68
+ Start an experience on the page, and this will follow you.
69
69
  </Text>
70
70
  {:else}
71
71
  <Stack direction="vertical" gap="none">
72
- <Text size="large" weight="semibold">{journey.stepName ?? 'Moving between screens'}</Text>
72
+ <Text size="large" weight="semibold">{journey.stepName ?? 'Moving between steps'}</Text>
73
73
  <Text size="small" color="subdued">in {journey.experienceName}</Text>
74
74
  </Stack>
75
75
  {#if journey.inTransit}
76
76
  <!-- The engine is transiting a condition or action node, so the recorded step is the one
77
77
  just LEFT. Saying so stops a "stuck on loading" report being filed against it. -->
78
78
  <Text size="small" color="subdued">
79
- Still working — the next screen has not settled yet.
79
+ Still working — the next step has not settled yet.
80
80
  </Text>
81
81
  {/if}
82
82
  {/if}
@@ -111,24 +111,24 @@
111
111
  <Card>
112
112
  <Stack direction="vertical" gap="small">
113
113
  <Stack direction="horizontal" gap="small" align="space-between" justify="center">
114
- <Text size="caption" color="subdued" weight="semibold">WHAT YOU ANSWERED</Text>
114
+ <Text size="caption" color="subdued" weight="semibold">YOUR RESPONSES</Text>
115
115
  {#if rows.length > 0}
116
- <Button variant="ghost" size="small" onclick={onShowAnswers}>Change an answer</Button>
116
+ <Button variant="ghost" size="small" onclick={onShowAnswers}>Change a response</Button>
117
117
  {/if}
118
118
  </Stack>
119
119
 
120
120
  {#if rows.length === 0}
121
121
  <EmptyState
122
- message="Nothing answered yet"
123
- hint="Each screen you answer is listed here, in order. Answers from before this page loaded are on the Answers tab."
122
+ message="No responses yet"
123
+ hint="Each step you submit is listed here, in order. Responses from before this page loaded are on the Responses tab."
124
124
  />
125
125
  {:else}
126
126
  <table class="history" data-testid="devtools-session-answers">
127
127
  <thead>
128
128
  <tr>
129
- <th scope="col">Screen</th>
130
- <th scope="col">Question</th>
131
- <th scope="col">Answer</th>
129
+ <th scope="col">Step</th>
130
+ <th scope="col">Field</th>
131
+ <th scope="col">Response</th>
132
132
  </tr>
133
133
  </thead>
134
134
  <tbody>
@@ -1,12 +1,11 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * @file The Variations tab: features as named variations, aimed at someone deciding whether a
4
- * variation looks right rather than someone debugging bucketing.
3
+ * @file The Feature flags tab: each feature flag by name, aimed at someone deciding whether a
4
+ * variant looks right.
5
5
  *
6
6
  * The mechanism is the SDK's: `?picked-feature=<id>:<value>` forces an arm on the next page load.
7
- * What changes is the framing — a variation is NAMED, its arms are radio buttons rather than an
8
- * include checkbox plus a value dropdown, and the payoff is a link the reviewer can send to
9
- * someone else so both people look at the same thing.
7
+ * A feature flag is NAMED, its variants are radio buttons, and the payoff is a link the reviewer
8
+ * can send to someone else so both people look at the same thing.
10
9
  */
11
10
  import type { FeatureBucket, Picked } from '@picked/client'
12
11
  import { Badge, Button, Card, CodeBlock, CopyButton, EmptyState, Stack, Text } from '../../ui'
@@ -158,8 +157,8 @@
158
157
  <Stack direction="vertical" gap="medium">
159
158
  {#if $features.length === 0}
160
159
  <EmptyState
161
- message="No variations are running on this site"
162
- hint="When an A/B test or a switched-on feature is published, it appears here and you can force either version."
160
+ message="No feature flags are running on this site"
161
+ hint="When a feature flag is published, it appears here and you can force any of its variants."
163
162
  />
164
163
  {:else}
165
164
  <Card sectioned>
@@ -174,13 +173,13 @@
174
173
  <Badge status="default" size="small">Now: {armLabel(feature, served)}</Badge>
175
174
  {:else}
176
175
  <Button variant="ghost" size="small" onclick={() => resolveBucket(feature)}>
177
- Check which version
176
+ Check which variant
178
177
  </Button>
179
178
  {/if}
180
179
  </div>
181
180
 
182
181
  {#if arms}
183
- <div class="arms" role="group" aria-label="Version of {feature.name}">
182
+ <div class="arms" role="group" aria-label="Variant of {feature.name}">
184
183
  {#each arms as arm, index (index)}
185
184
  <label class="option-label">
186
185
  <input
@@ -198,8 +197,8 @@
198
197
  class="inline-input"
199
198
  type="text"
200
199
  value={chosen[feature.id] ?? ''}
201
- placeholder="Type the version to force"
202
- aria-label="Version of {feature.name}"
200
+ placeholder="Type the variant to force"
201
+ aria-label="Variant of {feature.name}"
203
202
  oninput={(event) => choose(feature.id, event.currentTarget.value)}
204
203
  />
205
204
  {/if}
@@ -221,14 +220,14 @@
221
220
  <Stack direction="vertical" gap="small">
222
221
  {#if chosenCount === 0}
223
222
  <Text size="small" color="subdued">
224
- Pick a version above. The page reloads showing it, and the link below reproduces exactly
223
+ Pick a variant above. The page reloads showing it, and the link below reproduces exactly
225
224
  what you saw for anyone you send it to.
226
225
  </Text>
227
226
  {:else}
228
227
  <CodeBlock code={shareUrl} />
229
228
  <Stack direction="horizontal" gap="small">
230
229
  <Button variant="primary" size="small" onclick={apply} testId="devtools-variations-apply">
231
- Show me this version
230
+ Show me this variant
232
231
  </Button>
233
232
  <CopyButton
234
233
  text={shareUrl}
@@ -1,35 +1,21 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * @file The Custom Tools tab: every custom tool placed neither `'review'` nor `'footer'`, as
4
- * nested tabs. With none, it says what a custom tool is and links the how-to page.
3
+ * @file The Custom Tools page, listed while no tool sits in that group: it says what a custom
4
+ * tool is and links the how-to page.
5
5
  */
6
6
  import { getPicked } from '../../../../hooks'
7
- import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
8
- import Tabs from '../../components/Tabs.svelte'
9
- import { developerTools, readCustomTools } from '../../customTools'
10
7
  import { Button, EmptyState, Stack } from '../../ui'
11
8
 
12
9
  const client = getPicked()
13
10
  const docsUrl = `${client.documentationSiteUrl}/how-to/add-a-tool-to-the-devtools`
14
- const tools = developerTools(readCustomTools(client))
15
- const tabs = tools.map((tool) => ({
16
- id: tool.id,
17
- label: tool.label,
18
- component: AdaptedToolHost,
19
- props: { tool },
20
- }))
21
11
  </script>
22
12
 
23
- {#if tabs.length > 0}
24
- <Tabs {tabs} />
25
- {:else}
26
- <Stack direction="vertical" gap="medium" padding="medium" justify="center">
27
- <EmptyState
28
- message="No custom tools yet"
29
- hint="A developer can add tools for this site here, such as test customers or a way to request a change."
30
- />
31
- <Button variant="secondary" size="small" href={docsUrl} target="_blank">
32
- How to add a custom tool
33
- </Button>
34
- </Stack>
35
- {/if}
13
+ <Stack direction="vertical" gap="medium" padding="medium" justify="center">
14
+ <EmptyState
15
+ message="No custom tools yet"
16
+ hint="A developer can add tools for this site here, such as test accounts or a way to request a change."
17
+ />
18
+ <Button variant="secondary" size="small" href={docsUrl} target="_blank">
19
+ How to add a custom tool
20
+ </Button>
21
+ </Stack>
@@ -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: the main tabs, then the folding Developer group. */
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, developerOpen, onSelect, onToggleDeveloper }: Props = $props()
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, or the disclosure that folds the Developer group. */
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
- /** A tab row: whether it is the selected tab. */
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, depth, onPress, selected = false, expanded }: Props = $props()
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 depth-{depth}"
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 review- or footer-placed custom tool, the Custom Tools
6
- * tab, or a developer screen.
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' | 'developer';
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: the top-level tabs, the Developer group's screens, and the foot's icons. */
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
- /** `sessionStorage` key holding `'true'` while the Developer group is open. */
27
- export declare const DEVELOPER_OPEN_STORAGE_KEY = "picked-devtools-developer-open";
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 tab, which holds every tool placed neither `'review'` nor `'footer'`. */
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
- /** The developer screens, in the order the Developer group lists them. */
33
- export declare const DEVELOPER_SCREENS: readonly {
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 review- or footer-placed custom tool.
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 a developer screen.
48
+ * The sidebar id of one of the SDK's own screens.
47
49
  *
48
- * @param {string} screenId - The screen's id in {@link DEVELOPER_SCREENS}.
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 developerTabId('responses') // 'dev:responses'
52
+ * @example screenTabId('events') // 'screen:events'
51
53
  */
52
- export declare function developerTabId(screenId: string): string;
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 Variations. */
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 top-level tabs, the Developer group's screens and the foot's icons.
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
  *