@picked/svelte 0.0.37 → 0.0.38-next.1

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.
@@ -9,6 +9,7 @@
9
9
  import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
10
10
  import ProjectScreen from './screens/project/ProjectScreen.svelte'
11
11
  import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
12
+ import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
12
13
 
13
14
  const experience = getExperience()
14
15
 
@@ -25,6 +26,12 @@
25
26
  component: HistoryEntriesListScreen,
26
27
  props: {},
27
28
  },
29
+ {
30
+ id: 'responses',
31
+ label: 'Responses',
32
+ component: ResponsesListScreen,
33
+ props: {},
34
+ },
28
35
  {
29
36
  id: 'experience-instances',
30
37
  label: 'Experience Instances',
@@ -79,6 +86,15 @@
79
86
  </section>
80
87
  </div>
81
88
  </div>
89
+ {:else}
90
+ <button
91
+ class="floating-button"
92
+ on:click={() => experience.open()}
93
+ aria-label="Open Picked Devtools"
94
+ title="Open Picked Devtools (or press .)"
95
+ >
96
+ <PickedLogo />
97
+ </button>
82
98
  {/if}
83
99
 
84
100
  <style>
@@ -166,4 +182,37 @@
166
182
  font-weight: normal;
167
183
  background-color: #f0f0f0;
168
184
  }
185
+
186
+ .floating-button {
187
+ position: fixed;
188
+ bottom: 1.5rem;
189
+ right: 1.5rem;
190
+ width: 4.5rem;
191
+ height: 4.5rem;
192
+ border-radius: 50%;
193
+ background-color: white;
194
+ border: 1px solid lightgray;
195
+ box-shadow: 0 4px 12px rgba(21, 73, 255, 0.2);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ cursor: pointer;
200
+ z-index: 999999;
201
+ transition: all 0.2s ease;
202
+ padding: 0;
203
+ }
204
+
205
+ .floating-button:hover {
206
+ box-shadow: 0 6px 16px rgba(21, 73, 255, 0.3);
207
+ transform: scale(1.05);
208
+ }
209
+
210
+ .floating-button:active {
211
+ transform: scale(0.95);
212
+ }
213
+
214
+ .floating-button :global(svg) {
215
+ width: 2.25rem;
216
+ height: 2.25rem;
217
+ }
169
218
  </style>
