@picked/svelte 0.0.1-dev.1789541668638 → 0.0.1-next.2

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 (132) hide show
  1. package/dist/component.d.ts +5 -5
  2. package/dist/component.js +1 -1
  3. package/dist/context.js +1 -1
  4. package/dist/form/index.d.ts +15 -3
  5. package/dist/form/index.js +1 -1
  6. package/dist/hooks/client.d.ts +1 -1
  7. package/dist/hooks/client.js +1 -1
  8. package/dist/hooks/field.d.ts +5 -16
  9. package/dist/hooks/field.js +1 -1
  10. package/dist/hooks/index.d.ts +1 -9
  11. package/dist/hooks/index.js +1 -1
  12. package/dist/hooks/step.d.ts +15 -93
  13. package/dist/hooks/step.js +1 -1
  14. package/dist/hooks/storage.d.ts +2 -2
  15. package/dist/hooks/storage.js +1 -1
  16. package/dist/hooks/stores.d.ts +1 -1
  17. package/dist/hooks/stores.js +1 -1
  18. package/dist/index.d.ts +1 -8
  19. package/dist/index.js +1 -1
  20. package/dist/plugins/devtools/DevToolsExperience.svelte +53 -355
  21. package/dist/plugins/devtools/PickedLogo.svelte +58 -0
  22. package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +4 -17
  23. package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +19 -26
  24. package/dist/plugins/devtools/components/Tabs.svelte +12 -69
  25. package/dist/plugins/devtools/experience.d.ts +1 -0
  26. package/dist/plugins/devtools/experience.js +1 -0
  27. package/dist/plugins/devtools/index.d.ts +0 -2
  28. package/dist/plugins/devtools/index.js +1 -1
  29. package/dist/plugins/devtools/plugin.d.ts +5 -37
  30. package/dist/plugins/devtools/plugin.js +1 -1
  31. package/dist/plugins/devtools/screens/components/ComponentsListScreen.svelte +14 -18
  32. package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +3 -1
  33. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +33 -31
  34. package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +3 -1
  35. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +103 -293
  36. package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +1 -2
  37. package/dist/plugins/devtools/screens/frameworks/FrameworksListScreen.svelte +13 -15
  38. package/dist/plugins/devtools/screens/history/HistoryEntriesTable.svelte +16 -22
  39. package/dist/plugins/devtools/screens/history/HistoryEntryListItem.svelte +1 -7
  40. package/dist/plugins/devtools/screens/predicates/PredicatesListScreen.svelte +13 -15
  41. package/dist/plugins/devtools/screens/project/ProjectScreen.svelte +1 -46
  42. package/dist/plugins/framework/index.js +1 -1
  43. package/dist/plugins/framework/plugin.d.ts +4 -4
  44. package/dist/plugins/framework/plugin.js +1 -1
  45. package/dist/render/browser.d.ts +2 -35
  46. package/dist/render/browser.js +1 -1
  47. package/dist/render/server.d.ts +8 -22
  48. package/dist/render/server.js +1 -1
  49. package/dist/router/Step.svelte +4 -6
  50. package/dist/router/StepRouter.svelte +34 -14
  51. package/dist/router/context.d.ts +3 -3
  52. package/dist/router/context.js +1 -1
  53. package/dist/router/index.js +1 -1
  54. package/dist/server/index.d.ts +2 -2
  55. package/dist/server/index.js +1 -1
  56. package/dist/utils/array.js +1 -1
  57. package/dist/vite-env.d.js +1 -0
  58. package/package.json +33 -106
  59. package/dist/.tsbuildinfo +0 -1
  60. package/dist/_virtual/_vite/preload-helper.js +0 -1
  61. package/dist/components/LazyImage.svelte +0 -192
  62. package/dist/components/index.d.ts +0 -1
  63. package/dist/components/index.js +0 -1
  64. package/dist/experience/ExperienceHost.svelte +0 -112
  65. package/dist/experience/ExperienceSlot.svelte +0 -154
  66. package/dist/experience/HtmlRender.svelte +0 -52
  67. package/dist/experience/set-picked-context.d.ts +0 -29
  68. package/dist/experience/set-picked-context.js +0 -1
  69. package/dist/form/__tests__/CreateFormHarness.svelte +0 -30
  70. package/dist/form/create-form.js +0 -1
  71. package/dist/form/create-form.svelte.d.ts +0 -40
  72. package/dist/form/create-form.svelte.js +0 -1
  73. package/dist/form/get-form.d.ts +0 -30
  74. package/dist/form/get-form.js +0 -1
  75. package/dist/hooks/action.d.ts +0 -40
  76. package/dist/hooks/action.js +0 -1
  77. package/dist/hooks/content.d.ts +0 -18
  78. package/dist/hooks/content.js +0 -1
  79. package/dist/hooks/create-query/index.d.ts +0 -145
  80. package/dist/hooks/create-query/index.js +0 -1
  81. package/dist/hooks/events.d.ts +0 -2
  82. package/dist/hooks/events.js +0 -1
  83. package/dist/hooks/feature.d.ts +0 -47
  84. package/dist/hooks/feature.js +0 -1
  85. package/dist/hooks/predicate.d.ts +0 -3
  86. package/dist/hooks/predicate.js +0 -1
  87. package/dist/hooks/reference.d.ts +0 -61
  88. package/dist/hooks/reference.js +0 -1
  89. package/dist/plugins/devtools/PickedIcon.svelte +0 -19
  90. package/dist/plugins/devtools/components/AdaptedToolHost.svelte +0 -103
  91. package/dist/plugins/devtools/customTools.d.ts +0 -27
  92. package/dist/plugins/devtools/customTools.js +0 -1
  93. package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +0 -160
  94. package/dist/plugins/devtools/screens/content/ContentListScreen.svelte +0 -89
  95. package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +0 -29
  96. package/dist/plugins/devtools/screens/events/EventListItem.svelte +0 -216
  97. package/dist/plugins/devtools/screens/events/EventsListScreen.svelte +0 -18
  98. package/dist/plugins/devtools/screens/events/EventsTable.svelte +0 -76
  99. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
  100. package/dist/plugins/devtools/screens/fields/FieldTableRow.svelte +0 -276
  101. package/dist/plugins/devtools/screens/fields/FieldsListScreen.svelte +0 -12
  102. package/dist/plugins/devtools/screens/fields/FieldsTable.svelte +0 -78
  103. package/dist/plugins/devtools/screens/identity/IdentityScreen.svelte +0 -66
  104. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -280
  105. package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
  106. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
  107. package/dist/plugins/devtools/ui/actions/Button.svelte +0 -175
  108. package/dist/plugins/devtools/ui/actions/CopyButton.svelte +0 -88
  109. package/dist/plugins/devtools/ui/data/Badge.svelte +0 -62
  110. package/dist/plugins/devtools/ui/data/CodeBlock.svelte +0 -38
  111. package/dist/plugins/devtools/ui/data/DataTable.svelte +0 -144
  112. package/dist/plugins/devtools/ui/data/EmptyState.svelte +0 -26
  113. package/dist/plugins/devtools/ui/data/ExpandableRow.svelte +0 -66
  114. package/dist/plugins/devtools/ui/data/VisualizationContainer.svelte +0 -55
  115. package/dist/plugins/devtools/ui/feedback/Banner.svelte +0 -116
  116. package/dist/plugins/devtools/ui/feedback/Spinner.svelte +0 -79
  117. package/dist/plugins/devtools/ui/forms/Checkbox.svelte +0 -139
  118. package/dist/plugins/devtools/ui/forms/RadioGroup.svelte +0 -218
  119. package/dist/plugins/devtools/ui/forms/Select.svelte +0 -137
  120. package/dist/plugins/devtools/ui/forms/Switch.svelte +0 -158
  121. package/dist/plugins/devtools/ui/forms/TextField.svelte +0 -125
  122. package/dist/plugins/devtools/ui/index.d.ts +0 -22
  123. package/dist/plugins/devtools/ui/index.js +0 -1
  124. package/dist/plugins/devtools/ui/layout/Card.svelte +0 -55
  125. package/dist/plugins/devtools/ui/layout/Divider.svelte +0 -34
  126. package/dist/plugins/devtools/ui/layout/Spacer.svelte +0 -11
  127. package/dist/plugins/devtools/ui/layout/Stack.svelte +0 -129
  128. package/dist/plugins/devtools/ui/overlays/Tooltip.svelte +0 -90
  129. package/dist/plugins/devtools/ui/tokens.css +0 -144
  130. package/dist/plugins/devtools/ui/typography/Heading.svelte +0 -55
  131. package/dist/plugins/devtools/ui/typography/Text.svelte +0 -93
  132. package/dist/utils/ErrorBoundary.svelte +0 -27
