@picked/svelte 0.0.1-dev.1789541668638 → 0.0.1-dev.1793756239746

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 (118) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +51 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/_virtual/_vite/preload-helper.js +1 -1
  5. package/dist/components/index.js +1 -1
  6. package/dist/experience/ExperienceHost.svelte +29 -13
  7. package/dist/experience/ExperienceSlot.svelte +21 -3
  8. package/dist/experience/ExperienceWrapper.svelte +23 -0
  9. package/dist/form/index.js +1 -1
  10. package/dist/hooks/action.d.ts +47 -18
  11. package/dist/hooks/action.js +1 -1
  12. package/dist/hooks/index.d.ts +2 -2
  13. package/dist/hooks/index.js +1 -1
  14. package/dist/hooks/step.d.ts +14 -0
  15. package/dist/hooks/step.js +1 -1
  16. package/dist/plugins/devtools/DevToolsExperience.svelte +545 -203
  17. package/dist/plugins/devtools/components/AdaptedToolHost.svelte +2 -2
  18. package/dist/plugins/devtools/constants.d.ts +5 -0
  19. package/dist/plugins/devtools/constants.js +1 -0
  20. package/dist/plugins/devtools/customTools.d.ts +42 -6
  21. package/dist/plugins/devtools/customTools.js +1 -1
  22. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +112 -0
  23. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -0
  24. package/dist/plugins/devtools/derive/answer-display/index.d.ts +3 -0
  25. package/dist/plugins/devtools/derive/answer-display/index.js +1 -0
  26. package/dist/plugins/devtools/derive/answer-history/answer-history.d.ts +76 -0
  27. package/dist/plugins/devtools/derive/answer-history/answer-history.js +1 -0
  28. package/dist/plugins/devtools/derive/answer-history/index.d.ts +3 -0
  29. package/dist/plugins/devtools/derive/answer-history/index.js +1 -0
  30. package/dist/plugins/devtools/derive/guide-sections/guide-sections.d.ts +67 -0
  31. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -0
  32. package/dist/plugins/devtools/derive/guide-sections/index.d.ts +3 -0
  33. package/dist/plugins/devtools/derive/guide-sections/index.js +1 -0
  34. package/dist/plugins/devtools/derive/index.d.ts +28 -0
  35. package/dist/plugins/devtools/derive/index.js +1 -0
  36. package/dist/plugins/devtools/derive/journey-outline/index.d.ts +3 -0
  37. package/dist/plugins/devtools/derive/journey-outline/index.js +1 -0
  38. package/dist/plugins/devtools/derive/journey-outline/journey-outline.d.ts +86 -0
  39. package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -0
  40. package/dist/plugins/devtools/derive/journey-pick/index.d.ts +3 -0
  41. package/dist/plugins/devtools/derive/journey-pick/index.js +1 -0
  42. package/dist/plugins/devtools/derive/journey-pick/journey-pick.d.ts +73 -0
  43. package/dist/plugins/devtools/derive/journey-pick/journey-pick.js +1 -0
  44. package/dist/plugins/devtools/derive/journey-timeline/index.d.ts +3 -0
  45. package/dist/plugins/devtools/derive/journey-timeline/index.js +1 -0
  46. package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.d.ts +70 -0
  47. package/dist/plugins/devtools/derive/journey-timeline/journey-timeline.js +1 -0
  48. package/dist/plugins/devtools/derive/journey-tree/index.d.ts +3 -0
  49. package/dist/plugins/devtools/derive/journey-tree/index.js +1 -0
  50. package/dist/plugins/devtools/derive/journey-tree/journey-tree.d.ts +191 -0
  51. package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -0
  52. package/dist/plugins/devtools/derive/plain-language/index.d.ts +3 -0
  53. package/dist/plugins/devtools/derive/plain-language/index.js +1 -0
  54. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +73 -0
  55. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -0
  56. package/dist/plugins/devtools/derive/plain-logic/index.d.ts +3 -0
  57. package/dist/plugins/devtools/derive/plain-logic/index.js +1 -0
  58. package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +48 -0
  59. package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -0
  60. package/dist/plugins/devtools/derive/safety-posture/index.d.ts +3 -0
  61. package/dist/plugins/devtools/derive/safety-posture/index.js +1 -0
  62. package/dist/plugins/devtools/derive/safety-posture/safety-posture.d.ts +43 -0
  63. package/dist/plugins/devtools/derive/safety-posture/safety-posture.js +1 -0
  64. package/dist/plugins/devtools/derive/visited-steps/index.d.ts +3 -0
  65. package/dist/plugins/devtools/derive/visited-steps/index.js +1 -0
  66. package/dist/plugins/devtools/derive/visited-steps/visited-steps.d.ts +35 -0
  67. package/dist/plugins/devtools/derive/visited-steps/visited-steps.js +1 -0
  68. package/dist/plugins/devtools/derive/walk-route/index.d.ts +3 -0
  69. package/dist/plugins/devtools/derive/walk-route/index.js +1 -0
  70. package/dist/plugins/devtools/derive/walk-route/walk-route.d.ts +22 -0
  71. package/dist/plugins/devtools/derive/walk-route/walk-route.js +1 -0
  72. package/dist/plugins/devtools/index.d.ts +5 -2
  73. package/dist/plugins/devtools/index.js +1 -1
  74. package/dist/plugins/devtools/launcher/Launcher.svelte +150 -0
  75. package/dist/plugins/devtools/launcher/launcher.d.ts +36 -0
  76. package/dist/plugins/devtools/launcher/launcher.js +1 -0
  77. package/dist/plugins/devtools/options.d.ts +106 -0
  78. package/dist/plugins/devtools/options.js +1 -0
  79. package/dist/plugins/devtools/plugin.d.ts +8 -8
  80. package/dist/plugins/devtools/plugin.js +1 -1
  81. package/dist/plugins/devtools/posture-context.d.ts +15 -0
  82. package/dist/plugins/devtools/posture-context.js +1 -0
  83. package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +526 -0
  84. package/dist/plugins/devtools/review/guide/GuideLine.svelte +20 -0
  85. package/dist/plugins/devtools/review/guide/GuidePanel.svelte +133 -0
  86. package/dist/plugins/devtools/review/index.d.ts +6 -0
  87. package/dist/plugins/devtools/review/index.js +1 -0
  88. package/dist/plugins/devtools/review/journey/JourneyMap.svelte +1078 -0
  89. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +608 -0
  90. package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +756 -0
  91. package/dist/plugins/devtools/review/journey/TimelineMarker.svelte +59 -0
  92. package/dist/plugins/devtools/review/session/SessionPanel.svelte +214 -0
  93. package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +279 -0
  94. package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +6 -0
  95. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +3 -3
  96. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +6 -0
  97. package/dist/plugins/devtools/session/index.d.ts +3 -0
  98. package/dist/plugins/devtools/session/index.js +1 -0
  99. package/dist/plugins/devtools/session/session.d.ts +90 -0
  100. package/dist/plugins/devtools/session/session.js +1 -0
  101. package/dist/plugins/devtools/session-access.d.ts +32 -0
  102. package/dist/plugins/devtools/session-access.js +1 -0
  103. package/dist/plugins/devtools/sidebar/Sidebar.svelte +105 -0
  104. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +117 -0
  105. package/dist/plugins/devtools/sidebar/nav.d.ts +105 -0
  106. package/dist/plugins/devtools/sidebar/nav.js +1 -0
  107. package/dist/plugins/devtools/ui/actions/Button.svelte +10 -1
  108. package/dist/plugins/devtools/ui/actions/CopyButton.svelte +13 -1
  109. package/dist/plugins/devtools/ui/data/Badge.svelte +3 -2
  110. package/dist/plugins/devtools/ui/data/EmptyState.svelte +16 -1
  111. package/dist/plugins/devtools/ui/feedback/Banner.svelte +3 -2
  112. package/dist/plugins/framework/index.js +1 -1
  113. package/dist/render/browser.js +1 -1
  114. package/package.json +23 -12
  115. package/dist/form/__tests__/CreateFormHarness.svelte +0 -30
  116. package/dist/hooks/events.d.ts +0 -2
  117. package/dist/hooks/events.js +0 -1
  118. package/dist/vite-env.d.js +0 -0
