@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.
Files changed (53) hide show
  1. package/package.json +6 -16
  2. package/src/components/BreadCrumbs.svelte +25 -17
  3. package/src/components/Button.svelte +11 -5
  4. package/src/components/Carousel.svelte +11 -6
  5. package/src/components/Code.svelte +6 -2
  6. package/src/components/FloatingAction.svelte +24 -21
  7. package/src/components/FloatingNavigation.svelte +36 -29
  8. package/src/components/Grid.svelte +128 -0
  9. package/src/components/ItemContent.svelte +21 -20
  10. package/src/components/LazyTree.svelte +165 -0
  11. package/src/components/List.svelte +147 -435
  12. package/src/components/Menu.svelte +195 -346
  13. package/src/components/MultiSelect.svelte +238 -390
  14. package/src/components/PaletteManager.svelte +15 -5
  15. package/src/components/Pill.svelte +19 -14
  16. package/src/components/Range.svelte +8 -3
  17. package/src/components/Rating.svelte +19 -9
  18. package/src/components/SearchFilter.svelte +11 -3
  19. package/src/components/Select.svelte +265 -454
  20. package/src/components/Stepper.svelte +9 -6
  21. package/src/components/Switch.svelte +11 -11
  22. package/src/components/Table.svelte +0 -1
  23. package/src/components/Tabs.svelte +96 -172
  24. package/src/components/Timeline.svelte +5 -5
  25. package/src/components/Toggle.svelte +55 -119
  26. package/src/components/Toolbar.svelte +24 -23
  27. package/src/components/Tree.svelte +115 -584
  28. package/src/components/UploadFileStatus.svelte +83 -0
  29. package/src/components/UploadProgress.svelte +131 -0
  30. package/src/components/UploadTarget.svelte +124 -0
  31. package/src/components/index.ts +5 -0
  32. package/src/index.ts +6 -1
  33. package/src/types/button.ts +3 -0
  34. package/src/types/code.ts +4 -4
  35. package/src/types/floating-action.ts +13 -8
  36. package/src/types/floating-navigation.ts +14 -2
  37. package/src/types/index.ts +5 -3
  38. package/src/types/list.ts +10 -6
  39. package/src/types/menu.ts +38 -138
  40. package/src/types/palette.ts +17 -0
  41. package/src/types/select.ts +33 -63
  42. package/src/types/switch.ts +9 -5
  43. package/src/types/table.ts +6 -6
  44. package/src/types/tabs.ts +13 -34
  45. package/src/types/timeline.ts +5 -3
  46. package/src/types/toggle.ts +15 -56
  47. package/src/types/toolbar.ts +1 -1
  48. package/src/types/tree.ts +9 -18
  49. package/src/types/upload-file-status.ts +45 -0
  50. package/src/types/upload-progress.ts +111 -0
  51. package/src/types/upload-target.ts +68 -0
  52. package/src/utils/upload.js +128 -0
  53. package/src/types/item-proxy.ts +0 -358
@@ -1,186 +1,158 @@
1
1
  <script lang="ts">
