@picked/svelte 0.0.62-next.8 → 0.0.62

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.
@@ -88,7 +88,7 @@
88
88
  if (isConvincelyDomain(url)) {
89
89
  const encodedUrl = encodeURIComponent(url.toString())
90
90
  const optimizedSize = findClosestSize(targetWidth)
91
- return `https://img.convincely.io/_next/image?url=${encodedUrl}&w=${optimizedSize}&q=75`
91
+ return `https://img.picked-content.com/_next/image?url=${encodedUrl}&w=${optimizedSize}&q=75`
92
92
  }
93
93
 
94
94
  // For other domains, return the original URL
@@ -15,8 +15,12 @@
15
15
  import ComponentsListScreen from './screens/components/ComponentsListScreen.svelte'
16
16
  import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
17
17
  import ProjectScreen from './screens/project/ProjectScreen.svelte'
18
+ import ConfigScreen from './screens/config/ConfigScreen.svelte'
19
+ import IdentityScreen from './screens/identity/IdentityScreen.svelte'
18
20
  import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
19
21
  import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
22
+ import FieldsListScreen from './screens/fields/FieldsListScreen.svelte'
23
+ import ContentListScreen from './screens/content/ContentListScreen.svelte'
20
24
  import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
21
25
  import EventsListScreen from './screens/events/EventsListScreen.svelte'
22
26
  import PickedIcon from './PickedIcon.svelte'
@@ -35,21 +39,27 @@
35
39
  props: {},
36
40
  },
37
41
  {
38
- id: 'history',
39
- label: 'History',
40
- component: HistoryEntriesListScreen,
42
+ id: 'components',
43
+ label: 'Components',
44
+ component: ComponentsListScreen,
41
45
  props: {},
42
46
  },
43
47
  {
44
- id: 'events',
45
- label: 'Events',
46
- component: EventsListScreen,
48
+ id: 'config',
49
+ label: 'Config',
50
+ component: ConfigScreen,
47
51
  props: {},
48
52
  },
49
53
  {
50
- id: 'responses',
51
- label: 'Responses',
52
- component: ResponsesListScreen,
54
+ id: 'content',
55
+ label: 'Content',
56
+ component: ContentListScreen,
57
+ props: {},
58
+ },
59
+ {
60
+ id: 'events',
61
+ label: 'Events',
62
+ component: EventsListScreen,
53
63
  props: {},
54
64
  },
55
65
  {
@@ -64,12 +74,6 @@
64
74
  component: ExperiencesListScreen,
65
75
  props: {},
66
76
  },
67
- {
68
- id: 'predicates',
69
- label: 'Predicates',
70
- component: PredicatesListScreen,
71
- props: {},
72
- },
73
77
  {
74
78
  id: 'features',
75
79
  label: 'Features',
@@ -77,9 +81,9 @@
77
81
  props: {},
78
82
  },
79
83
  {
80
- id: 'components',
81
- label: 'Components',
82
- component: ComponentsListScreen,
84
+ id: 'fields',
85
+ label: 'Fields',
86
+ component: FieldsListScreen,
83
87
  props: {},
84
88
  },
85
89
  {
@@ -88,6 +92,30 @@
88
92
  component: FrameworksListScreen,
89
93
  props: {},
90
94
  },
95
+ {
96
+ id: 'history',
97
+ label: 'History',
98
+ component: HistoryEntriesListScreen,
99
+ props: {},
100
+ },
101
+ {
102
+ id: 'identity',
103
+ label: 'Identity',
104
+ component: IdentityScreen,
105
+ props: {},
106
+ },
107
+ {
108
+ id: 'predicates',
109
+ label: 'Predicates',
110
+ component: PredicatesListScreen,
111
+ props: {},
112
+ },
113
+ {
114
+ id: 'responses',
115
+ label: 'Responses',
116
+ component: ResponsesListScreen,
117
+ props: {},
118
+ },
91
119
  ]
92
120
 
93
121
  const isOpen = experience.isOpenStore
@@ -118,7 +146,7 @@
118
146
  </div>
119
147
  </header>
120
148
  <section class="main">
121
- <Tabs {tabs} initialTab={1} />
149
+ <Tabs {tabs} initialTab={0} />
122
150
  </section>
123
151
  </div>
124
152
  </div>
@@ -129,6 +157,7 @@
129
157
  {/if}
130
158
  <button
131
159
  class="floating-button"
160
+ class:has-preview-tag={isPreview}
132
161
  onclick={() => experience.open()}
133
162
  aria-label="Open Picked Devtools"
134
163
  title="Open Picked Devtools"
@@ -323,7 +352,7 @@
323
352
  }
324
353
 
325
354
  .floating-button {
326
- border-radius: 0 6px 6px 0;
355
+ border-radius: 6px;
327
356
  background-color: #ffffff;
328
357
  display: grid;
329
358
  place-items: center;
@@ -337,6 +366,10 @@
337
366
  }