@@ -0,0 +1,247 @@
1
+ <script lang="ts">
2
+ import type { ResponseListItem, PrimitiveFieldValue, Field } from '@picked/client'
3
+ import { getPicked } from '../../../../hooks'
4
+
5
+ export let field: Field
6
+ export let response: ResponseListItem | null
7
+
8
+ const client = getPicked()
9
+
10
+ let isEditing = false
11
+ let editValue = ''
12
+ let editError = ''
13
+ let selectedOptions: Set<PrimitiveFieldValue> = new Set()
14
+
15
+ $: hasOptions = field?.options && field.options.length > 0
16
+ $: currentValue = response?.value ?? null
17
+
18
+ function formatValue(value: unknown): string {
19
+ if (value === null || value === undefined) {
20
+ return '-'
21
+ }
22
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
23
+ return value.join(', ')
24
+ }
25
+ if (typeof value === 'object') {
26
+ return JSON.stringify(value, null, 2)
27
+ }
28
+ return String(value)
29
+ }
30
+
31
+ function startEdit() {
32
+ isEditing = true
33
+ editError = ''
34
+
35
+ if (hasOptions) {
36
+ // Initialize selected options from current value
37
+ selectedOptions = new Set()
38
+ if (Array.isArray(currentValue)) {
39
+ currentValue.forEach((v) => selectedOptions.add(v))
40
+ } else if (currentValue !== null && currentValue !== undefined) {
41
+ selectedOptions.add(currentValue)
42
+ }
43
+ } else {
44
+ editValue = formatValue(currentValue)
45
+ }
46
+ }
47
+
48
+ function cancelEdit() {
49
+ isEditing = false
50
+ editValue = ''
51
+ editError = ''
52
+ selectedOptions = new Set()
53
+ }
54
+
55
+ function toggleOption(optionValue: PrimitiveFieldValue) {
56
+ if (field?.multiple) {
57
+ // Multiple selection: toggle in set
58
+ if (selectedOptions.has(optionValue)) {
59
+ selectedOptions.delete(optionValue)
60
+ } else {
61
+ selectedOptions.add(optionValue)
62
+ }
63
+ selectedOptions = selectedOptions // Trigger reactivity
64
+ } else {
65
+ // Single selection: replace set
66
+ selectedOptions = new Set([optionValue])
67
+ }
68
+ }
69
+
70
+ function parseValue(input: string): unknown {
71
+ input = input.trim()
72
+
73
+ // Handle empty input
74
+ if (input === '' || input === '-') {
75
+ return null
76
+ }
77
+
78
+ // Try to parse as JSON first for arrays and objects
79
+ if (input.startsWith('{') || input.startsWith('[')) {
80
+ try {
81
+ return JSON.parse(input)
82
+ } catch {
83
+ throw new Error('Invalid JSON format')
84
+ }
85
+ }
86
+
87
+ // Handle specific types based on field type
88
+ if (field.type === 'boolean') {
89
+ if (input === 'true') return true
90
+ if (input === 'false') return false
91
+ throw new Error('Boolean must be "true" or "false"')
92
+ }
93
+
94
+ if (field.type === 'number') {
95
+ const num = Number(input)
96
+ if (isNaN(num)) {
97
+ throw new Error('Invalid number format')
98
+ }
99
+ return num
100
+ }
101
+
102
+ // Default to string
103
+ return input
104
+ }
105
+
106
+ async function saveEdit() {
107
+ try {
108
+ let newValue: unknown
109
+
110
+ if (hasOptions) {
111
+ // For fields with options, use selected options
112
+ const values = Array.from(selectedOptions)
113
+ if (field?.multiple) {
114
+ newValue = values
115
+ } else {
116
+ newValue = values[0] ?? null
117
+ }
118
+ } else {
119
+ // For free-form fields, parse the text input
120
+ newValue = parseValue(editValue)
121
+ }
122
+
123
+ const result = await client.responses.commit({ [field.id]: newValue })
124
+
125
+ if (result.errors.length > 0) {
126
+ // Show validation error
127
+ editError = result.errors[0].userMessage
128
+ return
129
+ }
130
+
131
+ isEditing = false
132
+ editError = ''
133
+ selectedOptions = new Set()
134
+ } catch (error) {
135
+ editError = error instanceof Error ? error.message : 'Failed to save'
136
+ }
137
+ }
138
+
139
+ function handleClear() {
140
+ client.responses.clear(field.id)
141
+ }
142
+ </script>
143
+
144
+ <tr>
145
+ <td>
146
+ <span style="font-size: 11px; color: #666; font-family: monospace;">{field.name}</span>
147
+ </td>
148
+ <td>
149
+ <strong style="font-size: 13px;">{field.label}</strong>
150
+ </td>
151
+ <td>
152
+ {#if isEditing}
153
+ <div>
154
+ {#if hasOptions && field?.options}
155
+ <!-- Show options UI for fields with defined options -->
156
+ <div style="display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem;">
157
+ {#each field.options as option (option.id)}
158
+ <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
159
+ {#if field.multiple}
160
+ <!-- Multiple selection: checkboxes -->
161
+ <input
162
+ type="checkbox"
163
+ checked={selectedOptions.has(option.value)}
164
+ on:change={() => toggleOption(option.value)}
165
+ />
166
+ {:else}
167
+ <!-- Single selection: radio buttons -->
168
+ <input
169
+ type="radio"
170
+ name="field-{field.id}"
171
+ checked={selectedOptions.has(option.value)}
172
+ on:change={() => toggleOption(option.value)}
173
+ />
174
+ {/if}
175
+ <span style="font-size: 12px;">
176
+ {option.label}
177
+ {#if option.description}
178
+ <span style="color: #666; font-size: 11px;">({option.description})</span>
179
+ {/if}
180
+ </span>
181
+ </label>
182
+ {/each}
183
+ </div>
184
+ {:else}
185
+ <!-- Free-form text input for fields without options -->
186
+ <textarea
187
+ bind:value={editValue}
188
+ style="width: 100%; min-height: 60px; font-family: monospace; font-size: 12px; padding: 0.25rem;"
189
+ rows="3"
190
+ placeholder="Enter {field.type} value..."
191
+ ></textarea>
192
+ {/if}
193
+ {#if editError}
194
+ <div style="color: red; font-size: 11px; margin-top: 0.25rem;">
195
+ {editError}
196
+ </div>
197
+ {/if}
198
+ </div>
199
+ {:else if response === null}
200
+ <span style="color: #999; font-style: italic; font-size: 12px;"> No value set </span>
201
+ {:else}
202
+ <code style="white-space: pre-wrap; word-break: break-all;">
203
+ {formatValue(currentValue)}
204
+ </code>
205
+ {/if}
206
+ </td>
207
+ <td>
208
+ <span style="font-family: monospace; color: #666; font-size: 12px;">
209
+ {field.type}{field.multiple ? '[]' : ''}
210
+ </span>
211
+ </td>
212
+ <td>
213
+ {#if isEditing}
214
+ <div style="display: flex; gap: 0.25rem; justify-content: flex-end;">
215
+ <button
216
+ on:click={saveEdit}
217
+ style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #4CAF50; color: white; border: none; border-radius: 2px;"
218
+ >
219
+ Save
220
+ </button>
221
+ <button
222
+ on:click={cancelEdit}
223
+ style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #f44336; color: white; border: none; border-radius: 2px;"
224
+ >
225
+ Cancel
226
+ </button>
227
+ </div>
228
+ {:else}
229
+ <div style="display: flex; gap: 0.25rem; justify-content: flex-end;">
230
+ <button
231
+ on:click={startEdit}
232
+ style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; border: none; border-radius: 2px;"
233
+ >
234
+ {response === null ? 'Set' : 'Edit'}
235
+ </button>
236
+ {#if response !== null}
237
+ <button
238
+ on:click={handleClear}
239
+ style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #f44336; color: white; border: none; border-radius: 2px;"
240
+ >
241
+ Clear
242
+ </button>
243
+ {/if}
244
+ </div>
245
+ {/if}
246
+ </td>
247
+ </tr>
@@ -0,0 +1,12 @@
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>
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import type { ResponseListItem, Field } from '@picked/client'
3
+ import { getPicked } from '../../../../hooks'
4
+ import ResponseTableRow from './ResponseTableRow.svelte'
5
+
6
+ export let responses: ResponseListItem[]
7
+
8
+ const client = getPicked()
9
+
10
+ type FilterType = 'all' | 'set' | 'unset'
11
+ let filter: FilterType = 'set'
12
+
13
+ $: fieldsStore = client.fields.listStore
14
+ $: allFields = $fieldsStore
15
+
16
+ // Create a map of fieldId -> response for quick lookup
17
+ $: responseMap = new Map(responses.map((r) => [r.fieldId, r]))
18
+
19
+ // Merge fields with responses, showing all fields
20
+ $: allFieldRows = allFields.map((field) => {
21
+ const response = responseMap.get(field.id)
22
+ return {
23
+ field,
24
+ response: response || null,
25
+ }
26
+ })
27
+
28
+ // Apply filter
29
+ $: fieldRows = allFieldRows.filter((row) => {
30
+ if (filter === 'set') return row.response !== null
31
+ if (filter === 'unset') return row.response === null
32
+ return true
33
+ })
34
+
35
+ function handleClearAll() {
36
+ client.responses.clearAll()
37
+ }
38
+ </script>
39
+
40
+ <div style="display: flex; flex-direction: column;">
41
+ <table>
42
+ <thead>
43
+ <tr>
44
+ <th>ID</th>
45
+ <th>Field</th>
46
+ <th>Value</th>
47
+ <th>Type</th>
48
+ <th style="width: 120px;">Actions</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {#if fieldRows.length === 0}
53
+ <tr>
54
+ <td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
55
+ No fields registered yet
56
+ </td>
57
+ </tr>
58
+ {:else}
59
+ {#each fieldRows as { field, response } (field.id)}
60
+ <ResponseTableRow {field} {response} />
61
+ {/each}
62
+ {/if}
63
+ </tbody>
64
+ </table>
65
+ <div
66
+ style="margin-bottom: 0.5rem; margin-top: auto; padding: 0.5rem; display: flex; justify-content: space-between; align-items: center;"
67
+ >
68
+ <div style="display: flex; gap: 0.5rem; align-items: center;">
69
+ <span style="font-size: 13px; color: #666;">Show:</span>
70
+ <label
71
+ style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
72
+ >
73
+ <input type="radio" bind:group={filter} value="all" />
74
+ All
75
+ </label>
76
+ <label
77
+ style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
78
+ >
79
+ <input type="radio" bind:group={filter} value="set" />
80
+ Set ({responses.length})
81
+ </label>
82
+ <label
83
+ style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
84
+ >
85
+ <input type="radio" bind:group={filter} value="unset" />
86
+ Unset ({allFields.length - responses.length})
87
+ </label>
88
+ </div>
89
+ {#if responses.length > 0}
90
+ <button
91
+ on:click={handleClearAll}
92
+ style="padding: 0.25rem 0.5rem; cursor: pointer; background: #f44336; color: white; border: none; border-radius: 2px; font-size: 11px;"
93
+ >
94
+ Clear All
95
+ </button>
96
+ {/if}
97
+ </div>
98
+ </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.37",
3
+ "version": "0.0.38-next.1",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -75,7 +75,7 @@
75
75
  ],
76
76
  "module": "./dist/index.js",
77
77
  "peerDependencies": {
78
- "@picked/client": "0.0.37",
78
+ "@picked/client": "0.0.38-next.1",
79
79
  "effector": "^23.3.0",
80
80
  "svelte": "^5.34.3"
81
81
  },