@@ -6,172 +6,415 @@
6
6
  the change in the paired React component to maintain visual and behavioral parity.
7
7
  -->
8
8
  <script lang="ts">
9
- import { getExperience, getPicked } from '../../hooks'
10
- import { Button, Heading, Stack } from './ui'
11
- import Tabs from './components/Tabs.svelte'
12
- import ExperiencesListScreen from './screens/experiences/ExperiencesListScreen.svelte'
13
- import ExperienceInstancesListScreen from './screens/experience-instances/ExperienceInstancesListScreen.svelte'
14
- import PredicatesListScreen from './screens/predicates/PredicatesListScreen.svelte'
15
- import ComponentsListScreen from './screens/components/ComponentsListScreen.svelte'
16
- import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
9
+ /** @file The devtools shell: the floating button, and a panel of tabs listed down its left-hand side. */
10
+ import type { Component } from 'svelte'
11
+ import { tick } from 'svelte'
12
+ import { getExperience, getPicked, useUnit } from '../../hooks'
13
+ import { Badge, Button, Heading, Switch } from './ui'
14
+ import PickedIcon from './PickedIcon.svelte'
15
+ import AdaptedToolHost from './components/AdaptedToolHost.svelte'
16
+ import Launcher from './launcher/Launcher.svelte'
17
+ import { readAutoHide, writeAutoHide } from './launcher/launcher'
18
+ import Sidebar from './sidebar/Sidebar.svelte'
19
+ import {
20
+ DEVELOPER_OPEN_STORAGE_KEY,
21
+ TAB_STORAGE_KEY,
22
+ buildNav,
23
+ developerTabId,
24
+ isInDeveloper,
25
+ readStored,
26
+ resolveTab,
27
+ toolTabId,
28
+ writeStored,
29
+ } from './sidebar/nav'
30
+ import { readDevtoolsOptions } from './options'
31
+ import { readCustomTools, reviewTools } from './customTools'
32
+ import { provideDevtoolsSession } from './session-access'
33
+ import { setPostureContext } from './posture-context'
34
+ import { createSessionData } from './session'
35
+ import {
36
+ TAB_INTROS,
37
+ configSourceLabel,
38
+ derivePosture,
39
+ guideFixes,
40
+ guideSections,
41
+ type GuideSection,
42
+ type ReviewTabId,
43
+ } from './derive'
44
+ import { AnswersPanel, GuidePanel, JourneyPanel, SessionPanel, VariationsPanel } from './review'
17
45
  import ProjectScreen from './screens/project/ProjectScreen.svelte'