338
367
  }
339
368
 
369
+ .floating-button.has-preview-tag {
370
+ border-radius: 0 6px 6px 0;
371
+ }
372
+
340
373
  .preview-tag {
341
374
  display: flex;
342
375
  align-items: center;
@@ -13,18 +13,21 @@
13
13
  export let initialTab = 0
14
14
 
15
15
  let activeTab = initialTab
16
+ const tabButtons: (HTMLButtonElement | undefined)[] = []
16
17
 
17
18
  /**
18
- * Sets the active tab to the given index.
19
+ * Sets the active tab to the given index and scrolls the corresponding button into the
20
+ * horizontal viewport so keyboard navigation never lands off-screen.
19
21
  *
20
22
  * @param {number} index - The zero-based tab index to activate.
21
23
  * @example
22
24
  * ```typescript
23
- * activateTab(2) // switches to the third tab
25
+ * activateTab(2) // switches to the third tab and scrolls it into view
24
26
  * ```
25
27
  */
26
28
  function activateTab(index: number) {
27
29
  activeTab = index
30
+ tabButtons[index]?.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' })
28
31
  }
29
32
 
30
33
  /**
@@ -66,6 +69,7 @@
66
69
  <div role="tablist" tabindex="0" on:keydown={handleKeydown} class="tab-list">
67
70
  {#each tabs as tab, i (tab.id)}
68
71
  <button
72
+ bind:this={tabButtons[i]}
69
73
  role="tab"
70
74
  id="tab-{tab.id}"
71
75
  aria-controls="panel-{tab.id}"
@@ -110,6 +114,23 @@
110
114
  border-bottom: 1px solid var(--color-gray-200);
111
115
  flex-shrink: 0;
112
116
  overflow-x: auto;
117
+ overflow-y: hidden;
118
+ scrollbar-width: thin;
119
+ scrollbar-color: var(--color-gray-300) transparent;
120
+ scroll-behavior: smooth;
121
+ }
122
+
123
+ .tab-list::-webkit-scrollbar {
124
+ height: 6px;
125
+ }
126
+
127
+ .tab-list::-webkit-scrollbar-thumb {
128
+ background-color: var(--color-gray-300);
129
+ border-radius: var(--radius-full);
130
+ }
131
+
132
+ .tab-list::-webkit-scrollbar-track {
133
+ background-color: transparent;
113
134
  }
114
135
 
115
136
  .tab-button {
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ /** @file Devtools screen for the resolved Picked client configuration and reactive lifecycle state. */
3
+ import { getPicked } from '../../../../hooks'
4
+ import { Badge, Card, CodeBlock, DataTable, EmptyState, Stack, Text } from '../../ui'
5
+
6
+ const client = getPicked()
7
+ const lifecycleStageStore = client.lifecycleStageStore
8
+ const readyStore = client.readyStore
9
+ const preconditionPassedStore = client.preconditionPassedStore
10
+ const conditionPassedStore = client.conditionPassedStore
11
+
12
+ const searchParamEntries = Object.entries(client.searchParams ?? {}).map(([key, value]) => ({
13
+ key,
14
+ value: Array.isArray(value) ? value.join(', ') : String(value ?? ''),
15
+ }))
16
+
17
+ const staticRows = [
18
+ { label: 'Project ID', value: client.projectId },
19
+ { label: 'API URL', value: client.apiUrl },
20
+ { label: 'Dashboard URL', value: client.dashboardUrl },
21
+ { label: 'Documentation URL', value: client.documentationSiteUrl },
22
+ { label: 'Data source', value: client.dataSource === null ? '—' : JSON.stringify(client.dataSource) },
23
+ ]
24
+
25
+ const configColumns = [
26
+ { key: 'label', header: 'Property' },
27
+ { key: 'value', header: 'Value' },
28
+ ]
29
+
30
+ const searchParamColumns = [
31
+ { key: 'key', header: 'Key' },
32
+ { key: 'value', header: 'Value' },
33
+ ]
34
+
35
+ /**
36
+ * Maps a lifecycle stage to a Badge status colour.
37
+ *
38
+ * @param {'idle' | 'starting' | 'ready' | 'failed'} stage - The current lifecycle stage.
39
+ * @returns {'default' | 'info' | 'success' | 'error'} The Badge status variant.
40
+ * @example
41
+ * ```typescript
42
+ * stageStatus('ready') // 'success'
43
+ * ```
44
+ */
45
+ function stageStatus(
46
+ stage: 'idle' | 'starting' | 'ready' | 'failed',
47
+ ): 'default' | 'info' | 'success' | 'error' {
48
+ if (stage === 'ready') return 'success'
49
+ if (stage === 'starting') return 'info'
50
+ if (stage === 'failed') return 'error'
51
+ return 'default'
52
+ }
53
+
54
+ /**
55
+ * Maps a tri-state boolean gate result to a Badge status colour.
56
+ *
57
+ * @param {boolean | undefined} value - The gate value: true (passed), false (blocked), or undefined (unevaluated).
58
+ * @returns {'default' | 'success' | 'error'} The Badge status variant.
59
+ * @example
60
+ * ```typescript
61
+ * gateStatus(undefined) // 'default'
62
+ * gateStatus(true) // 'success'
63
+ * ```
64
+ */
65
+ function gateStatus(value: boolean | undefined): 'default' | 'success' | 'error' {
66
+ if (value === true) return 'success'
67
+ if (value === false) return 'error'
68
+ return 'default'
69
+ }
70
+
71
+ /**
72
+ * Renders a tri-state boolean gate as a short label.
73
+ *
74
+ * @param {boolean | undefined} value - The gate value.
75
+ * @returns {string} The label text.
76
+ * @example
77
+ * ```typescript
78
+ * gateLabel(undefined) // 'pending'
79
+ * ```
80
+ */
81
+ function gateLabel(value: boolean | undefined): string {
82
+ if (value === true) return 'passed'
83
+ if (value === false) return 'blocked'
84
+ return 'pending'
85
+ }
86
+ </script>
87
+
88
+ <Stack direction="vertical" gap="medium" padding="medium">
89
+ <Text size="caption" color="subdued">
90
+ The resolved configuration and live lifecycle state for this Picked client instance.
91
+ </Text>
92
+
93
+ <Card sectioned>
94
+ {#snippet header()}
95
+ <Text size="caption" weight="semibold" color="subdued">CONFIGURATION</Text>
96
+ {/snippet}
97
+ <DataTable columns={configColumns} rows={staticRows}>
98
+ {#snippet row(item)}
99
+ <tr>
100
+ <td><Text size="small" weight="medium">{item.label}</Text></td>
101
+ <td><CodeBlock code={item.value} inline /></td>
102
+ </tr>
103
+ {/snippet}
104
+ </DataTable>
105
+ </Card>
106
+
107
+ <Card sectioned>
108
+ {#snippet header()}
109
+ <Text size="caption" weight="semibold" color="subdued">LIFECYCLE</Text>
110
+ {/snippet}
111
+ <Stack direction="vertical" gap="small" padding="medium">
112
+ <Stack direction="horizontal" gap="small">
113
+ <Text size="small" weight="medium">Stage:</Text>
114
+ <Badge status={stageStatus($lifecycleStageStore)} size="small">{$lifecycleStageStore}</Badge>
115
+ </Stack>
116
+ <Stack direction="horizontal" gap="small">
117
+ <Text size="small" weight="medium">Ready:</Text>
118
+ <Badge status={$readyStore ? 'success' : 'default'} size="small">{$readyStore ? 'true' : 'false'}</Badge>
119
+ </Stack>
120
+ <Stack direction="horizontal" gap="small">
121
+ <Text size="small" weight="medium">Precondition:</Text>
122
+ <Badge status={gateStatus($preconditionPassedStore)} size="small">
123
+ {gateLabel($preconditionPassedStore)}
124
+ </Badge>
125
+ </Stack>
126
+ <Stack direction="horizontal" gap="small">
127
+ <Text size="small" weight="medium">Condition:</Text>
128
+ <Badge status={gateStatus($conditionPassedStore)} size="small">
129
+ {gateLabel($conditionPassedStore)}
130
+ </Badge>
131
+ </Stack>
132
+ </Stack>
133
+ </Card>
134
+
135
+ <Card sectioned>
136
+ {#snippet header()}
137
+ <Text size="caption" weight="semibold" color="subdued">
138
+ PICKED SEARCH PARAMS ({searchParamEntries.length})
139
+ </Text>
140
+ {/snippet}
141
+ <DataTable columns={searchParamColumns} rows={searchParamEntries}>
142
+ {#snippet row(entry)}
143
+ <tr>
144
+ <td><CodeBlock code={entry.key} inline /></td>
145
+ <td><CodeBlock code={entry.value} inline /></td>
146
+ </tr>
147
+ {/snippet}
148
+ {#snippet empty()}
149
+ <EmptyState message="No picked-* search params on the current URL" />
150
+ {/snippet}
151
+ </DataTable>
152
+ </Card>
153
+ </Stack>
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ /** @file Devtools screen listing CMS content records registered with the client. */
3
+ import type { Content } from '@picked/client'
4
+ import { getPicked } from '../../../../hooks'
5
+ import { Badge, Card, CodeBlock, DataTable, EmptyState, ExpandableRow, Stack, Text } from '../../ui'
6
+
7
+ const client = getPicked()
8
+ const contentsStore = client.contents.listStore
9
+
10
+ let expandedId: string | null = null
11
+
12
+ const columns = [
13
+ { key: 'id', header: 'ID' },
14
+ { key: 'keys', header: 'Top-level keys' },
15
+ ]
16
+
17
+ /**
18
+ * Returns the top-level keys of a content record's `data` object, used to give a quick
19
+ * shape preview in the row summary without expanding.
20
+ *
21
+ * @param {Content} content - The content record.
22
+ * @returns {string[]} The data object's top-level keys.
23
+ * @example
24
+ * ```typescript
25
+ * dataKeys({ id: 'c1', data: { headline: 'hi' } }) // ['headline']
26
+ * ```
27
+ */
28
+ function dataKeys(content: Content): string[] {
29
+ return Object.keys(content.data ?? {})
30
+ }
31
+
32
+ /**
33
+ * Pretty-prints the content record's `data` object for use inside a multi-line CodeBlock.
34
+ *
35
+ * @param {Content} content - The content record.
36
+ * @returns {string} A two-space-indented JSON representation of `data`.
37
+ * @example
38
+ * ```typescript
39
+ * formatData({ id: 'c1', data: { a: 1 } }) // '{\n "a": 1\n}'
40
+ * ```
41
+ */
42
+ function formatData(content: Content): string {
43
+ return JSON.stringify(content.data, null, 2)
44
+ }
45
+ </script>
46
+
47
+ <Stack direction="vertical" gap="medium" padding="medium">
48
+ <Text size="caption" color="subdued">
49
+ Content records registered with the client by the active content loader.
50
+ </Text>
51
+
52
+ <Card sectioned>
53
+ <DataTable {columns} rows={$contentsStore}>
54
+ {#snippet row(content)}
55
+ {@const expanded = expandedId === content.id}
56
+ {@const keys = dataKeys(content)}
57
+ <ExpandableRow
58
+ {expanded}
59
+ onToggle={() => { expandedId = expanded ? null : content.id }}
60
+ colspan={columns.length}
61
+ >
62
+ {#snippet summary()}
63
+ <td><CodeBlock code={content.id} inline /></td>
64
+ <td>
65
+ {#if keys.length > 0}
66
+ <Stack direction="horizontal" gap="small" wrap={true}>
67
+ {#each keys as key (key)}
68
+ <Badge status="default" size="small">{key}</Badge>
69
+ {/each}
70
+ </Stack>
71
+ {:else}
72
+ <Text size="small" color="subdued">—</Text>
73
+ {/if}
74
+ </td>
75
+ {/snippet}
76
+ {#snippet details()}
77
+ <Stack direction="vertical" gap="small">
78
+ <Text size="caption" weight="semibold" color="subdued">DATA</Text>
79
+ <CodeBlock code={formatData(content)} />
80
+ </Stack>
81
+ {/snippet}
82
+ </ExpandableRow>
83
+ {/snippet}
84
+ {#snippet empty()}
85
+ <EmptyState message="No content registered" />
86
+ {/snippet}
87
+ </DataTable>
88
+ </Card>
89
+ </Stack>
@@ -0,0 +1,276 @@
1
+ <script lang="ts">
2
+ /** @file Expandable row presenting a single registered field's schema in the devtools fields screen. */
3
+ import type { Field } from '@picked/client'
4
+ import { Badge, CodeBlock, ExpandableRow, Stack, Text } from '../../ui'
5
+
6
+ export let field: Field
7
+ export let columnCount: number
8
+
9
+ let expanded = false
10
+
11
+ $: optionsList = Array.isArray(field.options) ? field.options : []
12
+ $: hasOptions = optionsList.length > 0
13
+ $: hasCondition = field.condition !== undefined && field.condition !== null
14
+ $: hasValidation = field.validation !== undefined && field.validation !== null
15
+ $: hasDefault = field.defaultValue !== undefined
16
+ $: doNotTrack = Boolean(field.policies?.doNotTrackValue)
17
+ $: doNotStore = Boolean(field.policies?.doNotStoreValue)
18
+ $: hasDetails =
19
+ hasCondition ||
20
+ hasValidation ||
21
+ hasOptions ||
22
+ Boolean(field.description) ||
23
+ Boolean(field.helpText) ||
24
+ Boolean(field.placeholder) ||
25
+ Boolean(field.label && field.label !== field.name) ||
26
+ Boolean(field.id)
27
+
28
+ /**
29
+ * Returns a Badge status colour for a given field type so each type renders consistently.
30
+ *
31
+ * @param {Field['type']} type - The field's primitive type.
32
+ * @returns {'default' | 'success' | 'warning' | 'info'} The Badge status variant to apply.
33
+ * @example
34
+ * ```typescript
35
+ * typeStatus('number') // 'success'
36
+ * ```
37
+ */
38
+ function typeStatus(type: Field['type']): 'default' | 'success' | 'warning' | 'info' {
39
+ if (type === 'text') return 'info'
40
+ if (type === 'number') return 'success'
41
+ if (type === 'boolean') return 'warning'
42
+ return 'default'
43
+ }
44
+
45
+ /**
46
+ * Renders an arbitrary value as a short, single-line preview for use in compact table cells.
47
+ * Strings are returned unquoted, primitives are stringified, and objects/arrays are JSON-encoded
48
+ * and truncated.
49
+ *
50
+ * @param {unknown} value - The value to preview.
51
+ * @param {number} [maxLength] - Maximum character length before the preview is truncated.
52
+ * @returns {string} The short preview string, or `'—'` when the value is null/undefined.
53
+ * @example
54
+ * ```typescript
55
+ * previewValue({ foo: 'bar' }) // '{"foo":"bar"}'
56
+ * previewValue(null) // '—'
57
+ * ```
58
+ */
59
+ function previewValue(value: unknown, maxLength = 48): string {
60
+ if (value === null || value === undefined) return '—'
61
+
62
+ let str: string
63
+ if (typeof value === 'string') {
64
+ str = value
65
+ } else if (typeof value === 'object') {
66
+ str = JSON.stringify(value)
67
+ } else {
68
+ str = String(value)
69
+ }
70
+
71
+ if (str.length > maxLength) return `${str.slice(0, maxLength - 1)}…`
72
+ return str
73
+ }
74
+
75
+ /**
76
+ * Pretty-prints a JSON-serializable value for use inside a multi-line CodeBlock.
77
+ *
78
+ * @param {unknown} value - The value to format.
79
+ * @returns {string} A two-space-indented JSON representation.
80
+ * @example
81
+ * ```typescript
82
+ * formatJson({ a: 1 }) // '{\n "a": 1\n}'
83
+ * ```
84
+ */
85
+ function formatJson(value: unknown): string {
86
+ return JSON.stringify(value, null, 2)
87
+ }
88
+
89
+ /**
90
+ * Renders a `FieldOption.value` (which may be any JSON-serializable shape across field types)
91
+ * as a short, copyable code string.
92
+ *
93
+ * @param {unknown} value - The option's underlying value.
94
+ * @returns {string} The string to display inside an inline CodeBlock.
95
+ * @example
96
+ * ```typescript
97
+ * formatOptionValue('us') // 'us'
98
+ * formatOptionValue({ id: 1 }) // '{"id":1}'
99
+ * ```
100
+ */
101
+ function formatOptionValue(value: unknown): string {
102
+ if (typeof value === 'string') return value
103
+ return JSON.stringify(value)
104
+ }
105
+ </script>
106
+
107
+ <ExpandableRow
108
+ {expanded}
109
+ onToggle={() => { if (hasDetails) expanded = !expanded }}
110
+ colspan={columnCount}
111
+ >
112
+ {#snippet summary()}
113
+ <td><CodeBlock code={field.key} inline /></td>
114
+ <td>
115
+ <Stack direction="vertical" gap="none">
116
+ <Text size="small" weight="medium">{field.name}</Text>
117
+ {#if field.label && field.label !== field.name}
118
+ <Text size="caption" color="subdued">{field.label}</Text>
119
+ {/if}
120
+ </Stack>
121
+ </td>
122
+ <td><Badge status={typeStatus(field.type)} size="small">{field.type}</Badge></td>
123
+ <td>
124
+ <Stack direction="horizontal" gap="small">
125
+ {#if field.multiple}
126
+ <Badge status="default" size="small">multiple</Badge>
127
+ {/if}
128
+ {#if field.required}
129
+ <Badge status="warning" size="small">required</Badge>
130
+ {/if}
131
+ {#if doNotTrack}
132
+ <Badge status="error" size="small">no-track</Badge>
133
+ {/if}
134
+ {#if doNotStore}
135
+ <Badge status="error" size="small">no-store</Badge>
136
+ {/if}
137
+ </Stack>
138
+ </td>
139
+ <td>
140
+ {#if hasDefault}
141
+ <CodeBlock code={previewValue(field.defaultValue)} inline />
142
+ {:else}
143
+ <Text size="small" color="subdued">—</Text>
144
+ {/if}
145
+ </td>
146
+ <td>
147
+ {#if hasOptions}
148
+ <Badge status="info" size="small">{optionsList.length}</Badge>
149
+ {:else}
150
+ <Text size="small" color="subdued">—</Text>
151
+ {/if}
152
+ </td>
153
+ <td>
154
+ <Stack direction="horizontal" gap="small">
155
+ {#if hasCondition}
156
+ <Badge status="info" size="small">condition</Badge>
157
+ {/if}
158
+ {#if hasValidation}
159
+ <Badge status="info" size="small">validation</Badge>
160
+ {/if}
161
+ {#if !hasCondition && !hasValidation}
162
+ <Text size="small" color="subdued">—</Text>
163
+ {/if}
164
+ </Stack>
165
+ </td>
166
+ {/snippet}
167
+
168
+ {#snippet details()}
169
+ <Stack direction="vertical" gap="medium">
170
+ <Stack direction="vertical" gap="small">
171
+ <Text size="caption" weight="semibold" color="subdued">IDENTITY</Text>
172
+ <Stack direction="horizontal" gap="medium">
173
+ <Text size="small"><strong>id:</strong></Text>
174
+ <CodeBlock code={field.id} inline />
175
+ </Stack>
176
+ </Stack>
177
+
178
+ {#if field.description || field.helpText || field.placeholder}
179
+ <Stack direction="vertical" gap="small">
180
+ <Text size="caption" weight="semibold" color="subdued">COPY</Text>
181
+ {#if field.description}
182
+ <Text size="small"><strong>description:</strong> {field.description}</Text>
183
+ {/if}
184
+ {#if field.helpText}
185
+ <Text size="small"><strong>help text:</strong> {field.helpText}</Text>
186
+ {/if}
187
+ {#if field.placeholder}
188
+ <Text size="small"><strong>placeholder:</strong> {field.placeholder}</Text>
189
+ {/if}
190
+ </Stack>
191
+ {/if}
192
+
193
+ {#if hasDefault}
194
+ <Stack direction="vertical" gap="small">
195
+ <Text size="caption" weight="semibold" color="subdued">DEFAULT VALUE</Text>
196
+ <CodeBlock code={formatJson(field.defaultValue)} />
197
+ </Stack>
198
+ {/if}
199
+
200
+ {#if hasOptions}
201
+ <Stack direction="vertical" gap="small">
202
+ <Text size="caption" weight="semibold" color="subdued">OPTIONS ({optionsList.length})</Text>
203
+ <table class="options-table">
204
+ <thead>
205
+ <tr>
206
+ <th>Label</th>
207
+ <th>Value</th>
208
+ <th>ID</th>
209
+ <th>Description</th>
210
+ </tr>
211
+ </thead>
212
+ <tbody>
213
+ {#each optionsList as option (option.id)}
214
+ <tr>
215
+ <td>{option.label}</td>
216
+ <td><CodeBlock code={formatOptionValue(option.value)} inline /></td>
217
+ <td><CodeBlock code={option.id} inline /></td>
218
+ <td>
219
+ {#if option.description}
220
+ {option.description}
221
+ {:else}
222
+ <Text size="small" color="subdued">—</Text>
223
+ {/if}
224
+ </td>
225
+ </tr>
226
+ {/each}
227
+ </tbody>
228
+ </table>
229
+ </Stack>
230
+ {/if}
231
+
232
+ {#if hasCondition}
233
+ <Stack direction="vertical" gap="small">
234
+ <Text size="caption" weight="semibold" color="subdued">CONDITION</Text>
235
+ <CodeBlock code={formatJson(field.condition)} />
236
+ </Stack>
237
+ {/if}
238
+
239
+ {#if hasValidation}
240
+ <Stack direction="vertical" gap="small">
241
+ <Text size="caption" weight="semibold" color="subdued">VALIDATION</Text>
242
+ <CodeBlock code={formatJson(field.validation)} />
243
+ </Stack>
244
+ {/if}
245
+ </Stack>
246
+ {/snippet}
247
+ </ExpandableRow>
248
+
249
+ <style>
250
+ .options-table {
251
+ width: 100%;
252
+ border-collapse: collapse;
253
+ background-color: var(--color-white);
254
+ border: 1px solid var(--color-gray-200);
255
+ border-radius: var(--radius-md);
256
+ overflow: hidden;
257
+ }
258
+
259
+ .options-table th,
260
+ .options-table td {
261
+ padding: var(--space-1-5) var(--space-2);
262
+ text-align: left;
263
+ font-size: var(--text-xs);
264
+ border-bottom: 1px solid var(--color-gray-200);
265
+ }
266
+
267
+ .options-table tr:last-child td {
268
+ border-bottom: 0;
269
+ }
270
+
271
+ .options-table th {
272
+ font-weight: var(--font-medium);
273
+ color: var(--color-gray-700);
274
+ background-color: var(--color-gray-50);
275
+ }
276
+ </style>
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ /** @file Devtools screen that lists every field registered with the SDK. */
3
+ import { getPicked } from '../../../../hooks'
4
+ import FieldsTable from './FieldsTable.svelte'
5
+
6
+ const client = getPicked()
7
+ const fieldsStore = client.fields.listStore
8
+ </script>
9
+
10
+ <div>
11
+ <FieldsTable fields={$fieldsStore} />
12
+ </div>
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ /** @file Table of registered fields with type filtering for the devtools fields screen. */
3
+ import type { Field } from '@picked/client'
4
+ import { Card, DataTable, EmptyState, RadioGroup, Stack } from '../../ui'
5
+ import FieldTableRow from './FieldTableRow.svelte'
6
+
7
+ export let fields: Field[] = []
8
+
9
+ type TypeFilter = 'all' | Field['type']
10
+
11
+ let filter: TypeFilter = 'all'
12
+
13
+ $: counts = {
14
+ all: fields.length,
15
+ text: fields.filter((f) => f.type === 'text').length,
16
+ number: fields.filter((f) => f.type === 'number').length,
17
+ boolean: fields.filter((f) => f.type === 'boolean').length,
18
+ object: fields.filter((f) => f.type === 'object').length,
19
+ }
20
+
21
+ $: visibleFields = filter === 'all' ? fields : fields.filter((f) => f.type === filter)
22
+
23
+ const columns = [
24
+ { key: 'key', header: 'Key' },
25
+ { key: 'name', header: 'Name' },
26
+ { key: 'type', header: 'Type' },
27
+ { key: 'flags', header: 'Flags' },
28
+ { key: 'default', header: 'Default' },
29
+ { key: 'options', header: 'Options' },
30
+ { key: 'rules', header: 'Rules' },
31
+ ]
32
+
33
+ $: filterOptions = [
34
+ { value: 'all', label: `All (${counts.all})` },
35
+ { value: 'text', label: `Text (${counts.text})` },
36
+ { value: 'number', label: `Number (${counts.number})` },
37
+ { value: 'boolean', label: `Boolean (${counts.boolean})` },
38
+ { value: 'object', label: `Object (${counts.object})` },
39
+ ]
40
+
41
+ /**
42
+ * Updates the active type filter when the user picks a different radio option.
43
+ *
44
+ * @param {string} value - The chosen filter value, expected to be `'all'` or a `Field['type']`.
45
+ * @example
46
+ * ```typescript
47
+ * handleFilterChange('number') // narrows the table to number fields
48
+ * ```
49
+ */
50
+ function handleFilterChange(value: string): void {
51
+ if (value === 'all' || value === 'text' || value === 'number' || value === 'boolean' || value === 'object') {
52
+ filter = value
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <Stack padding="medium">
58
+ <Card sectioned>
59
+ <Stack direction="horizontal" gap="small" align="space-between" justify="center" padding="medium">
60
+ <RadioGroup
61
+ label="Filter by type"
62
+ name="field-type-filter"
63
+ options={filterOptions}
64
+ value={filter}
65
+ onchange={handleFilterChange}
66
+ orientation="horizontal"
67
+ />
68
+ </Stack>
69
+ <DataTable {columns} rows={visibleFields}>
70
+ {#snippet row(field)}
71
+ <FieldTableRow {field} columnCount={columns.length} />
72
+ {/snippet}
73
+ {#snippet empty()}
74
+ <EmptyState message="No fields registered" />
75
+ {/snippet}
76
+ </DataTable>
77
+ </Card>
78
+ </Stack>
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ /** @file Devtools screen for the current visitor session — id, isUser flag, and identifiers map. */
3
+ import { getPicked } from '../../../../hooks'
4
+ import { Badge, Card, CodeBlock, DataTable, EmptyState, Stack, Text } from '../../ui'
5
+
6
+ const client = getPicked()
7
+ const sessionStore = client.session.store
8
+
9
+ $: identifierEntries = Object.entries($sessionStore.identifiers ?? {})
10
+
11
+ const overviewColumns = [
12
+ { key: 'label', header: 'Property' },
13
+ { key: 'value', header: 'Value' },
14
+ ]
15
+
16
+ const identifierColumns = [
17
+ { key: 'key', header: 'Key' },
18
+ { key: 'value', header: 'Value' },
19
+ ]
20
+ </script>
21
+
22
+ <Stack direction="vertical" gap="medium" padding="medium">
23
+ <Text size="caption" color="subdued">
24
+ The current visitor session as tracked by the SDK. Persisted to localStorage when available.
25
+ </Text>
26
+
27
+ <Card sectioned>
28
+ <DataTable columns={overviewColumns} rows={[{ label: 'Session ID', value: $sessionStore.id }]}>
29
+ {#snippet row(item)}
30
+ <tr>
31
+ <td><Text size="small" weight="medium">{item.label}</Text></td>
32
+ <td><CodeBlock code={item.value} inline /></td>
33
+ </tr>
34
+ {/snippet}
35
+ </DataTable>
36
+ <Stack direction="horizontal" gap="small" padding="medium">
37
+ <Text size="small" weight="medium">Identified as user:</Text>
38
+ {#if $sessionStore.isUser === true}
39
+ <Badge status="success" size="small">true</Badge>
40
+ {:else if $sessionStore.isUser === false}
41
+ <Badge status="default" size="small">false</Badge>
42
+ {:else}
43
+ <Badge status="default" size="small">unset</Badge>
44
+ {/if}
45
+ </Stack>
46
+ </Card>
47
+
48
+ <Card sectioned>
49
+ {#snippet header()}
50
+ <Text size="caption" weight="semibold" color="subdued">
51
+ IDENTIFIERS ({identifierEntries.length})
52
+ </Text>
53
+ {/snippet}
54
+ <DataTable columns={identifierColumns} rows={identifierEntries}>
55
+ {#snippet row(entry)}
56
+ <tr>
57
+ <td><CodeBlock code={entry[0]} inline /></td>
58
+ <td><CodeBlock code={entry[1]} inline /></td>
59
+ </tr>
60
+ {/snippet}
61
+ {#snippet empty()}
62
+ <EmptyState message="No identifiers set" />
63
+ {/snippet}
64
+ </DataTable>
65
+ </Card>
66
+ </Stack>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  /** @file Editable table row for a single field response in the devtools responses screen. */
3
- import type { ResponseListItem, PrimitiveFieldValue, Field, FieldValue } from '@picked/client'
3
+ import type { ResponseListItem, Field, FieldValue, JsonValue } from '@picked/client'
4
4
  import { getPicked } from '../../../../hooks'
5
5
  import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
6
6
 
@@ -13,7 +13,7 @@
13
13
  let editValue = ''
14
14
  let editError = ''
15
15
  // eslint-disable-next-line svelte/prefer-svelte-reactivity -- Set is reassigned for Svelte reactivity via `selectedOptions = selectedOptions`
16
- let selectedOptions: Set<PrimitiveFieldValue> = new Set()
16
+ let selectedOptions: Set<JsonValue> = new Set()
17
17
 
18
18
  $: hasOptions = Array.isArray(field?.options) && field.options.length > 0
19
19
  $: staticOptions = Array.isArray(field?.options) ? field.options : null
@@ -22,7 +22,8 @@
22
22
  /**
23
23
  * Formats a response value for display, handling arrays, objects, and primitives.
24
24
  *
25
- * @param value - The response value to format.
25
+ * @param {unknown} value - The response value to format.
26
+ * @returns {string} The display string for the value.
26
27
  * @example
27
28
  * ```typescript
28
29
  * formatValue(['a', 'b']) // 'a, b'
@@ -87,13 +88,13 @@
87
88
  /**
88
89
  * Toggles selection of a field option, supporting both single and multi-select modes.
89
90
  *
90
- * @param optionValue - The option value to toggle.
91
+ * @param {JsonValue} optionValue - The option value to toggle.
91
92
  * @example
92
93
  * ```typescript
93
94
  * toggleOption('red') // selects or deselects the 'red' option
94
95
  * ```
95
96
  */
96
- function toggleOption(optionValue: PrimitiveFieldValue) {
97
+ function toggleOption(optionValue: JsonValue) {
97
98
  if (field?.multiple) {
98
99
  if (selectedOptions.has(optionValue)) {
99
100
  selectedOptions.delete(optionValue)
@@ -110,14 +111,15 @@
110
111
  /**
111
112
  * Parses a string input into a typed value based on the field's type.
112
113
  *
113
- * @param input - The raw string input to parse.
114
+ * @param {string} input - The raw string input to parse.
115
+ * @returns {FieldValue} The parsed value (null, number, boolean, JSON, or string).
114
116
  * @example
115
117
  * ```typescript
116
118
  * parseValue('42') // 42 (when field.type is 'number')
117
119
  * parseValue('true') // true (when field.type is 'boolean')
118
120
  * ```
119
121
  */
120
- function parseValue(input: string): unknown {
122
+ function parseValue(input: string): FieldValue {
121
123
  const trimmed = input.trim()
122
124
 
123
125
  if (trimmed === '' || trimmed === '-') {
@@ -173,7 +175,7 @@
173
175
  newValue = parseValue(editValue)
174
176
  }
175
177
 
176
- const result = await client.responses.commit({ [field.id]: newValue })
178
+ const result = await client.responses.commit({ [field.key]: newValue })
177
179
 
178
180
  if (result.errors.length > 0) {
179
181
  editError = result.errors[0].userMessage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.62-next.8",
3
+ "version": "0.0.62",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -88,7 +88,7 @@
88
88
  },
89
89
  "module": "./dist/index.js",
90
90
  "peerDependencies": {
91
- "@picked/client": "0.0.62-next.8",
91
+ "@picked/client": "0.0.62",
92
92
  "effector": "^23.3.0",
93
93
  "svelte": "^5.46.4"
94
94
  },