@rokkit/ui 1.0.0-next.124 → 1.0.0-next.127

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 (146) hide show
  1. package/README.md +198 -101
  2. package/package.json +52 -34
  3. package/src/components/BreadCrumbs.svelte +82 -0
  4. package/src/components/Button.svelte +87 -0
  5. package/src/components/ButtonGroup.svelte +18 -0
  6. package/src/components/Card.svelte +61 -0
  7. package/src/components/Carousel.svelte +169 -0
  8. package/src/components/Code.svelte +185 -0
  9. package/src/components/Connector.svelte +46 -0
  10. package/src/components/FloatingAction.svelte +331 -0
  11. package/src/components/FloatingNavigation.svelte +228 -0
  12. package/src/components/ItemContent.svelte +24 -0
  13. package/src/components/List.svelte +476 -0
  14. package/src/components/Menu.svelte +421 -0
  15. package/src/components/MultiSelect.svelte +521 -0
  16. package/src/components/PaletteManager.svelte +354 -0
  17. package/src/components/Pill.svelte +78 -0
  18. package/src/components/ProgressBar.svelte +31 -0
  19. package/src/components/Range.svelte +325 -0
  20. package/src/components/Rating.svelte +91 -0
  21. package/src/components/Reveal.svelte +58 -0
  22. package/src/components/SearchFilter.svelte +80 -0
  23. package/src/components/Select.svelte +585 -0
  24. package/src/{Shine.svelte → components/Shine.svelte} +29 -21
  25. package/src/components/Stepper.svelte +169 -0
  26. package/src/components/Switch.svelte +75 -0
  27. package/src/components/Table.svelte +243 -0
  28. package/src/components/Tabs.svelte +268 -0
  29. package/src/components/Tilt.svelte +68 -0
  30. package/src/components/Timeline.svelte +61 -0
  31. package/src/components/Toggle.svelte +157 -0
  32. package/src/components/Toolbar.svelte +307 -0
  33. package/src/components/ToolbarGroup.svelte +17 -0
  34. package/src/components/Tree.svelte +613 -0
  35. package/src/components/index.ts +33 -0
  36. package/src/index.ts +41 -0
  37. package/src/types/button.ts +83 -0
  38. package/src/types/code.ts +46 -0
  39. package/src/types/floating-action.ts +118 -0
  40. package/src/types/floating-navigation.ts +68 -0
  41. package/src/types/index.ts +53 -0
  42. package/src/types/item-proxy.ts +358 -0
  43. package/src/types/list.ts +196 -0
  44. package/src/types/menu.ts +195 -0
  45. package/src/types/palette.ts +143 -0
  46. package/src/types/range.ts +51 -0
  47. package/src/types/search-filter.ts +67 -0
  48. package/src/types/select.ts +206 -0
  49. package/src/types/switch.ts +64 -0
  50. package/src/types/table.ts +210 -0
  51. package/src/types/tabs.ts +124 -0
  52. package/src/types/timeline.ts +51 -0
  53. package/src/types/toggle.ts +109 -0
  54. package/src/types/toolbar.ts +164 -0
  55. package/src/types/tree.ts +259 -0
  56. package/src/utils/palette.ts +582 -0
  57. package/src/utils/shiki.ts +122 -0
  58. package/dist/constants.d.ts +0 -2
  59. package/dist/index.d.ts +0 -41
  60. package/dist/lib/fields.d.ts +0 -16
  61. package/dist/lib/form.d.ts +0 -95
  62. package/dist/lib/index.d.ts +0 -6
  63. package/dist/lib/layout.d.ts +0 -7
  64. package/dist/lib/nested.d.ts +0 -48
  65. package/dist/lib/schema.d.ts +0 -7
  66. package/dist/lib/select.d.ts +0 -8
  67. package/dist/lib/tree.d.ts +0 -9
  68. package/dist/tree/List.spec.svelte.d.ts +0 -1
  69. package/dist/tree/Node.spec.svelte.d.ts +0 -1
  70. package/dist/tree/Root.spec.svelte.d.ts +0 -1
  71. package/dist/types.d.ts +0 -5
  72. package/dist/wrappers/index.d.ts +0 -3
  73. package/src/Accordion.svelte +0 -118
  74. package/src/BreadCrumbs.svelte +0 -32
  75. package/src/Button.svelte +0 -57
  76. package/src/Calendar.svelte +0 -93
  77. package/src/Card.svelte +0 -45
  78. package/src/Carousel.svelte +0 -49
  79. package/src/CheckBox.svelte +0 -56
  80. package/src/Connector.svelte +0 -40
  81. package/src/DropDown.svelte +0 -68
  82. package/src/DropSearch.svelte +0 -37
  83. package/src/Fillable.svelte +0 -19
  84. package/src/GraphPaper.svelte +0 -43
  85. package/src/Icon.svelte +0 -81
  86. package/src/Item.svelte +0 -25
  87. package/src/Link.svelte +0 -21
  88. package/src/List.svelte +0 -89
  89. package/src/ListBody.svelte +0 -43
  90. package/src/Message.svelte +0 -11
  91. package/src/MultiSelect.svelte +0 -48
  92. package/src/NestedList.svelte +0 -78
  93. package/src/NestedPaginator.svelte +0 -63
  94. package/src/Node.svelte +0 -76
  95. package/src/Overlay.svelte +0 -21
  96. package/src/PageNavigator.svelte +0 -94
  97. package/src/PickOne.svelte +0 -60
  98. package/src/Pill.svelte +0 -41
  99. package/src/ProgressBar.svelte +0 -21
  100. package/src/ProgressDots.svelte +0 -53
  101. package/src/RadioGroup.svelte +0 -52
  102. package/src/Range.svelte +0 -45
  103. package/src/RangeMinMax.svelte +0 -124
  104. package/src/RangeSlider.svelte +0 -79
  105. package/src/RangeTick.svelte +0 -28
  106. package/src/Rating.svelte +0 -95
  107. package/src/ResponsiveGrid.svelte +0 -88
  108. package/src/Scrollable.svelte +0 -7
  109. package/src/Select.svelte +0 -114
  110. package/src/Separator.svelte +0 -1
  111. package/src/Slider.svelte +0 -14
  112. package/src/SlidingColumns.svelte +0 -50
  113. package/src/Stage.svelte +0 -41
  114. package/src/Stepper.svelte +0 -66
  115. package/src/Summary.svelte +0 -22
  116. package/src/Switch.svelte +0 -106
  117. package/src/TableCell.svelte +0 -51
  118. package/src/TableHeaderCell.svelte +0 -54
  119. package/src/Tabs.svelte +0 -176
  120. package/src/Tilt.svelte +0 -66
  121. package/src/Toggle.svelte +0 -58
  122. package/src/ToggleThemeMode.svelte +0 -23
  123. package/src/Tree.svelte +0 -80
  124. package/src/TreeTable.svelte +0 -171
  125. package/src/ValidationReport.svelte +0 -23
  126. package/src/constants.js +0 -4
  127. package/src/index.js +0 -48
  128. package/src/lib/fields.js +0 -118
  129. package/src/lib/form.js +0 -72
  130. package/src/lib/index.js +0 -13
  131. package/src/lib/layout.js +0 -63
  132. package/src/lib/nested.js +0 -192
  133. package/src/lib/schema.js +0 -32
  134. package/src/lib/select.js +0 -38
  135. package/src/lib/tree.js +0 -22
  136. package/src/tree/List.spec.svelte.js +0 -84
  137. package/src/tree/List.svelte +0 -78
  138. package/src/tree/Node.spec.svelte.js +0 -104
  139. package/src/tree/Node.svelte +0 -80
  140. package/src/tree/Root.spec.svelte.js +0 -63
  141. package/src/tree/Root.svelte +0 -81
  142. package/src/types.js +0 -9
  143. package/src/wrappers/Category.svelte +0 -27
  144. package/src/wrappers/Section.svelte +0 -16
  145. package/src/wrappers/Wrapper.svelte +0 -12
  146. package/src/wrappers/index.js +0 -3
