@rokkit/ui 1.0.0-next.98 → 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,224 @@
1
+ <script lang="ts">
2
+ /**
3
+ * List — ProxyItem + Wrapper + Navigator implementation.
4
+ *
5
+ * Architecture:
6
+ * Wrapper — owns focusedKey $state + flatView $derived
7
+ * Navigator — attaches DOM event handlers, calls wrapper[action](path)
8
+ * owns focus + scrollIntoView after every keyboard action
9
+ * flatView loop — single flat {#each}, no nested groups in template
10
+ *
11
+ * Snippet customization:
12
+ * itemContent — replaces inner content of <a>/<button> for leaf items
13
+ * groupContent — replaces inner content of group header <button>
14
+ * [named] — per-item override via item.snippet = 'name'; falls back to itemContent
15
+ *
16
+ * Snippets receive (proxy) only — the <a>/<button> wrapper with data-path is
17
+ * always rendered by this component, so snippets never need to handle navigation.
18
+ *
19
+ * Data attributes on rendered elements:
20
+ * data-path — required by Navigator for click detection + scroll
21
+ * data-level — nesting depth (1=root); theme CSS uses for indentation
22
+ * data-accordion-trigger — tells Navigator to dispatch toggle (not select) on click
23
+ * data-list-item — theme hook for leaf items
24
+ * data-list-item-icon — icon span inside leaf items
25
+ * data-list-group — theme hook for group headers
26
+ * data-list-group-icon — icon span inside group headers
27
+ * data-active — highlights current value match
28
+ * data-disabled — disabled state
29
+ */
30
+ import type { ProxyItem } from '@rokkit/states'
31
+ import { Wrapper, ProxyTree, messages } from '@rokkit/states'
32
+ import { Navigator } from '@rokkit/actions'
33
+ import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
34
+ import ItemContent from './ItemContent.svelte'
35
+
36
+ interface ListIcons {
37
+ opened?: string
38
+ closed?: string
39
+ }
40
+
41
+ let {
42
+ items = [],
43
+ fields = {},
44
+ value,
45
+ size = 'md',
46
+ disabled = false,
47
+ collapsible = false,
48
+ label = messages.list.label,
49
+ icons: userIcons = {} as ListIcons,
50
+ onselect,
51
+ class: className = '',
52
+ ...snippets
53
+ }: {
54
+ items?: unknown[]
55
+ fields?: Record<string, string>
56
+ value?: unknown
57
+ size?: string
58
+ disabled?: boolean
59
+ collapsible?: boolean
60
+ label?: string
61
+ icons?: ListIcons
62
+ onselect?: (value: unknown, proxy: ProxyItem) => void
63
+ class?: string
64
+ [key: string]: unknown
65
+ } = $props()
66
+
67
+ const icons = $derived({ ...DEFAULT_STATE_ICONS.accordion, ...userIcons })
68
+
69
+ // Single source of truth.
70
+ // Navigator calls wrapper[action](path) → focusedKey / proxy.expanded updates →
71
+ // flatView $derived re-computes → Svelte re-renders the changed nodes.
72
+ const proxyTree = $derived(new ProxyTree(items, fields))
73
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect, collapsible }))
74
+
75
+ let listRef = $state<HTMLElement | null>(null)
76
+
77
+ // Mount Navigator on the root element; destroy when component unmounts.
78
+ $effect(() => {
79
+ if (!listRef) return
80
+ const dir = getComputedStyle(listRef).direction || 'ltr'
81
+ const nav = new Navigator(listRef, wrapper, { collapsible, dir })
82
+ return () => nav.destroy()
83
+ })
84
+
85
+ // Expand groups based on collapsible mode and active value.
86
+ // collapsible=false: groups are fixed section headers — always expanded.
87
+ // collapsible=true: only expand the ancestor group of the active value;
88
+ // all other groups start collapsed (user can toggle them).
89
+ function expandAllGroups() {
90
+ for (const [, proxy] of wrapper.lookup) {
91
+ if (proxy.hasChildren) proxy.expanded = true
92
+ }
93
+ }
94
+
95
+ function findActiveKey(): string | null {
96
+ for (const [key, proxy] of wrapper.lookup) {
97
+ if (proxy.value === value) return key
98
+ }
99
+ return null
100
+ }
101
+
102
+ function expandAncestorGroups(activeKey: string | null) {
103
+ for (const [key, proxy] of wrapper.lookup) {
104
+ if (!proxy.hasChildren) continue
105
+ proxy.expanded = activeKey !== null && (activeKey === key || activeKey.startsWith(`${key}-`))
106
+ }
107
+ }
108
+
109
+ function syncExpandedGroups() {
110
+ if (!collapsible) {
111
+ expandAllGroups()
112
+ return
113
+ }
114
+ expandAncestorGroups(findActiveKey())
115
+ }
116
+
117
+ $effect(syncExpandedGroups)
118
+
119
+ // ─── Sync external value → focused key ────────────────────────────────────
120
+
121
+ $effect(() => {
122
+ wrapper.moveToValue(value)
123
+ })
124
+ </script>
125
+
126
+ {#snippet collapsibleIcon(proxy: ProxyItem)}
127
+ {#if collapsible && proxy.hasChildren}
128
+ <span
129
+ data-list-expand-icon
130
+ class={proxy.expanded ? icons.opened : icons.closed}
131
+ aria-hidden="true"
132
+ ></span>
133
+ {/if}
134
+ {/snippet}
135
+
136
+ <nav
137
+ bind:this={listRef}
138
+ data-list
139
+ data-size={size}
140
+ data-disabled={disabled || undefined}
141
+ data-collapsible={collapsible || undefined}
142
+ class={className || undefined}
143
+ aria-label={label}
144
+ >
145
+ {#each wrapper.flatView as node (node.key)}
146
+ {@const proxy = node.proxy}
147
+ {@const isActive = proxy.value === value}
148
+ {@const content = resolveSnippet(
149
+ snippets as Record<string, unknown>,
150
+ proxy,
151
+ node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET
152
+ )}
153
+
154
+ {#if node.type === 'separator'}
155
+ <hr data-list-separator />
156
+ {:else if node.type === 'spacer'}
157
+ <div data-list-spacer></div>
158
+ {:else if node.hasChildren}
159
+ <!--
160
+ Group header — data-accordion-trigger tells Navigator to dispatch
161
+ toggle() instead of select() when this element is clicked.
162
+ aria-expanded reflects the reactive proxy.expanded state.
163
+ -->
164
+ <button
165
+ type="button"
166
+ data-list-group
167
+ data-path={node.key}
168
+ data-accordion-trigger
169
+ data-level={node.level}
170
+ aria-expanded={proxy.expanded}
171
+ disabled={!collapsible}
172
+ >
173
+ {#if content}
174
+ {@render content(proxy)}
175
+ {:else}
176
+ <ItemContent {proxy} />
177
+ {/if}
178
+
179
+ {@render collapsibleIcon(proxy)}
180
+ </button>
181
+ {:else if proxy.get('href')}
182
+ <!--
183
+ Navigation link — native <a> handles click; Navigator updates state.
184
+ aria-current marks the active route for screen readers.
185
+ -->
186
+ <a
187
+ href={proxy.get('href')}
188
+ title={proxy.get('tooltip')}
189
+ data-list-item
190
+ data-path={node.key}
191
+ data-level={node.level}
192
+ data-active={isActive || undefined}
193
+ aria-current={isActive ? 'page' : undefined}
194
+ >
195
+ {#if content}
196
+ {@render content(proxy)}
197
+ {:else}
198
+ <ItemContent {proxy} />
199
+ {/if}
200
+ </a>
201
+ {:else}
202
+ <!--
203
+ Button item — Navigator calls wrapper.select(path) on click/Enter/Space.
204
+ The wrapper fires the onselect callback for non-group items.
205
+ -->
206
+ <button
207
+ type="button"
208
+ title={proxy.get('tooltip')}
209
+ data-list-item
210
+ data-path={node.key}
211
+ data-level={node.level}
212
+ data-active={isActive || undefined}
213
+ data-disabled={proxy.disabled || undefined}
214
+ disabled={proxy.disabled || disabled}
215
+ >
216
+ {#if content}
217
+ {@render content(proxy)}
218
+ {:else}
219
+ <ItemContent {proxy} />
220
+ {/if}
221
+ </button>
222
+ {/if}
223
+ {/each}
224
+ </nav>
@@ -0,0 +1,270 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Menu — Trigger + dropdown with List-identical content.
4
+ *
5
+ * Architecture:
6
+ * Trigger — manages open/close from trigger button (click, Enter, Escape, click-outside)
7
+ * Wrapper — owns focusedKey $state + flatView $derived (cancel/blur overridden to close)
8
+ * Navigator — attaches DOM event handlers on dropdown, calls wrapper[action](path)
9
+ * flatView loop — single flat {#each}, same as List
10
+ *
11
+ * The dropdown content is identical to List: same flatView loop, same snippet system,
12
+ * same collapsible groups. The only additions are trigger, alignment, and direction.
13
+ *
14
+ * Snippet customization (same as List):
15
+ * itemContent — replaces inner content of <button> for leaf items
16
+ * groupContent — replaces inner content of group header <button>
17
+ * [named] — per-item override via item.snippet = 'name'; falls back to itemContent
18
+ *
19
+ * Data attributes on rendered elements:
20
+ * data-menu — root container
21
+ * data-menu-trigger — trigger button
22
+ * data-menu-label — trigger label text
23
+ * data-menu-icon — trigger icon
24
+ * data-menu-arrow — trigger dropdown arrow
25
+ * data-menu-dropdown — dropdown container
26
+ * data-menu-item — leaf items (like data-list-item)
27
+ * data-menu-item-icon — icon span inside leaf items
28
+ * data-menu-item-text — text span inside leaf items
29
+ * data-menu-group — group headers (like data-list-group)
30
+ * data-menu-group-icon — icon span inside group headers
31
+ * data-menu-group-text — text span inside group headers
32
+ * data-menu-expand-icon — expand/collapse chevron in group headers
33
+ * data-menu-separator — separator
34
+ * data-menu-spacer — spacer
35
+ * data-path — required by Navigator for click detection + scroll
36
+ * data-level — nesting depth (1=root)
37
+ * data-accordion-trigger — tells Navigator to dispatch toggle on click
38
+ * data-active — highlights current value match
39
+ * data-disabled — disabled state
40
+ * data-open — dropdown is open
41
+ * data-size — size variant
42
+ * data-align — dropdown alignment
43
+ * data-direction — dropdown direction
44
+ */
45
+ // @ts-nocheck
46
+ import type { ProxyItem } from '@rokkit/states'
47
+ import { Wrapper, ProxyTree, messages } from '@rokkit/states'
48
+ import { Navigator, Trigger } from '@rokkit/actions'
49
+ import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
50
+ import ItemContent from './ItemContent.svelte'
51
+
52
+ interface MenuIcons {
53
+ opened?: string
54
+ closed?: string
55
+ }
56
+
57
+ let {
58
+ items = [],
59
+ fields = {},
60
+ value,
61
+ size = 'md',
62
+ disabled = false,
63
+ collapsible = false,
64
+ label = messages.menu.label,
65
+ icon,
66
+ showArrow = true,
67
+ align = 'start',
68
+ direction = 'down',
69
+ icons: userIcons = {} as MenuIcons,
70
+ onselect,
71
+ class: className = '',
72
+ ...snippets
73
+ }: {
74
+ items?: unknown[]
75
+ fields?: Record<string, string>
76
+ value?: unknown
77
+ size?: string
78
+ disabled?: boolean
79
+ collapsible?: boolean
80
+ label?: string
81
+ icon?: string
82
+ showArrow?: boolean
83
+ align?: 'start' | 'end'
84
+ direction?: 'up' | 'down'
85
+ icons?: MenuIcons
86
+ onselect?: (value: unknown, proxy: ProxyItem) => void
87
+ class?: string
88
+ [key: string]: unknown
89
+ } = $props()
90
+
91
+ const icons = $derived({ ...DEFAULT_STATE_ICONS.selector, ...userIcons })
92
+
93
+ // ─── Dropdown state ───────────────────────────────────────────────────────
94
+
95
+ let isOpen = $state(false)
96
+ let menuRef = $state<HTMLElement | null>(null)
97
+ let triggerRef = $state<HTMLElement | null>(null)
98
+ let dropdownRef = $state<HTMLElement | null>(null)
99
+
100
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
101
+
102
+ function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
103
+ if (proxy.disabled) return
104
+ onselect?.(extractedValue, proxy)
105
+ isOpen = false
106
+ triggerRef?.focus()
107
+ }
108
+
109
+ const proxyTree = $derived(new ProxyTree(items, fields))
110
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect, collapsible }))
111
+
112
+ // Override cancel/blur to close dropdown (Navigator dispatches these on Escape/focusout)
113
+ $effect(() => {
114
+ const w = wrapper
115
+ w.cancel = () => {
116
+ isOpen = false
117
+ triggerRef?.focus()
118
+ }
119
+ w.blur = () => {
120
+ isOpen = false
121
+ }
122
+ })
123
+
124
+ // When wrapper recreates while open, focus first item
125
+ $effect(() => {
126
+ const _w = wrapper
127
+ if (isOpen) _w.first(null)
128
+ })
129
+
130
+ // ─── Trigger action ───────────────────────────────────────────────────────
131
+
132
+ $effect(() => {
133
+ if (!triggerRef || !menuRef || disabled) return
134
+ const t = new Trigger(triggerRef, menuRef, {
135
+ isOpen: () => isOpen,
136
+ onopen: () => {
137
+ isOpen = true
138
+ requestAnimationFrame(() => wrapper.first(null))
139
+ },
140
+ onclose: () => {
141
+ isOpen = false
142
+ },
143
+ onlast: () => requestAnimationFrame(() => wrapper.last(null))
144
+ })
145
+ return () => t.destroy()
146
+ })
147
+
148
+ // ─── Navigator on dropdown ────────────────────────────────────────────────
149
+
150
+ $effect(() => {
151
+ if (!isOpen || !dropdownRef) return
152
+ const dir = getComputedStyle(dropdownRef).direction || 'ltr'
153
+ const nav = new Navigator(dropdownRef, wrapper, { collapsible, dir, containScroll: true })
154
+ return () => nav.destroy()
155
+ })
156
+
157
+ // DOM focus sync — move focus to focusedKey in dropdown
158
+ $effect(() => {
159
+ const key = wrapper.focusedKey
160
+ if (!isOpen || !dropdownRef || !key) return
161
+ requestAnimationFrame(() => {
162
+ const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
163
+ if (target && target !== document.activeElement) {
164
+ target.focus()
165
+ }
166
+ })
167
+ })
168
+ </script>
169
+
170
+ {#snippet defaultItemContent(proxy: ProxyItem)}
171
+ <ItemContent {proxy} />
172
+ {/snippet}
173
+
174
+ {#snippet defaultGroupContent(proxy: ProxyItem)}
175
+ {#if proxy.get('icon')}
176
+ <span data-menu-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
177
+ {/if}
178
+ <span data-menu-group-text>{proxy.label}</span>
179
+ {#if collapsible}
180
+ <span
181
+ data-menu-expand-icon
182
+ class={proxy.expanded ? icons.opened : icons.closed}
183
+ aria-hidden="true"
184
+ ></span>
185
+ {/if}
186
+ {/snippet}
187
+
188
+ <div
189
+ bind:this={menuRef}
190
+ data-menu
191
+ data-open={isOpen || undefined}
192
+ data-size={size}
193
+ data-disabled={disabled || undefined}
194
+ data-align={align}
195
+ data-direction={direction}
196
+ class={className || undefined}
197
+ >
198
+ <button
199
+ bind:this={triggerRef}
200
+ type="button"
201
+ data-menu-trigger
202
+ {disabled}
203
+ aria-haspopup="menu"
204
+ aria-expanded={isOpen}
205
+ aria-label={label}
206
+ >
207
+ {#if icon}
208
+ <span data-menu-icon class={icon} aria-hidden="true"></span>
209
+ {/if}
210
+ <span data-menu-label>{label}</span>
211
+ {#if showArrow}
212
+ <span data-menu-arrow class={isOpen ? icons.opened : icons.closed} aria-hidden="true"></span>
213
+ {/if}
214
+ </button>
215
+
216
+ {#if isOpen}
217
+ <div bind:this={dropdownRef} data-menu-dropdown role="menu" aria-orientation="vertical">
218
+ {#each wrapper.flatView as node (node.key)}
219
+ {@const proxy = node.proxy}
220
+ {@const isActive = proxy.value === value}
221
+ {@const content = resolveSnippet(
222
+ snippets as Record<string, unknown>,
223
+ proxy,
224
+ node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET
225
+ )}
226
+
227
+ {#if node.type === 'separator'}
228
+ <hr data-menu-separator />
229
+ {:else if node.type === 'spacer'}
230
+ <div data-menu-spacer></div>
231
+ {:else if node.hasChildren}
232
+ <button
233
+ type="button"
234
+ data-menu-group
235
+ data-path={node.key}
236
+ data-accordion-trigger
237
+ data-level={node.level}
238
+ aria-expanded={proxy.expanded}
239
+ disabled={!collapsible}
240
+ >
241
+ {#if content}
242
+ {@render content(proxy)}
243
+ {:else}
244
+ {@render defaultGroupContent(proxy)}
245
+ {/if}
246
+ </button>
247
+ {:else}
248
+ <button
249
+ type="button"
250
+ title={proxy.get('tooltip')}
251
+ data-menu-item
252
+ data-path={node.key}
253
+ data-level={node.level}
254
+ data-active={isActive || undefined}
255
+ data-disabled={proxy.disabled || undefined}
256
+ disabled={proxy.disabled || disabled}
257
+ role="menuitem"
258
+ tabindex="-1"
259
+ >
260
+ {#if content}
261
+ {@render content(proxy)}
262
+ {:else}
263
+ {@render defaultItemContent(proxy)}
264
+ {/if}
265
+ </button>
266
+ {/if}
267
+ {/each}
268
+ </div>
269
+ {/if}
270
+ </div>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
4
+
5
+ interface MessageProps {
6
+ /** Alert type — controls color and icon */
7
+ type?: 'error' | 'info' | 'success' | 'warning'
8
+ /** Icon class overrides per type */
9
+ icons?: { error?: string; info?: string; success?: string; warning?: string }
10
+ /** Text content (shorthand; children takes precedence) */
11
+ text?: string
12
+ /** Show dismiss button */
13
+ dismissible?: boolean
14
+ /** Auto-dismiss after N ms. Defaults to 4000 unless dismissible (persistent). Pass 0 to disable. */
15
+ timeout?: number
16
+ /** Optional action buttons snippet */
17
+ actions?: Snippet
18
+ /** Rich content (takes precedence over text) */
19
+ children?: Snippet
20
+ /** Called when dismissed (button click or timeout) */
21
+ ondismiss?: () => void
22
+ /** Additional CSS class */
23
+ class?: string
24
+ }
25
+
26
+ const {
27
+ type = 'error',
28
+ icons = DEFAULT_STATE_ICONS.state as Record<string, string>,
29
+ text = undefined,
30
+ dismissible = false,
31
+ timeout = dismissible ? 0 : 4000,
32
+ actions,
33
+ children,
34
+ ondismiss,
35
+ class: className = ''
36
+ }: MessageProps = $props()
37
+
38
+ const role = $derived(type === 'error' || type === 'warning' ? 'alert' : 'status')
39
+ const icon = $derived(icons[type] ?? '')
40
+
41
+ $effect(() => {
42
+ if (timeout > 0) {
43
+ const timer = setTimeout(() => ondismiss?.(), timeout)
44
+ return () => clearTimeout(timer)
45
+ }
46
+ })
47
+ </script>
48
+
49
+ <div
50
+ data-message-root
51
+ data-type={type}
52
+ data-dismissible={dismissible}
53
+ {role}
54
+ class={className || undefined}
55
+ >
56
+ <span data-message-icon class={icon} aria-hidden="true"></span>
57
+
58
+ <span data-message-text>
59
+ {#if children}
60
+ {@render children()}
61
+ {:else}
62
+ {text}
63
+ {/if}
64
+ </span>
65
+
66
+ {#if actions}
67
+ <div data-message-actions>
68
+ {@render actions()}
69
+ </div>
70
+ {/if}
71
+
72
+ {#if dismissible}
73
+ <button type="button" data-message-dismiss aria-label="Dismiss" onclick={ondismiss}>×</button>
74
+ {/if}
75
+ </div>