46
+ import ComponentsListScreen from './screens/components/ComponentsListScreen.svelte'
18
47
  import ConfigScreen from './screens/config/ConfigScreen.svelte'
19
- import IdentityScreen from './screens/identity/IdentityScreen.svelte'
20
- import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
21
- import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
22
- import FieldsListScreen from './screens/fields/FieldsListScreen.svelte'
23
48
  import ContentListScreen from './screens/content/ContentListScreen.svelte'
24
- import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
25
49
  import EventsListScreen from './screens/events/EventsListScreen.svelte'
50
+ import ExperienceInstancesListScreen from './screens/experience-instances/ExperienceInstancesListScreen.svelte'
51
+ import ExperiencesListScreen from './screens/experiences/ExperiencesListScreen.svelte'
52
+ import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
53
+ import FieldsListScreen from './screens/fields/FieldsListScreen.svelte'
54
+ import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
55
+ import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
56
+ import IdentityScreen from './screens/identity/IdentityScreen.svelte'
57
+ import PredicatesListScreen from './screens/predicates/PredicatesListScreen.svelte'
58
+ import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
26
59
  import CustomToolsScreen from './screens/custom-tools/CustomToolsScreen.svelte'
27
- import PickedIcon from './PickedIcon.svelte'
28
60
 
29
61
  const experience = getExperience()
30
62
  const client = getPicked()
31
63
 
64
+ const options = readDevtoolsOptions(client)
65
+ const tools = reviewTools(readCustomTools(client))
66
+ const posture = derivePosture({
67
+ readOnly: options.readOnly,
68
+ environmentLabel: options.environmentLabel,
69
+ })
70
+ setPostureContext(posture)
71
+
32
72
  const dashboardProjectUrl = `${client.dashboardUrl}/p/${client.projectId}`