2
- import type {
3
- MultiSelectProps,
4
- SelectItem,
5
- SelectItemSnippet,
6
- SelectItemHandlers,
7
- SelectStateIcons
8
- } from '../types/select.js'
9
- import { getSnippet, defaultSelectStateIcons } from '../types/select.js'
10
- import { ItemProxy } from '../types/item-proxy.js'
2
+ /**
3
+ * MultiSelect — Trigger + dropdown with List-style flatView content.
4
+ *
5
+ * Same architecture as Select but with toggle selection (dropdown stays open),
6
+ * checkbox indicators, and tags display in trigger.
7
+ *
8
+ * Data attributes:
9
+ * data-select / data-multiselect root container (both for theme compat)
10
+ * data-select-trigger — trigger button
11
+ * data-select-value — selected value display area
12
+ * data-select-tags — tags container
13
+ * data-select-tag — individual tag
14
+ * data-select-tag-text — tag text
15
+ * data-select-tag-remove — tag remove button
16
+ * data-select-count — count indicator when exceeding maxDisplay
17
+ * data-select-placeholder — placeholder text
18
+ * data-select-arrow — dropdown arrow icon
19
+ * data-select-dropdown — dropdown container
20
+ * data-select-option — option items
21
+ * data-select-checkbox — checkbox indicator
22
+ * data-select-group-label — group header (non-interactive)
23
+ * data-select-group-icon — icon inside group label
24
+ * data-select-divider — divider between groups
25
+ * data-path, data-selected, data-checked, data-disabled, data-open, data-size
26
+ */
27
+ // @ts-nocheck
28
+ import type { ProxyItem } from '@rokkit/states'
29
+ import { Wrapper, ProxyTree } from '@rokkit/states'
30
+ import { Navigator, Trigger } from '@rokkit/actions'
31
+ import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
11
32
  import ItemContent from './ItemContent.svelte'
12
- import { ListController } from '@rokkit/states'
13
- import { navigator } from '@rokkit/actions'
14
- import { untrack } from 'svelte'
33
+
34
+ interface MultiSelectIcons {
35
+ opened?: string
36
+ closed?: string
37
+ checked?: string
38
+ remove?: string
39
+ }
15
40
 
16
41
  let {
17
- options = [],
18
- fields: userFields,
42
+ items = [],
43
+ fields = {},
19
44
  value = $bindable<unknown[]>([]),
20
- selected = $bindable<SelectItem[]>([]),
45
+ selected = $bindable<unknown[]>([]),
21
46
  placeholder = 'Select...',
22
47
  size = 'md',
23
- align = 'left',
24
- direction = 'down',
25
- maxRows = 5,
26
- maxDisplay = 3,
27
48
  disabled = false,
49
+ maxDisplay = 3,
50
+ align = 'start',
51
+ direction = 'down',
52
+ icons: userIcons = {} as MultiSelectIcons,
28
53
  onchange,
29
54
  class: className = '',
30
- icons: userIcons,
31
- item: itemSnippet,
32
- groupLabel: groupLabelSnippet,
33
- selectedValues: selectedValuesSnippet,
34
55
  ...snippets
35
- }: MultiSelectProps & { [key: string]: SelectItemSnippet | unknown } = $props()
36
-
37
- // Merge icons with defaults
38
- const icons = $derived<SelectStateIcons>({ ...defaultSelectStateIcons, ...userIcons })
56
+ }: {
57
+ items?: unknown[]
58
+ fields?: Record<string, string>
59
+ value?: unknown[]
60
+ selected?: unknown[]
61
+ placeholder?: string
62
+ size?: string
63
+ disabled?: boolean
64
+ maxDisplay?: number
65
+ align?: 'start' | 'end'
66
+ direction?: 'up' | 'down'
67
+ icons?: MultiSelectIcons
68
+ onchange?: (values: unknown[], items: unknown[]) => void
69
+ class?: string
70
+ [key: string]: unknown
71
+ } = $props()
72
+
73
+ const icons = $derived({ ...DEFAULT_STATE_ICONS.selector, ...DEFAULT_STATE_ICONS.checkbox, ...DEFAULT_STATE_ICONS.action, ...userIcons })
74
+
75
+ // ─── Dropdown state ───────────────────────────────────────────────────────
39
76
 
40
- // Normalize alignment value
41
- const normalizedAlign = $derived(align === 'left' || align === 'start' ? 'left' : 'right')
42
-
43
- // Default row heights by size (used until we measure actual height)
44
- const defaultRowHeight = $derived(size === 'sm' ? 28 : size === 'lg' ? 40 : 34)
45
-
46
- // Measured row height (updated when dropdown opens)
47
- let measuredRowHeight = $state<number | null>(null)
48
-
49
- // Use measured height if available, otherwise fall back to default
50
- const maxHeight = $derived(maxRows * (measuredRowHeight ?? defaultRowHeight))
77
+ let isOpen = $state(false)
78
+ let selectRef = $state<HTMLElement | null>(null)
79
+ let triggerRef = $state<HTMLElement | null>(null)
80
+ let dropdownRef = $state<HTMLElement | null>(null)
51
81
 
