@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.
Files changed (44) hide show
  1. package/dist/plugins/devtools/DevToolsExperience.svelte +61 -44
  2. package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +2 -1
  3. package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +22 -20
  4. package/dist/plugins/devtools/components/Tabs.svelte +20 -7
  5. package/dist/plugins/devtools/experience.js +1 -1
  6. package/dist/plugins/devtools/screens/components/ComponentsListScreen.svelte +18 -18
  7. package/dist/plugins/devtools/screens/events/EventListItem.svelte +28 -42
  8. package/dist/plugins/devtools/screens/events/EventsListScreen.svelte +10 -11
  9. package/dist/plugins/devtools/screens/events/EventsTable.svelte +31 -33
  10. package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +13 -16
  11. package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +20 -71
  12. package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +96 -232
  13. package/dist/plugins/devtools/screens/frameworks/FrameworksListScreen.svelte +14 -16
  14. package/dist/plugins/devtools/screens/history/HistoryEntriesTable.svelte +18 -16
  15. package/dist/plugins/devtools/screens/predicates/PredicatesListScreen.svelte +14 -16
  16. package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +25 -63
  17. package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +43 -64
  18. package/dist/plugins/devtools/ui/actions/Button.svelte +171 -0
  19. package/dist/plugins/devtools/ui/actions/CopyButton.svelte +88 -0
  20. package/dist/plugins/devtools/ui/data/Badge.svelte +62 -0
  21. package/dist/plugins/devtools/ui/data/CodeBlock.svelte +38 -0
  22. package/dist/plugins/devtools/ui/data/DataTable.svelte +134 -0
  23. package/dist/plugins/devtools/ui/data/EmptyState.svelte +26 -0
  24. package/dist/plugins/devtools/ui/data/ExpandableRow.svelte +66 -0
  25. package/dist/plugins/devtools/ui/data/VisualizationContainer.svelte +55 -0
  26. package/dist/plugins/devtools/ui/feedback/Banner.svelte +116 -0
  27. package/dist/plugins/devtools/ui/feedback/Spinner.svelte +79 -0
  28. package/dist/plugins/devtools/ui/forms/Checkbox.svelte +139 -0
  29. package/dist/plugins/devtools/ui/forms/RadioGroup.svelte +218 -0
  30. package/dist/plugins/devtools/ui/forms/Select.svelte +137 -0
  31. package/dist/plugins/devtools/ui/forms/Switch.svelte +158 -0
  32. package/dist/plugins/devtools/ui/forms/TextField.svelte +125 -0
  33. package/dist/plugins/devtools/ui/index.d.ts +22 -0
  34. package/dist/plugins/devtools/ui/index.js +1 -0
  35. package/dist/plugins/devtools/ui/layout/Card.svelte +55 -0
  36. package/dist/plugins/devtools/ui/layout/Divider.svelte +34 -0
  37. package/dist/plugins/devtools/ui/layout/Spacer.svelte +11 -0
  38. package/dist/plugins/devtools/ui/layout/Stack.svelte +129 -0
  39. package/dist/plugins/devtools/ui/overlays/Tooltip.svelte +90 -0
  40. package/dist/plugins/devtools/ui/tokens.css +144 -0
  41. package/dist/plugins/devtools/ui/tokens.css.js +1 -0
  42. package/dist/plugins/devtools/ui/typography/Heading.svelte +55 -0
  43. package/dist/plugins/devtools/ui/typography/Text.svelte +93 -0
  44. 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 // Trigger reactivity
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
- <span style="font-size: 11px; color: #666; font-family: monospace;">{field.name}</span>
137
+ <CodeBlock code={field.name} inline />
147
138
  </td>
148
139
  <td>
149
- <strong style="font-size: 13px;">{field.label}</strong>
140
+ <Text size="small" weight="semibold">{field.label}</Text>
150
141
  </td>
151
142
  <td>
152
143
  {#if isEditing}
153
- <div>
144
+ <Stack direction="vertical" gap="small">
154
145
  {#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;">
146
+ <Stack direction="vertical" gap="small">
157
147
  {#each field.options as option (option.id)}
158
- <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
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
- <span style="font-size: 12px;">
163
+ <Text size="caption" inline>
176
164
  {option.label}
177
165
  {#if option.description}
178
- <span style="color: #666; font-size: 11px;">({option.description})</span>
166
+ <Text size="caption" color="subdued" inline>({option.description})</Text>
179
167
  {/if}
180
- </span>
168
+ </Text>
181
169
  </label>
182
170
  {/each}
183
- </div>
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
- <div style="color: red; font-size: 11px; margin-top: 0.25rem;">
195
- {editError}
196
- </div>
180
+ <Banner status="error">{editError}</Banner>
197
181
  {/if}
198
- </div>
182
+ </Stack>
199
183
  {:else if response === null}
200
- <span style="color: #999; font-style: italic; font-size: 12px;"> No value set </span>
184
+ <Text size="caption" color="subdued">No value set</Text>
201
185
  {:else}
202
- <code style="white-space: pre-wrap; word-break: break-all;">
203
- {formatValue(currentValue)}
204
- </code>
186
+ <CodeBlock code={formatValue(currentValue)} />
205
187
  {/if}
206
188
  </td>
207
189
  <td>
208
- <span style="font-family: monospace; color: #666; font-size: 12px;">
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
- <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>
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
- <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
- >
199
+ <Stack direction="horizontal" gap="none" align="end">
200
+ <Button variant="secondary" size="small" onclick={startEdit}>
234
201
  {response === null ? 'Set' : 'Edit'}
235
- </button>
202
+ </Button>
236
203
  {#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>
204
+ <Button variant="destructive" size="small" onclick={handleClear}>Clear</Button>
243
205
  {/if}
244
- </div>
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
- type FilterType = 'all' | 'set' | 'unset'
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
- <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>
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
- <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>
65
+ <Button variant="destructive" size="small" onclick={handleClearAll}>Clear All</Button>
96
66
  {/if}
97
- </div>
98
- </div>
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>