33
- const isPreview = Boolean(client.searchParams['preview-token']?.[0])
34
-
35
- const tabs = [
36
- {
37
- id: 'project',
38
- label: 'Project',
39
- component: ProjectScreen,
40
- props: {},
41
- },
42
- {
43
- id: 'components',
44
- label: 'Components',
45
- component: ComponentsListScreen,
46
- props: {},
47
- },
48
- {
49
- id: 'config',
50
- label: 'Config',
51
- component: ConfigScreen,
52
- props: {},
53
- },
54
- {
55
- id: 'content',
56
- label: 'Content',
57
- component: ContentListScreen,
58
- props: {},
59
- },
60
- {
61
- id: 'events',
62
- label: 'Events',
63
- component: EventsListScreen,
64
- props: {},
65
- },
66
- {
67
- id: 'experience-instances',
68
- label: 'Experience Instances',
69
- component: ExperienceInstancesListScreen,
70
- props: {},
71
- },
72
- {
73
- id: 'experiences',
74
- label: 'Experiences',
75
- component: ExperiencesListScreen,
76
- props: {},
77
- },
78
- {
79
- id: 'features',
80
- label: 'Features',
81
- component: FeaturesListScreen,
82
- props: {},
83
- },
84
- {
85
- id: 'fields',
86
- label: 'Fields',
87
- component: FieldsListScreen,
88
- props: {},
89
- },
90
- {
91
- id: 'frameworks',
92
- label: 'Frameworks',
93
- component: FrameworksListScreen,
94
- props: {},
95
- },
96
- {
97
- id: 'history',
98
- label: 'History',
99
- component: HistoryEntriesListScreen,
100
- props: {},
101
- },
102
- {
103
- id: 'identity',
104
- label: 'Identity',
105
- component: IdentityScreen,
106
- props: {},
107
- },
108
- {
109
- id: 'predicates',
110
- label: 'Predicates',
111
- component: PredicatesListScreen,
112
- props: {},
113
- },
114
- {
115
- id: 'responses',
116
- label: 'Responses',
117
- component: ResponsesListScreen,
118
- props: {},
119
- },
120
- {
121
- id: 'custom-tools',
122
- label: 'Custom Tools',
123
- component: CustomToolsScreen,
124
- props: {},
125
- },
126
- ]
127
-
128
- const isOpen = experience.isOpenStore
73
+ const previewToken = client.searchParams['preview-token']?.[0]
74
+ const preview = previewTag()
75
+ const configSource = configSourceLabel(previewToken)
76
+
77
+ /**
78
+ * The tag beside the floating button: a preview session, a preview whose workspace is gone, or none.
79
+ *
80
+ * @returns {'preview' | 'unavailable' | null} The tag to show.
81
+ * @example previewTag() // 'preview' with a preview token
82
+ */
83
+ function previewTag(): 'preview' | 'unavailable' | null {
84
+ if (client.previewUnavailable) return 'unavailable'
85
+ return previewToken ? 'preview' : null
86
+ }
87
+
88
+ const sessionStorageOf = (): Storage => window.sessionStorage
89
+ const localStorageOf = (): Storage => window.localStorage
90
+
91
+ /** The developer screens, keyed by their sidebar id. */
92
+ const DEVELOPER_COMPONENTS: Record<string, Component> = {
93
+ [developerTabId('project')]: ProjectScreen,
94
+ [developerTabId('components')]: ComponentsListScreen,
95
+ [developerTabId('config')]: ConfigScreen,
96
+ [developerTabId('content')]: ContentListScreen,
97
+ [developerTabId('events')]: EventsListScreen,
98
+ [developerTabId('experience-instances')]: ExperienceInstancesListScreen,
99
+ [developerTabId('experiences')]: ExperiencesListScreen,
100
+ [developerTabId('features')]: FeaturesListScreen,
101
+ [developerTabId('fields')]: FieldsListScreen,
102
+ [developerTabId('frameworks')]: FrameworksListScreen,
103
+ [developerTabId('history')]: HistoryEntriesListScreen,
104
+ [developerTabId('identity')]: IdentityScreen,
105
+ [developerTabId('predicates')]: PredicatesListScreen,
106
+ [developerTabId('responses')]: ResponsesListScreen,
107
+ [developerTabId('custom-tools')]: CustomToolsScreen,
108
+ }
109
+
110
+ // ── The session record ───────────────────────────────────────────────────────────────────────
111
+ // Made at mount, which is launch: the launcher renders while the panel is closed, so the trail
112
+ // and the answer history listen from the start.
113
+ const session = createSessionData(client, {
114
+ exclude: options.hiddenExperienceKeys,
115
+ precedence: options.journeyPrecedence,
116
+ codeOwned: options.codeOwnedFields,
117
+ })
118
+ const releaseSession = provideDevtoolsSession(client, { session, posture })
119
+ $effect(() => () => {
120
+ releaseSession()
121
+ session.destroy()
122
+ })
123
+
124
+ const trails = useUnit(session.trailsStore)
125
+ const answerHistory = useUnit(session.historyStore)
126
+ const asked = useUnit(session.askedStore)
127
+ const journey = useUnit(session.journeyStore)
128
+ const features = useUnit(client.features.listStore)
129
+ const isOpen = useUnit(experience.isOpenStore)
130
+
131
+ // ── Tabs ─────────────────────────────────────────────────────────────────────────────────────
132
+ const nav = $derived(buildNav({ variations: $features.length > 0, tools }))
133
+ const storedTab = readStored(sessionStorageOf, TAB_STORAGE_KEY)
134
+ let tab = $state(storedTab ?? 'guide')
135
+ let developerOpen = $state(readStored(sessionStorageOf, DEVELOPER_OPEN_STORAGE_KEY) === 'true')
136
+ let drawerOpen = $state(false)
137
+
138
+ // A tab can disappear under the viewer (Variations once the feature list is empty), and a stored
139
+ // developer screen opens its group.
140
+ $effect(() => {
141
+ const resolved = resolveTab(tab, nav)
142
+ if (resolved !== tab) tab = resolved
143
+ if (isInDeveloper(tab, nav) && !developerOpen) developerOpen = true
144
+ })
145
+
146
+ const selectedItem = $derived([...nav.primary, ...nav.developer].find((item) => item.id === tab))
147
+ const selectedTool = $derived(tools.find((tool) => toolTabId(tool.id) === tab))
148
+ const developerScreen = $derived(DEVELOPER_COMPONENTS[tab])
149
+
150
+ /**
151
+ * Narrows a tab id to one of the review tabs.
152
+ *
153
+ * @param {string} id - A sidebar tab id.
154
+ * @returns {boolean} `true` for a review tab.
155
+ * @example isReviewTab('answers') // true
156
+ */
157
+ function isReviewTab(id: string): id is ReviewTabId {
158
+ return id in TAB_INTROS
159
+ }
160
+
161
+ /**
162
+ * Selects a tab, remembers it, and closes the narrow-panel drawer.
163
+ *
164
+ * @param {string} id - The tab to select.
165
+ * @example selectTab('journey')
166
+ */
167
+ function selectTab(id: string): void {
168
+ tab = id
169
+ drawerOpen = false
170
+ writeStored(sessionStorageOf, TAB_STORAGE_KEY, id)
171
+ }
172
+
173
+ /**
174
+ * Opens or folds the Developer group, and remembers it.
175
+ *
176
+ * @example toggleDeveloper()
177
+ */
178
+ function toggleDeveloper(): void {
179
+ developerOpen = !developerOpen
180
+ writeStored(sessionStorageOf, DEVELOPER_OPEN_STORAGE_KEY, String(developerOpen))
181
+ }
182
+
183
+ // ── How to use ───────────────────────────────────────────────────────────────────────────────
184
+ const guide = $derived.by(() => {
185
+ const available = { variations: $features.length > 0 }
186
+ const lead: GuideSection[] = options.guide.sections.map((section) => ({ ...section, tab: null }))
187
+ const toolSections: GuideSection[] = tools.flatMap((tool) =>
188
+ tool.guide
189
+ ? [
190
+ {
191
+ id: `tool-${tool.id}`,
192
+ title: tool.label,
193
+ steps: tool.guide.steps,
194
+ notes: tool.guide.notes ?? [],
195
+ tab: toolTabId(tool.id),
196
+ },
197
+ ]
198
+ : [],
199
+ )
200
+ return {
201
+ sections: guideSections(available, { lead, tools: toolSections }),
202
+ fixes: guideFixes(available, options.guide.fixes),
203
+ tabLabels: Object.fromEntries(nav.primary.map((item) => [item.id, item.label])),
204
+ }
205
+ })
206
+
207
+ const followUp = options.answerFollowUp
208
+ ? { label: options.answerFollowUp.label, run: () => options.answerFollowUp?.run(client) }
209
+ : null
210
+
211
+ // ── The floating button ──────────────────────────────────────────────────────────────────────
212
+ let autoHide = $state(readAutoHide(localStorageOf, options.launcher.autoHide))
213
+
214
+ /**
215
+ * Records the viewer's auto-hide choice.
216
+ *
217
+ * @param {boolean} value - Whether the button auto-hides.
218
+ * @example setAutoHide(false)
219
+ */
220
+ function setAutoHide(value: boolean): void {
221
+ autoHide = value
222
+ writeAutoHide(localStorageOf, value)
223
+ }
224
+
225
+ // ── Dialog behaviour ─────────────────────────────────────────────────────────────────────────
226
+ /** The open panel; Svelte sets it back to `null` when the panel unmounts. */
227
+ let panel: HTMLDivElement | null = $state(null)
228
+
229
+ const FOCUSABLE =
230
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
231
+
232
+ /**
233
+ * The panel's focusable elements, in document order.
234
+ *
235
+ * @returns {HTMLElement[]} Every focusable element inside the panel.
236
+ * @example focusables().at(0)?.focus()
237
+ */
238
+ function focusables(): HTMLElement[] {
239
+ if (panel === null) return []
240
+ return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
241
+ (node): node is HTMLElement => node instanceof HTMLElement && node.offsetParent !== null,
242
+ )
243
+ }
244
+
245
+ /**
246
+ * Keeps Tab inside the panel, and closes the drawer, then the panel, on Escape.
247
+ *
248
+ * @param {KeyboardEvent} event - A key pressed inside the panel.
249
+ * @example onPanelKeydown(new KeyboardEvent('keydown', { key: 'Escape' }))
250
+ */
251
+ function onPanelKeydown(event: KeyboardEvent): void {
252
+ if (event.key === 'Escape') {
253
+ event.stopPropagation()
254
+ if (drawerOpen) drawerOpen = false
255
+ else experience.close()
256
+ return
257
+ }
258
+
259
+ if (event.key !== 'Tab') return
260
+
261
+ const nodes = focusables()
262
+ const first = nodes.at(0)
263
+ const last = nodes.at(-1)
264
+ if (first === undefined || last === undefined) return
265
+ if (event.shiftKey && event.target === first) {
266
+ event.preventDefault()
267
+ last.focus()
268
+ } else if (!event.shiftKey && event.target === last) {
269
+ event.preventDefault()
270
+ first.focus()
271
+ }
272
+ }
273
+
274
+ $effect(() => {
275
+ if (!$isOpen) return
276
+ void tick().then(() => focusables().at(0)?.focus())
277
+ })
129
278
  </script>