52
- /**
53
- * Create an ItemProxy for the given item
54
- */
55
- function createProxy(item: SelectItem): ItemProxy {
56
- return new ItemProxy(item, userFields)
57
- }
82
+ // ─── Pre-process items ────────────────────────────────────────────────────
58
83
 
59
- // ─── Flatten options into navigable items for the controller ────
84
+ const childrenField = $derived(fields?.children || 'children')
60
85
 
61
- /** Flat array of raw selectable items (for controller) */
62
- const flatItems = $derived.by(() => {
63
- const items: SelectItem[] = []
64
- for (const option of options) {
65
- const proxy = createProxy(option)
66
- if (proxy.hasChildren) {
67
- for (const child of proxy.children) {
68
- items.push(child as SelectItem)
69
- }
70
- } else {
71
- items.push(option)
86
+ // Force groups expanded + disabled (non-navigable labels)
87
+ const processedItems = $derived(
88
+ items.map((item) => {
89
+ const children = item[childrenField]
90
+ if (Array.isArray(children) && children.length > 0) {
91
+ return { ...item, expanded: true, disabled: true }
72
92
  }
73
- }
74
- return items
75
- })
76
-
77
- /** Map from raw item → flat index key (for data-path). Uses Map to support primitives. */
78
- const itemPathMap = $derived.by(() => {
79
- const map = new Map<unknown, string>()
80
- flatItems.forEach((item, index) => {
81
- map.set(item, String(index))
93
+ return item
82
94
  })
83
- return map
84
- })
95
+ )
85
96
 
86
- // ─── Controller + Navigator ────────────────────────────────────
97
+ // ─── Wrapper ──────────────────────────────────────────────────────────────
87
98
 
88
- let isOpen = $state(false)
89
- let selectRef = $state<HTMLDivElement | null>(null)
90
- let dropdownRef = $state<HTMLDivElement | null>(null)
99
+ function handleSelect(extractedValue: unknown, proxy: ProxyItem) {
100
+ if (proxy.disabled) return
101
+ toggleItemSelection(extractedValue)
102
+ }
91
103
 
92
- let controller = untrack(() => new ListController(flatItems, undefined, userFields))
104
+ const proxyTree = $derived(new ProxyTree(processedItems, fields))
105
+ const wrapper = $derived(new Wrapper(proxyTree, { onselect: handleSelect }))
93
106
 
107
+ // Override cancel/blur to close dropdown
94
108
  $effect(() => {
95
- controller.update(flatItems)
96
- })
97
-
98
- // Find selected items based on current value (extracted primitives)
99
- const selectedItems = $derived.by(() => {
100
- if (!value || value.length === 0) return [] as { proxy: ItemProxy; original: SelectItem }[]
101
- return flatItems
102
- .filter((item) => {
103
- const proxy = createProxy(item)
104
- const extracted = proxy.itemValue
105
- return value.some((v) => v === extracted)
106
- })
107
- .map((item) => ({ proxy: createProxy(item), original: item }))
109
+ const w = wrapper
110
+ w.cancel = () => {
111
+ isOpen = false
112
+ triggerRef?.focus()
113
+ }
114
+ w.blur = () => {
115
+ isOpen = false
116
+ }
108
117
  })
109
118
 
110
- // Focus the element matching controller.focusedKey on navigator action events
119
+ // When wrapper recreates while open, focus first item
111
120
  $effect(() => {
112
- if (!dropdownRef) return
113
- const el = dropdownRef
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' })
125
- }
126
- }
127
- }
128
-
129
- if (detail.name === 'select') {
130
- handleSelectAction()
131
- }
132
- }
133
-
134
- el.addEventListener('action', onAction)
135
- return () => el.removeEventListener('action', onAction)
121
+ const _w = wrapper
122
+ if (isOpen) _w.first(null)
136
123
  })
