@picked/svelte 0.0.42-next.1 → 0.0.42-next.3
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.
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import ProjectScreen from './screens/project/ProjectScreen.svelte'
|
|
11
11
|
import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
|
|
12
12
|
import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
|
|
13
|
+
import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
|
|
13
14
|
import EventsListScreen from './screens/events/EventsListScreen.svelte'
|
|
14
15
|
|
|
15
16
|
const experience = getExperience()
|
|
@@ -57,6 +58,12 @@
|
|
|
57
58
|
component: PredicatesListScreen,
|
|
58
59
|
props: {},
|
|
59
60
|
},
|
|
61
|
+
{
|
|
62
|
+
id: 'features',
|
|
63
|
+
label: 'Features',
|
|
64
|
+
component: FeaturesListScreen,
|
|
65
|
+
props: {},
|
|
66
|
+
},
|
|
60
67
|
{
|
|
61
68
|
id: 'components',
|
|
62
69
|
label: 'Components',
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createStore, createEvent } from 'effector'
|
|
3
|
+
import type { FeatureBucket } from '@picked/client'
|
|
4
|
+
import { getPicked, useUnit } from '../../../../hooks'
|
|
5
|
+
|
|
6
|
+
interface FeatureSelection {
|
|
7
|
+
included: boolean
|
|
8
|
+
value: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { features: { listStore: featuresStore } } = getPicked()
|
|
12
|
+
const featuresList = useUnit(featuresStore)
|
|
13
|
+
|
|
14
|
+
const initSelections = createEvent<FeatureBucket[]>()
|
|
15
|
+
const featureToggled = createEvent<string>()
|
|
16
|
+
const valueChanged = createEvent<{ id: string; value: string }>()
|
|
17
|
+
const copySucceeded = createEvent()
|
|
18
|
+
const copyReset = createEvent()
|
|
19
|
+
|
|
20
|
+
const selectionsStore = createStore<Record<string, FeatureSelection>>({})
|
|
21
|
+
// Triggered whenever the features list changes. Adds an entry for any feature
|
|
22
|
+
// not yet tracked, seeding its override value from the already-resolved bucket
|
|
23
|
+
// (or 'true' as a sensible default for unresolved boolean features).
|
|
24
|
+
.on(initSelections, (state, feats) => {
|
|
25
|
+
const next = { ...state }
|
|
26
|
+
feats.forEach((feature) => {
|
|
27
|
+
if (!(feature.id in next)) {
|
|
28
|
+
const bucket = feature.isResolved ? feature.getBucket().bucket : null
|
|
29
|
+
next[feature.id] = {
|
|
30
|
+
included: false,
|
|
31
|
+
value: bucket !== null ? String(bucket) : 'true',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
return next
|
|
36
|
+
})
|
|
37
|
+
// Toggles whether the feature is included in the generated URL.
|
|
38
|
+
.on(featureToggled, (state, id) => ({
|
|
39
|
+
...state,
|
|
40
|
+
[id]: { ...state[id], included: !state[id].included },
|
|
41
|
+
}))
|
|
42
|
+
// Updates the override value for a feature (edited via the text/select input).
|
|
43
|
+
.on(valueChanged, (state, { id, value }) => ({
|
|
44
|
+
...state,
|
|
45
|
+
[id]: { ...state[id], value },
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
const copySuccessStore = createStore(false)
|
|
49
|
+
.on(copySucceeded, () => true) // Show "Copied!" feedback after a successful clipboard write.
|
|
50
|
+
.on(copyReset, () => false) // Reset back to "Copy" after the feedback timeout.
|
|
51
|
+
|
|
52
|
+
const selections = useUnit(selectionsStore)
|
|
53
|
+
const copySuccess = useUnit(copySuccessStore)
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
initSelections($featuresList)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
let urlInputRef: HTMLInputElement | null = $state(null)
|
|
60
|
+
|
|
61
|
+
function getBucketValue(feature: FeatureBucket): string | null {
|
|
62
|
+
return feature.isResolved ? String(feature.getBucket().bucket) : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isBooleanFeature(feature: FeatureBucket): boolean {
|
|
66
|
+
if (!feature.isResolved) return true
|
|
67
|
+
const bucket = getBucketValue(feature)
|
|
68
|
+
return bucket === 'true' || bucket === 'false'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const generatedUrl = $derived.by(() => {
|
|
72
|
+
const url = new URL(window.location.href)
|
|
73
|
+
url.searchParams.delete('picked-feature')
|
|
74
|
+
const featureParams = $featuresList
|
|
75
|
+
.filter((feature) => $selections[feature.id]?.included)
|
|
76
|
+
.map((feature) => `picked-feature=${feature.id}:${$selections[feature.id].value}`)
|
|
77
|
+
const base = url.toString()
|
|
78
|
+
if (featureParams.length === 0) return base
|
|
79
|
+
const separator = url.search ? '&' : '?'
|
|
80
|
+
return base + separator + featureParams.join('&')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const hasSelections = $derived(Object.values($selections).some((s) => s.included))
|
|
84
|
+
|
|
85
|
+
async function copyUrl() {
|
|
86
|
+
try {
|
|
87
|
+
await navigator.clipboard.writeText(generatedUrl)
|
|
88
|
+
copySucceeded()
|
|
89
|
+
setTimeout(copyReset, 2000)
|
|
90
|
+
} catch {
|
|
91
|
+
urlInputRef?.select()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function refreshPage() {
|
|
96
|
+
window.location.href = generatedUrl
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<div class="url-builder">
|
|
101
|
+
<p class="description">
|
|
102
|
+
Select feature overrides to generate a URL. Anyone who loads this URL will get the selected
|
|
103
|
+
feature values from page load — before any resolution occurs.
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
<table>
|
|
107
|
+
<thead>
|
|
108
|
+
<tr>
|
|
109
|
+
<th style="width: 2.5rem;">Include</th>
|
|
110
|
+
<th>Feature</th>
|
|
111
|
+
<th>Current Value</th>
|
|
112
|
+
<th>Override Value</th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
{#each $featuresList as feature (feature.id)}
|
|
117
|
+
{@const selection = $selections[feature.id]}
|
|
118
|
+
{@const currentValue = getBucketValue(feature)}
|
|
119
|
+
{@const isBoolean = isBooleanFeature(feature)}
|
|
120
|
+
{#if selection}
|
|
121
|
+
<tr class:selected={selection.included}>
|
|
122
|
+
<td style="text-align: center;">
|
|
123
|
+
<input
|
|
124
|
+
type="checkbox"
|
|
125
|
+
checked={selection.included}
|
|
126
|
+
onchange={() => featureToggled(feature.id)}
|
|
127
|
+
aria-label="Include {feature.name} in URL"
|
|
128
|
+
/>
|
|
129
|
+
</td>
|
|
130
|
+
<td>
|
|
131
|
+
<span style="font-weight: 500;">{feature.name}</span>
|
|
132
|
+
<code
|
|
133
|
+
style="
|
|
134
|
+
display: block;
|
|
135
|
+
font-size: 10px;
|
|
136
|
+
color: #888;
|
|
137
|
+
margin-top: 2px;
|
|
138
|
+
">{feature.key}</code>
|
|
139
|
+
</td>
|
|
140
|
+
<td>
|
|
141
|
+
{#if currentValue !== null}
|
|
142
|
+
<code style="font-size: 11px;">{currentValue}</code>
|
|
143
|
+
{:else}
|
|
144
|
+
<button onclick={() => feature.getBucket()} style="margin-left: 0.5rem; font-size: 10px; padding: 0.125rem 0.25rem;">
|
|
145
|
+
Click to resolve
|
|
146
|
+
</button>
|
|
147
|
+
{/if}
|
|
148
|
+
</td>
|
|
149
|
+
<td>
|
|
150
|
+
{#if isBoolean}
|
|
151
|
+
<select
|
|
152
|
+
value={selection.value}
|
|
153
|
+
onchange={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
|
|
154
|
+
disabled={!selection.included}
|
|
155
|
+
style="font-size: 11px; padding: 0.125rem 0.25rem;"
|
|
156
|
+
>
|
|
157
|
+
<option value="true">true</option>
|
|
158
|
+
<option value="false">false</option>
|
|
159
|
+
</select>
|
|
160
|
+
{:else}
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
value={selection.value}
|
|
164
|
+
oninput={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
|
|
165
|
+
disabled={!selection.included}
|
|
166
|
+
style="width: 100%; font-size: 11px; padding: 0.125rem 0.25rem;"
|
|
167
|
+
placeholder="value"
|
|
168
|
+
/>
|
|
169
|
+
{/if}
|
|
170
|
+
</td>
|
|
171
|
+
</tr>
|
|
172
|
+
{/if}
|
|
173
|
+
{/each}
|
|
174
|
+
</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
|
|
177
|
+
{#if $featuresList.length === 0}
|
|
178
|
+
<div style="padding: 2rem; text-align: center; color: #999;">No features registered</div>
|
|
179
|
+
{/if}
|
|
180
|
+
|
|
181
|
+
<div class="url-output">
|
|
182
|
+
<div class="url-label">Generated URL</div>
|
|
183
|
+
<div class="url-row">
|
|
184
|
+
<input
|
|
185
|
+
bind:this={urlInputRef}
|
|
186
|
+
type="text"
|
|
187
|
+
readonly
|
|
188
|
+
value={generatedUrl}
|
|
189
|
+
class="url-input"
|
|
190
|
+
onclick={(e) => e.currentTarget.select()}
|
|
191
|
+
aria-label="Generated URL"
|
|
192
|
+
/>
|
|
193
|
+
<button
|
|
194
|
+
onclick={copyUrl}
|
|
195
|
+
class="action-button copy-button"
|
|
196
|
+
class:success={copySuccess}
|
|
197
|
+
title="Copy URL to clipboard"
|
|
198
|
+
>
|
|
199
|
+
{copySuccess ? 'Copied!' : 'Copy'}
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
onclick={refreshPage}
|
|
203
|
+
class="action-button refresh-button"
|
|
204
|
+
disabled={!hasSelections}
|
|
205
|
+
title="Reload the page with this URL"
|
|
206
|
+
>
|
|
207
|
+
Reload
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
{#if !hasSelections}
|
|
211
|
+
<p class="hint">Select at least one feature above to add overrides to the URL.</p>
|
|
212
|
+
{/if}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<style>
|
|
217
|
+
.url-builder {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.description {
|
|
224
|
+
margin: 0;
|
|
225
|
+
padding: 0.75rem 0.5rem;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
color: #555;
|
|
228
|
+
border-bottom: 1px solid lightgray;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
tr.selected {
|
|
232
|
+
background-color: #f0f7ff;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.url-output {
|
|
236
|
+
padding: 0.75rem 0.5rem;
|
|
237
|
+
border-top: 1px solid lightgray;
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 0.5rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.url-label {
|
|
244
|
+
font-size: 11px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
color: #555;
|
|
247
|
+
text-transform: uppercase;
|
|
248
|
+
letter-spacing: 0.05em;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.url-row {
|
|
252
|
+
display: flex;
|
|
253
|
+
gap: 0.5rem;
|
|
254
|
+
align-items: center;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.url-input {
|
|
258
|
+
flex: 1;
|
|
259
|
+
font-size: 11px;
|
|
260
|
+
font-family: monospace;
|
|
261
|
+
padding: 0.375rem 0.5rem;
|
|
262
|
+
border: 1px solid #ccc;
|
|
263
|
+
border-radius: 3px;
|
|
264
|
+
background-color: #f9f9f9;
|
|
265
|
+
color: #333;
|
|
266
|
+
min-width: 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.url-input:focus {
|
|
270
|
+
outline: 2px solid #0066cc;
|
|
271
|
+
outline-offset: -1px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.action-button {
|
|
275
|
+
padding: 0.375rem 0.75rem;
|
|
276
|
+
font-size: 11px;
|
|
277
|
+
border: none;
|
|
278
|
+
border-radius: 3px;
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
white-space: nowrap;
|
|
281
|
+
font-weight: 500;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.action-button:disabled {
|
|
285
|
+
opacity: 0.5;
|
|
286
|
+
cursor: not-allowed;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.copy-button {
|
|
290
|
+
background-color: #2196f3;
|
|
291
|
+
color: white;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.copy-button.success {
|
|
295
|
+
background-color: #4caf50;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.refresh-button {
|
|
299
|
+
background-color: #555;
|
|
300
|
+
color: white;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.hint {
|
|
304
|
+
margin: 0;
|
|
305
|
+
font-size: 11px;
|
|
306
|
+
color: #999;
|
|
307
|
+
font-style: italic;
|
|
308
|
+
}
|
|
309
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picked/svelte",
|
|
3
|
-
"version": "0.0.42-next.
|
|
3
|
+
"version": "0.0.42-next.3",
|
|
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.42-next.
|
|
78
|
+
"@picked/client": "0.0.42-next.3",
|
|
79
79
|
"effector": "^23.3.0",
|
|
80
80
|
"svelte": "^5.34.3"
|
|
81
81
|
},
|