130
279
 
131
280
  {#if $isOpen}
132
- <div
133
- data-testid="devtools"
134
- class="devtools"
135
- role="dialog"
136
- aria-modal="true"
137
- aria-labelledby="devtools-header"
138
- >
139
- <div class="backdrop"></div>
140
- <div class="container">
141
- <header class="toolbar" id="devtools-header">
281
+ <div data-testid="devtools" class="devtools">
282
+ <!-- The backdrop is a pointer target only; the keyboard closes the dialog with Escape. -->
283
+ <div class="backdrop" aria-hidden="true" onclick={() => experience.close()}></div>
284
+ <div
285
+ bind:this={panel}
286
+ class="container"
287
+ role="dialog"
288
+ aria-modal="true"
289
+ aria-labelledby="devtools-header"
290
+ tabindex="-1"
291
+ onkeydown={onPanelKeydown}
292
+ >
293
+ <header class="toolbar">
142
294
  <div class="toolbar-left">
143
- <Stack direction="horizontal" gap="small" >
144
- <PickedIcon height={25} width={25}/>
145
- <Heading size="small">Developer Tools</Heading>
146
- </Stack>
295
+ <button
296
+ type="button"
297
+ class="menu-button"
298
+ aria-expanded={drawerOpen}
299
+ aria-controls="devtools-nav"
300
+ data-testid="devtools-menu"
301
+ onclick={() => (drawerOpen = !drawerOpen)}
302
+ >
303
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
304
+ <path d="M2.5 4h11M2.5 8h11M2.5 12h11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
305
+ </svg>
306
+ <span class="menu-label">{selectedItem?.label ?? 'Menu'}</span>
307
+ </button>
308
+ <span class="brand wide-only">
309
+ <PickedIcon height={22} width={22} />
310
+ <span id="devtools-header"><Heading size="small">Picked Devtools</Heading></span>
311
+ </span>
312
+ {#if posture.label !== null}
313
+ <Badge status={posture.readOnly ? 'warning' : 'default'} size="small" testId="devtools-posture">
314
+ {posture.label}
315
+ </Badge>
316
+ {/if}
317
+ <span class="config-source wide-only" data-testid="devtools-config-source">{configSource}</span>
147
318
  </div>
148
- <div class="toolbar-center"></div>
149
319
  <div class="toolbar-right">
150
- <Button variant="ghost" size="small" href={client.documentationSiteUrl} target="_blank">Docs</Button>
151
- <Button variant="ghost" size="small" href={dashboardProjectUrl} target="_blank">Dashboard</Button>
320
+ <span class="auto-hide" data-testid="devtools-auto-hide">
321
+ <Switch
322
+ label="Auto-hide button"
323
+ name="devtools-auto-hide"
324
+ size="small"
325
+ checked={autoHide}
326
+ onchange={setAutoHide}
327
+ />
328
+ </span>
329
+ <span class="wide-only">
330
+ <Button variant="ghost" size="small" href={client.documentationSiteUrl} target="_blank">Docs</Button>
331
+ </span>
332
+ <span class="wide-only">
333
+ <Button variant="ghost" size="small" href={dashboardProjectUrl} target="_blank">Dashboard</Button>
334
+ </span>
152
335
  <Button variant="ghost" size="small" onclick={() => experience.close()}>Close</Button>
153
336
  </div>
154
337
  </header>
155
- <section class="main">
156
- <Tabs {tabs} initialTab={0} />
157
- </section>
338
+
339
+ <div class="body">
340
+ {#if drawerOpen}
341
+ <div class="drawer-scrim" aria-hidden="true" onclick={() => (drawerOpen = false)}></div>
342
+ {/if}
343
+ <div class="nav-column" class:drawer-open={drawerOpen} id="devtools-nav">
344
+ <Sidebar
345
+ {nav}
346
+ selected={tab}
347
+ {developerOpen}
348
+ onSelect={selectTab}
349
+ onToggleDeveloper={toggleDeveloper}
350
+ />
351
+ </div>
352
+
353
+ <section class="pane" aria-label={selectedItem?.label} data-testid="devtools-pane">
354
+ {#if developerScreen}
355
+ {@const Screen = developerScreen}
356
+ <div class="developer-body">
357
+ <Screen />
358
+ </div>
359
+ {:else if selectedTool}
360
+ <div class="pane-header">
361
+ <h2 class="pane-title">{selectedTool.label}</h2>
362
+ {#if selectedTool.description}
363
+ <p class="intro">{selectedTool.description}</p>
364
+ {/if}
365
+ </div>
366
+ <div class="tool-body">
367
+ <AdaptedToolHost tool={selectedTool} />
368
+ </div>
369
+ {:else if isReviewTab(tab)}
370
+ <div class="pane-header">
371
+ <h2 class="pane-title">{selectedItem?.label}</h2>
372
+ {#if tab !== 'journey'}
373
+ <p class="intro">{TAB_INTROS[tab]}</p>
374
+ {/if}
375
+ </div>
376
+ <div class="review-body" class:fill={tab === 'journey'}>
377
+ {#if tab === 'guide'}
378
+ <GuidePanel
379
+ sections={guide.sections}
380
+ fixes={guide.fixes}
381
+ tabLabels={guide.tabLabels}
382
+ onOpenTab={selectTab}
383
+ />
384
+ {:else if tab === 'session'}
385
+ <SessionPanel
386
+ {client}
387
+ {posture}
388
+ journey={$journey}
389
+ history={$answerHistory}
390
+ onShowAnswers={() => selectTab('answers')}
391
+ />
392
+ {:else if tab === 'answers'}
393
+ <AnswersPanel {client} {posture} asked={$asked} {followUp} />
394
+ {:else if tab === 'journey'}
395
+ <JourneyPanel
396
+ {client}
397
+ {posture}
398
+ journey={$journey}
399
+ trails={$trails}
400
+ hiddenKeys={options.hiddenExperienceKeys}
401
+ />
402
+ {:else if tab === 'variations'}
403
+ <VariationsPanel {client} />
404
+ {/if}
405
+ </div>
406
+ {/if}
407
+ </section>
408
+ </div>
158
409
  </div>
159
410
  </div>
160
411
  {:else}
161
- <div class="launcher">
162
- {#if isPreview}
163
- <span class="preview-tag" role="status" aria-live="polite">Preview mode</span>
164
- {/if}
165
- <button
166
- class="floating-button"
167
- class:has-preview-tag={isPreview}
168
- onclick={() => experience.open()}
169
- aria-label="Open Picked Devtools"
170
- title="Open Picked Devtools"
171
- >
172
- <PickedIcon height={25} width={25}/>
173
- </button>
174
- </div>
412
+ <Launcher
413
+ position={options.launcher.position}
414
+ {autoHide}
415
+ {preview}
416
+ onOpen={() => experience.open()}
417
+ />
175
418
  {/if}
176
419
 
177
420
  <style>
@@ -264,6 +507,12 @@
264
507
  overflow: hidden;
265
508
  }
266
509
 
510
+ @media (max-width: 640px) {
511
+ .devtools {
512
+ padding: var(--space-2);
513
+ }
514
+ }
515
+
267
516
  .backdrop {
268
517
  position: fixed;
269
518
  inset: 0;
@@ -281,44 +530,188 @@
281
530
  z-index: 2;
282
531
  width: 100%;
283
532
  height: 100%;
533
+ container-type: inline-size;
534
+ container-name: devtools-panel;
284
535
  }
285
536
 
286
537
  .toolbar {
287
- height: 40px;
538
+ min-height: 40px;
288
539
  background-color: var(--color-white);
289
540
  border-bottom: 1px solid var(--color-gray-200);
290
541
  padding: 0 var(--space-4);
291
542
  display: flex;
292
543
  align-items: center;
293
544
  justify-content: space-between;
545
+ gap: var(--space-3);
294
546
  flex-shrink: 0;
295
547
  }
296
548
 
297
549
  .toolbar-left {
298
550
  display: flex;
299
551
  align-items: center;
552
+ gap: var(--space-2);
553
+ min-width: 0;
300
554
  }
301
555
 
302
- .toolbar-center {
556
+ .toolbar-right {
303
557
  display: flex;
304
558
  align-items: center;
305
- justify-content: center;
559
+ gap: var(--space-1);
560
+ flex-shrink: 0;
306
561
  }
307
562
 
308
- .toolbar-right {
563
+ .brand {
309
564
  display: flex;
310
565
  align-items: center;
311
- gap: var(--space-1);
566
+ gap: var(--space-2);
567
+ }
568
+
569
+ .config-source {
570
+ font-size: var(--text-xs);
571
+ color: var(--color-gray-500);
572
+ white-space: nowrap;
573
+ }
574
+
575
+ .auto-hide {
576
+ margin-right: var(--space-2);
577
+ }
578
+
579
+ .menu-button {
580
+ display: none;
581
+ align-items: center;
582
+ gap: var(--space-2);
583
+ min-width: 0;
584
+ height: 28px;
585
+ padding: 0 var(--space-2);
586
+ border: 1px solid var(--color-gray-200);
587
+ border-radius: var(--radius-md);
588
+ background-color: var(--color-white);
589
+ font: inherit;
590
+ font-size: var(--text-sm);
591
+ font-weight: var(--font-medium);
592
+ color: var(--color-gray-900);
593
+ cursor: pointer;
594
+ }
595
+
596
+ .menu-button:focus-visible {
597
+ outline: var(--ring-width) solid var(--ring-color);
598
+ outline-offset: var(--ring-offset);
312
599
  }
313
600
 
314
- .main {
601
+ .menu-label {
602
+ overflow: hidden;
603
+ text-overflow: ellipsis;
604
+ white-space: nowrap;
605
+ }
606
+
607
+ .body {
608
+ position: relative;
315
609
  flex: 1;
316
- background-color: var(--color-gray-50);
317
- overflow: auto;
610
+ display: grid;
611
+ grid-template-columns: 200px minmax(0, 1fr);
612
+ min-height: 0;
613
+ }
614
+
615
+ .nav-column {
616
+ min-height: 0;
617
+ border-right: 1px solid var(--color-gray-200);
618
+ background-color: var(--color-white);
619
+ }
620
+
621
+ .drawer-scrim {
622
+ display: none;
623
+ }
624
+
625
+ .pane {
626
+ display: flex;
627
+ flex-direction: column;
628
+ min-width: 0;
629
+ min-height: 0;
630
+ overflow: hidden;
631
+ background-color: var(--color-white);
632
+ }
633
+
634
+ .pane-header {
635
+ flex-shrink: 0;
636
+ padding: var(--space-3) var(--space-4) 0;
637
+ }
638
+
639
+ .pane-title {
640
+ margin: 0;
641
+ font-size: var(--text-base);
642
+ font-weight: var(--font-semibold);
643
+ color: var(--color-gray-900);
644
+ }
645
+
646
+ .intro {
647
+ margin: var(--space-1) 0 0;
648
+ font-size: var(--text-xs);
649
+ color: var(--color-gray-500);
650
+ }
651
+
652
+ .review-body {
653
+ flex: 1;
654
+ min-height: 0;
655
+ overflow-y: auto;
656
+ padding: var(--space-3) var(--space-4) var(--space-4);
657
+ scrollbar-width: thin;
658
+ }
659
+
660
+ .review-body.fill {
318
661
  display: flex;
319
662
  flex-direction: column;
320
663
  }
321
664
 
665
+ .tool-body {
666
+ flex: 1;
667
+ min-height: 0;
668
+ overflow: auto;
669
+ display: grid;
670
+ }
671
+
672
+ .developer-body {
673
+ flex: 1 1 auto;
674
+ display: grid;
675
+ min-height: 0;
676
+ overflow: auto;
677
+ background-color: var(--color-gray-50);
678
+ }
679
+
680
+ @container devtools-panel (max-width: 639px) {
681
+ .body {
682
+ grid-template-columns: minmax(0, 1fr);
683
+ }
684
+
685
+ .nav-column {
686
+ display: none;
687
+ position: absolute;
688
+ inset: 0 auto 0 0;
689
+ z-index: 3;
690
+ width: min(280px, 85%);
691
+ box-shadow: var(--shadow-xl);
692
+ }
693
+
694
+ .nav-column.drawer-open {
695
+ display: block;
696
+ }
697
+
698
+ .drawer-scrim {
699
+ display: block;
700
+ position: absolute;
701
+ inset: 0;
702
+ z-index: 2;
703
+ background-color: rgba(0, 0, 0, 0.3);
704
+ }
705
+
706
+ .menu-button {
707
+ display: inline-flex;
708
+ }
709
+
710
+ .wide-only {
711
+ display: none;
712
+ }
713
+ }
714
+
322
715
  /* Global table styles for screens that still use raw tables */
323
716
  .devtools :global(table) {
324
717
  width: 100%;
@@ -349,57 +742,6 @@
349
742
  padding-right: var(--space-4);
350
743
  }
351
744
 
352
- .launcher {
353
- position: fixed;
354
- bottom: 1rem;
355
- right: 0;
356
- z-index: 999999;
357
- display: flex;
358
- align-items: stretch;
359
- cursor: pointer;
360
- transform: translateX(calc(100% - 8px));
361
- transition: transform 180ms ease-out;
362
-
363
- &:hover,
364
- &:focus-within {
365
- transform: translateX(0);
366
- }
367
- }
368
-
369
- .floating-button {
370
- border-radius: 6px;
371
- background-color: #ffffff;
372
- display: grid;
373
- place-items: center;
374
- cursor: pointer;
375
- border: 0 transparent;
376
- padding: 6px;
377
- box-shadow: 2px 2px 4px #00000029;
378
-
379
- &:hover {
380
- background-color: #f5f5f5;
381
- }
382
- }
383
-
384
- .floating-button.has-preview-tag {
385
- border-radius: 0 6px 6px 0;
386
- }
387
-
388
- .preview-tag {
389
- display: flex;
390
- align-items: center;
391
- padding: 0 10px;
392
- background: #2563eb;
393
- color: #ffffff;
394
- border-radius: 6px 0 0 6px;
395
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
396
- 'Helvetica Neue', Arial, sans-serif;
397
- font-size: 12px;
398
- font-weight: 500;
399
- line-height: 1.4;
400
- box-shadow: 2px 2px 4px #00000029;
401
- }
402
-
403
745
  /** Compact inline form elements for use within table cells */
404
746
  .devtools :global(.inline-select),
405
747
  .devtools :global(.inline-input) {