sv5ui 1.1.3 → 1.3.0

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 (187) hide show
  1. package/README.md +6 -0
  2. package/dist/Alert/Alert.svelte +33 -22
  3. package/dist/Alert/Alert.svelte.d.ts +1 -1
  4. package/dist/Alert/alert.types.d.ts +4 -0
  5. package/dist/Avatar/Avatar.svelte +72 -46
  6. package/dist/Avatar/avatar.types.d.ts +36 -3
  7. package/dist/Avatar/avatar.variants.d.ts +138 -0
  8. package/dist/Avatar/avatar.variants.js +23 -12
  9. package/dist/Avatar/index.d.ts +1 -1
  10. package/dist/AvatarGroup/AvatarGroup.svelte +11 -6
  11. package/dist/AvatarGroup/AvatarGroup.svelte.d.ts +1 -1
  12. package/dist/AvatarGroup/avatar-group.types.d.ts +18 -3
  13. package/dist/AvatarGroup/avatar-group.variants.d.ts +85 -0
  14. package/dist/AvatarGroup/avatar-group.variants.js +19 -29
  15. package/dist/Badge/Badge.svelte +4 -3
  16. package/dist/Badge/Badge.svelte.d.ts +1 -1
  17. package/dist/Badge/badge.types.d.ts +9 -0
  18. package/dist/Breadcrumb/Breadcrumb.svelte +20 -7
  19. package/dist/Breadcrumb/Breadcrumb.svelte.d.ts +1 -1
  20. package/dist/Breadcrumb/breadcrumb.types.d.ts +5 -1
  21. package/dist/Breadcrumb/breadcrumb.variants.d.ts +15 -5
  22. package/dist/Breadcrumb/breadcrumb.variants.js +7 -3
  23. package/dist/Button/Button.svelte +71 -16
  24. package/dist/Button/Button.svelte.d.ts +0 -1
  25. package/dist/Button/button.types.d.ts +61 -2
  26. package/dist/Calendar/Calendar.svelte +4 -0
  27. package/dist/Calendar/Calendar.svelte.d.ts +1 -1
  28. package/dist/Calendar/calendar.types.d.ts +4 -0
  29. package/dist/Card/Card.svelte +5 -4
  30. package/dist/Card/Card.svelte.d.ts +1 -1
  31. package/dist/Card/card.types.d.ts +5 -1
  32. package/dist/Checkbox/Checkbox.svelte +37 -11
  33. package/dist/Checkbox/Checkbox.svelte.d.ts +1 -1
  34. package/dist/Checkbox/checkbox.types.d.ts +16 -1
  35. package/dist/Checkbox/checkbox.variants.d.ts +90 -0
  36. package/dist/Checkbox/checkbox.variants.js +73 -4
  37. package/dist/CheckboxGroup/CheckboxGroup.svelte +215 -0
  38. package/dist/CheckboxGroup/CheckboxGroup.svelte.d.ts +5 -0
  39. package/dist/CheckboxGroup/checkbox-group.types.d.ts +130 -0
  40. package/dist/CheckboxGroup/checkbox-group.types.js +1 -0
  41. package/dist/CheckboxGroup/checkbox-group.variants.d.ts +553 -0
  42. package/dist/CheckboxGroup/checkbox-group.variants.js +231 -0
  43. package/dist/CheckboxGroup/index.d.ts +2 -0
  44. package/dist/CheckboxGroup/index.js +1 -0
  45. package/dist/Chip/Chip.svelte +3 -2
  46. package/dist/Chip/Chip.svelte.d.ts +1 -1
  47. package/dist/Chip/chip.types.d.ts +5 -1
  48. package/dist/Chip/chip.variants.d.ts +135 -45
  49. package/dist/Chip/chip.variants.js +9 -9
  50. package/dist/ContextMenu/ContextMenu.svelte +87 -77
  51. package/dist/ContextMenu/ContextMenu.svelte.d.ts +1 -1
  52. package/dist/ContextMenu/context-menu.types.d.ts +9 -3
  53. package/dist/ContextMenu/context-menu.types.js +1 -1
  54. package/dist/ContextMenu/context-menu.variants.d.ts +74 -160
  55. package/dist/ContextMenu/context-menu.variants.js +63 -95
  56. package/dist/DropdownMenu/DropdownMenu.svelte +37 -43
  57. package/dist/DropdownMenu/DropdownMenu.svelte.d.ts +1 -1
  58. package/dist/DropdownMenu/dropdown-menu.types.d.ts +9 -3
  59. package/dist/DropdownMenu/dropdown-menu.types.js +1 -1
  60. package/dist/DropdownMenu/dropdown-menu.variants.d.ts +79 -230
  61. package/dist/DropdownMenu/dropdown-menu.variants.js +68 -111
  62. package/dist/DropdownMenu/index.d.ts +1 -1
  63. package/dist/Empty/Empty.svelte +68 -33
  64. package/dist/Empty/Empty.svelte.d.ts +1 -1
  65. package/dist/Empty/empty.types.d.ts +26 -9
  66. package/dist/Empty/empty.variants.d.ts +150 -130
  67. package/dist/Empty/empty.variants.js +33 -324
  68. package/dist/FieldGroup/FieldGroup.svelte +11 -6
  69. package/dist/FieldGroup/FieldGroup.svelte.d.ts +1 -1
  70. package/dist/FieldGroup/field-group.types.d.ts +4 -0
  71. package/dist/FileUpload/FileUpload.svelte +561 -0
  72. package/dist/FileUpload/FileUpload.svelte.d.ts +8 -0
  73. package/dist/FileUpload/file-upload.types.d.ts +164 -0
  74. package/dist/FileUpload/file-upload.types.js +1 -0
  75. package/dist/FileUpload/file-upload.variants.d.ts +397 -0
  76. package/dist/FileUpload/file-upload.variants.js +224 -0
  77. package/dist/FileUpload/index.d.ts +2 -0
  78. package/dist/FileUpload/index.js +1 -0
  79. package/dist/FormField/FormField.svelte +17 -18
  80. package/dist/FormField/FormField.svelte.d.ts +1 -1
  81. package/dist/FormField/form-field.types.d.ts +4 -0
  82. package/dist/Icon/Icon.svelte +13 -7
  83. package/dist/Icon/icon.types.d.ts +18 -9
  84. package/dist/Input/Input.svelte +30 -29
  85. package/dist/Kbd/Kbd.svelte +13 -3
  86. package/dist/Kbd/Kbd.svelte.d.ts +1 -1
  87. package/dist/Kbd/index.d.ts +1 -1
  88. package/dist/Kbd/kbd.types.d.ts +15 -1
  89. package/dist/Kbd/kbd.variants.d.ts +92 -30
  90. package/dist/Kbd/kbd.variants.js +55 -35
  91. package/dist/Kbd/useKbd.svelte.d.ts +2 -2
  92. package/dist/Kbd/useKbd.svelte.js +34 -41
  93. package/dist/Link/Link.svelte +69 -24
  94. package/dist/Link/Link.svelte.d.ts +1 -1
  95. package/dist/Link/link.types.d.ts +26 -8
  96. package/dist/Link/link.variants.d.ts +35 -60
  97. package/dist/Link/link.variants.js +8 -110
  98. package/dist/Modal/Modal.svelte +9 -1
  99. package/dist/Modal/modal.types.d.ts +5 -0
  100. package/dist/Modal/modal.variants.d.ts +5 -0
  101. package/dist/Modal/modal.variants.js +1 -0
  102. package/dist/Pagination/Pagination.svelte +143 -94
  103. package/dist/Pagination/Pagination.svelte.d.ts +1 -1
  104. package/dist/Pagination/index.d.ts +1 -1
  105. package/dist/Pagination/pagination.types.d.ts +21 -2
  106. package/dist/Pagination/pagination.variants.d.ts +21 -387
  107. package/dist/Pagination/pagination.variants.js +63 -59
  108. package/dist/PinInput/PinInput.svelte +150 -0
  109. package/dist/PinInput/PinInput.svelte.d.ts +6 -0
  110. package/dist/PinInput/index.d.ts +2 -0
  111. package/dist/PinInput/index.js +1 -0
  112. package/dist/PinInput/pin-input.types.d.ts +99 -0
  113. package/dist/PinInput/pin-input.types.js +1 -0
  114. package/dist/PinInput/pin-input.variants.d.ts +303 -0
  115. package/dist/PinInput/pin-input.variants.js +196 -0
  116. package/dist/Popover/Popover.svelte +9 -12
  117. package/dist/Popover/Popover.svelte.d.ts +1 -1
  118. package/dist/Popover/popover.types.d.ts +4 -0
  119. package/dist/Popover/popover.variants.d.ts +5 -75
  120. package/dist/Popover/popover.variants.js +6 -16
  121. package/dist/Progress/Progress.svelte +58 -30
  122. package/dist/Progress/progress.types.d.ts +9 -1
  123. package/dist/Progress/progress.variants.d.ts +55 -25
  124. package/dist/Progress/progress.variants.js +34 -28
  125. package/dist/RadioGroup/RadioGroup.svelte +105 -61
  126. package/dist/RadioGroup/RadioGroup.svelte.d.ts +1 -1
  127. package/dist/RadioGroup/radio-group.types.d.ts +16 -1
  128. package/dist/RadioGroup/radio-group.variants.d.ts +90 -0
  129. package/dist/RadioGroup/radio-group.variants.js +73 -4
  130. package/dist/Select/Select.svelte +9 -6
  131. package/dist/Select/Select.svelte.d.ts +1 -1
  132. package/dist/Select/select.types.d.ts +4 -0
  133. package/dist/SelectMenu/SelectMenu.svelte +436 -0
  134. package/dist/SelectMenu/SelectMenu.svelte.d.ts +5 -0
  135. package/dist/SelectMenu/index.d.ts +2 -0
  136. package/dist/SelectMenu/index.js +1 -0
  137. package/dist/SelectMenu/select-menu.types.d.ts +262 -0
  138. package/dist/SelectMenu/select-menu.types.js +1 -0
  139. package/dist/SelectMenu/select-menu.variants.d.ts +759 -0
  140. package/dist/SelectMenu/select-menu.variants.js +33 -0
  141. package/dist/Separator/Separator.svelte +1 -2
  142. package/dist/Separator/separator.variants.d.ts +1 -5
  143. package/dist/Separator/separator.variants.js +2 -2
  144. package/dist/Skeleton/Skeleton.svelte +18 -2
  145. package/dist/Skeleton/Skeleton.svelte.d.ts +1 -1
  146. package/dist/Skeleton/skeleton.types.d.ts +10 -1
  147. package/dist/Slideover/Slideover.svelte +9 -1
  148. package/dist/Slideover/slideover.types.d.ts +5 -0
  149. package/dist/Slideover/slideover.variants.d.ts +20 -5
  150. package/dist/Slideover/slideover.variants.js +4 -29
  151. package/dist/Slider/Slider.svelte +135 -0
  152. package/dist/Slider/Slider.svelte.d.ts +6 -0
  153. package/dist/Slider/index.d.ts +2 -0
  154. package/dist/Slider/index.js +1 -0
  155. package/dist/Slider/slider.types.d.ts +55 -0
  156. package/dist/Slider/slider.types.js +1 -0
  157. package/dist/Slider/slider.variants.d.ts +383 -0
  158. package/dist/Slider/slider.variants.js +102 -0
  159. package/dist/Switch/Switch.svelte +32 -31
  160. package/dist/Switch/Switch.svelte.d.ts +1 -1
  161. package/dist/Switch/switch.types.d.ts +6 -1
  162. package/dist/Switch/switch.variants.js +6 -6
  163. package/dist/Tabs/Tabs.svelte +6 -9
  164. package/dist/Tabs/Tabs.svelte.d.ts +1 -1
  165. package/dist/Tabs/tabs.types.d.ts +4 -0
  166. package/dist/Tabs/tabs.variants.js +2 -0
  167. package/dist/Textarea/Textarea.svelte +26 -25
  168. package/dist/ThemeModeButton/theme-mode-button.types.d.ts +7 -2
  169. package/dist/Timeline/Timeline.svelte +62 -19
  170. package/dist/Timeline/Timeline.svelte.d.ts +1 -1
  171. package/dist/Timeline/index.d.ts +1 -1
  172. package/dist/Timeline/timeline.types.d.ts +8 -0
  173. package/dist/Tooltip/Tooltip.svelte +12 -10
  174. package/dist/Tooltip/Tooltip.svelte.d.ts +1 -1
  175. package/dist/Tooltip/tooltip.types.d.ts +8 -4
  176. package/dist/Tooltip/tooltip.variants.d.ts +10 -75
  177. package/dist/Tooltip/tooltip.variants.js +8 -17
  178. package/dist/User/User.svelte +13 -9
  179. package/dist/User/User.svelte.d.ts +1 -1
  180. package/dist/User/user.types.d.ts +4 -0
  181. package/dist/User/user.variants.d.ts +60 -0
  182. package/dist/User/user.variants.js +13 -1
  183. package/dist/config.d.ts +8 -0
  184. package/dist/config.js +9 -1
  185. package/dist/index.d.ts +5 -0
  186. package/dist/index.js +5 -0
  187. package/package.json +2 -2
