@rokkit/ui 1.0.0-next.99 → 1.0.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.
Files changed (81) hide show
  1. package/README.md +159 -1
  2. package/package.json +52 -53
  3. package/src/MarkdownRenderer.svelte +106 -0
  4. package/src/components/AlertList.svelte +59 -0
  5. package/src/components/Avatar.svelte +68 -0
  6. package/src/components/Badge.svelte +57 -0
  7. package/src/components/BreadCrumbs.svelte +94 -0
  8. package/src/components/Button.svelte +91 -0
  9. package/src/components/ButtonGroup.svelte +9 -0
  10. package/src/components/Card.svelte +71 -0
  11. package/src/components/Carousel.svelte +169 -0
  12. package/src/components/Code.svelte +189 -0
  13. package/src/components/Connector.svelte +46 -0
  14. package/src/components/Divider.svelte +24 -0
  15. package/src/components/Dropdown.svelte +202 -0
  16. package/src/components/FloatingAction.svelte +367 -0
  17. package/src/components/FloatingNavigation.svelte +256 -0
  18. package/src/components/Grid.svelte +128 -0
  19. package/src/components/ItemContent.svelte +25 -0
  20. package/src/components/LazyTree.svelte +171 -0
  21. package/src/components/List.svelte +224 -0
  22. package/src/components/Menu.svelte +270 -0
  23. package/src/components/Message.svelte +75 -0
  24. package/src/components/MultiSelect.svelte +367 -0
  25. package/src/components/PaletteManager.svelte +360 -0
  26. package/src/components/Pill.svelte +83 -0
  27. package/src/components/ProgressBar.svelte +31 -0
  28. package/src/components/Range.svelte +346 -0
  29. package/src/components/Rating.svelte +115 -0
  30. package/src/components/Reveal.svelte +46 -0
  31. package/src/components/SearchFilter.svelte +88 -0
  32. package/src/components/Select.svelte +424 -0
  33. package/src/components/Shine.svelte +87 -0
  34. package/src/components/Stack.svelte +38 -0
  35. package/src/components/StatusList.svelte +18 -0
  36. package/src/components/Stepper.svelte +174 -0
  37. package/src/components/Swatch.svelte +102 -0
  38. package/src/components/Switch.svelte +71 -0
  39. package/src/components/Table.svelte +245 -0
  40. package/src/components/Tabs.svelte +202 -0
  41. package/src/components/Tilt.svelte +68 -0
  42. package/src/components/Timeline.svelte +61 -0
  43. package/src/components/Toggle.svelte +93 -0
  44. package/src/components/Toolbar.svelte +306 -0
  45. package/src/components/ToolbarGroup.svelte +17 -0
  46. package/src/components/Tooltip.svelte +121 -0
  47. package/src/components/Tree.svelte +143 -0
  48. package/src/components/UploadFileStatus.svelte +102 -0
  49. package/src/components/UploadProgress.svelte +127 -0
  50. package/src/components/UploadTarget.svelte +126 -0
  51. package/src/components/index.ts +48 -0
  52. package/src/index.ts +59 -0
  53. package/src/markdown-plugin.ts +12 -0
  54. package/src/types/button.ts +86 -0
  55. package/src/types/code.ts +46 -0
  56. package/src/types/floating-action.ts +123 -0
  57. package/src/types/floating-navigation.ts +80 -0
  58. package/src/types/index.ts +58 -0
  59. package/src/types/list.ts +200 -0
  60. package/src/types/menu.ts +95 -0
  61. package/src/types/palette.ts +160 -0
  62. package/src/types/range.ts +51 -0
  63. package/src/types/search-filter.ts +67 -0
  64. package/src/types/select.ts +176 -0
  65. package/src/types/switch.ts +68 -0
  66. package/src/types/table.ts +219 -0
  67. package/src/types/tabs.ts +103 -0
  68. package/src/types/timeline.ts +53 -0
  69. package/src/types/toggle.ts +68 -0
  70. package/src/types/toolbar.ts +167 -0
  71. package/src/types/tooltip.ts +21 -0
  72. package/src/types/tree.ts +250 -0
  73. package/src/types/upload-file-status.ts +45 -0
  74. package/src/types/upload-progress.ts +111 -0
  75. package/src/types/upload-target.ts +71 -0
  76. package/src/utils/palette.ts +618 -0
  77. package/src/utils/shiki.ts +114 -0
  78. package/src/utils/upload.js +145 -0
  79. package/LICENSE +0 -21
  80. package/src/index.js +0 -8
  81. package/src/wrappers.js +0 -2