137
124
 
138
- /**
139
- * Handle the navigator's select action — toggle selection (don't close)
140
- */
141
- function handleSelectAction() {
142
- const key = controller.focusedKey
143
- if (!key) return
144
-
145
- const proxy = controller.lookup.get(key)
146
- if (!proxy) return
125
+ // ─── Selection logic ──────────────────────────────────────────────────────
147
126
 
148
- const item = proxy.value as SelectItem
149
- toggleItemSelection(item)
150
- }
151
-
152
- /**
153
- * Sync DOM focus to controller state
154
- */
155
- function handleFocusIn(event: FocusEvent) {
156
- const target = event.target as HTMLElement
157
- if (!target) return
158
- const path = target.dataset.path
159
- if (path !== undefined) {
160
- controller.moveTo(path)
161
- }
127
+ function isItemSelected(extractedValue: unknown): boolean {
128
+ return (value ?? []).some((v) => v === extractedValue)
162
129
  }
163
130
 
164
- // ─── Selection logic ───────────────────────────────────────────
165
-
166
- function toggleItemSelection(item: SelectItem) {
167
- const proxy = createProxy(item)
168
- if (proxy.disabled) return
169
-
170
- const extracted = proxy.itemValue
171
- const isAlreadySelected = (value ?? []).some((v) => v === extracted)
131
+ function toggleItemSelection(extractedValue: unknown) {
132
+ const currentValues = value ?? []
133
+ const alreadySelected = currentValues.some((v) => v === extractedValue)
172
134
 
173
135
  let newValues: unknown[]
174
- let newItems: SelectItem[]
175
-
176
- if (isAlreadySelected) {
177
- newValues = (value ?? []).filter((v) => v !== extracted)
178
- newItems = selectedItems
179
- .filter((si) => si.proxy.itemValue !== extracted)
180
- .map((si) => si.original)
136
+ let newItems: unknown[]
137
+
138
+ if (alreadySelected) {
139
+ newValues = currentValues.filter((v) => v !== extractedValue)
140
+ // Rebuild selected items from remaining values
141
+ newItems = []
142
+ for (const [, proxy] of wrapper.lookup) {
143
+ if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
144
+ newItems.push(proxy.original)
145
+ }
146
+ }
181
147
  } else {
182
- newValues = [...(value ?? []), extracted]
183
- newItems = [...selectedItems.map((si) => si.original), item]
148
+ newValues = [...currentValues, extractedValue]
149
+ // Rebuild selected items from lookup to include all values
150
+ newItems = []
151
+ for (const [, proxy] of wrapper.lookup) {
152
+ if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
153
+ newItems.push(proxy.original)
154
+ }
155
+ }
184
156
  }
185
157
 
186
158
  value = newValues
@@ -188,234 +160,96 @@
188
160
  onchange?.(newValues, newItems)
189
161
  }
190
162
 
191
- function removeItem(item: { proxy: ItemProxy; original: SelectItem }) {
192
- const extracted = item.proxy.itemValue
193
- const newValues = (value ?? []).filter((v) => v !== extracted)
194
- const newItems = selectedItems
195
- .filter((si) => si.proxy.itemValue !== extracted)
196
- .map((si) => si.original)
197
-
163
+ function removeTag(extractedValue: unknown) {
164
+ const currentValues = value ?? []
165
+ const newValues = currentValues.filter((v) => v !== extractedValue)
166
+ const newItems: unknown[] = []
167
+ for (const [, proxy] of wrapper.lookup) {
168
+ if (!proxy.hasChildren && newValues.some((v) => v === proxy.value)) {
169
+ newItems.push(proxy.original)
170
+ }
171
+ }
198
172
  value = newValues
199
173
  selected = newItems
200
174
  onchange?.(newValues, newItems)
201
175
  }
202
176
 
203
- // ─── Dropdown open/close ───────────────────────────────────────
204
-
205
- function toggleDropdown() {
206
- if (disabled) return
207
- if (isOpen) {
208
- closeDropdown()
209
- } else {
210
- openDropdown()
211
- }
212
- }
213
-
214
- function openDropdown() {
215
- if (disabled || isOpen) return
216
- isOpen = true
217
- controller.moveFirst()
218
- requestAnimationFrame(() => {
219
- measureRowHeight()
220
- focusCurrentItem()
221
- })
222
- }
223
-
224
- function closeDropdown() {
225
- isOpen = false
226
- }
177
+ // ─── Selected items for tags display ──────────────────────────────────────
227
178
 
228
- function focusCurrentItem() {
229
- if (!dropdownRef || !controller.focusedKey) return
230
- const target = dropdownRef.querySelector(
231
- `[data-path="${controller.focusedKey}"]`
232
- ) as HTMLElement | null
233
- if (target) {
234
- target.focus()
235
- target.scrollIntoView?.({ block: 'nearest' })
236
- }
237
- }
238
-
239
- function measureRowHeight() {
240
- const dropdown = selectRef?.querySelector('[data-select-dropdown]')
241
- if (dropdown) {
242
- const firstOption = dropdown.querySelector('[data-select-option]')
243
- if (firstOption) {
244
- const height = firstOption.getBoundingClientRect().height
245
- if (height > 0) {
246
- measuredRowHeight = height
247
- }
179
+ const selectedProxies = $derived.by(() => {
180
+ const vals = value ?? []
181
+ if (vals.length === 0) return []
182
+ const result: ProxyItem[] = []
183
+ for (const [, proxy] of wrapper.lookup) {
184
+ if (!proxy.hasChildren && vals.some((v) => v === proxy.value)) {
185
+ result.push(proxy)
248
186
  }
249
187
  }
250
- }
251
-
252
- // ─── Trigger keyboard handling ─────────────────────────────────
253
-
254
- function handleTriggerKeyDown(event: KeyboardEvent) {
255
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
256
- event.preventDefault()
257
- openDropdown()
258
- } else if (event.key === 'Enter' || event.key === ' ') {
259
- event.preventDefault()
260
- toggleDropdown()
261
- }
262
- }
263
-
264
- // ─── Escape + click-outside ────────────────────────────────────
265
-
266
- function handleEscapeKey(event: KeyboardEvent) {
267
- if (!isOpen) return
268
- if (event.key === 'Escape') {
269
- event.preventDefault()
270
- closeDropdown()
271
- const trigger = selectRef?.querySelector('[data-select-trigger]') as HTMLElement | undefined
272
- trigger?.focus()
273
- }
274
- }
188
+ return result
189
+ })
275
190
 
276
- function handleClickOutside(event: MouseEvent) {
277
- if (selectRef && !selectRef.contains(event.target as Node)) {
278
- closeDropdown()
279
- }
280
- }
191
+ // ─── Trigger action ───────────────────────────────────────────────────────
281
192
 
282
193
  $effect(() => {
283
- if (isOpen) {
284
- document.addEventListener('click', handleClickOutside, true)
285
- document.addEventListener('keydown', handleEscapeKey)
286
- }
287
- return () => {
288
- document.removeEventListener('click', handleClickOutside, true)
289
- document.removeEventListener('keydown', handleEscapeKey)
290
- }
194
+ if (!triggerRef || !selectRef || disabled) return
195
+ const t = new Trigger(triggerRef, selectRef, {
196
+ isOpen: () => isOpen,
197
+ onopen: () => {
198
+ isOpen = true
199
+ requestAnimationFrame(() => wrapper.first(null))
200
+ },
201
+ onclose: () => { isOpen = false },
202
+ onlast: () => requestAnimationFrame(() => wrapper.last(null))
203
+ })
204
+ return () => t.destroy()
291
205
  })
292
206
 
293
- // ─── Snippet + rendering helpers ───────────────────────────────
207
+ // ─── Navigator on dropdown ────────────────────────────────────────────────
294
208
 
295
- /**
296
- * Handle direct Enter/Space on an option.
297
- * Stops propagation to prevent navigator from double-handling.
298
- */
299
- function handleItemToggle(item: SelectItem) {
300
- toggleItemSelection(item)
301
- }
209
+ $effect(() => {
210
+ if (!isOpen || !dropdownRef) return
211
+ const dir = getComputedStyle(dropdownRef).direction || 'ltr'
212
+ const nav = new Navigator(dropdownRef, wrapper, { dir })
213
+ return () => nav.destroy()
214
+ })
302
215
 
303
- /**
304
- * Create handlers object for custom snippets
305
- */
306
- function createHandlers(item: SelectItem): SelectItemHandlers {
307
- return {
308
- onclick: () => handleItemToggle(item),
309
- onkeydown: (event: KeyboardEvent) => {
310
- if (event.key === 'Enter' || event.key === ' ') {
311
- event.preventDefault()
312
- event.stopPropagation()
313
- handleItemToggle(item)
314
- }
216
+ // DOM focus sync
217
+ $effect(() => {
218
+ const key = wrapper.focusedKey
219
+ if (!isOpen || !dropdownRef || !key) return
220
+ requestAnimationFrame(() => {
221
+ const target = dropdownRef?.querySelector(`[data-path="${key}"]`) as HTMLElement | null
222
+ if (target && target !== document.activeElement) {
223
+ target.focus()
224
+ target.scrollIntoView?.({ block: 'nearest' })
315
225
  }
316
- }
317
- }
226
+ })
227
+ })
318
228
 
319
- /**
320
- * Resolve which snippet to use for an item
321
- */
322
- function resolveItemSnippet(proxy: ItemProxy): SelectItemSnippet | null {
323
- const snippetName = proxy.snippetName
324
- if (snippetName) {
325
- const namedSnippet = getSnippet(snippets, snippetName)
326
- if (namedSnippet) {
327
- return namedSnippet as SelectItemSnippet
229
+ // ─── Helpers ──────────────────────────────────────────────────────────────
230
+
231
+ const groupDividers = $derived.by(() => {
232
+ const set = new Set<string>()
233
+ let foundFirst = false
234
+ for (const node of wrapper.flatView) {
235
+ if (node.hasChildren) {
236
+ if (foundFirst) set.add(node.key)
237
+ foundFirst = true
328
238
  }
329
239
  }
330
- return itemSnippet ?? null
331
- }
332
-
333
- /**
334
- * Check if an item is currently selected
335
- */
336
- function isSelected(proxy: ItemProxy): boolean {
337
- const extracted = proxy.itemValue
338
- return (value ?? []).some((v) => v === extracted)
339
- }
340
-
341
- function shouldShowDivider(optionIndex: number, isGroup: boolean): boolean {
342
- return isGroup && optionIndex > 0
343
- }
344
-
345
- /**
346
- * Get the data-path key for a raw item
347
- */
348
- function getPathKey(item: SelectItem): string | undefined {
349
- return itemPathMap.get(item)
350
- }
240
+ return set
241
+ })
351
242
  </script>
352
243
 
353
- {#snippet defaultOption(proxy: ItemProxy, handlers: SelectItemHandlers, isItemSelected: boolean, pathKey: string | undefined)}
354
- <button
355
- type="button"
356
- data-select-option
357
- data-path={pathKey}
358
- data-disabled={proxy.disabled || undefined}
359
- data-selected={isItemSelected || undefined}
360
- role="option"
361
- aria-selected={isItemSelected}
362
- disabled={proxy.disabled}
363
- aria-label={proxy.label}
364
- onkeydown={handlers.onkeydown}
365
- >
366
- <span data-select-checkbox data-checked={isItemSelected || undefined}>
367
- {#if isItemSelected}
368
- <span class={icons.checked} aria-hidden="true"></span>
369
- {/if}
370
- </span>
371
- <ItemContent {proxy} />
372
- </button>
373
- {/snippet}
374
-
375
- {#snippet defaultGroupLabel(proxy: ItemProxy)}
376
- <div data-select-group-label role="presentation">
377
- {#if proxy.icon}
378
- <span data-select-group-icon class={proxy.icon} aria-hidden="true"></span>
379
- {/if}
380
- <span>{proxy.text}</span>
381
- </div>
382
- {/snippet}
383
-
384
- {#snippet renderOption(item: SelectItem, proxy: ItemProxy, pathKey: string | undefined)}
385
- {@const customSnippet = resolveItemSnippet(proxy)}
386
- {@const handlers = createHandlers(item)}
387
- {@const isItemSelected = isSelected(proxy)}
388
- {#if customSnippet}
389
- <div
390
- data-select-option
391
- data-select-option-custom
392
- data-path={pathKey}
393
- data-disabled={proxy.disabled || undefined}
394
- data-selected={isItemSelected || undefined}
395
- >
396
- <svelte:boundary>
397
- {@render customSnippet(item, proxy.fields, handlers, isItemSelected)}
398
- {#snippet failed()}
399
- {@render defaultOption(proxy, handlers, isItemSelected, pathKey)}
400
- {/snippet}
401
- </svelte:boundary>
402
- </div>
403
- {:else}
404
- {@render defaultOption(proxy, handlers, isItemSelected, pathKey)}
405
- {/if}
244
+ {#snippet defaultOptionContent(proxy: ProxyItem)}
245
+ <ItemContent {proxy} />
406
246
  {/snippet}
407
247
 
408
- {#snippet renderGroupLabel(proxy: ItemProxy)}
409
- {#if groupLabelSnippet}
410
- <svelte:boundary>
411
- {@render groupLabelSnippet(proxy.original as SelectItem, proxy.fields)}
412
- {#snippet failed()}
413
- {@render defaultGroupLabel(proxy)}
414
- {/snippet}
415
- </svelte:boundary>
416
- {:else}
417
- {@render defaultGroupLabel(proxy)}
248
+ {#snippet defaultGroupContent(proxy: ProxyItem)}
249
+ {#if proxy.get('icon')}
250
+ <span data-select-group-icon class={proxy.get('icon')} aria-hidden="true"></span>
418
251
  {/if}
252
+ <span>{proxy.label}</span>
419
253
  {/snippet}
420
254
 
421
255
  <div
@@ -425,45 +259,39 @@
425
259
  data-open={isOpen || undefined}
426
260
  data-size={size}
427
261
  data-disabled={disabled || undefined}
428
- data-align={normalizedAlign}
262
+ data-align={align}
429
263
  data-direction={direction}
430
264
  class={className || undefined}
431
265
  >
432
266
  <button
267
+ bind:this={triggerRef}
433
268
  type="button"
434
269
  data-select-trigger
435
270
  {disabled}
436
271
  aria-haspopup="listbox"
437
272
  aria-expanded={isOpen}
438
- onclick={toggleDropdown}
439
- onkeydown={handleTriggerKeyDown}
440
273
  >
441
274
  <span data-select-value>
442
- {#if selectedValuesSnippet && selectedItems.length > 0}
443
- {@render selectedValuesSnippet(
444
- selectedItems.map((item) => item.original),
445
- selectedItems[0]?.proxy.fields ?? {}
446
- )}
447
- {:else if selectedItems.length > 0}
448
- {#if selectedItems.length <= maxDisplay}
275
+ {#if selectedProxies.length > 0}
276
+ {#if selectedProxies.length <= maxDisplay}
449
277
  <span data-select-tags>
450
- {#each selectedItems as item (item.proxy.itemValue)}
278
+ {#each selectedProxies as proxy (proxy.value)}
451
279
  <span data-select-tag>
452
- <span data-select-tag-text>{item.proxy.text}</span>
280
+ <span data-select-tag-text>{proxy.label}</span>
453
281
  <span
454
282
  role="button"
455
283
  tabindex="0"
456
284
  data-select-tag-remove
457
- aria-label="Remove {item.proxy.text}"
285
+ aria-label="Remove {proxy.label}"
458
286
  onclick={(e) => {
459
287
  e.stopPropagation()
460
- removeItem(item)
288
+ removeTag(proxy.value)
461
289
  }}
462
290
  onkeydown={(e) => {
463
291
  if (e.key === 'Enter' || e.key === ' ') {
464
292
  e.preventDefault()
465
293
  e.stopPropagation()
466
- removeItem(item)
294
+ removeTag(proxy.value)
467
295
  }
468
296
  }}
469
297
  >
@@ -473,7 +301,7 @@
473
301
  {/each}
474
302
  </span>
475
303
  {:else}
476
- <span data-select-count>{selectedItems.length} selected</span>
304
+ <span data-select-count>{selectedProxies.length} selected</span>
477
305
  {/if}
478
306
  {:else}
479
307
  <span data-select-placeholder>{placeholder}</span>
@@ -490,30 +318,50 @@
490
318
  role="listbox"
491
319
  aria-multiselectable="true"
492
320
  aria-orientation="vertical"
493
- style="max-height: {maxHeight}px"
494
- onfocusin={handleFocusIn}
495
- use:navigator={{ wrapper: controller, orientation: 'vertical' }}
496
321
  >
497
- {#each options as option, optionIndex (optionIndex)}
498
- {@const proxy = createProxy(option)}
499
-
500
- {#if proxy.hasChildren}
501
- {#if shouldShowDivider(optionIndex, true)}
502
- <div data-select-divider role="separator"></div>
322
+ {#each wrapper.flatView as node (node.key)}
323
+ {@const proxy = node.proxy}
324
+ {@const sel = !node.hasChildren && isItemSelected(proxy.value)}
325
+ {@const content = resolveSnippet(snippets as Record<string, unknown>, proxy, node.hasChildren ? GROUP_SNIPPET : ITEM_SNIPPET)}
326
+
327
+ {#if node.type === 'separator'}
328
+ <hr data-select-separator />
329
+ {:else if node.hasChildren}
330
+ {#if groupDividers.has(node.key)}
331
+ <div data-select-divider></div>
503
332
  {/if}
504
-
505
- <div data-select-group>
506
- {@render renderGroupLabel(proxy)}
507
-
508
- {#each proxy.children as child, childIndex (childIndex)}
509
- {@const childProxy = proxy.createChildProxy(child)}
510
- {@const pathKey = getPathKey(child as SelectItem)}
511
- {@render renderOption(child as SelectItem, childProxy, pathKey)}
512
- {/each}
333
+ <div data-select-group-label role="presentation">
334
+ {#if content}
335
+ {@render content(proxy)}
336
+ {:else}
337
+ {@render defaultGroupContent(proxy)}
338
+ {/if}
513
339
  </div>
514
340
  {:else}
515
- {@const pathKey = getPathKey(option)}
516
- {@render renderOption(option, proxy, pathKey)}
341
+ <button
342
+ type="button"
343
+ data-select-option
344
+ data-path={node.key}
345
+ data-level={node.level}
346
+ data-selected={sel || undefined}
347
+ data-disabled={proxy.disabled || undefined}
348
+ role="option"
349
+ aria-selected={sel}
350
+ aria-label={proxy.label}
351
+ disabled={proxy.disabled || disabled}
352
+ tabindex="-1"
353
+ >
354
+ <span data-select-checkbox data-checked={sel || undefined}>
355
+ {#if sel}
356
+ <span class={icons.checked} aria-hidden="true"></span>
357
+ {/if}
358
+ </span>
359
+ {#if content}
360
+ {@render content(proxy)}
361
+ {:else}
362
+ {@render defaultOptionContent(proxy)}
363
+ {/if}
364
+ </button>
517
365
  {/if}
518
366
  {/each}
519
367
  </div>