@rokkit/ui 1.0.0-next.127 → 1.0.0-next.128
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/package.json +6 -16
- package/src/components/BreadCrumbs.svelte +25 -17
- package/src/components/Button.svelte +11 -5
- package/src/components/Carousel.svelte +11 -6
- package/src/components/Code.svelte +6 -2
- package/src/components/FloatingAction.svelte +24 -21
- package/src/components/FloatingNavigation.svelte +36 -29
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +21 -20
- package/src/components/LazyTree.svelte +165 -0
- package/src/components/List.svelte +147 -435
- package/src/components/Menu.svelte +195 -346
- package/src/components/MultiSelect.svelte +238 -390
- package/src/components/PaletteManager.svelte +15 -5
- package/src/components/Pill.svelte +19 -14
- package/src/components/Range.svelte +8 -3
- package/src/components/Rating.svelte +19 -9
- package/src/components/SearchFilter.svelte +11 -3
- package/src/components/Select.svelte +265 -454
- package/src/components/Stepper.svelte +9 -6
- package/src/components/Switch.svelte +11 -11
- package/src/components/Table.svelte +0 -1
- package/src/components/Tabs.svelte +96 -172
- package/src/components/Timeline.svelte +5 -5
- package/src/components/Toggle.svelte +55 -119
- package/src/components/Toolbar.svelte +24 -23
- package/src/components/Tree.svelte +115 -584
- package/src/components/UploadFileStatus.svelte +83 -0
- package/src/components/UploadProgress.svelte +131 -0
- package/src/components/UploadTarget.svelte +124 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +6 -1
- package/src/types/button.ts +3 -0
- package/src/types/code.ts +4 -4
- package/src/types/floating-action.ts +13 -8
- package/src/types/floating-navigation.ts +14 -2
- package/src/types/index.ts +5 -3
- package/src/types/list.ts +10 -6
- package/src/types/menu.ts +38 -138
- package/src/types/palette.ts +17 -0
- package/src/types/select.ts +33 -63
- package/src/types/switch.ts +9 -5
- package/src/types/table.ts +6 -6
- package/src/types/tabs.ts +13 -34
- package/src/types/timeline.ts +5 -3
- package/src/types/toggle.ts +15 -56
- package/src/types/toolbar.ts +1 -1
- package/src/types/tree.ts +9 -18
- package/src/types/upload-file-status.ts +45 -0
- package/src/types/upload-progress.ts +111 -0
- package/src/types/upload-target.ts +68 -0
- package/src/utils/upload.js +128 -0
- package/src/types/item-proxy.ts +0 -358
|
@@ -1,430 +1,102 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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'
|
|
11
34
|
import ItemContent from './ItemContent.svelte'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
|
|
36
|
+
interface ListIcons {
|
|
37
|
+
opened?: string
|
|
38
|
+
closed?: string
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
let {
|
|
17
42
|
items = [],
|
|
18
|
-
fields
|
|
43
|
+
fields = {},
|
|
19
44
|
value,
|
|
20
45
|
size = 'md',
|
|
21
46
|
disabled = false,
|
|
22
47
|
collapsible = false,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
selected = $bindable([]),
|
|
26
|
-
active,
|
|
48
|
+
label = messages.current.list.label,
|
|
49
|
+
icons: userIcons = {} as ListIcons,
|
|
27
50
|
onselect,
|
|
28
|
-
onselectedchange,
|
|
29
|
-
onexpandedchange,
|
|
30
51
|
class: className = '',
|
|
31
|
-
icons: userIcons,
|
|
32
|
-
item: itemSnippet,
|
|
33
|
-
groupLabel: groupLabelSnippet,
|
|
34
52
|
...snippets
|
|
35
|
-
}:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 }))
|
|
48
74
|
|
|
49
|
-
let controller = untrack(() => new NestedController(items, value, userFields, { multiselect }))
|
|
50
75
|
let listRef = $state<HTMLElement | null>(null)
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
* Get expanded state for a group key from the expanded prop
|
|
54
|
-
* Default to expanded (true) when not explicitly set
|
|
55
|
-
*/
|
|
56
|
-
function getExpandedState(groupKey: string): boolean {
|
|
57
|
-
if (!collapsible) return true
|
|
58
|
-
const externalKeys = Object.keys(expanded)
|
|
59
|
-
if (externalKeys.length > 0) {
|
|
60
|
-
return expanded[groupKey] !== false
|
|
61
|
-
}
|
|
62
|
-
return true // Default: expanded
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Sync expansion state: expanded prop → controller.expandedKeys
|
|
66
|
-
function syncExpandedToController() {
|
|
67
|
-
for (const [key, proxy] of controller.lookup.entries()) {
|
|
68
|
-
if (!proxy.hasChildren) continue
|
|
69
|
-
const groupProxy = createProxy(proxy.value)
|
|
70
|
-
const groupKey = getGroupKey(groupProxy)
|
|
71
|
-
const shouldExpand = getExpandedState(groupKey)
|
|
72
|
-
if (shouldExpand) {
|
|
73
|
-
controller.expandedKeys.add(key)
|
|
74
|
-
} else {
|
|
75
|
-
controller.expandedKeys.delete(key)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Sync on init
|
|
81
|
-
syncExpandedToController()
|
|
82
|
-
|
|
77
|
+
// Mount Navigator on the root element; destroy when component unmounts.
|
|
83
78
|
$effect(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
if (!listRef) return
|
|
80
|
+
const dir = getComputedStyle(listRef).direction || 'ltr'
|
|
81
|
+
const nav = new Navigator(listRef, wrapper, { collapsible, dir })
|
|
82
|
+
return () => nav.destroy()
|
|
87
83
|
})
|
|
88
84
|
|
|
89
|
-
// Sync
|
|
90
|
-
$effect(() => {
|
|
91
|
-
// Track both expanded and collapsible to re-sync when either changes
|
|
92
|
-
void expanded
|
|
93
|
-
void collapsible
|
|
94
|
-
syncExpandedToController()
|
|
95
|
-
})
|
|
85
|
+
// ─── Sync external value → focused key ────────────────────────────────────
|
|
96
86
|
|
|
97
|
-
// Derive expanded prop from controller.expandedKeys (pathKey → groupKey mapping)
|
|
98
|
-
function deriveExpandedFromController(): Record<string, boolean> {
|
|
99
|
-
const result: Record<string, boolean> = {}
|
|
100
|
-
items.forEach((item, index) => {
|
|
101
|
-
const proxy = createProxy(item)
|
|
102
|
-
if (!proxy.hasChildren) return
|
|
103
|
-
const pathKey = String(index)
|
|
104
|
-
const groupKey = getGroupKey(proxy)
|
|
105
|
-
result[groupKey] = controller.expandedKeys.has(pathKey)
|
|
106
|
-
})
|
|
107
|
-
return result
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Focus the element matching controller.focusedKey on navigator action events
|
|
111
87
|
$effect(() => {
|
|
112
|
-
|
|
113
|
-
const el = listRef
|
|
114
|
-
|
|
115
|
-
function onAction(event: Event) {
|
|
116
|
-
const detail = (event as CustomEvent).detail
|
|
117
|
-
|
|
118
|
-
if (detail.name === 'move') {
|
|
119
|
-
const key = controller.focusedKey
|
|
120
|
-
if (key) {
|
|
121
|
-
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
122
|
-
if (target && target !== document.activeElement) {
|
|
123
|
-
target.focus()
|
|
124
|
-
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (detail.name === 'select') {
|
|
130
|
-
handleSelectAction()
|
|
131
|
-
syncSelectedFromController()
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (detail.name === 'toggle') {
|
|
135
|
-
// Controller already toggled expandedKeys. Derive the expanded prop.
|
|
136
|
-
const newExpanded = deriveExpandedFromController()
|
|
137
|
-
expanded = newExpanded
|
|
138
|
-
onexpandedchange?.(newExpanded)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
el.addEventListener('action', onAction)
|
|
143
|
-
return () => el.removeEventListener('action', onAction)
|
|
88
|
+
wrapper.moveToValue(value)
|
|
144
89
|
})
|
|
145
90
|
|
|
146
|
-
/**
|
|
147
|
-
* Sync DOM focus to controller state.
|
|
148
|
-
* When a user tabs into the list or clicks an item, the controller
|
|
149
|
-
* needs to know which element is focused for arrow keys to work correctly.
|
|
150
|
-
*/
|
|
151
|
-
function handleFocusIn(event: FocusEvent) {
|
|
152
|
-
const target = event.target as HTMLElement
|
|
153
|
-
if (!target) return
|
|
154
|
-
const path = target.dataset.path
|
|
155
|
-
if (path !== undefined) {
|
|
156
|
-
controller.moveTo(path)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Handle the navigator's select action (Enter/Space on focused item, or click)
|
|
162
|
-
*/
|
|
163
|
-
function handleSelectAction() {
|
|
164
|
-
const key = controller.focusedKey
|
|
165
|
-
if (!key) return
|
|
166
|
-
|
|
167
|
-
const proxy = controller.lookup.get(key)
|
|
168
|
-
if (!proxy) return
|
|
169
|
-
|
|
170
|
-
// If it's a group, toggle expansion
|
|
171
|
-
if (proxy.hasChildren) {
|
|
172
|
-
controller.toggleExpansion(key)
|
|
173
|
-
const newExpanded = deriveExpandedFromController()
|
|
174
|
-
expanded = newExpanded
|
|
175
|
-
onexpandedchange?.(newExpanded)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Otherwise fire onselect for button items
|
|
180
|
-
const itemProxy = createProxy(proxy.value)
|
|
181
|
-
if (!itemProxy.disabled && !disabled) {
|
|
182
|
-
const href = itemProxy.get<string>('href')
|
|
183
|
-
if (!href) {
|
|
184
|
-
onselect?.(itemProxy.itemValue, proxy.value as ListItem)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Handle keyboard events the navigator doesn't cover:
|
|
191
|
-
* - Enter/Space on link items: let native <a> behavior through
|
|
192
|
-
*
|
|
193
|
-
* Fires before navigator's keydown handler.
|
|
194
|
-
*/
|
|
195
|
-
function handleListKeyDown(event: KeyboardEvent) {
|
|
196
|
-
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
197
|
-
|
|
198
|
-
const key = controller.focusedKey
|
|
199
|
-
if (!key) return
|
|
200
|
-
|
|
201
|
-
const proxy = controller.lookup.get(key)
|
|
202
|
-
if (!proxy) return
|
|
203
|
-
|
|
204
|
-
// Link items: stop propagation to prevent navigator's preventDefault
|
|
205
|
-
const itemProxy = createProxy(proxy.value)
|
|
206
|
-
const href = itemProxy.get<string>('href')
|
|
207
|
-
if (href) {
|
|
208
|
-
event.stopPropagation()
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─── Multi-selection helpers ────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Sync the selected bindable prop from controller.selected
|
|
216
|
-
*/
|
|
217
|
-
function syncSelectedFromController() {
|
|
218
|
-
if (!multiselect) return
|
|
219
|
-
selected = [...controller.selected]
|
|
220
|
-
onselectedchange?.(selected)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Check if an item is in the current selection (for data-selected attribute)
|
|
225
|
-
*/
|
|
226
|
-
function isItemSelected(pathKey: string): boolean {
|
|
227
|
-
if (!multiselect) return false
|
|
228
|
-
return controller.selectedKeys.has(pathKey)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ─── Group helpers ──────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Get the key for a group (for expanded state tracking)
|
|
235
|
-
*/
|
|
236
|
-
function getGroupKey(proxy: ItemProxy): string {
|
|
237
|
-
const val = proxy.itemValue
|
|
238
|
-
return typeof val === 'string' ? val : proxy.text
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Check if a group is expanded (reads from controller.expandedKeys)
|
|
243
|
-
*/
|
|
244
|
-
function isGroupExpandedByKey(pathKey: string): boolean {
|
|
245
|
-
if (!collapsible) return true
|
|
246
|
-
return controller.expandedKeys.has(pathKey)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Toggle group expansion via the controller
|
|
251
|
-
*/
|
|
252
|
-
function toggleGroupByKey(pathKey: string) {
|
|
253
|
-
if (!collapsible) return
|
|
254
|
-
controller.toggleExpansion(pathKey)
|
|
255
|
-
const newExpanded = deriveExpandedFromController()
|
|
256
|
-
expanded = newExpanded
|
|
257
|
-
onexpandedchange?.(newExpanded)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ─── Unchanged helpers ──────────────────────────────────────────
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Check if an item is currently active
|
|
264
|
-
*/
|
|
265
|
-
function checkIsActive(proxy: ItemProxy): boolean {
|
|
266
|
-
if (active !== undefined) {
|
|
267
|
-
return proxy.itemValue === active
|
|
268
|
-
}
|
|
269
|
-
return value !== undefined && proxy.itemValue === value
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Handle item click (for button items)
|
|
274
|
-
*/
|
|
275
|
-
function handleItemClick(proxy: ItemProxy) {
|
|
276
|
-
if (proxy.disabled || disabled) return
|
|
277
|
-
onselect?.(proxy.itemValue, proxy.original as ListItem)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Create handlers object for custom snippets
|
|
282
|
-
*/
|
|
283
|
-
function createHandlers(proxy: ItemProxy): ListItemHandlers {
|
|
284
|
-
return {
|
|
285
|
-
onclick: () => handleItemClick(proxy),
|
|
286
|
-
onkeydown: () => {}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Resolve which snippet to use for an item
|
|
292
|
-
*/
|
|
293
|
-
function resolveItemSnippet(proxy: ItemProxy): ListItemSnippet | null {
|
|
294
|
-
const snippetName = proxy.snippetName
|
|
295
|
-
if (snippetName) {
|
|
296
|
-
const namedSnippet = getSnippet(snippets, snippetName)
|
|
297
|
-
if (namedSnippet) {
|
|
298
|
-
return namedSnippet as ListItemSnippet
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return itemSnippet ?? null
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Track option index for divider logic
|
|
305
|
-
function shouldShowDivider(index: number, isGroup: boolean): boolean {
|
|
306
|
-
return isGroup && index > 0
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Get the controller path key for a given item index and optional child index.
|
|
311
|
-
* Maps to the same format as getKeyFromPath: "0", "0-0", "1-2", etc.
|
|
312
|
-
*/
|
|
313
|
-
function getPathKey(itemIndex: number, childIndex?: number): string {
|
|
314
|
-
if (childIndex !== undefined) return `${itemIndex}-${childIndex}`
|
|
315
|
-
return String(itemIndex)
|
|
316
|
-
}
|
|
317
91
|
</script>
|
|
318
92
|
|
|
319
|
-
{#snippet
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
{@const href = proxy.get<string>('href')}
|
|
327
|
-
{@const itemSelected = isItemSelected(pathKey)}
|
|
328
|
-
{#if href}
|
|
329
|
-
<a
|
|
330
|
-
{href}
|
|
331
|
-
data-list-item
|
|
332
|
-
data-list-index={listIndex}
|
|
333
|
-
data-path={pathKey}
|
|
334
|
-
data-active={active || undefined}
|
|
335
|
-
data-selected={itemSelected || undefined}
|
|
336
|
-
data-disabled={proxy.disabled || undefined}
|
|
337
|
-
aria-label={proxy.label}
|
|
338
|
-
aria-current={active ? 'page' : undefined}
|
|
339
|
-
aria-selected={multiselect ? itemSelected : undefined}
|
|
340
|
-
>
|
|
341
|
-
<ItemContent {proxy} />
|
|
342
|
-
</a>
|
|
343
|
-
{:else}
|
|
344
|
-
<button
|
|
345
|
-
type="button"
|
|
346
|
-
data-list-item
|
|
347
|
-
data-list-index={listIndex}
|
|
348
|
-
data-path={pathKey}
|
|
349
|
-
data-active={active || undefined}
|
|
350
|
-
data-selected={itemSelected || undefined}
|
|
351
|
-
data-disabled={proxy.disabled || undefined}
|
|
352
|
-
disabled={proxy.disabled || disabled}
|
|
353
|
-
aria-label={proxy.label}
|
|
354
|
-
aria-pressed={active}
|
|
355
|
-
aria-selected={multiselect ? itemSelected : undefined}
|
|
356
|
-
>
|
|
357
|
-
<ItemContent {proxy} />
|
|
358
|
-
</button>
|
|
359
|
-
{/if}
|
|
360
|
-
{/snippet}
|
|
361
|
-
|
|
362
|
-
{#snippet defaultGroupLabel(
|
|
363
|
-
proxy: ItemProxy,
|
|
364
|
-
_toggle: () => void,
|
|
365
|
-
isExpanded: boolean,
|
|
366
|
-
listIndex: string,
|
|
367
|
-
pathKey: string
|
|
368
|
-
)}
|
|
369
|
-
<button
|
|
370
|
-
type="button"
|
|
371
|
-
data-list-group-label
|
|
372
|
-
data-list-index={listIndex}
|
|
373
|
-
data-path={pathKey}
|
|
374
|
-
data-list-group-key={getGroupKey(proxy)}
|
|
375
|
-
aria-expanded={isExpanded}
|
|
376
|
-
disabled={!collapsible}
|
|
377
|
-
>
|
|
378
|
-
{#if proxy.icon}
|
|
379
|
-
<span data-list-group-icon class={proxy.icon} aria-hidden="true"></span>
|
|
380
|
-
{/if}
|
|
381
|
-
<span data-list-group-text>{proxy.text}</span>
|
|
382
|
-
{#if collapsible}
|
|
383
|
-
<span data-list-group-arrow class={icons.opened} aria-hidden="true"></span>
|
|
384
|
-
{/if}
|
|
385
|
-
</button>
|
|
386
|
-
{/snippet}
|
|
387
|
-
|
|
388
|
-
{#snippet renderItem(proxy: ItemProxy, listIndex: string, pathKey: string)}
|
|
389
|
-
{@const customSnippet = resolveItemSnippet(proxy)}
|
|
390
|
-
{@const handlers = createHandlers(proxy)}
|
|
391
|
-
{@const active = checkIsActive(proxy)}
|
|
392
|
-
{@const itemSelected = isItemSelected(pathKey)}
|
|
393
|
-
{#if customSnippet}
|
|
394
|
-
<div
|
|
395
|
-
data-list-item
|
|
396
|
-
data-list-item-custom
|
|
397
|
-
data-list-index={listIndex}
|
|
398
|
-
data-path={pathKey}
|
|
399
|
-
data-active={active || undefined}
|
|
400
|
-
data-selected={itemSelected || undefined}
|
|
401
|
-
data-disabled={proxy.disabled || undefined}
|
|
402
|
-
aria-selected={multiselect ? itemSelected : undefined}
|
|
403
|
-
>
|
|
404
|
-
<svelte:boundary>
|
|
405
|
-
{@render customSnippet(proxy.original as ListItem, proxy.fields, handlers, active)}
|
|
406
|
-
{#snippet failed()}
|
|
407
|
-
{@render defaultItem(proxy, handlers, active, listIndex, pathKey)}
|
|
408
|
-
{/snippet}
|
|
409
|
-
</svelte:boundary>
|
|
410
|
-
</div>
|
|
411
|
-
{:else}
|
|
412
|
-
{@render defaultItem(proxy, handlers, active, listIndex, pathKey)}
|
|
413
|
-
{/if}
|
|
414
|
-
{/snippet}
|
|
415
|
-
|
|
416
|
-
{#snippet renderGroupLabel(proxy: ItemProxy, listIndex: string, pathKey: string)}
|
|
417
|
-
{@const toggle = () => toggleGroupByKey(pathKey)}
|
|
418
|
-
{@const isExpanded = isGroupExpandedByKey(pathKey)}
|
|
419
|
-
{#if groupLabelSnippet}
|
|
420
|
-
<svelte:boundary>
|
|
421
|
-
{@render groupLabelSnippet(proxy.original as ListItem, proxy.fields, toggle, isExpanded)}
|
|
422
|
-
{#snippet failed()}
|
|
423
|
-
{@render defaultGroupLabel(proxy, toggle, isExpanded, listIndex, pathKey)}
|
|
424
|
-
{/snippet}
|
|
425
|
-
</svelte:boundary>
|
|
426
|
-
{:else}
|
|
427
|
-
{@render defaultGroupLabel(proxy, toggle, isExpanded, listIndex, pathKey)}
|
|
93
|
+
{#snippet collapsibleIcon(proxy: ProxyItem)}
|
|
94
|
+
{#if collapsible}
|
|
95
|
+
<span
|
|
96
|
+
data-list-expand-icon
|
|
97
|
+
class={proxy.expanded ? icons.opened : icons.closed}
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
></span>
|
|
428
100
|
{/if}
|
|
429
101
|
{/snippet}
|
|
430
102
|
|
|
@@ -435,42 +107,82 @@
|
|
|
435
107
|
data-size={size}
|
|
436
108
|
data-disabled={disabled || undefined}
|
|
437
109
|
data-collapsible={collapsible || undefined}
|
|
438
|
-
data-multiselect={multiselect || undefined}
|
|
439
110
|
class={className || undefined}
|
|
440
|
-
aria-label=
|
|
441
|
-
aria-multiselectable={multiselect || undefined}
|
|
442
|
-
onkeydown={handleListKeyDown}
|
|
443
|
-
onfocusin={handleFocusIn}
|
|
444
|
-
use:navigator={{ wrapper: controller, orientation: 'vertical', nested: collapsible, typeahead: true }}
|
|
111
|
+
aria-label={label}
|
|
445
112
|
>
|
|
446
|
-
{#each
|
|
447
|
-
{@const proxy =
|
|
448
|
-
{@const
|
|
449
|
-
{@const
|
|
450
|
-
|
|
451
|
-
{#if
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
113
|
+
{#each wrapper.flatView as node (node.key)}
|
|
114
|
+
{@const proxy = node.proxy}
|
|
115
|
+
{@const isActive = proxy.value === value}
|
|
116
|
+
{@const content = resolveSnippet(snippets as Record<string, unknown>, proxy, node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET)}
|
|
117
|
+
|
|
118
|
+
{#if node.type === 'separator'}
|
|
119
|
+
<hr data-list-separator />
|
|
120
|
+
{:else if node.type === 'spacer'}
|
|
121
|
+
<div data-list-spacer></div>
|
|
122
|
+
{:else if node.hasChildren}
|
|
123
|
+
<!--
|
|
124
|
+
Group header — data-accordion-trigger tells Navigator to dispatch
|
|
125
|
+
toggle() instead of select() when this element is clicked.
|
|
126
|
+
aria-expanded reflects the reactive proxy.expanded state.
|
|
127
|
+
-->
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
data-list-group
|
|
131
|
+
data-path={node.key}
|
|
132
|
+
data-accordion-trigger
|
|
133
|
+
data-level={node.level}
|
|
134
|
+
aria-expanded={proxy.expanded}
|
|
135
|
+
disabled={!collapsible}
|
|
136
|
+
>
|
|
137
|
+
{#if content}
|
|
138
|
+
{@render content(proxy)}
|
|
139
|
+
{:else}
|
|
140
|
+
<ItemContent {proxy} />
|
|
141
|
+
{/if}
|
|
142
|
+
{@render collapsibleIcon(proxy)}
|
|
143
|
+
</button>
|
|
144
|
+
{:else if proxy.get('href')}
|
|
145
|
+
<!--
|
|
146
|
+
Navigation link — native <a> handles click; Navigator updates state.
|
|
147
|
+
aria-current marks the active route for screen readers.
|
|
148
|
+
-->
|
|
149
|
+
<a
|
|
150
|
+
href={proxy.get('href')}
|
|
151
|
+
title={proxy.get('tooltip')}
|
|
152
|
+
data-list-item
|
|
153
|
+
data-path={node.key}
|
|
154
|
+
data-level={node.level}
|
|
155
|
+
data-active={isActive || undefined}
|
|
156
|
+
aria-current={isActive ? 'page' : undefined}
|
|
157
|
+
>
|
|
158
|
+
{#if content}
|
|
159
|
+
{@render content(proxy)}
|
|
160
|
+
{:else}
|
|
161
|
+
<ItemContent {proxy} />
|
|
469
162
|
{/if}
|
|
470
|
-
|
|
163
|
+
{@render collapsibleIcon(proxy)}
|
|
164
|
+
</a>
|
|
471
165
|
{:else}
|
|
472
|
-
<!--
|
|
473
|
-
|
|
166
|
+
<!--
|
|
167
|
+
Button item — Navigator calls wrapper.select(path) on click/Enter/Space.
|
|
168
|
+
The wrapper fires the onselect callback for non-group items.
|
|
169
|
+
-->
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
title={proxy.get('tooltip')}
|
|
173
|
+
data-list-item
|
|
174
|
+
data-path={node.key}
|
|
175
|
+
data-level={node.level}
|
|
176
|
+
data-active={isActive || undefined}
|
|
177
|
+
data-disabled={proxy.disabled || undefined}
|
|
178
|
+
disabled={proxy.disabled || disabled}
|
|
179
|
+
>
|
|
180
|
+
{#if content}
|
|
181
|
+
{@render content(proxy)}
|
|
182
|
+
{:else}
|
|
183
|
+
<ItemContent {proxy} />
|
|
184
|
+
{/if}
|
|
185
|
+
</button>
|
|
474
186
|
{/if}
|
|
475
187
|
{/each}
|
|
476
188
|
</nav>
|