@picked/svelte 0.0.1-dev.1793776638554 → 0.0.1-dev.1793923852884

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 (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/experience/set-picked-context.js +1 -1
  3. package/dist/plugins/devtools/DevToolsExperience.svelte +155 -80
  4. package/dist/plugins/devtools/customTools.d.ts +2 -2
  5. package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +14 -0
  6. package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
  7. package/dist/plugins/devtools/derive/answer-display/index.d.ts +2 -2
  8. package/dist/plugins/devtools/derive/answer-display/index.js +1 -1
  9. package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
  10. package/dist/plugins/devtools/derive/index.d.ts +5 -5
  11. package/dist/plugins/devtools/derive/index.js +1 -1
  12. package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
  13. package/dist/plugins/devtools/derive/personas/index.js +1 -1
  14. package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
  15. package/dist/plugins/devtools/derive/personas/personas.js +1 -1
  16. package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
  17. package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
  18. package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +5 -5
  19. package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
  20. package/dist/plugins/devtools/index.d.ts +1 -1
  21. package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
  22. package/dist/plugins/devtools/options.d.ts +6 -4
  23. package/dist/plugins/devtools/options.js +1 -1
  24. package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
  25. package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
  26. package/dist/plugins/devtools/review/personas/ChoiceButton.svelte +40 -6
  27. package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +69 -45
  28. package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
  29. package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
  30. package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
  31. package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
  32. package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
  33. package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
  34. package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
  35. package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
  36. package/dist/plugins/devtools/sidebar/Sidebar.svelte +14 -48
  37. package/dist/plugins/devtools/sidebar/SidebarRow.svelte +2 -2
  38. package/dist/plugins/devtools/sidebar/nav.d.ts +26 -39
  39. package/dist/plugins/devtools/sidebar/nav.js +1 -1
  40. package/package.json +2 -2
  41. package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
  42. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
  43. package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
  44. package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
  45. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
  46. package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
  47. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
  48. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
  49. package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
  50. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
@@ -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 flag' },
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 feature flags below, choose values, then copy
144
- the 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>
@@ -1,286 +0,0 @@
1
- <script lang="ts">
2
- /** @file Editable table row for a single field response in the devtools responses screen. */
3
- import type { ResponseListItem, Field, FieldValue, JsonValue } from '@picked/client'
4
- import { getPicked } from '../../../../hooks'
5
- import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
6
- import { readOnlyReasonFromContext } from '../../posture-context'
7
-
8
- export let field: Field
9
- export let response: ResponseListItem | null
10
-
11
- const client = getPicked()
12
- const readOnlyReason = readOnlyReasonFromContext()
13
-
14
- let isEditing = false
15
- let editValue = ''
16
- let editError = ''
17
- // eslint-disable-next-line svelte/prefer-svelte-reactivity -- Set is reassigned for Svelte reactivity via `selectedOptions = selectedOptions`
18
- let selectedOptions: Set<JsonValue> = new Set()
19
-
20
- $: hasOptions = Array.isArray(field?.options) && field.options.length > 0
21
- $: staticOptions = Array.isArray(field?.options) ? field.options : null
22
- $: currentValue = response?.value ?? null
23
-
24
- /**
25
- * Formats a response value for display, handling arrays, objects, and primitives.
26
- *
27
- * @param {unknown} value - The response value to format.
28
- * @returns {string} The display string for the value.
29
- * @example
30
- * ```typescript
31
- * formatValue(['a', 'b']) // 'a, b'
32
- * formatValue(null) // '-'
33
- * ```
34
- */
35
- function formatValue(value: unknown): string {
36
- if (value === null || value === undefined) {
37
- return '-'
38
- }
39
-
40
- if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
41
- return value.join(', ')
42
- }
43
-
44
- if (typeof value === 'object') {
45
- return JSON.stringify(value, null, 2)
46
- }
47
-
48
- return String(value)
49
- }
50
-
51
- /**
52
- * Enters edit mode for this field, initializing the edit state from the current value.
53
- *
54
- * @example
55
- * ```typescript
56
- * startEdit() // opens the inline editor
57
- * ```
58
- */
59
- function startEdit() {
60
- isEditing = true
61
- editError = ''
62
-
63
- if (hasOptions) {
64
- selectedOptions = new Set()
65
- if (Array.isArray(currentValue)) {
66
- currentValue.forEach((v) => selectedOptions.add(v))
67
- } else if (currentValue !== null && currentValue !== undefined) {
68
- selectedOptions.add(currentValue)
69
- }
70
- } else {
71
- editValue = formatValue(currentValue)
72
- }
73
- }
74
-
75
- /**
76
- * Cancels the current edit, discarding changes and resetting edit state.
77
- *
78
- * @example
79
- * ```typescript
80
- * cancelEdit() // closes editor without saving
81
- * ```
82
- */
83
- function cancelEdit() {
84
- isEditing = false
85
- editValue = ''
86
- editError = ''
87
- selectedOptions = new Set()
88
- }
89
-
90
- /**
91
- * Toggles selection of a field option, supporting both single and multi-select modes.
92
- *
93
- * @param {JsonValue} optionValue - The option value to toggle.
94
- * @example
95
- * ```typescript
96
- * toggleOption('red') // selects or deselects the 'red' option
97
- * ```
98
- */
99
- function toggleOption(optionValue: JsonValue) {
100
- if (field?.multiple) {
101
- if (selectedOptions.has(optionValue)) {
102
- selectedOptions.delete(optionValue)
103
- } else {
104
- selectedOptions.add(optionValue)
105
- }
106
-
107
- selectedOptions = selectedOptions
108
- } else {
109
- selectedOptions = new Set([optionValue])
110
- }
111
- }
112
-
113
- /**
114
- * Parses a string input into a typed value based on the field's type.
115
- *
116
- * @param {string} input - The raw string input to parse.
117
- * @returns {FieldValue} The parsed value (null, number, boolean, JSON, or string).
118
- * @example
119
- * ```typescript
120
- * parseValue('42') // 42 (when field.type is 'number')
121
- * parseValue('true') // true (when field.type is 'boolean')
122
- * ```
123
- */
124
- function parseValue(input: string): FieldValue {
125
- const trimmed = input.trim()
126
-
127
- if (trimmed === '' || trimmed === '-') {
128
- return null
129
- }
130
-
131
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
132
- try {
133
- return JSON.parse(trimmed)
134
- } catch {
135
- throw new Error('Invalid JSON format')
136
- }
137
- }
138
-
139
- if (field.type === 'boolean') {
140
- if (trimmed === 'true') return true
141
- if (trimmed === 'false') return false
142
- throw new Error('Boolean must be "true" or "false"')
143
- }
144
-
145
- if (field.type === 'number') {
146
- const num = Number(trimmed)
147
- if (isNaN(num)) {
148
- throw new Error('Invalid number format')
149
- }
150
-
151
- return num
152
- }
153
-
154
- return trimmed
155
- }
156
-
157
- /**
158
- * Commits the edited value via the client responses API and exits edit mode.
159
- *
160
- * @example
161
- * ```typescript
162
- * await saveEdit() // saves the current edit value
163
- * ```
164
- */
165
- async function saveEdit() {
166
- try {
167
- let newValue: FieldValue
168
-
169
- if (hasOptions) {
170
- const values = Array.from(selectedOptions)
171
- if (field?.multiple) {
172
- newValue = values
173
- } else {
174
- newValue = values.at(0) ?? null
175
- }
176
- } else {
177
- newValue = parseValue(editValue)
178
- }
179
-
180
- const result = await client.responses.commit({ [field.key]: newValue })
181
-
182
- if (result.errors.length > 0) {
183
- editError = result.errors[0].userMessage
184
- return
185
- }
186
-
187
- isEditing = false
188
- editError = ''
189
- selectedOptions = new Set()
190
- } catch (error) {
191
- editError = error instanceof Error ? error.message : 'Failed to save'
192
- }
193
- }
194
-
195
- /**
196
- * Clears the response value for this field via the client responses API.
197
- *
198
- * @example
199
- * ```typescript
200
- * handleClear() // removes the response for this field
201
- * ```
202
- */
203
- function handleClear() {
204
- client.responses.clear(field.key)
205
- }
206
- </script>
207
-
208
- <tr>
209
- <td>
210
- <CodeBlock code={field.key} inline />
211
- </td>
212
- <td>
213
- <Text size="small" weight="semibold">{field.label}</Text>
214
- </td>
215
- <td>
216
- {#if isEditing}
217
- <Stack direction="vertical" gap="small">
218
- {#if hasOptions && staticOptions}
219
- <Stack direction="vertical" gap="small">
220
- {#each staticOptions as option (option.id)}
221
- <label class="option-label">
222
- {#if field.multiple}
223
- <input
224
- type="checkbox"
225
- checked={selectedOptions.has(option.value)}
226
- on:change={() => toggleOption(option.value)}
227
- />
228
- {:else}
229
- <input
230
- type="radio"
231
- name="field-{field.id}"
232
- checked={selectedOptions.has(option.value)}
233
- on:change={() => toggleOption(option.value)}
234
- />
235
- {/if}
236
- <Text size="caption" inline>
237
- {option.label}
238
- {#if option.description}
239
- <Text size="caption" color="subdued" inline>({option.description})</Text>
240
- {/if}
241
- </Text>
242
- </label>
243
- {/each}
244
- </Stack>
245
- {:else}
246
- <textarea
247
- bind:value={editValue}
248
- rows="3"
249
- placeholder="Enter {field.type} value..."
250
- ></textarea>
251
- {/if}
252
- {#if editError}
253
- <Banner status="error">{editError}</Banner>
254
- {/if}
255
- </Stack>
256
- {:else if response === null}
257
- <Text size="caption" color="subdued">No value set</Text>
258
- {:else}
259
- <CodeBlock code={formatValue(currentValue)} />
260
- {/if}
261
- </td>
262
- <td>
263
- <CodeBlock code="{field.type}{field.multiple ? '[]' : ''}" inline />
264
- </td>
265
- <td>
266
- {#if isEditing}
267
- <Stack direction="horizontal" gap="none" align="end">
268
- <Button variant="primary" size="small" onclick={saveEdit}>Save</Button>
269
- <Button variant="destructive" size="small" onclick={cancelEdit}>Cancel</Button>
270
- </Stack>
271
- {:else if readOnlyReason !== null}
272
- <span title={readOnlyReason} data-testid="devtools-response-read-only">
273
- <Text size="caption" color="subdued">Read-only</Text>
274
- </span>
275
- {:else}
276
- <Stack direction="horizontal" gap="none" align="end">
277
- <Button variant="secondary" size="small" onclick={startEdit}>
278
- {response === null ? 'Set' : 'Edit'}
279
- </Button>
280
- {#if response !== null}
281
- <Button variant="destructive" size="small" onclick={handleClear}>Clear</Button>
282
- {/if}
283
- </Stack>
284
- {/if}
285
- </td>
286
- </tr>
@@ -1,12 +0,0 @@
1
- <script lang="ts">
2
- import { getPicked } from '../../../../hooks'
3
- import ResponsesTable from './ResponsesTable.svelte'
4
-
5
- const client = getPicked()
6
-
7
- $: responsesStore = client.responses.listStore
8
- </script>
9
-
10
- <div>
11
- <ResponsesTable responses={$responsesStore} />
12
- </div>
@@ -1,88 +0,0 @@
1
- <script lang="ts">
2
- /** @file Table displaying all field responses with filtering and clear-all functionality in the devtools panel. */
3
- import type { ResponseListItem } from '@picked/client'
4
- import { getPicked } from '../../../../hooks'
5
- import { Card, DataTable, EmptyState, Button, RadioGroup, Stack } from '../../ui'
6
- import ResponseTableRow from './ResponseTableRow.svelte'
7
-
8
- export let responses: ResponseListItem[] = []
9
-
10
- const client = getPicked()
11
-
12
- let filter = 'set'
13
-
14
- const fieldsStore = client.fields.listStore
15
- $: allFields = $fieldsStore ?? []
16
-
17
- $: responseMap = new Map(responses.map((r) => [r.fieldKey, r]))
18
-
19
- $: allFieldRows = allFields.map((field) => {
20
- const response = responseMap.get(field.key)
21
- return {
22
- field,
23
- response: response || null,
24
- }
25
- })
26
-
27
- $: fieldRows = allFieldRows.filter((row) => {
28
- if (filter === 'set') return row.response !== null
29
- if (filter === 'unset') return row.response === null
30
- return true
31
- })
32
-
33
- /**
34
- * Clears all committed field responses via the client responses API.
35
- *
36
- * @example
37
- * ```typescript
38
- * handleClearAll() // removes all responses
39
- * ```
40
- */
41
- function handleClearAll() {
42
- client.responses.clearAll()
43
- }
44
-
45
- const columns = [
46
- { key: 'key', header: 'Key' },
47
- { key: 'field', header: 'Field' },
48
- { key: 'value', header: 'Value' },
49
- { key: 'type', header: 'Type' },
50
- { key: 'actions', header: 'Actions' },
51
- ]
52
-
53
- const filterOptions = [
54
- { value: 'all', label: 'All' },
55
- { value: 'set', label: 'Set (0)' },
56
- { value: 'unset', label: 'Unset (0)' },
57
- ]
58
-
59
- $: filterOptions[1] = { value: 'set', label: `Set (${responses.length})` }
60
- $: filterOptions[2] = { value: 'unset', label: `Unset (${allFields.length - responses.length})` }
61
- </script>
62
-
63
- <Stack padding="medium">
64
- <Card sectioned>
65
- <Stack direction="horizontal" gap="small" align="space-between" justify="center" padding="medium">
66
- <RadioGroup
67
- label="Show"
68
- name="filter"
69
- options={filterOptions}
70
- value={filter}
71
- onchange={(val) => { filter = val }}
72
- orientation="horizontal"
73
- />
74
- {#if responses.length > 0}
75
- <Button variant="destructive" size="small" onclick={handleClearAll}>Clear All</Button>
76
- {/if}
77
- </Stack>
78
- <DataTable columns={columns} rows={fieldRows}>
79
- {#snippet row(item)}
80
- {@const rowData = item}
81
- <ResponseTableRow field={rowData.field} response={rowData.response} />
82
- {/snippet}
83
- {#snippet empty()}
84
- <EmptyState message="No fields registered yet" />
85
- {/snippet}
86
- </DataTable>
87
- </Card>
88
- </Stack>