@@ -1,216 +0,0 @@
1
- <script lang="ts">
2
- /** @file Expandable list item row for displaying an event bus history entry in the devtools events screen. */
3
- import type { EventBusPayload, EventHistoryEntry } from '@picked/client'
4
- import { Badge, Card, CodeBlock, Stack, Text } from '../../ui'
5
- import { getPicked } from '../../../../hooks'
6
-
7
- export let event: EventHistoryEntry
8
- export let isExpanded: boolean
9
- export let onToggle: () => void
10
-
11
- const client = getPicked()
12
-
13
- const eventExpId =
14
- 'experience' in event.payload && event.payload.experience
15
- ? event.payload.experience.experienceId
16
- : undefined
17
- const eventExperienceStore = client.experiences.getById(eventExpId ?? '')
18
- $: eventExpName = eventExpId
19
- ? ($eventExperienceStore?.name ?? eventExpId)
20
- : '-'
21
-
22
- /**
23
- * Formats a Date as a 24-hour time string with milliseconds.
24
- *
25
- * @param {Date} date - The date to format.
26
- * @returns {string} The formatted time string.
27
- * @example
28
- * ```typescript
29
- * formatTime(new Date()) // '14:30:05.123'
30
- * ```
31
- */
32
- function formatTime(date: Date): string {
33
- return date.toLocaleTimeString('en-US', {
34
- hour12: false,
35
- hour: '2-digit',
36
- minute: '2-digit',
37
- second: '2-digit',
38
- fractionalSecondDigits: 3,
39
- })
40
- }
41
-
42
- interface PayloadField {
43
- label: string
44
- value: unknown
45
- }
46
-
47
- /**
48
- * Extracts experience-related fields from an event payload's experience object.
49
- *
50
- * @param {unknown} exp - The experience object from the event payload.
51
- * @returns {PayloadField[]} Extracted experience fields.
52
- * @example
53
- * ```typescript
54
- * getExperienceFields({ id: 'abc', experienceId: 'quiz' })
55
- * // [{ label: 'Experience ID', value: 'abc' }, { label: 'Experience Type', value: 'quiz' }]
56
- * ```
57
- */
58
- function getExperienceFields(exp: unknown): PayloadField[] {
59
- if (!exp || typeof exp !== 'object') return []
60
- const fields: PayloadField[] = []
61
- if ('id' in exp && exp.id) fields.push({ label: 'Experience ID', value: exp.id })
62
- if ('experienceId' in exp && exp.experienceId)
63
- fields.push({ label: 'Experience Type', value: exp.experienceId })
64
- return fields
65
- }
66
-
67
- /**
68
- * Extracts step-related fields from an event payload's step object.
69
- *
70
- * @param {unknown} step - The step object from the event payload.
71
- * @returns {PayloadField[]} Extracted step fields.
72
- * @example
73
- * ```typescript
74
- * getStepFields({ id: 's1', name: 'Welcome' })
75
- * // [{ label: 'Step ID', value: 's1' }, { label: 'Step Name', value: 'Welcome' }]
76
- * ```
77
- */
78
- function getStepFields(step: unknown): PayloadField[] {
79
- if (!step || typeof step !== 'object'){
80
- return []
81
- }
82
-
83
- const fields: PayloadField[] = []
84
- if ('key' in step && step.key) {
85
- fields.push({ label: 'Step Key', value: step.key })
86
- }
87
-
88
- if ('name' in step && step.name) {
89
- fields.push({ label: 'Step Name', value: step.name })
90
- }
91
-
92
- return fields
93
- }
94
-
95
- /** Scalar payload keys to extract and their display labels. JSON-serializable values are stringified. */
96
- const SCALAR_KEYS: { key: string; label: string; serialize?: boolean }[] = [
97
- { key: 'value', label: 'Value' },
98
- { key: 'form', label: 'Form', serialize: true },
99
- { key: 'feature', label: 'Feature', serialize: true },
100
- { key: 'exception', label: 'Exception', serialize: true },
101
- { key: 'version', label: 'Version' },
102
- ]
103
-
104
- /**
105
- * Extracts scalar (non-nested) fields from an event bus payload.
106
- *
107
- * @param {EventBusPayload} payload - The event bus payload to extract scalar fields from.
108
- * @returns {PayloadField[]} Extracted scalar fields.
109
- * @example
110
- * ```typescript
111
- * getScalarFields({ type: 'step', value: 42 })
112
- * // [{ label: 'Value', value: 42 }]
113
- * ```
114
- */
115
- function getScalarFields(payload: EventBusPayload): PayloadField[] {
116
- const fields: PayloadField[] = []
117
- const record = payload as Record<string, unknown>
118
-
119
- for (const { key, label, serialize } of SCALAR_KEYS) {
120
- const raw = record[key]
121
- if (raw !== undefined) {
122
- fields.push({ label, value: serialize ? JSON.stringify(raw) : raw })
123
- }
124
- }
125
-
126
- return fields
127
- }
128
-
129
- /**
130
- * Builds a flat list of display fields from an event bus payload.
131
- *
132
- * @param {EventBusPayload} payload - The event bus payload to extract fields from.
133
- * @returns {PayloadField[]} All extracted fields.
134
- * @example
135
- * ```typescript
136
- * getPayloadFields({ type: 'session', id: 'sess-1' })
137
- * // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
138
- * ```
139
- */
140
- function getPayloadFields(payload: EventBusPayload): PayloadField[] {
141
- const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
142
- fields.push(...getScalarFields(payload))
143
-
144
- if ('experience' in payload && payload.experience) {
145
- fields.push(...getExperienceFields(payload.experience))
146
- }
147
-
148
- if ('step' in payload && payload.step) {
149
- fields.push(...getStepFields(payload.step))
150
- }
151
-
152
- if ('id' in payload && payload.id && payload.type === 'session') {
153
- fields.push({ label: 'Session ID', value: payload.id })
154
- }
155
-
156
- return fields
157
- }
158
-
159
- /**
160
- * Formats a field value for display as a string.
161
- *
162
- * @param {unknown} value - The value to format.
163
- * @returns {string} The formatted string representation.
164
- * @example
165
- * ```typescript
166
- * formatValue(42) // '42'
167
- * formatValue(null) // 'null'
168
- * ```
169
- */
170
- function formatValue(value: unknown): string {
171
- if (value === null) return 'null'
172
- if (value === undefined) return 'undefined'
173
- if (typeof value === 'string') return value
174
- if (typeof value === 'number' || typeof value === 'boolean') return String(value)
175
- return JSON.stringify(value)
176
- }
177
- </script>
178
-
179
- <tr class="event-row" onclick={onToggle}>
180
- <td>
181
- <span class="expand-icon">
182
- {isExpanded ? '▼' : '▶'}
183
- </span>
184
- <CodeBlock code={formatTime(event.at)} inline />
185
- </td>
186
- <td>
187
- <Badge status="info">{event.payload.type}</Badge>
188
- </td>
189
- <td>
190
- <Text size="small">{eventExpName}</Text>
191
- </td>
192
- <td>
193
- <Text size="caption" color="subdued">
194
- {isExpanded ? 'Click to collapse' : 'Click to expand payload'}
195
- </Text>
196
- </td>
197
- </tr>
198
- {#if isExpanded}
199
- <tr>
200
- <td colspan="4" class="details-cell">
201
- <Stack direction="vertical" gap="small">
202
- <Text size="small" weight="semibold">Event Payload</Text>
203
- <Card>
204
- <Stack direction="vertical" gap="small">
205
- {#each getPayloadFields(event.payload) as field (field.label)}
206
- <Text size="caption" inline>
207
- <Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
208
- <CodeBlock code={formatValue(field.value)} inline />
209
- </Text>
210
- {/each}
211
- </Stack>
212
- </Card>
213
- </Stack>
214
- </td>
215
- </tr>
216
- {/if}
@@ -1,18 +0,0 @@
1
- <script lang="ts">
2
- import { getPicked } from '../../../../hooks'
3
- import EventsTable from './EventsTable.svelte'
4
- import { Divider, Spacer, Stack, Text } from '../../ui'
5
-
6
- const { events } = getPicked()
7
- </script>
8
-
9
- <Stack direction="vertical" gap="none" fill>
10
- <EventsTable />
11
- <Spacer />
12
- <Divider />
13
- <Stack padding="small">
14
- <Text size="caption" color="subdued">
15
- {$events.length} events
16
- </Text>
17
- </Stack>
18
- </Stack>
@@ -1,76 +0,0 @@
1
- <script lang="ts">
2
- import { combine, createStore, createEvent, sample } from 'effector'
3
- import { getPicked } from '../../../../hooks'
4
- import EventListItem from './EventListItem.svelte'
5
- import { Card, DataTable, EmptyState, Stack } from '../../ui'
6
- import type { EventHistoryEntry } from '@picked/client'
7
-
8
- const { events } = getPicked()
9
-
10
- let expandedRowIds = new Set<number>()
11
-
12
- function toggleRow(eventId: number) {
13
- const newSet = new Set(expandedRowIds)
14
- if (newSet.has(eventId)) {
15
- newSet.delete(eventId)
16
- } else {
17
- newSet.add(eventId)
18
- }
19
- expandedRowIds = newSet
20
- }
21
-
22
- const sortDirectionStore = createStore<'asc' | 'desc'>('desc')
23
- const toggleSortDirection = createEvent<void>()
24
- const oppositeSortDirection: Record<'asc' | 'desc', 'asc' | 'desc'> = { asc: 'desc', desc: 'asc' }
25
-
26
- sample({
27
- clock: toggleSortDirection,
28
- source: sortDirectionStore,
29
- fn: (dir) => oppositeSortDirection[dir],
30
- target: sortDirectionStore,
31
- })
32
-
33
- function sortEvents(history: EventHistoryEntry[], sort: 'asc' | 'desc'): EventHistoryEntry[] {
34
- return [...history].sort((a, b) => {
35
- const diff = a.at.getTime() - b.at.getTime()
36
- return sort === 'asc' ? diff : -diff
37
- })
38
- }
39
-
40
- const sortedEvents = combine(events, sortDirectionStore, sortEvents)
41
-
42
- const columns = [
43
- { key: 'time', header: 'Time', sortable: true },
44
- { key: 'eventType', header: 'Event Type' },
45
- { key: 'experience', header: 'Experience' },
46
- { key: 'actions', header: '' },
47
- ]
48
-
49
- function handleSort() {
50
- toggleSortDirection()
51
- }
52
- </script>
53
-
54
- <Stack padding="medium">
55
- <Card sectioned>
56
- <DataTable
57
- columns={columns}
58
- rows={$sortedEvents}
59
- sortKey="time"
60
- sortDirection={$sortDirectionStore}
61
- onSort={handleSort}
62
- >
63
- {#snippet row(item)}
64
- {@const event = item}
65
- <EventListItem
66
- {event}
67
- isExpanded={expandedRowIds.has(event.id)}
68
- onToggle={() => toggleRow(event.id)}
69
- />
70
- {/snippet}
71
- {#snippet empty()}
72
- <EmptyState message="No events yet. Interact with the page to see events." />
73
- {/snippet}
74
- </DataTable>
75
- </Card>
76
- </Stack>
@@ -1,239 +0,0 @@
1
- <script lang="ts">
2
- import { createEvent, createStore } from 'effector'
3
- import type { FeatureBucket } from '@picked/client'
4
- import { getPicked, useUnit } from '../../../../hooks'
5
- import { Button, Card, CodeBlock, CopyButton, DataTable, EmptyState, Stack, Text } from '../../ui'
6
-
7
- interface FeatureSelection {
8
- included: boolean
9
- value: string
10
- }
11
-
12
- /** A boolean feature's two arms. */
13
- const BOOLEAN_VALUES = ['true', 'false']
14
-
15
- /** What every feature's control arm serves. */
16
- const CONTROL_VALUE = 'false'
17
-
18
- const { features: { listStore: featuresStore } } = getPicked()
19
- const featuresList = useUnit(featuresStore)
20
-
21
- const initSelections = createEvent<FeatureBucket[]>()
22
- const featureToggled = createEvent<string>()
23
- const valueChanged = createEvent<{ id: string; value: string }>()
24
-
25
- const selectionsStore = createStore<Record<string, FeatureSelection>>({})
26
- .on(initSelections, (state, feats) => {
27
- const next = { ...state }
28
- feats.forEach((feature) => {
29
- if (!(feature.id in next)) {
30
- next[feature.id] = { included: false, value: defaultValue(feature) }
31
- }
32
- })
33
- return next
34
- })
35
- .on(featureToggled, (state, id) => ({
36
- ...state,
37
- [id]: { ...state[id], included: !state[id].included },
38
- }))
39
- .on(valueChanged, (state, { id, value }) => ({
40
- ...state,
41
- [id]: { ...state[id], value },
42
- }))
43
-
44
- const selections = useUnit(selectionsStore)
45
-
46
- $effect(() => {
47
- initSelections($featuresList)
48
- })
49
-
50
- /**
51
- * The bucket a feature currently serves, or null while it is still undecided.
52
- *
53
- * @param {FeatureBucket} feature - The feature to read.
54
- * @returns {string | null} The bucket as text, or null when the feature is unresolved.
55
- * @example getBucketValue(feature) // 'video-hero'
56
- */
57
- function getBucketValue(feature: FeatureBucket): string | null {
58
- return feature.isResolved ? String(feature.getBucket().bucket) : null
59
- }
60
-
61
- /**
62
- * The buckets a feature can be forced to: a text feature's published arms, or the
63
- * two structural arms of a boolean one. A feature of unknown shape answers null,
64
- * and the caller takes free text.
65
- *
66
- * @param {FeatureBucket} feature - The feature to offer overrides for.
67
- * @returns {string[] | null} The values to list, or null for a feature of unknown shape.
68
- * @example overrideValues(feature) // ['video-hero', 'false']
69
- */
70
- function overrideValues(feature: FeatureBucket): string[] | null {
71
- const variants = feature.variants
72
- if (variants && variants.length > 0) {
73
- return variants.map((variant) => String(variant.value))
74
- }
75
-
76
- // A text feature with no arms, and a feature resolved outside `true`/`false`, are
77
- // both of unknown shape.
78
- const current = getBucketValue(feature)
79
- if (feature.valueType === 'text' || (current !== null && !BOOLEAN_VALUES.includes(current))) {
80
- return null
81
- }
82
-
83
- return BOOLEAN_VALUES
84
- }
85
-
86
- /**
87
- * The bucket a feature's override starts on: whatever it currently serves, else its
88
- * first arm outside control, so one click on Include forces the treatment.
89
- *
90
- * @param {FeatureBucket} feature - The feature the selection is being seeded for.
91
- * @returns {string} The bucket to preselect, or the empty string when there is none to offer.
92
- * @example defaultValue(feature) // 'video-hero'
93
- */
94
- function defaultValue(feature: FeatureBucket): string {
95
- const current = getBucketValue(feature)
96
- if (current !== null) {
97
- return current
98
- }
99
-
100
- const values = overrideValues(feature)
101
- if (!values || values.length === 0) {
102
- return ''
103
- }
104
-
105
- return values.find((value) => value !== CONTROL_VALUE) ?? values[0]
106
- }
107
-
108
- const generatedUrl = $derived.by(() => {
109
- const url = new URL(window.location.href)
110
- url.searchParams.delete('picked-feature')
111
- // `searchParams` escapes a value holding a colon or an ampersand, which is what
112
- // keeps the round trip exact.
113
- $featuresList
114
- .filter((feature) => $selections[feature.id]?.included)
115
- .forEach((feature) => {
116
- url.searchParams.append('picked-feature', `${feature.id}:${$selections[feature.id].value}`)
117
- })
118
-
119
- return url.toString()
120
- })
121
-
122
- const hasSelections = $derived(Object.values($selections).some((s) => s.included))
123
-
124
- /**
125
- * Navigates to the generated URL, which loads this page with the overrides applied.
126
- *
127
- * @example refreshPage()
128
- */
129
- function refreshPage() {
130
- window.location.href = generatedUrl
131
- }
132
-
133
- const columns = [
134
- { key: 'include', header: 'Include' },
135
- { key: 'feature', header: 'Feature' },
136
- { key: 'currentValue', header: 'Current Value' },
137
- { key: 'overrideValue', header: 'Override Value' },
138
- ]
139
- </script>
140
-
141
- <Stack direction="vertical" gap="medium" padding="medium">
142
- <Text size="caption" color="subdued">
143
- Override feature flags via URL parameters. Select features below, choose values, then copy the
144
- generated URL. Recipients will see the overridden values on page load.
145
- </Text>
146
-
147
- <Card sectioned>
148
- <DataTable columns={columns} rows={$featuresList}>
149
- {#snippet row(item)}
150
- {@const feature = item}
151
- {@const selection = $selections[feature.id]}
152
- {@const currentValue = getBucketValue(feature)}
153
- {@const values = overrideValues(feature)}
154
- {#if selection}
155
- <tr class:selected={selection.included}>
156
- <td class="include-cell">
157
- <input
158
- type="checkbox"
159
- data-feature-id={feature.id}
160
- checked={selection.included}
161
- onchange={() => featureToggled(feature.id)}
162
- aria-label="Include {feature.name} in URL"
163
- />
164
- </td>
165
- <td>
166
- <Text size="small" weight="medium">{feature.name}</Text>
167
- <CodeBlock code={`${feature.id}:${feature.key}`} inline />
168
- </td>
169
- <td>
170
- {#if currentValue !== null}
171
- <CodeBlock code={currentValue} inline />
172
- {:else}
173
- <Button variant="ghost" size="small" onclick={() => feature.getBucket()}>
174
- Click to resolve
175
- </Button>
176
- {/if}
177
- </td>
178
- <td>
179
- {#if values}
180
- <select
181
- class="inline-select"
182
- data-override-for={feature.id}
183
- value={selection.value}
184
- onchange={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
185
- disabled={!selection.included}
186
- aria-label="Override value for {feature.name}"
187
- >
188
- <!-- Keyed by position: two arms may serve the same string. -->
189
- {#each values as value, index (index)}
190
- <option {value}>{value}</option>
191
- {/each}
192
- </select>
193
- {:else}
194
- <input
195
- class="inline-input"
196
- type="text"
197
- data-override-for={feature.id}
198
- value={selection.value}
199
- oninput={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
200
- disabled={!selection.included}
201
- placeholder="value"
202
- aria-label="Override value for {feature.name}"
203
- />
204
- {/if}
205
- </td>
206
- </tr>
207
- {/if}
208
- {/snippet}
209
- {#snippet empty()}
210
- <EmptyState message="No features registered" />
211
- {/snippet}
212
- </DataTable>
213
- </Card>
214
-
215
- <Card>
216
- {#snippet header()}
217
- <Text size="caption" weight="semibold" color="subdued">GENERATED URL</Text>
218
- {/snippet}
219
- <Stack direction="vertical" gap="small">
220
- <CodeBlock code={generatedUrl} />
221
- <Stack direction="horizontal" gap="small" justify="center">
222
- <CopyButton text={generatedUrl} label="Copy URL" />
223
- <Button
224
- variant="secondary"
225
- size="small"
226
- onclick={refreshPage}
227
- disabled={!hasSelections}
228
- >
229
- Reload
230
- </Button>
231
- </Stack>
232
- {#if !hasSelections}
233
- <Text size="caption" color="subdued">
234
- Select at least one feature above to add overrides to the URL.
235
- </Text>
236
- {/if}
237
- </Stack>
238
- </Card>
239
- </Stack>