@@ -0,0 +1,521 @@
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'
11
+ import ItemContent from './ItemContent.svelte'
12
+ import { ListController } from '@rokkit/states'
13
+ import { navigator } from '@rokkit/actions'
14
+ import { untrack } from 'svelte'
15
+
16
+ let {
17
+ options = [],
18
+ fields: userFields,
19
+ value = $bindable<unknown[]>([]),
20
+ selected = $bindable<SelectItem[]>([]),
21
+ placeholder = 'Select...',
22
+ size = 'md',
23
+ align = 'left',
24
+ direction = 'down',
25
+ maxRows = 5,
26
+ maxDisplay = 3,
27
+ disabled = false,
28
+ onchange,
29
+ class: className = '',
30
+ icons: userIcons,
31
+ item: itemSnippet,
32
+ groupLabel: groupLabelSnippet,
33
+ selectedValues: selectedValuesSnippet,
34
+ ...snippets
35
+ }: MultiSelectProps & { [key: string]: SelectItemSnippet | unknown } = $props()
36
+
37
+ // Merge icons with defaults
38
+ const icons = $derived<SelectStateIcons>({ ...defaultSelectStateIcons, ...userIcons })
39
+
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))
51
+
52
+ /**
53
+ * Create an ItemProxy for the given item
54
+ */
55
+ function createProxy(item: SelectItem): ItemProxy {
56
+ return new ItemProxy(item, userFields)
57
+ }
58
+
59
+ // ─── Flatten options into navigable items for the controller ────
60
+
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)
72
+ }
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))
82
+ })
83
+ return map
84
+ })
85
+
86
+ // ─── Controller + Navigator ────────────────────────────────────
87
+
88
+ let isOpen = $state(false)
89
+ let selectRef = $state<HTMLDivElement | null>(null)
90
+ let dropdownRef = $state<HTMLDivElement | null>(null)
91
+
92
+ let controller = untrack(() => new ListController(flatItems, undefined, userFields))
93
+
94
+ $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 }))
108
+ })
109
+
110
+ // Focus the element matching controller.focusedKey on navigator action events
111
+ $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)
136
+ })
137
+
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
147
+
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
+ }
162
+ }
163
+
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)
172
+
173
+ 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)
181
+ } else {
182
+ newValues = [...(value ?? []), extracted]
183
+ newItems = [...selectedItems.map((si) => si.original), item]
184
+ }
185
+
186
+ value = newValues
187
+ selected = newItems
188
+ onchange?.(newValues, newItems)
189
+ }
190
+
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
+
198
+ value = newValues
199
+ selected = newItems
200
+ onchange?.(newValues, newItems)
201
+ }
202
+
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
+ }
227
+
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
+ }
248
+ }
249
+ }
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
+ }
275
+
276
+ function handleClickOutside(event: MouseEvent) {
277
+ if (selectRef && !selectRef.contains(event.target as Node)) {
278
+ closeDropdown()
279
+ }
280
+ }
281
+
282
+ $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
+ }
291
+ })
292
+
293
+ // ─── Snippet + rendering helpers ───────────────────────────────
294
+
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
+ }
302
+
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
+ }
315
+ }
316
+ }
317
+ }
318
+
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
328
+ }
329
+ }
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
+ }
351
+ </script>
352
+
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}
406
+ {/snippet}
407
+
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)}
418
+ {/if}
419
+ {/snippet}
420
+
421
+ <div
422
+ bind:this={selectRef}
423
+ data-select
424
+ data-multiselect
425
+ data-open={isOpen || undefined}
426
+ data-size={size}
427
+ data-disabled={disabled || undefined}
428
+ data-align={normalizedAlign}
429
+ data-direction={direction}
430
+ class={className || undefined}
431
+ >
432
+ <button
433
+ type="button"
434
+ data-select-trigger
435
+ {disabled}
436
+ aria-haspopup="listbox"
437
+ aria-expanded={isOpen}
438
+ onclick={toggleDropdown}
439
+ onkeydown={handleTriggerKeyDown}
440
+ >
441
+ <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}
449
+ <span data-select-tags>
450
+ {#each selectedItems as item (item.proxy.itemValue)}
451
+ <span data-select-tag>
452
+ <span data-select-tag-text>{item.proxy.text}</span>
453
+ <span
454
+ role="button"
455
+ tabindex="0"
456
+ data-select-tag-remove
457
+ aria-label="Remove {item.proxy.text}"
458
+ onclick={(e) => {
459
+ e.stopPropagation()
460
+ removeItem(item)
461
+ }}
462
+ onkeydown={(e) => {
463
+ if (e.key === 'Enter' || e.key === ' ') {
464
+ e.preventDefault()
465
+ e.stopPropagation()
466
+ removeItem(item)
467
+ }
468
+ }}
469
+ >
470
+ <span class={icons.remove} aria-hidden="true"></span>
471
+ </span>
472
+ </span>
473
+ {/each}
474
+ </span>
475
+ {:else}
476
+ <span data-select-count>{selectedItems.length} selected</span>
477
+ {/if}
478
+ {:else}
479
+ <span data-select-placeholder>{placeholder}</span>
480
+ {/if}
481
+ </span>
482
+ <span data-select-arrow class={icons.opened} aria-hidden="true"></span>
483
+ </button>
484
+
485
+ {#if isOpen}
486
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
487
+ <div
488
+ bind:this={dropdownRef}
489
+ data-select-dropdown
490
+ role="listbox"
491
+ aria-multiselectable="true"
492
+ aria-orientation="vertical"
493
+ style="max-height: {maxHeight}px"
494
+ onfocusin={handleFocusIn}
495
+ use:navigator={{ wrapper: controller, orientation: 'vertical' }}
496
+ >
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>
503
+ {/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}
513
+ </div>
514
+ {:else}
515
+ {@const pathKey = getPathKey(option)}
516
+ {@render renderOption(option, proxy, pathKey)}
517
+ {/if}
518
+ {/each}
519
+ </div>
520
+ {/if}
521
+ </div>