@picked/svelte 0.0.45-next.1 → 0.0.45-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.
- package/dist/plugins/devtools/DevToolsExperience.svelte +61 -44
- package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +2 -1
- package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +22 -20
- package/dist/plugins/devtools/components/Tabs.svelte +20 -7
- package/dist/plugins/devtools/experience.js +1 -1
- package/dist/plugins/devtools/screens/components/ComponentsListScreen.svelte +18 -18
- package/dist/plugins/devtools/screens/events/EventListItem.svelte +28 -42
- package/dist/plugins/devtools/screens/events/EventsListScreen.svelte +10 -11
- package/dist/plugins/devtools/screens/events/EventsTable.svelte +31 -33
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +13 -16
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +20 -71
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +96 -232
- package/dist/plugins/devtools/screens/frameworks/FrameworksListScreen.svelte +14 -16
- package/dist/plugins/devtools/screens/history/HistoryEntriesTable.svelte +18 -16
- package/dist/plugins/devtools/screens/predicates/PredicatesListScreen.svelte +14 -16
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +25 -63
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +43 -64
- package/dist/plugins/devtools/ui/actions/Button.svelte +171 -0
- package/dist/plugins/devtools/ui/actions/CopyButton.svelte +88 -0
- package/dist/plugins/devtools/ui/data/Badge.svelte +62 -0
- package/dist/plugins/devtools/ui/data/CodeBlock.svelte +38 -0
- package/dist/plugins/devtools/ui/data/DataTable.svelte +134 -0
- package/dist/plugins/devtools/ui/data/EmptyState.svelte +26 -0
- package/dist/plugins/devtools/ui/data/ExpandableRow.svelte +66 -0
- package/dist/plugins/devtools/ui/data/VisualizationContainer.svelte +55 -0
- package/dist/plugins/devtools/ui/feedback/Banner.svelte +116 -0
- package/dist/plugins/devtools/ui/feedback/Spinner.svelte +79 -0
- package/dist/plugins/devtools/ui/forms/Checkbox.svelte +139 -0
- package/dist/plugins/devtools/ui/forms/RadioGroup.svelte +218 -0
- package/dist/plugins/devtools/ui/forms/Select.svelte +137 -0
- package/dist/plugins/devtools/ui/forms/Switch.svelte +158 -0
- package/dist/plugins/devtools/ui/forms/TextField.svelte +125 -0
- package/dist/plugins/devtools/ui/index.d.ts +22 -0
- package/dist/plugins/devtools/ui/index.js +1 -0
- package/dist/plugins/devtools/ui/layout/Card.svelte +55 -0
- package/dist/plugins/devtools/ui/layout/Divider.svelte +34 -0
- package/dist/plugins/devtools/ui/layout/Spacer.svelte +11 -0
- package/dist/plugins/devtools/ui/layout/Stack.svelte +129 -0
- package/dist/plugins/devtools/ui/overlays/Tooltip.svelte +90 -0
- package/dist/plugins/devtools/ui/tokens.css +144 -0
- package/dist/plugins/devtools/ui/tokens.css.js +1 -0
- package/dist/plugins/devtools/ui/typography/Heading.svelte +55 -0
- package/dist/plugins/devtools/ui/typography/Text.svelte +93 -0
- package/package.json +5 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ResponseListItem, PrimitiveFieldValue, Field } from '@picked/client'
|
|
3
3
|
import { getPicked } from '../../../../hooks'
|
|
4
|
+
import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
|
|
4
5
|
|
|
5
6
|
export let field: Field
|
|
6
7
|
export let response: ResponseListItem | null
|
|
@@ -33,7 +34,6 @@
|
|
|
33
34
|
editError = ''
|
|
34
35
|
|
|
35
36
|
if (hasOptions) {
|
|
36
|
-
// Initialize selected options from current value
|
|
37
37
|
selectedOptions = new Set()
|
|
38
38
|
if (Array.isArray(currentValue)) {
|
|
39
39
|
currentValue.forEach((v) => selectedOptions.add(v))
|
|
@@ -54,15 +54,13 @@
|
|
|
54
54
|
|
|
55
55
|
function toggleOption(optionValue: PrimitiveFieldValue) {
|
|
56
56
|
if (field?.multiple) {
|
|
57
|
-
// Multiple selection: toggle in set
|
|
58
57
|
if (selectedOptions.has(optionValue)) {
|
|
59
58
|
selectedOptions.delete(optionValue)
|
|
60
59
|
} else {
|
|
61
60
|
selectedOptions.add(optionValue)
|
|
62
61
|
}
|
|
63
|
-
selectedOptions = selectedOptions
|
|
62
|
+
selectedOptions = selectedOptions
|
|
64
63
|
} else {
|
|
65
|
-
// Single selection: replace set
|
|
66
64
|
selectedOptions = new Set([optionValue])
|
|
67
65
|
}
|
|
68
66
|
}
|
|
@@ -70,12 +68,10 @@
|
|
|
70
68
|
function parseValue(input: string): unknown {
|
|
71
69
|
input = input.trim()
|
|
72
70
|
|
|
73
|
-
// Handle empty input
|
|
74
71
|
if (input === '' || input === '-') {
|
|
75
72
|
return null
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
// Try to parse as JSON first for arrays and objects
|
|
79
75
|
if (input.startsWith('{') || input.startsWith('[')) {
|
|
80
76
|
try {
|
|
81
77
|
return JSON.parse(input)
|
|
@@ -84,7 +80,6 @@
|
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
// Handle specific types based on field type
|
|
88
83
|
if (field.type === 'boolean') {
|
|
89
84
|
if (input === 'true') return true
|
|
90
85
|
if (input === 'false') return false
|
|
@@ -99,7 +94,6 @@
|
|
|
99
94
|
return num
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
// Default to string
|
|
103
97
|
return input
|
|
104
98
|
}
|
|
105
99
|
|
|
@@ -108,7 +102,6 @@
|
|
|
108
102
|
let newValue: unknown
|
|
109
103
|
|
|
110
104
|
if (hasOptions) {
|
|
111
|
-
// For fields with options, use selected options
|
|
112
105
|
const values = Array.from(selectedOptions)
|
|
113
106
|
if (field?.multiple) {
|
|
114
107
|
newValue = values
|
|
@@ -116,14 +109,12 @@
|
|
|
116
109
|
newValue = values[0] ?? null
|
|
117
110
|
}
|
|
118
111
|
} else {
|
|
119
|
-
// For free-form fields, parse the text input
|
|
120
112
|
newValue = parseValue(editValue)
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
const result = await client.responses.commit({ [field.id]: newValue })
|
|
124
116
|
|
|
125
117
|
if (result.errors.length > 0) {
|
|
126
|
-
// Show validation error
|
|
127
118
|
editError = result.errors[0].userMessage
|
|
128
119
|
return
|
|
129
120
|
}
|
|
@@ -143,28 +134,25 @@
|
|
|
143
134
|
|
|
144
135
|
<tr>
|
|
145
136
|
<td>
|
|
146
|
-
<
|
|
137
|
+
<CodeBlock code={field.name} inline />
|
|
147
138
|
</td>
|
|
148
139
|
<td>
|
|
149
|
-
<
|
|
140
|
+
<Text size="small" weight="semibold">{field.label}</Text>
|
|
150
141
|
</td>
|
|
151
142
|
<td>
|
|
152
143
|
{#if isEditing}
|
|
153
|
-
<
|
|
144
|
+
<Stack direction="vertical" gap="small">
|
|
154
145
|
{#if hasOptions && field?.options}
|
|
155
|
-
|
|
156
|
-
<div style="display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem;">
|
|
146
|
+
<Stack direction="vertical" gap="small">
|
|
157
147
|
{#each field.options as option (option.id)}
|
|
158
|
-
<label
|
|
148
|
+
<label class="option-label">
|
|
159
149
|
{#if field.multiple}
|
|
160
|
-
<!-- Multiple selection: checkboxes -->
|
|
161
150
|
<input
|
|
162
151
|
type="checkbox"
|
|
163
152
|
checked={selectedOptions.has(option.value)}
|
|
164
153
|
on:change={() => toggleOption(option.value)}
|
|
165
154
|
/>
|
|
166
155
|
{:else}
|
|
167
|
-
<!-- Single selection: radio buttons -->
|
|
168
156
|
<input
|
|
169
157
|
type="radio"
|
|
170
158
|
name="field-{field.id}"
|
|
@@ -172,76 +160,50 @@
|
|
|
172
160
|
on:change={() => toggleOption(option.value)}
|
|
173
161
|
/>
|
|
174
162
|
{/if}
|
|
175
|
-
<
|
|
163
|
+
<Text size="caption" inline>
|
|
176
164
|
{option.label}
|
|
177
165
|
{#if option.description}
|
|
178
|
-
<
|
|
166
|
+
<Text size="caption" color="subdued" inline>({option.description})</Text>
|
|
179
167
|
{/if}
|
|
180
|
-
</
|
|
168
|
+
</Text>
|
|
181
169
|
</label>
|
|
182
170
|
{/each}
|
|
183
|
-
</
|
|
171
|
+
</Stack>
|
|
184
172
|
{:else}
|
|
185
|
-
<!-- Free-form text input for fields without options -->
|
|
186
173
|
<textarea
|
|
187
174
|
bind:value={editValue}
|
|
188
|
-
style="width: 100%; min-height: 60px; font-family: monospace; font-size: 12px; padding: 0.25rem;"
|
|
189
175
|
rows="3"
|
|
190
176
|
placeholder="Enter {field.type} value..."
|
|
191
177
|
></textarea>
|
|
192
178
|
{/if}
|
|
193
179
|
{#if editError}
|
|
194
|
-
<
|
|
195
|
-
{editError}
|
|
196
|
-
</div>
|
|
180
|
+
<Banner status="error">{editError}</Banner>
|
|
197
181
|
{/if}
|
|
198
|
-
</
|
|
182
|
+
</Stack>
|
|
199
183
|
{:else if response === null}
|
|
200
|
-
<
|
|
184
|
+
<Text size="caption" color="subdued">No value set</Text>
|
|
201
185
|
{:else}
|
|
202
|
-
<code
|
|
203
|
-
{formatValue(currentValue)}
|
|
204
|
-
</code>
|
|
186
|
+
<CodeBlock code={formatValue(currentValue)} />
|
|
205
187
|
{/if}
|
|
206
188
|
</td>
|
|
207
189
|
<td>
|
|
208
|
-
<
|
|
209
|
-
{field.type}{field.multiple ? '[]' : ''}
|
|
210
|
-
</span>
|
|
190
|
+
<CodeBlock code="{field.type}{field.multiple ? '[]' : ''}" inline />
|
|
211
191
|
</td>
|
|
212
192
|
<td>
|
|
213
193
|
{#if isEditing}
|
|
214
|
-
<
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
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>
|
|
194
|
+
<Stack direction="horizontal" gap="none" align="end">
|
|
195
|
+
<Button variant="primary" size="small" onclick={saveEdit}>Save</Button>
|
|
196
|
+
<Button variant="destructive" size="small" onclick={cancelEdit}>Cancel</Button>
|
|
197
|
+
</Stack>
|
|
228
198
|
{:else}
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
on:click={startEdit}
|
|
232
|
-
style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; border: none; border-radius: 2px;"
|
|
233
|
-
>
|
|
199
|
+
<Stack direction="horizontal" gap="none" align="end">
|
|
200
|
+
<Button variant="secondary" size="small" onclick={startEdit}>
|
|
234
201
|
{response === null ? 'Set' : 'Edit'}
|
|
235
|
-
</
|
|
202
|
+
</Button>
|
|
236
203
|
{#if response !== null}
|
|
237
|
-
<
|
|
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>
|
|
204
|
+
<Button variant="destructive" size="small" onclick={handleClear}>Clear</Button>
|
|
243
205
|
{/if}
|
|
244
|
-
</
|
|
206
|
+
</Stack>
|
|
245
207
|
{/if}
|
|
246
208
|
</td>
|
|
247
209
|
</tr>
|
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
import type { ResponseListItem, Field } from '@picked/client'
|
|
3
3
|
import { getPicked } from '../../../../hooks'
|
|
4
4
|
import ResponseTableRow from './ResponseTableRow.svelte'
|
|
5
|
+
import { DataTable, EmptyState, Button, RadioGroup, Stack } from '../../ui'
|
|
5
6
|
|
|
6
|
-
export let responses: ResponseListItem[]
|
|
7
|
+
export let responses: ResponseListItem[] = []
|
|
7
8
|
|
|
8
9
|
const client = getPicked()
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
let filter: FilterType = 'set'
|
|
11
|
+
let filter = 'set'
|
|
12
12
|
|
|
13
13
|
$: fieldsStore = client.fields.listStore
|
|
14
|
-
$: allFields = $fieldsStore
|
|
14
|
+
$: allFields = $fieldsStore ?? []
|
|
15
15
|
|
|
16
|
-
// Create a map of fieldId -> response for quick lookup
|
|
17
16
|
$: responseMap = new Map(responses.map((r) => [r.fieldId, r]))
|
|
18
17
|
|
|
19
|
-
// Merge fields with responses, showing all fields
|
|
20
18
|
$: allFieldRows = allFields.map((field) => {
|
|
21
19
|
const response = responseMap.get(field.id)
|
|
22
20
|
return {
|
|
@@ -25,7 +23,6 @@
|
|
|
25
23
|
}
|
|
26
24
|
})
|
|
27
25
|
|
|
28
|
-
// Apply filter
|
|
29
26
|
$: fieldRows = allFieldRows.filter((row) => {
|
|
30
27
|
if (filter === 'set') return row.response !== null
|
|
31
28
|
if (filter === 'unset') return row.response === null
|
|
@@ -35,64 +32,46 @@
|
|
|
35
32
|
function handleClearAll() {
|
|
36
33
|
client.responses.clearAll()
|
|
37
34
|
}
|
|
35
|
+
|
|
36
|
+
const columns = [
|
|
37
|
+
{ key: 'id', header: 'ID' },
|
|
38
|
+
{ key: 'field', header: 'Field' },
|
|
39
|
+
{ key: 'value', header: 'Value' },
|
|
40
|
+
{ key: 'type', header: 'Type' },
|
|
41
|
+
{ key: 'actions', header: 'Actions' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const filterOptions = [
|
|
45
|
+
{ value: 'all', label: 'All' },
|
|
46
|
+
{ value: 'set', label: 'Set (0)' },
|
|
47
|
+
{ value: 'unset', label: 'Unset (0)' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
$: filterOptions[1] = { value: 'set', label: `Set (${responses.length})` }
|
|
51
|
+
$: filterOptions[2] = { value: 'unset', label: `Unset (${allFields.length - responses.length})` }
|
|
38
52
|
</script>
|
|
39
53
|
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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>
|
|
54
|
+
<Stack direction="vertical" gap="none">
|
|
55
|
+
<Stack direction="horizontal" gap="small" align="space-between" justify="center" padding="small">
|
|
56
|
+
<RadioGroup
|
|
57
|
+
label="Show"
|
|
58
|
+
name="filter"
|
|
59
|
+
options={filterOptions}
|
|
60
|
+
value={filter}
|
|
61
|
+
onchange={(val) => { filter = val }}
|
|
62
|
+
orientation="horizontal"
|
|
63
|
+
/>
|
|
89
64
|
{#if responses.length > 0}
|
|
90
|
-
<
|
|
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>
|
|
65
|
+
<Button variant="destructive" size="small" onclick={handleClearAll}>Clear All</Button>
|
|
96
66
|
{/if}
|
|
97
|
-
</
|
|
98
|
-
|
|
67
|
+
</Stack>
|
|
68
|
+
<DataTable columns={columns} rows={fieldRows}>
|
|
69
|
+
{#snippet row(item)}
|
|
70
|
+
{@const rowData = item}
|
|
71
|
+
<ResponseTableRow field={rowData.field} response={rowData.response} />
|
|
72
|
+
{/snippet}
|
|
73
|
+
{#snippet empty()}
|
|
74
|
+
<EmptyState message="No fields registered yet" />
|
|
75
|
+
{/snippet}
|
|
76
|
+
</DataTable>
|
|
77
|
+
</Stack>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/actions/button.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { Snippet } from 'svelte'
|
|
9
|
+
import Spinner from '../feedback/Spinner.svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
children: Snippet
|
|
13
|
+
icon?: Snippet
|
|
14
|
+
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost'
|
|
15
|
+
size?: 'small' | 'medium' | 'large'
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
loading?: boolean
|
|
18
|
+
type?: 'button' | 'submit' | 'reset'
|
|
19
|
+
fullWidth?: boolean
|
|
20
|
+
href?: string
|
|
21
|
+
onclick?: (event: MouseEvent) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
children,
|
|
26
|
+
icon,
|
|
27
|
+
variant = 'primary',
|
|
28
|
+
size = 'medium',
|
|
29
|
+
disabled = false,
|
|
30
|
+
loading = false,
|
|
31
|
+
type = 'button',
|
|
32
|
+
fullWidth = false,
|
|
33
|
+
href,
|
|
34
|
+
onclick,
|
|
35
|
+
}: Props = $props()
|
|
36
|
+
|
|
37
|
+
const isDisabled = $derived(disabled || loading)
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if href}
|
|
41
|
+
<a
|
|
42
|
+
class="button variant-{variant} size-{size}"
|
|
43
|
+
class:full-width={fullWidth}
|
|
44
|
+
class:is-disabled={isDisabled}
|
|
45
|
+
{href}
|
|
46
|
+
aria-disabled={isDisabled || undefined}
|
|
47
|
+
tabindex={isDisabled ? -1 : undefined}
|
|
48
|
+
onclick={isDisabled
|
|
49
|
+
? (e: MouseEvent) => e.preventDefault()
|
|
50
|
+
: onclick}
|
|
51
|
+
>
|
|
52
|
+
{#if loading}
|
|
53
|
+
<Spinner size="small" color="current" label="Loading" />
|
|
54
|
+
{:else if icon}
|
|
55
|
+
{@render icon()}
|
|
56
|
+
{/if}
|
|
57
|
+
{@render children()}
|
|
58
|
+
</a>
|
|
59
|
+
{:else}
|
|
60
|
+
<button
|
|
61
|
+
class="button variant-{variant} size-{size}"
|
|
62
|
+
class:full-width={fullWidth}
|
|
63
|
+
{type}
|
|
64
|
+
disabled={isDisabled}
|
|
65
|
+
{onclick}
|
|
66
|
+
>
|
|
67
|
+
{#if loading}
|
|
68
|
+
<Spinner size="small" color="current" label="Loading" />
|
|
69
|
+
{:else if icon}
|
|
70
|
+
{@render icon()}
|
|
71
|
+
{/if}
|
|
72
|
+
{@render children()}
|
|
73
|
+
</button>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.button {
|
|
78
|
+
position: relative;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
gap: var(--space-2);
|
|
84
|
+
border-radius: var(--radius-md);
|
|
85
|
+
font-weight: var(--font-medium);
|
|
86
|
+
font-family: inherit;
|
|
87
|
+
line-height: 1.5;
|
|
88
|
+
border: none;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.button:focus-visible {
|
|
94
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
95
|
+
outline-offset: var(--ring-offset);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Variants */
|
|
99
|
+
.variant-primary {
|
|
100
|
+
background-color: var(--color-blue-600);
|
|
101
|
+
color: var(--color-white);
|
|
102
|
+
}
|
|
103
|
+
.variant-primary:hover:not(:disabled):not(.is-disabled) {
|
|
104
|
+
background-color: var(--color-blue-700);
|
|
105
|
+
}
|
|
106
|
+
.variant-primary:active:not(:disabled):not(.is-disabled) {
|
|
107
|
+
background-color: var(--color-blue-800);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.variant-secondary {
|
|
111
|
+
background-color: var(--color-gray-100);
|
|
112
|
+
color: var(--color-gray-900);
|
|
113
|
+
}
|
|
114
|
+
.variant-secondary:hover:not(:disabled):not(.is-disabled) {
|
|
115
|
+
background-color: var(--color-gray-200);
|
|
116
|
+
}
|
|
117
|
+
.variant-secondary:active:not(:disabled):not(.is-disabled) {
|
|
118
|
+
background-color: var(--color-gray-300);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.variant-destructive {
|
|
122
|
+
background-color: var(--color-red-600);
|
|
123
|
+
color: var(--color-white);
|
|
124
|
+
}
|
|
125
|
+
.variant-destructive:hover:not(:disabled):not(.is-disabled) {
|
|
126
|
+
background-color: var(--color-red-700);
|
|
127
|
+
}
|
|
128
|
+
.variant-destructive:active:not(:disabled):not(.is-disabled) {
|
|
129
|
+
background-color: var(--color-red-800);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.variant-ghost {
|
|
133
|
+
background-color: transparent;
|
|
134
|
+
color: var(--color-gray-700);
|
|
135
|
+
}
|
|
136
|
+
.variant-ghost:hover:not(:disabled):not(.is-disabled) {
|
|
137
|
+
background-color: var(--color-gray-100);
|
|
138
|
+
}
|
|
139
|
+
.variant-ghost:active:not(:disabled):not(.is-disabled) {
|
|
140
|
+
background-color: var(--color-gray-200);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Sizes */
|
|
144
|
+
.size-small {
|
|
145
|
+
padding: var(--space-1) var(--space-2-5);
|
|
146
|
+
font-size: var(--text-sm);
|
|
147
|
+
}
|
|
148
|
+
.size-medium {
|
|
149
|
+
padding: var(--space-1-5) var(--space-3);
|
|
150
|
+
font-size: var(--text-sm);
|
|
151
|
+
}
|
|
152
|
+
.size-large {
|
|
153
|
+
padding: var(--space-2) var(--space-5);
|
|
154
|
+
font-size: var(--text-base);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* States */
|
|
158
|
+
.button:disabled,
|
|
159
|
+
.is-disabled {
|
|
160
|
+
cursor: not-allowed;
|
|
161
|
+
opacity: 0.5;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
a.is-disabled {
|
|
165
|
+
pointer-events: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.full-width {
|
|
169
|
+
width: 100%;
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/actions/copy-button.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
text: string
|
|
10
|
+
label?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { text, label = 'Copy to clipboard' }: Props = $props()
|
|
14
|
+
|
|
15
|
+
let copied = $state(false)
|
|
16
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
17
|
+
|
|
18
|
+
async function handleCopy(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
await navigator.clipboard.writeText(text)
|
|
21
|
+
copied = true
|
|
22
|
+
clearTimeout(timeoutId)
|
|
23
|
+
timeoutId = setTimeout(() => {
|
|
24
|
+
copied = false
|
|
25
|
+
}, 1500)
|
|
26
|
+
} catch {
|
|
27
|
+
// Clipboard API may not be available
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<button
|
|
33
|
+
class="copy-button"
|
|
34
|
+
onclick={handleCopy}
|
|
35
|
+
aria-label={label}
|
|
36
|
+
>
|
|
37
|
+
{#if copied}
|
|
38
|
+
<svg class="icon icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
39
|
+
<polyline points="20 6 9 17 4 12" />
|
|
40
|
+
</svg>
|
|
41
|
+
<span class="copied-text" aria-live="polite">Copied!</span>
|
|
42
|
+
{:else}
|
|
43
|
+
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
44
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
45
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
46
|
+
</svg>
|
|
47
|
+
{/if}
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.copy-button {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--space-1);
|
|
55
|
+
border-radius: var(--radius-sm);
|
|
56
|
+
padding: var(--space-1) var(--space-2);
|
|
57
|
+
font-size: var(--text-sm);
|
|
58
|
+
font-family: inherit;
|
|
59
|
+
color: var(--color-gray-600);
|
|
60
|
+
background: none;
|
|
61
|
+
border: none;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.copy-button:hover {
|
|
67
|
+
background-color: var(--color-gray-100);
|
|
68
|
+
color: var(--color-gray-900);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.copy-button:focus-visible {
|
|
72
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
73
|
+
outline-offset: var(--ring-offset);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.icon {
|
|
77
|
+
width: 1rem;
|
|
78
|
+
height: 1rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.icon-check {
|
|
82
|
+
color: var(--color-green-700);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.copied-text {
|
|
86
|
+
color: var(--color-green-700);
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/data/badge.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { Snippet } from 'svelte'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
children: Snippet
|
|
12
|
+
status?: 'default' | 'success' | 'warning' | 'error' | 'info'
|
|
13
|
+
size?: 'small' | 'medium'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { children, status = 'default', size = 'medium' }: Props = $props()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span class="badge status-{status} size-{size}">
|
|
20
|
+
{@render children()}
|
|
21
|
+
</span>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.badge {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
border-radius: var(--radius-full);
|
|
28
|
+
font-weight: var(--font-medium);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Sizes */
|
|
32
|
+
.size-small {
|
|
33
|
+
padding: 1px var(--space-1-5);
|
|
34
|
+
font-size: 10px;
|
|
35
|
+
}
|
|
36
|
+
.size-medium {
|
|
37
|
+
padding: var(--space-0-5) var(--space-2);
|
|
38
|
+
font-size: var(--text-xs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Status colors */
|
|
42
|
+
.status-default {
|
|
43
|
+
background-color: var(--color-gray-100);
|
|
44
|
+
color: var(--color-gray-700);
|
|
45
|
+
}
|
|
46
|
+
.status-success {
|
|
47
|
+
background-color: var(--color-green-100);
|
|
48
|
+
color: var(--color-green-800);
|
|
49
|
+
}
|
|
50
|
+
.status-warning {
|
|
51
|
+
background-color: var(--color-yellow-100);
|
|
52
|
+
color: var(--color-yellow-800);
|
|
53
|
+
}
|
|
54
|
+
.status-error {
|
|
55
|
+
background-color: #fee2e2;
|
|
56
|
+
color: var(--color-red-800, #991b1b);
|
|
57
|
+
}
|
|
58
|
+
.status-info {
|
|
59
|
+
background-color: #dbeafe;
|
|
60
|
+
color: var(--color-blue-800, #1e40af);
|
|
61
|
+
}
|
|
62
|
+
</style>
|