@@ -0,0 +1,424 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Select — Trigger + dropdown with List-style flatView content.
4
+ *
5
+ * Architecture:
6
+ * Trigger — manages open/close (click, Enter, Escape, click-outside)
7
+ * Wrapper — owns focusedKey $state + flatView $derived
8
+ * Navigator — attaches DOM event handlers on dropdown
9
+ * flatView loop — single flat {#each}, groups rendered as non-interactive labels
10
+ *
11
+ * Groups are pre-processed with expanded:true (always show children) and
12
+ * disabled:true (excluded from keyboard navigation). Group labels have no
13
+ * data-path so Navigator ignores them entirely.
14
+ *
15
+ * Data attributes:
16
+ * data-select — root container
17
+ * data-select-trigger — trigger button
18
+ * data-select-value — selected value display area
19
+ * data-select-value-text — selected item text
20
+ * data-select-value-icon — selected item icon
21
+ * data-select-placeholder — placeholder text
22
+ * data-select-arrow — dropdown arrow icon
23
+ * data-select-dropdown — dropdown container
24
+ * data-select-filter — filter input wrapper
25
+ * data-select-filter-input — filter text input
26
+ * data-select-option — leaf option items
27
+ * data-select-group-label — group header label (non-interactive)
28
+ * data-select-group-icon — icon inside group label
29
+ * data-select-divider — divider between groups
30
+ * data-select-check — check icon on selected item
31
+ * data-select-empty — no results message
32
+ * data-path — required by Navigator
33
+ * data-selected — selected state
34
+ * data-disabled — disabled state
35
+ * data-open — dropdown is open
36
+ * data-size — size variant
37
+ * data-align — dropdown alignment
38
+ * data-direction — dropdown direction
39
+ */
40
+ // @ts-nocheck
41
+ import type { ProxyItem } from '@rokkit/states'
42
+ import { Wrapper, ProxyTree, messages } from '@rokkit/states'
43
+ import { SvelteSet } from 'svelte/reactivity'
44
+ import { Navigator, Trigger } from '@rokkit/actions'
45
+ import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
46
+ import ItemContent from './ItemContent.svelte'
47
+
48
+ interface SelectIcons {
49
+ opened?: string
50
+ closed?: string
51
+ checked?: string
52
+ }
53
+
54
+ let {
55
+ items = [],
56
+ fields = {},
57
+ value = $bindable(),
58
+ selected = $bindable<unknown>(null),
59
+ placeholder = messages.select,
60
+ size = 'md',
61
+ disabled = false,
62
+ filterable = false,
63
+ filterPlaceholder = 'Search...',
64
+ align = 'start',
65
+ direction = 'down',
66
+ icons: userIcons = {} as SelectIcons,
67
+ onchange,
68
+ class: className = '',
69
+ ...snippets
70
+ }: {
71
+ items?: unknown[]
72
+ fields?: Record<string, string>
73
+ value?: unknown
74
+ selected?: unknown
75
+ placeholder?: string
76
+ size?: string
77
+ disabled?: boolean
78
+ filterable?: boolean
79
+ filterPlaceholder?: string
80
+ align?: 'start' | 'end'
81
+ direction?: 'up' | 'down'
82
+ icons?: SelectIcons
83
+ onchange?: (value: unknown, item: unknown) => void
84
+ class?: string
85
+ [key: string]: unknown
86
+ } = $props()
87
+
88
+ const icons = $derived({
89
+ ...DEFAULT_STATE_ICONS.selector,
90
+ ...DEFAULT_STATE_ICONS.checkbox,
91
+ ...userIcons
92
+ })
93
+
94
+ // ─── Dropdown state ───────────────────────────────────────────────────────
95
+
96
+ let isOpen = $state(false)
97
+ let selectRef = $state<HTMLElement | null>(null)
98
+ let triggerRef = $state<HTMLElement | null>(null)
99
+ let dropdownRef = $state<HTMLElement | null>(null)
100
+
101
+ // ─── Filter ───────────────────────────────────────────────────────────────
102
+
103
+ let filterQuery = $state('')
104
+ let filterInputRef = $state<HTMLInputElement | null>(null)
105
+
106
+ const textField = $derived(fields?.label || 'label')
107
+ const childrenField = $derived(fields?.children || 'children')
108
+
109
+ function childMatchesQuery(child: unknown, query: string): boolean {
110
+ return String((child as Record<string, unknown>)[textField] ?? '')
111
+ .toLowerCase()
112
+ .includes(query)
113
+ }
114
+
115
+ function filterGroupChildren(asRecord: Record<string, unknown>, query: string): unknown | null {
116
+ const children = asRecord[childrenField] as unknown[]
117
+ const matching = children.filter((child: unknown) => childMatchesQuery(child, query))
118
+ return matching.length > 0 ? { ...asRecord, [childrenField]: matching } : null
119
+ }
120
+
121
+ function filterItem(item: unknown, query: string): unknown | null {
122
+ const asRecord = item as Record<string, unknown>
123
+ const children = asRecord[childrenField]
124
+ if (Array.isArray(children) && children.length > 0) {
125
+ return filterGroupChildren(asRecord, query)
126
+ }
127
+ const text = String(asRecord[textField] ?? '').toLowerCase()
128
+ return text.includes(query) ? item : null
129
+ }
130
+
131
+ const filteredItems = $derived.by(() => {
132
+ if (!filterable || !filterQuery) return items
133
+ const query = filterQuery.toLowerCase()
134
+ return items.map((item) => filterItem(item, query)).filter(Boolean)
135
+ })
136
+
137
+ // Pre-process: force groups expanded + disabled (non-navigable labels)
138
+ const processedItems = $derived(
139
+ filteredItems.map((item) => {
140
+ const children = item[childrenField]
141
+ if (Array.isArray(children) && children.length > 0) {
142
+ return { ...item, expanded: true, disabled: true }
143
+ }
144
+ return item
145
+ })
146
+ )
147
+
148
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
149
+
150
+ function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
151
+ if (proxy.disabled) return
152
+ value = extractedValue
153
+ selected = proxy.original
154
+ onchange?.(extractedValue, proxy.original)
155
+ isOpen = false
156
+ filterQuery = ''
157
+ triggerRef?.focus()
158
+ }
159
+
160
+ const proxyTree = $derived(new ProxyTree(processedItems, fields))
161
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
162
+
163
+ // Override cancel/blur to close dropdown
164
+ $effect(() => {
165
+ const w = wrapper
166
+ w.cancel = () => {
167
+ isOpen = false
168
+ filterQuery = ''
169
+ triggerRef?.focus()
170
+ }
171
+ w.blur = () => {
172
+ isOpen = false
173
+ filterQuery = ''
174
+ }
175
+ })
176
+
177
+ // When wrapper recreates while open, focus first item
178
+ $effect(() => {
179
+ const _w = wrapper
180
+ if (isOpen && !filterable) _w.first(null)
181
+ })
182
+
183
+ // ─── Selected proxy for trigger display ───────────────────────────────────
184
+
185
+ function isValueUnset(): boolean {
186
+ return value === undefined || value === null
187
+ }
188
+
189
+ function isMatchingLeaf(proxy: ProxyItem): boolean {
190
+ return !proxy.hasChildren && proxy.value === value
191
+ }
192
+
193
+ function findSelectedProxy(): ProxyItem | null {
194
+ if (isValueUnset()) return null
195
+ for (const [, proxy] of wrapper.lookup) {
196
+ if (isMatchingLeaf(proxy)) return proxy
197
+ }
198
+ return null
199
+ }
200
+
201
+ const selectedProxy = $derived.by(findSelectedProxy)
202
+
203
+ // Sync selected raw item
204
+ $effect(() => {
205
+ selected = selectedProxy?.original ?? null
206
+ })
207
+
208
+ // ─── Trigger action ───────────────────────────────────────────────────────
209
+
210
+ $effect(() => {
211
+ if (!triggerRef || !selectRef || disabled) return
212
+ const t = new Trigger(triggerRef, selectRef, {
213
+ isOpen: () => isOpen,
214
+ onopen: () => {
215
+ isOpen = true
216
+ requestAnimationFrame(() => {
217
+ if (filterable) {
218
+ filterInputRef?.focus()
219
+ } else {
220
+ focusSelectedOrFirst()
221
+ }
222
+ })
223
+ },
224
+ onclose: () => {
225
+ isOpen = false
226
+ filterQuery = ''
227
+ },
228
+ onlast: () => requestAnimationFrame(() => wrapper.last(null))
229
+ })
230
+ return () => t.destroy()
231
+ })
232
+
233
+ function moveToSelectedValue(): boolean {
234
+ for (const node of wrapper.flatView) {
235
+ if (!node.proxy.disabled && node.proxy.value === value) {
236
+ wrapper.moveTo(node.key)
237
+ return true
238
+ }
239
+ }
240
+ return false
241
+ }
242
+
243
+ function focusSelectedOrFirst() {
244
+ if (value !== undefined && value !== null && moveToSelectedValue()) return
245
+ wrapper.first(null)
246
+ }
247
+
248
+ // ─── Navigator on dropdown ────────────────────────────────────────────────
249
+
250
+ $effect(() => {
251
+ if (!isOpen || !dropdownRef) return
252
+ const dir = getComputedStyle(dropdownRef).direction || 'ltr'
253
+ const nav = new Navigator(dropdownRef, wrapper, { dir, containScroll: true })
254
+ return () => nav.destroy()
255
+ })
256
+
257
+ // DOM focus sync
258
+ $effect(() => {
259
+ const key = wrapper.focusedKey
260
+ if (!isOpen || !dropdownRef || !key) return
261
+ requestAnimationFrame(() => {
262
+ const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
263
+ if (target && target !== document.activeElement) {
264
+ target.focus()
265
+ target.scrollIntoView?.({ block: 'nearest' })
266
+ }
267
+ })
268
+ })
269
+
270
+ // ─── Filter keyboard (native listener, fires before Navigator) ───────────
271
+
272
+ function selectFocused() {
273
+ if (wrapper.focusedKey) wrapper.select(null)
274
+ }
275
+
276
+ function handleFilterKeyDown(event: KeyboardEvent) {
277
+ if (event.key === 'ArrowDown') {
278
+ event.preventDefault()
279
+ event.stopPropagation()
280
+ wrapper.first(null)
281
+ } else if (event.key === 'Escape' && filterQuery) {
282
+ event.preventDefault()
283
+ event.stopPropagation()
284
+ filterQuery = ''
285
+ } else if (event.key === 'Enter') {
286
+ event.preventDefault()
287
+ event.stopPropagation()
288
+ selectFocused()
289
+ }
290
+ }
291
+
292
+ $effect(() => {
293
+ if (!isOpen || !filterable || !filterInputRef) return
294
+ const el = filterInputRef
295
+ el.addEventListener('keydown', handleFilterKeyDown)
296
+ return () => el.removeEventListener('keydown', handleFilterKeyDown)
297
+ })
298
+
299
+ // ─── Helpers ──────────────────────────────────────────────────────────────
300
+
301
+ /** Set of group keys that need a divider before them (not the first group) */
302
+ const groupDividers = $derived.by(() => {
303
+ const set = new SvelteSet<string>()
304
+ let foundFirst = false
305
+ for (const node of wrapper.flatView) {
306
+ if (node.hasChildren) {
307
+ if (foundFirst) set.add(node.key)
308
+ foundFirst = true
309
+ }
310
+ }
311
+ return set
312
+ })
313
+ </script>
314
+
315
+ {#snippet defaultOptionContent(proxy: ProxyItem)}
316
+ <ItemContent {proxy} />
317
+ {/snippet}
318
+
319
+ {#snippet defaultGroupContent(proxy: ProxyItem)}
320
+ {#if proxy.get('icon')}
321
+ <span data-select-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
322
+ {/if}
323
+ <span>{proxy.label}</span>
324
+ {/snippet}
325
+
326
+ <div
327
+ bind:this={selectRef}
328
+ data-select
329
+ data-open={isOpen || undefined}
330
+ data-size={size}
331
+ data-disabled={disabled || undefined}
332
+ data-align={align}
333
+ data-direction={direction}
334
+ class={className || undefined}
335
+ >
336
+ <button
337
+ bind:this={triggerRef}
338
+ type="button"
339
+ data-select-trigger
340
+ {disabled}
341
+ aria-haspopup="listbox"
342
+ aria-expanded={isOpen}
343
+ >
344
+ <span data-select-value>
345
+ {#if selectedProxy}
346
+ {#if selectedProxy.get('icon')}
347
+ <span data-select-value-icon class={selectedProxy.get('icon')} aria-hidden="true"></span>
348
+ {/if}
349
+ <span data-select-value-text>{selectedProxy.label}</span>
350
+ {:else}
351
+ <span data-select-placeholder>{placeholder}</span>
352
+ {/if}
353
+ </span>
354
+ <span data-select-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"></span>
355
+ </button>
356
+
357
+ {#if isOpen}
358
+ <div bind:this={dropdownRef} data-select-dropdown role="listbox" aria-orientation="vertical">
359
+ {#if filterable}
360
+ <div data-select-filter>
361
+ <input
362
+ bind:this={filterInputRef}
363
+ type="text"
364
+ data-select-filter-input
365
+ placeholder={filterPlaceholder}
366
+ bind:value={filterQuery}
367
+ />
368
+ </div>
369
+ {/if}
370
+
371
+ {#each wrapper.flatView as node (node.key)}
372
+ {@const proxy = node.proxy}
373
+ {@const sel = !node.hasChildren && proxy.value === value}
374
+ {@const content = resolveSnippet(
375
+ snippets as Record<string, unknown>,
376
+ proxy,
377
+ node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET
378
+ )}
379
+
380
+ {#if node.type === 'separator'}
381
+ <hr data-select-separator />
382
+ {:else if node.hasChildren}
383
+ {#if groupDividers.has(node.key)}
384
+ <div data-select-divider></div>
385
+ {/if}
386
+ <div data-select-group-label role="presentation">
387
+ {#if content}
388
+ {@render content(proxy)}
389
+ {:else}
390
+ {@render defaultGroupContent(proxy)}
391
+ {/if}
392
+ </div>
393
+ {:else}
394
+ <button
395
+ type="button"
396
+ title={proxy.get('tooltip')}
397
+ data-select-option
398
+ data-path={node.key}
399
+ data-level={node.level}
400
+ data-selected={sel || undefined}
401
+ data-disabled={proxy.disabled || undefined}
402
+ role="option"
403
+ aria-selected={sel}
404
+ disabled={proxy.disabled || disabled}
405
+ tabindex="-1"
406
+ >
407
+ {#if content}
408
+ {@render content(proxy)}
409
+ {:else}
410
+ {@render defaultOptionContent(proxy)}
411
+ {/if}
412
+ {#if sel}
413
+ <span data-select-check class={icons.checked} aria-hidden="true"></span>
414
+ {/if}
415
+ </button>
416
+ {/if}
417
+ {/each}
418
+
419
+ {#if filterable && filterQuery && filteredItems.length === 0}
420
+ <div data-select-empty>No results</div>
421
+ {/if}
422
+ </div>
423
+ {/if}
424
+ </div>
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { id } from '@rokkit/core'
4
+
5
+ interface ShineProps {
6
+ /** Light color (default: 'rgb(var(--primary-500))') */
7
+ color?: string
8
+ /** Light source distance/height — controls spread (default: 300) */
9
+ radius?: number
10
+ /** Gaussian blur depth (default: 1) */
11
+ depth?: number
12
+ /** Height of the surface for the light filter (default: 2) */
13
+ surfaceScale?: number
14
+ /** The bigger the value the bigger the reflection (default: 0.75) */
15
+ specularConstant?: number
16
+ /** Controls focus for the light source — bigger = brighter (default: 120) */
17
+ specularExponent?: number
18
+ /** Additional CSS class */
19
+ class?: string
20
+ children?: Snippet
21
+ }
22
+
23
+ const {
24
+ color = 'rgb(var(--primary-500))',
25
+ radius = 300,
26
+ depth = 1,
27
+ surfaceScale = 2,
28
+ specularConstant = 0.75,
29
+ specularExponent = 120,
30
+ class: className = '',
31
+ children
32
+ }: ShineProps = $props()
33
+
34
+ const filterId = id('filter')
35
+
36
+ let mouse = $state({ x: 0, y: 0 })
37
+ let wrapperBox = $state({ left: 0, top: 0 })
38
+ let wrapperEl: HTMLDivElement | null = $state(null)
39
+
40
+ function onPointerMove(e: PointerEvent) {
41
+ wrapperBox = wrapperEl?.getBoundingClientRect() ?? { left: 0, top: 0 }
42
+ mouse = { x: e.clientX, y: e.clientY }
43
+ }
44
+
45
+ function onScroll() {
46
+ wrapperBox = wrapperEl?.getBoundingClientRect() ?? { left: 0, top: 0 }
47
+ }
48
+ </script>
49
+
50
+ <svelte:window onpointermove={onPointerMove} onscroll={onScroll} />
51
+
52
+ <svg data-shine-filter>
53
+ <filter id={filterId} color-interpolation-filters="sRGB">
54
+ <feGaussianBlur in="SourceAlpha" stdDeviation={depth} />
55
+
56
+ <feSpecularLighting
57
+ result="light-source"
58
+ {surfaceScale}
59
+ {specularConstant}
60
+ {specularExponent}
61
+ lighting-color={color}
62
+ >
63
+ <fePointLight x={mouse.x - wrapperBox.left} y={mouse.y - wrapperBox.top} z={radius} />
64
+ </feSpecularLighting>
65
+
66
+ <feComposite result="reflections" in="light-source" in2="SourceAlpha" operator="in" />
67
+
68
+ <feComposite
69
+ in="SourceGraphic"
70
+ in2="reflections"
71
+ operator="arithmetic"
72
+ k1="0"
73
+ k2="1"
74
+ k3="1"
75
+ k4="0"
76
+ />
77
+ </filter>
78
+ </svg>
79
+
80
+ <div
81
+ data-shine
82
+ style:filter="url(#{filterId})"
83
+ class={className || undefined}
84
+ bind:this={wrapperEl}
85
+ >
86
+ {@render children?.()}
87
+ </div>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ interface StackProps {
5
+ /** Layout direction */
6
+ direction?: 'vertical' | 'horizontal'
7
+ /** Gap size between children */
8
+ gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
9
+ /** Cross-axis alignment */
10
+ align?: 'start' | 'center' | 'end' | 'stretch'
11
+ /** Main-axis justification */
12
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around'
13
+ /** Stack children */
14
+ children: Snippet
15
+ /** Additional CSS class */
16
+ class?: string
17
+ }
18
+
19
+ const {
20
+ direction = 'vertical',
21
+ gap = 'md',
22
+ align,
23
+ justify,
24
+ children,
25
+ class: className = ''
26
+ }: StackProps = $props()
27
+ </script>
28
+
29
+ <div
30
+ data-stack
31
+ data-direction={direction}
32
+ data-gap={gap}
33
+ data-align={align || undefined}
34
+ data-justify={justify || undefined}
35
+ class={className || undefined}
36
+ >
37
+ {@render children()}
38
+ </div>
@@ -0,0 +1,18 @@
1
+ <script>
2
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
3
+
4
+ const DEFAULT_ICONS = DEFAULT_STATE_ICONS.badge
5
+
6
+ let { class: className = '', items, icons = {} } = $props()
7
+
8
+ const resolvedIcons = $derived({ ...DEFAULT_ICONS, ...icons })
9
+ </script>
10
+
11
+ <div data-status-list class={className} role="status">
12
+ {#each items as { text, status }, index (index)}
13
+ <div data-status-item data-status={status}>
14
+ <span class={resolvedIcons[status]} aria-hidden="true"></span>
15
+ <p>{text}</p>
16
+ </div>
17
+ {/each}
18
+ </div>