@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.
- package/README.md +159 -1
- package/package.json +52 -53
- package/src/MarkdownRenderer.svelte +106 -0
- package/src/components/AlertList.svelte +59 -0
- package/src/components/Avatar.svelte +68 -0
- package/src/components/Badge.svelte +57 -0
- package/src/components/BreadCrumbs.svelte +94 -0
- package/src/components/Button.svelte +91 -0
- package/src/components/ButtonGroup.svelte +9 -0
- package/src/components/Card.svelte +71 -0
- package/src/components/Carousel.svelte +169 -0
- package/src/components/Code.svelte +189 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/Divider.svelte +24 -0
- package/src/components/Dropdown.svelte +202 -0
- package/src/components/FloatingAction.svelte +367 -0
- package/src/components/FloatingNavigation.svelte +256 -0
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +25 -0
- package/src/components/LazyTree.svelte +171 -0
- package/src/components/List.svelte +224 -0
- package/src/components/Menu.svelte +270 -0
- package/src/components/Message.svelte +75 -0
- package/src/components/MultiSelect.svelte +367 -0
- package/src/components/PaletteManager.svelte +360 -0
- package/src/components/Pill.svelte +83 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +346 -0
- package/src/components/Rating.svelte +115 -0
- package/src/components/Reveal.svelte +46 -0
- package/src/components/SearchFilter.svelte +88 -0
- package/src/components/Select.svelte +424 -0
- package/src/components/Shine.svelte +87 -0
- package/src/components/Stack.svelte +38 -0
- package/src/components/StatusList.svelte +18 -0
- package/src/components/Stepper.svelte +174 -0
- package/src/components/Swatch.svelte +102 -0
- package/src/components/Switch.svelte +71 -0
- package/src/components/Table.svelte +245 -0
- package/src/components/Tabs.svelte +202 -0
- package/src/components/Tilt.svelte +68 -0
- package/src/components/Timeline.svelte +61 -0
- package/src/components/Toggle.svelte +93 -0
- package/src/components/Toolbar.svelte +306 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tooltip.svelte +121 -0
- package/src/components/Tree.svelte +143 -0
- package/src/components/UploadFileStatus.svelte +102 -0
- package/src/components/UploadProgress.svelte +127 -0
- package/src/components/UploadTarget.svelte +126 -0
- package/src/components/index.ts +48 -0
- package/src/index.ts +59 -0
- package/src/markdown-plugin.ts +12 -0
- package/src/types/button.ts +86 -0
- package/src/types/code.ts +46 -0
- package/src/types/floating-action.ts +123 -0
- package/src/types/floating-navigation.ts +80 -0
- package/src/types/index.ts +58 -0
- package/src/types/list.ts +200 -0
- package/src/types/menu.ts +95 -0
- package/src/types/palette.ts +160 -0
- package/src/types/range.ts +51 -0
- package/src/types/search-filter.ts +67 -0
- package/src/types/select.ts +176 -0
- package/src/types/switch.ts +68 -0
- package/src/types/table.ts +219 -0
- package/src/types/tabs.ts +103 -0
- package/src/types/timeline.ts +53 -0
- package/src/types/toggle.ts +68 -0
- package/src/types/toolbar.ts +167 -0
- package/src/types/tooltip.ts +21 -0
- package/src/types/tree.ts +250 -0
- package/src/types/upload-file-status.ts +45 -0
- package/src/types/upload-progress.ts +111 -0
- package/src/types/upload-target.ts +71 -0
- package/src/utils/palette.ts +618 -0
- package/src/utils/shiki.ts +114 -0
- package/src/utils/upload.js +145 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -8
- 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>
|