@@ -0,0 +1,436 @@
1
+ <script lang="ts" module>
2
+ import type {
3
+ SelectMenuProps,
4
+ SelectMenuItem,
5
+ SelectMenuItemType
6
+ } from './select-menu.types.js'
7
+
8
+ export type Props = SelectMenuProps
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ import { Combobox } from 'bits-ui'
13
+ import { selectMenuVariants, selectMenuDefaults } from './select-menu.variants.js'
14
+ import { getComponentConfig, iconsDefaults } from '../config.js'
15
+ import { getContext } from 'svelte'
16
+ import {
17
+ fieldGroupVariantWithRoot,
18
+ type FieldGroupVariantProps
19
+ } from '../FieldGroup/field-group.variants.js'
20
+ import Icon from '../Icon/Icon.svelte'
21
+ import Avatar from '../Avatar/Avatar.svelte'
22
+ import Input from '../Input/Input.svelte'
23
+ import type { AvatarSize } from '../Avatar/avatar.types.js'
24
+ import type { FormFieldProps } from '../FormField/form-field.types.js'
25
+
26
+ const config = getComponentConfig('selectMenu', selectMenuDefaults)
27
+ const icons = getComponentConfig('icons', iconsDefaults)
28
+
29
+ let {
30
+ ref = $bindable(null),
31
+ value = $bindable(),
32
+ open = $bindable(false),
33
+ onOpenChange,
34
+ items = [],
35
+ placeholder,
36
+ searchPlaceholder = 'Search...',
37
+ name,
38
+ required = false,
39
+ disabled = false,
40
+ ui,
41
+ id,
42
+ color = config.defaultVariants.color,
43
+ variant = config.defaultVariants.variant,
44
+ size,
45
+ highlight,
46
+ loading = false,
47
+ loadingIcon = icons.loading,
48
+ icon,
49
+ leadingIcon,
50
+ trailingIcon = icons.chevronDown,
51
+ selectedIcon = icons.check,
52
+ avatar,
53
+ filterFields = ['label', 'value'] as string[],
54
+ ignoreFilter = false,
55
+ emptyText = 'No results found.',
56
+ transition = config.defaultVariants.transition ?? true,
57
+ portal = true,
58
+ side = config.defaultVariants.side ?? 'bottom',
59
+ sideOffset = 8,
60
+ align = 'start',
61
+ alignOffset = 0,
62
+ avoidCollisions = true,
63
+ collisionBoundary,
64
+ collisionPadding = 8,
65
+ onEscapeKeydown,
66
+ onInteractOutside,
67
+ forceMount,
68
+ class: className,
69
+ leadingSlot,
70
+ trailingSlot,
71
+ item: itemSlot,
72
+ itemLeading,
73
+ itemLabel: itemLabelSlot,
74
+ itemTrailing,
75
+ empty: emptySlot,
76
+ content: contentSlot
77
+ }: Props = $props()
78
+
79
+ // ---- Form context ----
80
+ const formFieldContext = getContext<
81
+ | {
82
+ name?: string
83
+ size: NonNullable<FormFieldProps['size']>
84
+ error?: string | boolean
85
+ ariaId: string
86
+ }
87
+ | undefined
88
+ >('formField')
89
+
90
+ const fieldGroupContext = getContext<
91
+ | {
92
+ orientation: NonNullable<FieldGroupVariantProps['orientation']>
93
+ size: NonNullable<FieldGroupVariantProps['size']>
94
+ }
95
+ | undefined
96
+ >('fieldGroup')
97
+
98
+ const hasError = $derived(
99
+ formFieldContext?.error !== undefined && formFieldContext?.error !== false
100
+ )
101
+ const resolvedSize = $derived(
102
+ size ?? formFieldContext?.size ?? fieldGroupContext?.size ?? config.defaultVariants.size
103
+ )
104
+ const resolvedColor = $derived(hasError ? 'error' : color)
105
+ const resolvedHighlight = $derived(highlight ?? hasError)
106
+ const fieldGroupClass = $derived(
107
+ fieldGroupContext
108
+ ? fieldGroupVariantWithRoot.fieldGroup[fieldGroupContext.orientation ?? 'horizontal']
109
+ : undefined
110
+ )
111
+ const resolvedId = $derived(id ?? formFieldContext?.ariaId)
112
+ const resolvedName = $derived(name ?? formFieldContext?.name)
113
+
114
+ // ---- ARIA ----
115
+ const ariaDescribedBy = $derived(
116
+ !formFieldContext
117
+ ? undefined
118
+ : hasError
119
+ ? `${formFieldContext.ariaId}-error`
120
+ : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`
121
+ )
122
+
123
+ // ---- Items lookup (O(1) via Map) ----
124
+ const itemsMap = $derived(
125
+ new Map(
126
+ (items as SelectMenuItemType[])
127
+ .filter((i): i is SelectMenuItem => !('type' in i))
128
+ .map((i) => [i.value, i])
129
+ )
130
+ )
131
+
132
+ const selectedItem = $derived(value ? itemsMap.get(value) : undefined)
133
+ const displayLabel = $derived(selectedItem?.label ?? selectedItem?.value ?? '')
134
+
135
+ // ---- Search & filtering ----
136
+ let searchTerm = $state('')
137
+
138
+ const filteredItems = $derived(
139
+ ignoreFilter || !searchTerm.trim()
140
+ ? items
141
+ : items.filter((item) => {
142
+ if ('type' in item) return true
143
+ const query = searchTerm.toLowerCase()
144
+ return filterFields.some((field) => {
145
+ const val = (item as unknown as Record<string, unknown>)[field]
146
+ return typeof val === 'string' && val.toLowerCase().includes(query)
147
+ })
148
+ })
149
+ )
150
+
151
+ const hasFilteredSelectItems = $derived(filteredItems.some((item) => !('type' in item)))
152
+
153
+ // ---- Leading / trailing ----
154
+ const displayAvatar = $derived(selectedItem?.avatar ?? avatar)
155
+ const displayIcon = $derived(selectedItem?.icon ?? leadingIcon ?? icon)
156
+ const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
157
+ const leadingIconName = $derived(
158
+ loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
159
+ )
160
+
161
+ // ---- Trailing icon ----
162
+ const trailingIconName = $derived(loading && !isLeading ? loadingIcon : trailingIcon)
163
+ const trailingIconClass = $derived(
164
+ `${loading && !isLeading ? 'animate-spin' : 'transition-transform'} ${open && !loading ? 'rotate-180' : ''}`
165
+ )
166
+
167
+ // ---- Variant slots ----
168
+ const variantSlots = $derived(
169
+ selectMenuVariants({
170
+ variant,
171
+ color: resolvedColor,
172
+ size: resolvedSize,
173
+ leading: isLeading,
174
+ trailing: true,
175
+ loading,
176
+ highlight: resolvedHighlight,
177
+ side,
178
+ transition
179
+ })
180
+ )
181
+
182
+ // ---- Trigger classes ----
183
+ const rootClass = $derived(
184
+ variantSlots.root({
185
+ class: [config.slots.root, fieldGroupClass?.root, className, ui?.root]
186
+ })
187
+ )
188
+ const baseClass = $derived(
189
+ variantSlots.base({
190
+ class: [config.slots.base, fieldGroupClass?.base, ui?.base]
191
+ })
192
+ )
193
+ const leadingClass = $derived(
194
+ variantSlots.leading({ class: [config.slots.leading, ui?.leading] })
195
+ )
196
+ const leadingIconStyleClass = $derived(
197
+ variantSlots.leadingIcon({ class: [config.slots.leadingIcon, ui?.leadingIcon] })
198
+ )
199
+ const leadingAvatarClass = $derived(
200
+ variantSlots.leadingAvatar({ class: [config.slots.leadingAvatar, ui?.leadingAvatar] })
201
+ )
202
+ const leadingAvatarSizeClass = $derived(variantSlots.leadingAvatarSize() as AvatarSize)
203
+ const trailingStyleClass = $derived(
204
+ variantSlots.trailing({ class: [config.slots.trailing, ui?.trailing] })
205
+ )
206
+ const trailingIconBaseClass = $derived(
207
+ variantSlots.trailingIcon({ class: [config.slots.trailingIcon, ui?.trailingIcon] })
208
+ )
209
+ const valueClass = $derived(variantSlots.value({ class: [config.slots.value, ui?.value] }))
210
+ const placeholderClass = $derived(
211
+ variantSlots.placeholder({ class: [config.slots.placeholder, ui?.placeholder] })
212
+ )
213
+
214
+ // ---- Content classes ----
215
+ const contentClass = $derived(
216
+ variantSlots.content({ class: [config.slots.content, ui?.content] })
217
+ )
218
+ const inputClass = $derived(variantSlots.input({ class: [config.slots.input, ui?.input] }))
219
+ const viewportClass = $derived(
220
+ variantSlots.viewport({ class: [config.slots.viewport, ui?.viewport] })
221
+ )
222
+ const groupLabelClass = $derived(
223
+ variantSlots.groupLabel({ class: [config.slots.groupLabel, ui?.groupLabel] })
224
+ )
225
+ const separatorClass = $derived(
226
+ variantSlots.separator({ class: [config.slots.separator, ui?.separator] })
227
+ )
228
+ const emptyClass = $derived(variantSlots.empty({ class: [config.slots.empty, ui?.empty] }))
229
+
230
+ // ---- Item classes ----
231
+ const itemClass = $derived(variantSlots.item({ class: [config.slots.item, ui?.item] }))
232
+ const itemIconClass = $derived(
233
+ variantSlots.itemIcon({ class: [config.slots.itemIcon, ui?.itemIcon] })
234
+ )
235
+ const itemAvatarClass = $derived(
236
+ variantSlots.itemAvatar({ class: [config.slots.itemAvatar, ui?.itemAvatar] })
237
+ )
238
+ const itemAvatarSizeClass = $derived(variantSlots.itemAvatarSize() as AvatarSize)
239
+ const itemLabelClass = $derived(
240
+ variantSlots.itemLabel({ class: [config.slots.itemLabel, ui?.itemLabel] })
241
+ )
242
+ const itemDescriptionClass = $derived(
243
+ variantSlots.itemDescription({
244
+ class: [config.slots.itemDescription, ui?.itemDescription]
245
+ })
246
+ )
247
+ const itemIndicatorClass = $derived(
248
+ variantSlots.itemIndicator({ class: [config.slots.itemIndicator, ui?.itemIndicator] })
249
+ )
250
+
251
+ // ---- Type guards ----
252
+ function isSelectItem(item: SelectMenuItemType): item is SelectMenuItem {
253
+ return !('type' in item)
254
+ }
255
+
256
+ function isSeparator(item: SelectMenuItemType): item is { type: 'separator' } {
257
+ return 'type' in item && item.type === 'separator'
258
+ }
259
+
260
+ function isLabel(item: SelectMenuItemType): item is { type: 'label'; label: string } {
261
+ return 'type' in item && item.type === 'label'
262
+ }
263
+
264
+ // ---- Event handlers (Nuxt UI v4 pattern) ----
265
+ function onUpdateOpen(val: boolean) {
266
+ if (!val) {
267
+ searchTerm = ''
268
+ }
269
+ onOpenChange?.(val)
270
+ }
271
+ </script>
272
+
273
+ {#snippet renderItem(item: SelectMenuItem, index: number)}
274
+ {@const isSelected = value === item.value}
275
+ <Combobox.Item
276
+ value={item.value}
277
+ label={item.label ?? item.value}
278
+ disabled={item.disabled}
279
+ class={itemClass}
280
+ >
281
+ {#if itemLeading}
282
+ {@render itemLeading({ item, index, selected: isSelected })}
283
+ {:else if item.avatar}
284
+ <Avatar {...item.avatar} size={itemAvatarSizeClass} class={itemAvatarClass} />
285
+ {:else if item.icon}
286
+ <Icon name={item.icon} class={itemIconClass} />
287
+ {/if}
288
+
289
+ {#if itemLabelSlot}
290
+ {@render itemLabelSlot({ item, index, selected: isSelected })}
291
+ {:else}
292
+ <div class={item.description ? 'flex flex-col' : undefined}>
293
+ <span class={itemLabelClass}>{item.label ?? item.value}</span>
294
+ {#if item.description}
295
+ <span class={itemDescriptionClass}>{item.description}</span>
296
+ {/if}
297
+ </div>
298
+ {/if}
299
+
300
+ {#if itemTrailing}
301
+ {@render itemTrailing({ item, index, selected: isSelected })}
302
+ {:else if isSelected}
303
+ <Icon name={selectedIcon} class={itemIndicatorClass} />
304
+ {/if}
305
+ </Combobox.Item>
306
+ {/snippet}
307
+
308
+ {#snippet contentEl()}
309
+ <Combobox.Content
310
+ {side}
311
+ {sideOffset}
312
+ {align}
313
+ {alignOffset}
314
+ {avoidCollisions}
315
+ {collisionBoundary}
316
+ {collisionPadding}
317
+ {onEscapeKeydown}
318
+ {onInteractOutside}
319
+ {forceMount}
320
+ class={contentClass}
321
+ >
322
+ <Input
323
+ autofocus
324
+ placeholder={searchPlaceholder}
325
+ value={searchTerm}
326
+ oninput={(e) => (searchTerm = (e.currentTarget as HTMLInputElement).value)}
327
+ variant="none"
328
+ size={resolvedSize}
329
+ class={inputClass}
330
+ />
331
+
332
+ {#if contentSlot}
333
+ {@render contentSlot({ open, searchTerm })}
334
+ {:else}
335
+ <div class={viewportClass}>
336
+ {#each filteredItems as selectItem, index ('value' in selectItem ? selectItem.value : `${selectItem.type}-${index}`)}
337
+ {#if isSeparator(selectItem)}
338
+ <div role="separator" class={separatorClass}></div>
339
+ {:else if isLabel(selectItem)}
340
+ <Combobox.Group>
341
+ <Combobox.GroupHeading class={groupLabelClass}
342
+ >{selectItem.label}</Combobox.GroupHeading
343
+ >
344
+ </Combobox.Group>
345
+ {:else if isSelectItem(selectItem)}
346
+ {#if itemSlot}
347
+ {@render itemSlot({
348
+ item: selectItem,
349
+ index,
350
+ selected: value === selectItem.value
351
+ })}
352
+ {:else}
353
+ {@render renderItem(selectItem, index)}
354
+ {/if}
355
+ {/if}
356
+ {/each}
357
+
358
+ {#if !hasFilteredSelectItems}
359
+ {#if emptySlot}
360
+ {@render emptySlot({ searchTerm })}
361
+ {:else}
362
+ <div class={emptyClass}>{emptyText}</div>
363
+ {/if}
364
+ {/if}
365
+ </div>
366
+ {/if}
367
+ </Combobox.Content>
368
+ {/snippet}
369
+
370
+ <Combobox.Root
371
+ type="single"
372
+ bind:open
373
+ onOpenChange={onUpdateOpen}
374
+ {disabled}
375
+ {required}
376
+ {value}
377
+ onValueChange={(val) => (value = val)}
378
+ name={resolvedName}
379
+ >
380
+ <div bind:this={ref} class={rootClass}>
381
+ {#if leadingSlot}
382
+ <span class={leadingClass}>
383
+ {@render leadingSlot()}
384
+ </span>
385
+ {:else if isLeading && leadingIconName}
386
+ <span class={leadingClass}>
387
+ <Icon name={leadingIconName} class={leadingIconStyleClass} />
388
+ </span>
389
+ {:else if displayAvatar}
390
+ <span class={leadingClass}>
391
+ <Avatar
392
+ {...displayAvatar}
393
+ size={leadingAvatarSizeClass}
394
+ class={leadingAvatarClass}
395
+ />
396
+ </span>
397
+ {/if}
398
+
399
+ <Combobox.Input
400
+ class="pointer-events-none absolute inset-0 opacity-0"
401
+ tabindex={-1}
402
+ aria-hidden="true"
403
+ />
404
+
405
+ <Combobox.Trigger
406
+ id={resolvedId}
407
+ aria-describedby={ariaDescribedBy}
408
+ aria-invalid={resolvedHighlight ? true : undefined}
409
+ class={baseClass}
410
+ >
411
+ {#if value && displayLabel}
412
+ <span class={valueClass}>{displayLabel}</span>
413
+ {:else if placeholder}
414
+ <span class={placeholderClass}>{placeholder}</span>
415
+ {/if}
416
+ </Combobox.Trigger>
417
+
418
+ {#if trailingSlot}
419
+ <span class={trailingStyleClass}>
420
+ {@render trailingSlot()}
421
+ </span>
422
+ {:else}
423
+ <span class={trailingStyleClass}>
424
+ <Icon name={trailingIconName} class="{trailingIconBaseClass} {trailingIconClass}" />
425
+ </span>
426
+ {/if}
427
+ </div>
428
+
429
+ {#if portal}
430
+ <Combobox.Portal>
431
+ {@render contentEl()}
432
+ </Combobox.Portal>
433
+ {:else}
434
+ {@render contentEl()}
435
+ {/if}
436
+ </Combobox.Root>
@@ -0,0 +1,5 @@
1
+ import type { SelectMenuProps } from './select-menu.types.js';
2
+ export type Props = SelectMenuProps;
3
+ declare const SelectMenu: import("svelte").Component<SelectMenuProps, {}, "ref" | "value" | "open">;
4
+ type SelectMenu = ReturnType<typeof SelectMenu>;
5
+ export default SelectMenu;
@@ -0,0 +1,2 @@
1
+ export { default as SelectMenu } from './SelectMenu.svelte';
2
+ export type { SelectMenuProps, SelectMenuItem, SelectMenuItemType } from './select-menu.types.js';
@@ -0,0 +1 @@
1
+ export { default as SelectMenu } from './SelectMenu.svelte';
@@ -0,0 +1,262 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ClassNameValue } from 'tailwind-merge';
3
+ import type { SelectMenuVariantProps, SelectMenuSlots } from './select-menu.variants.js';
4
+ import type { AvatarProps } from '../Avatar/avatar.types.js';
5
+ import type { ComboboxContentPropsWithoutHTML } from 'bits-ui';
6
+ /**
7
+ * A single selectable option within the SelectMenu.
8
+ */
9
+ export interface SelectMenuItem {
10
+ /**
11
+ * The value submitted on form submission or returned via binding.
12
+ */
13
+ value: string;
14
+ /**
15
+ * The display text for this item.
16
+ * Falls back to `value` when not provided.
17
+ */
18
+ label?: string;
19
+ /**
20
+ * Optional description shown below the label.
21
+ */
22
+ description?: string;
23
+ /**
24
+ * Icon displayed before the label.
25
+ * Supports any Iconify icon name.
26
+ */
27
+ icon?: string;
28
+ /**
29
+ * Avatar displayed before the label.
30
+ * Takes precedence over `icon`.
31
+ */
32
+ avatar?: AvatarProps;
33
+ /**
34
+ * Whether this item is disabled.
35
+ * @default false
36
+ */
37
+ disabled?: boolean;
38
+ }
39
+ /**
40
+ * A visual separator between items.
41
+ */
42
+ export interface SelectMenuItemSeparator {
43
+ type: 'separator';
44
+ }
45
+ /**
46
+ * A non-selectable group label/heading.
47
+ */
48
+ export interface SelectMenuItemLabel {
49
+ type: 'label';
50
+ /**
51
+ * The text displayed as the group label.
52
+ */
53
+ label: string;
54
+ }
55
+ /**
56
+ * Union type for all possible select menu items.
57
+ */
58
+ export type SelectMenuItemType = SelectMenuItem | SelectMenuItemSeparator | SelectMenuItemLabel;
59
+ /**
60
+ * Props passed to select menu item snippet slots.
61
+ */
62
+ export interface SelectMenuItemSlotProps {
63
+ /** The current select menu item data */
64
+ item: SelectMenuItem;
65
+ /** Zero-based index of the item */
66
+ index: number;
67
+ /** Whether the item is currently selected */
68
+ selected?: boolean;
69
+ }
70
+ type ContentProps = Pick<ComboboxContentPropsWithoutHTML, 'side' | 'sideOffset' | 'align' | 'alignOffset' | 'avoidCollisions' | 'collisionBoundary' | 'collisionPadding' | 'onEscapeKeydown' | 'onInteractOutside' | 'forceMount'>;
71
+ /**
72
+ * Props for the SelectMenu component.
73
+ *
74
+ * A searchable select dropdown built on bits-ui Combobox.
75
+ * Supports filtering, icons, avatars, groups, and empty states.
76
+ *
77
+ * @see https://bits-ui.com/docs/components/combobox
78
+ */
79
+ export interface SelectMenuProps extends ContentProps {
80
+ /**
81
+ * Bindable reference to the root DOM element.
82
+ */
83
+ ref?: HTMLElement | null;
84
+ /**
85
+ * The currently selected value. Supports two-way binding with `bind:value`.
86
+ */
87
+ value?: string;
88
+ /**
89
+ * Whether the dropdown is open. Supports two-way binding with `bind:open`.
90
+ * @default false
91
+ */
92
+ open?: boolean;
93
+ /**
94
+ * Callback fired when open state changes.
95
+ */
96
+ onOpenChange?: (open: boolean) => void;
97
+ /**
98
+ * Array of items to display in the select menu dropdown.
99
+ */
100
+ items?: SelectMenuItemType[];
101
+ /**
102
+ * Placeholder text shown when no value is selected.
103
+ */
104
+ placeholder?: string;
105
+ /**
106
+ * The search input placeholder text.
107
+ * @default 'Search...'
108
+ */
109
+ searchPlaceholder?: string;
110
+ /**
111
+ * The form field name attribute.
112
+ */
113
+ name?: string;
114
+ /**
115
+ * Whether the field is required.
116
+ * @default false
117
+ */
118
+ required?: boolean;
119
+ /**
120
+ * Whether the select menu is disabled.
121
+ * @default false
122
+ */
123
+ disabled?: boolean;
124
+ /**
125
+ * Controls the visual style of the select trigger.
126
+ * @default 'outline'
127
+ */
128
+ variant?: NonNullable<SelectMenuVariantProps['variant']>;
129
+ /**
130
+ * Sets the color scheme for focus ring and highlight.
131
+ * @default 'primary'
132
+ */
133
+ color?: NonNullable<SelectMenuVariantProps['color']>;
134
+ /**
135
+ * Controls the dimensions and text size.
136
+ * @default 'md'
137
+ */
138
+ size?: NonNullable<SelectMenuVariantProps['size']>;
139
+ /**
140
+ * Emphasizes ring color like focus state, even when not focused.
141
+ * Automatically enabled when used inside a FormField with an error.
142
+ * @default false
143
+ */
144
+ highlight?: boolean;
145
+ /**
146
+ * Icon displayed on the leading side of the trigger.
147
+ * Supports any valid Iconify icon name.
148
+ */
149
+ icon?: string;
150
+ /**
151
+ * Icon placed before the selected value.
152
+ * Supports any valid Iconify icon name.
153
+ */
154
+ leadingIcon?: string;
155
+ /**
156
+ * Icon placed after the selected value (chevron area).
157
+ * @default 'lucide:chevron-down'
158
+ */
159
+ trailingIcon?: string;
160
+ /**
161
+ * Icon displayed next to selected items in the dropdown.
162
+ * @default 'lucide:check'
163
+ */
164
+ selectedIcon?: string;
165
+ /**
166
+ * Renders a loading spinner.
167
+ * @default false
168
+ */
169
+ loading?: boolean;
170
+ /**
171
+ * Icon displayed as the loading indicator.
172
+ * @default Uses `icons.loading` from app config
173
+ */
174
+ loadingIcon?: string;
175
+ /**
176
+ * Avatar displayed before the selected value on the trigger.
177
+ * Takes precedence over `leadingIcon`.
178
+ */
179
+ avatar?: AvatarProps;
180
+ /**
181
+ * Fields to search when filtering items.
182
+ * @default ['label', 'value']
183
+ */
184
+ filterFields?: string[];
185
+ /**
186
+ * Disables the default filtering. Use this for server-side / custom filtering.
187
+ * @default false
188
+ */
189
+ ignoreFilter?: boolean;
190
+ /**
191
+ * Text to display when no items match the search.
192
+ * @default 'No results found.'
193
+ */
194
+ emptyText?: string;
195
+ /**
196
+ * Animate the dropdown on open and close.
197
+ * @default true
198
+ */
199
+ transition?: SelectMenuVariantProps['transition'];
200
+ /**
201
+ * Render the dropdown content in a portal.
202
+ * @default true
203
+ */
204
+ portal?: boolean;
205
+ /**
206
+ * The HTML `id` attribute for the trigger element.
207
+ */
208
+ id?: string;
209
+ /**
210
+ * Additional CSS classes for the root wrapper.
211
+ */
212
+ class?: ClassNameValue;
213
+ /**
214
+ * Override classes for specific select menu slots.
215
+ */
216
+ ui?: Partial<Record<SelectMenuSlots, ClassNameValue>>;
217
+ /**
218
+ * Custom content rendered before the selected value in the trigger.
219
+ * Takes precedence over `avatar` and `leadingIcon`.
220
+ */
221
+ leadingSlot?: Snippet;
222
+ /**
223
+ * Custom content rendered after the selected value in the trigger.
224
+ * Takes precedence over `trailingIcon`.
225
+ */
226
+ trailingSlot?: Snippet;
227
+ /**
228
+ * Custom snippet for rendering individual items in the dropdown.
229
+ * When provided, replaces the default item rendering.
230
+ */
231
+ item?: Snippet<[SelectMenuItemSlotProps]>;
232
+ /**
233
+ * Custom snippet for the leading section of items.
234
+ * Replaces the default icon/avatar when provided.
235
+ */
236
+ itemLeading?: Snippet<[SelectMenuItemSlotProps]>;
237
+ /**
238
+ * Custom snippet for the label section of items.
239
+ * Replaces the default label text when provided.
240
+ */
241
+ itemLabel?: Snippet<[SelectMenuItemSlotProps]>;
242
+ /**
243
+ * Custom snippet for the trailing section of items (selected indicator area).
244
+ * Replaces the default check icon when provided.
245
+ */
246
+ itemTrailing?: Snippet<[SelectMenuItemSlotProps]>;
247
+ /**
248
+ * Custom empty state snippet.
249
+ */
250
+ empty?: Snippet<[{
251
+ searchTerm: string;
252
+ }]>;
253
+ /**
254
+ * Custom dropdown content.
255
+ * When provided, replaces the default items rendering entirely.
256
+ */
257
+ content?: Snippet<[{
258
+ open: boolean;
259
+ searchTerm: string;
260
+ }]>;
261
+ }
262
+ export {};