svelora 3.0.1 → 3.0.2

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 (107) hide show
  1. package/dist/Accordion/Accordion.svelte +66 -97
  2. package/dist/Alert/Alert.svelte +39 -64
  3. package/dist/Alert/Alert.svelte.d.ts +1 -1
  4. package/dist/Avatar/Avatar.svelte +35 -75
  5. package/dist/AvatarGroup/AvatarGroup.svelte +38 -55
  6. package/dist/Badge/Badge.svelte +28 -50
  7. package/dist/Banner/Banner.svelte +46 -41
  8. package/dist/Banner/Banner.svelte.d.ts +1 -1
  9. package/dist/Breadcrumb/Breadcrumb.svelte +32 -26
  10. package/dist/Button/Button.svelte +70 -138
  11. package/dist/Calendar/Calendar.svelte +94 -157
  12. package/dist/Calendar/Calendar.svelte.d.ts +1 -1
  13. package/dist/Card/Card.svelte +18 -31
  14. package/dist/Carousel/Carousel.svelte +118 -173
  15. package/dist/Checkbox/Checkbox.svelte +52 -97
  16. package/dist/CheckboxGroup/CheckboxGroup.svelte +62 -107
  17. package/dist/CheckboxGroup/CheckboxGroup.svelte.d.ts +1 -1
  18. package/dist/Chip/Chip.svelte +22 -34
  19. package/dist/CodeBlock/CodeBlock.svelte +42 -59
  20. package/dist/Collapsible/Collapsible.svelte +22 -38
  21. package/dist/Collapsible/Collapsible.svelte.d.ts +1 -1
  22. package/dist/Collapsible/CollapsibleTestWrapper.svelte +2 -5
  23. package/dist/Collapsible/CollapsibleTestWrapper.svelte.d.ts +1 -1
  24. package/dist/Command/Command.svelte +40 -77
  25. package/dist/Command/Command.svelte.d.ts +1 -1
  26. package/dist/Command/CommandTestWrapper.svelte +2 -10
  27. package/dist/Command/CommandTestWrapper.svelte.d.ts +1 -1
  28. package/dist/Container/Container.svelte +11 -14
  29. package/dist/ContextMenu/ContextMenu.svelte +51 -114
  30. package/dist/ContextMenu/ContextMenu.svelte.d.ts +1 -1
  31. package/dist/Drawer/Drawer.svelte +72 -110
  32. package/dist/Drawer/DrawerTriggerTestWrapper.svelte +1 -2
  33. package/dist/DropdownMenu/DropdownMenu.svelte +63 -124
  34. package/dist/DropdownMenu/DropdownMenu.svelte.d.ts +1 -1
  35. package/dist/DropdownMenu/DropdownMenuTriggerTestWrapper.svelte +2 -5
  36. package/dist/Editor/Editor.svelte +441 -576
  37. package/dist/Editor/Editor.svelte.d.ts +1 -1
  38. package/dist/Editor/EditorUrlPrompt.svelte +40 -53
  39. package/dist/Editor/SlashPopup.svelte +12 -24
  40. package/dist/Empty/Empty.svelte +32 -63
  41. package/dist/FieldGroup/FieldGroup.svelte +23 -38
  42. package/dist/FileUpload/FileUpload.svelte +242 -320
  43. package/dist/FileUpload/FileUpload.svelte.d.ts +1 -1
  44. package/dist/Fonts/Fonts.svelte +15 -37
  45. package/dist/Form/Form.svelte +112 -170
  46. package/dist/FormField/FormField.svelte +102 -135
  47. package/dist/Icon/Icon.svelte +7 -32
  48. package/dist/Input/Input.svelte +71 -141
  49. package/dist/Input/Input.svelte.d.ts +2 -2
  50. package/dist/Kbd/Kbd.svelte +18 -34
  51. package/dist/Link/Link.svelte +129 -196
  52. package/dist/LocaleButton/LocaleButton.svelte +165 -0
  53. package/dist/LocaleButton/LocaleButton.svelte.d.ts +5 -0
  54. package/dist/LocaleButton/index.d.ts +2 -0
  55. package/dist/LocaleButton/index.js +1 -0
  56. package/dist/LocaleButton/locale-button.types.d.ts +182 -0
  57. package/dist/LocaleButton/locale-button.types.js +1 -0
  58. package/dist/LocaleButton/locale-button.variants.d.ts +61 -0
  59. package/dist/LocaleButton/locale-button.variants.js +34 -0
  60. package/dist/Modal/Modal.svelte +52 -106
  61. package/dist/Modal/ModalTriggerTestWrapper.svelte +1 -2
  62. package/dist/Pagination/Pagination.svelte +48 -92
  63. package/dist/Pagination/pagination.variants.d.ts +1 -1
  64. package/dist/PinInput/PinInput.svelte +57 -111
  65. package/dist/PinInput/PinInput.svelte.d.ts +1 -1
  66. package/dist/Popover/Popover.svelte +28 -61
  67. package/dist/Popover/Popover.svelte.d.ts +1 -1
  68. package/dist/Progress/Progress.svelte +75 -94
  69. package/dist/RadioGroup/RadioGroup.svelte +54 -99
  70. package/dist/RadioGroup/RadioGroup.svelte.d.ts +1 -1
  71. package/dist/Select/Select.svelte +112 -269
  72. package/dist/Select/Select.svelte.d.ts +1 -1
  73. package/dist/SelectMenu/SelectMenu.svelte +211 -409
  74. package/dist/SelectMenu/SelectMenu.svelte.d.ts +1 -1
  75. package/dist/SelectMenu/SelectMenuFormFieldTestWrapper.svelte +3 -6
  76. package/dist/Separator/Separator.svelte +29 -44
  77. package/dist/Skeleton/Skeleton.svelte +11 -23
  78. package/dist/Slideover/Slideover.svelte +52 -106
  79. package/dist/Slideover/SlideoverTriggerTestWrapper.svelte +1 -2
  80. package/dist/Slider/Slider.svelte +48 -84
  81. package/dist/Slider/Slider.svelte.d.ts +1 -1
  82. package/dist/Stepper/Stepper.svelte +139 -132
  83. package/dist/Stepper/Stepper.svelte.d.ts +1 -1
  84. package/dist/Switch/Switch.svelte +62 -98
  85. package/dist/Table/Table.svelte +232 -283
  86. package/dist/Table/table.variants.d.ts +1 -1
  87. package/dist/Tabs/Tabs.svelte +96 -129
  88. package/dist/Tabs/Tabs.svelte.d.ts +1 -1
  89. package/dist/Textarea/Textarea.svelte +90 -173
  90. package/dist/Textarea/Textarea.svelte.d.ts +1 -1
  91. package/dist/ThemeModeButton/ThemeModeButton.svelte +16 -38
  92. package/dist/Timeline/Timeline.svelte +75 -54
  93. package/dist/Toast/Toaster.svelte +8 -25
  94. package/dist/Tooltip/Tooltip.svelte +34 -66
  95. package/dist/Tooltip/Tooltip.svelte.d.ts +1 -1
  96. package/dist/Tooltip/TooltipTestWrapper.svelte +2 -5
  97. package/dist/User/User.svelte +33 -49
  98. package/dist/docs/navigation.js +6 -0
  99. package/dist/hooks/HookContextProbe.svelte +2 -4
  100. package/dist/hooks/HookContextProvider.svelte +8 -6
  101. package/dist/hooks/HookEmitProbe.svelte +8 -11
  102. package/dist/i18n.d.ts +2 -0
  103. package/dist/i18n.js +19 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +1 -0
  106. package/dist/mcp/svelora-docs.data.json +4 -2
  107. package/package.json +16 -8
@@ -1,414 +1,216 @@
1
- <script lang="ts" module>
2
- import type {
3
- SelectMenuItem,
4
- SelectMenuItemType,
5
- SelectMenuProps
6
- } from './select-menu.types.js'
7
-
8
- export type Props = SelectMenuProps
9
-
10
- const CREATE_ITEM_VALUE = '@@svelora/select-menu/create-item'
1
+ <script lang="ts" module>const CREATE_ITEM_VALUE = "@@svelora/select-menu/create-item";
2
+ export {};
11
3
  </script>
12
4
 
13
- <script lang="ts">
14
- import { Combobox } from 'bits-ui'
15
- import { getContext } from 'svelte'
16
- import Avatar from '../Avatar/Avatar.svelte'
17
- import type { AvatarSize } from '../Avatar/avatar.types.js'
18
- import { getComponentConfig, iconsDefaults } from '../config.js'
19
- import {
20
- type FieldGroupVariantProps,
21
- fieldGroupVariantWithRoot
22
- } from '../FieldGroup/field-group.variants.js'
23
- import { useDebounce } from '../hooks/useDebounce.svelte.js'
24
- import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
25
- import Icon from '../Icon/Icon.svelte'
26
- import { selectMenuDefaults, selectMenuVariants } from './select-menu.variants.js'
27
-
28
- const config = getComponentConfig('selectMenu', selectMenuDefaults)
29
- const icons = getComponentConfig('icons', iconsDefaults)
30
-
31
- let {
32
- ref = $bindable(null),
33
- value = $bindable(),
34
- open = $bindable(false),
35
- onOpenChange,
36
- items = [],
37
- placeholder,
38
- searchPlaceholder = 'Search...',
39
- name,
40
- required = false,
41
- disabled = false,
42
- multiple = false,
43
- separator = ', ',
44
- ui,
45
- id,
46
- color = config.defaultVariants.color,
47
- variant = config.defaultVariants.variant,
48
- size,
49
- highlight,
50
- loading = false,
51
- loadingIcon = icons.loading,
52
- icon,
53
- leadingIcon,
54
- trailingIcon = icons.chevronDown,
55
- selectedIcon = icons.check,
56
- avatar,
57
- filterFields = ['label', 'value'] as string[],
58
- ignoreFilter = false,
59
- emptyText = 'No results found.',
60
- createItem = false,
61
- createItemLabel = (value: string) => `Create "${value}"`,
62
- createItemIcon,
63
- onCreate,
64
- transition = config.defaultVariants.transition ?? true,
65
- portal = true,
66
- side = config.defaultVariants.side ?? 'bottom',
67
- sideOffset = 8,
68
- align = 'start',
69
- alignOffset = 0,
70
- avoidCollisions = true,
71
- collisionBoundary,
72
- collisionPadding = 8,
73
- onEscapeKeydown,
74
- onInteractOutside,
75
- forceMount,
76
- class: className,
77
- leadingSlot,
78
- trailingSlot,
79
- item: itemSlot,
80
- itemLeading,
81
- itemLabel: itemLabelSlot,
82
- itemTrailing,
83
- selected: selectedSlot,
84
- empty: emptySlot,
85
- content: contentSlot,
86
- ...restProps
87
- }: Props = $props()
88
-
89
- // ---- Form context ----
90
- const formFieldContext = useFormField()
91
- const emit = useFormFieldEmit()
92
-
93
- const fieldGroupContext = getContext<
94
- | {
95
- orientation: NonNullable<FieldGroupVariantProps['orientation']>
96
- size: NonNullable<FieldGroupVariantProps['size']>
97
- }
98
- | undefined
99
- >('fieldGroup')
100
-
101
- const hasError = $derived(
102
- formFieldContext?.error !== undefined && formFieldContext?.error !== false
103
- )
104
- const resolvedSize = $derived(
105
- size ?? formFieldContext?.size ?? fieldGroupContext?.size ?? config.defaultVariants.size
106
- )
107
- const resolvedColor = $derived(hasError ? 'error' : color)
108
- const resolvedHighlight = $derived(highlight ?? hasError)
109
- const fieldGroupClass = $derived(
110
- fieldGroupContext
111
- ? fieldGroupVariantWithRoot.fieldGroup[fieldGroupContext.orientation ?? 'horizontal']
112
- : undefined
113
- )
114
- const resolvedId = $derived(id ?? formFieldContext?.ariaId)
115
- const resolvedName = $derived(name ?? formFieldContext?.name)
116
-
117
- // ---- ARIA ----
118
- const ariaDescribedBy = $derived(
119
- !formFieldContext
120
- ? undefined
121
- : hasError
122
- ? `${formFieldContext.ariaId}-error`
123
- : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`
124
- )
125
-
126
- // ---- Created items (internal state for createItem) ----
127
- let createdItems = $state<SelectMenuItem[]>([])
128
-
129
- const combinedItems = $derived.by(() => {
130
- const propValues = new Set(
131
- (items as SelectMenuItemType[])
132
- .filter((i): i is SelectMenuItem => !('type' in i))
133
- .map((i) => i.value)
134
- )
135
- const extras = createdItems.filter((c) => !propValues.has(c.value))
136
- return [...(items as SelectMenuItemType[]), ...extras]
137
- })
138
-
139
- // ---- Items lookup (O(1) via Map) ----
140
- const itemsMap = $derived(
141
- new Map(
142
- combinedItems
143
- .filter((i): i is SelectMenuItem => !('type' in i))
144
- .map((i) => [i.value, i])
145
- )
146
- )
147
-
148
- // ---- Selection (single + multiple) ----
149
- const selectedValues = $derived(
150
- multiple
151
- ? Array.isArray(value)
152
- ? (value as string[])
153
- : []
154
- : typeof value === 'string' && value !== ''
155
- ? [value]
156
- : []
157
- )
158
- const selectedItems = $derived(
159
- selectedValues
160
- .map((v) => itemsMap.get(v))
161
- .filter((i): i is SelectMenuItem => i !== undefined)
162
- )
163
- const hasSelection = $derived(selectedValues.length > 0)
164
- const singleSelectedItem = $derived(multiple ? undefined : selectedItems[0])
165
- const displayLabel = $derived(
166
- multiple
167
- ? selectedItems.map((i) => i.label ?? i.value).join(separator)
168
- : (singleSelectedItem?.label ?? singleSelectedItem?.value ?? '')
169
- )
170
-
171
- function removeValue(val: string) {
172
- if (!multiple) return
173
- value = selectedValues.filter((v) => v !== val)
174
- emit.onChange()
175
- }
176
-
177
- function clearSelection() {
178
- if (!multiple) return
179
- value = []
180
- emit.onChange()
181
- }
182
-
183
- // ---- Search & filtering ----
184
- let searchTerm = $state('')
185
- let debouncedSearch = $state('')
186
- const searchDebounce = useDebounce({ delay: 200 })
187
-
188
- function setSearch(term: string) {
189
- searchTerm = term
190
- searchDebounce.run(() => {
191
- debouncedSearch = term
192
- })
193
- }
194
-
195
- function resetSearch() {
196
- searchDebounce.cancel()
197
- searchTerm = ''
198
- debouncedSearch = ''
199
- }
200
-
201
- const filteredItems = $derived(
202
- ignoreFilter || !debouncedSearch.trim()
203
- ? combinedItems
204
- : combinedItems.filter((item) => {
205
- if ('type' in item) return true
206
- const query = debouncedSearch.toLowerCase()
207
- return filterFields.some((field) => {
208
- const val = (item as unknown as Record<string, unknown>)[field]
209
- return typeof val === 'string' && val.toLowerCase().includes(query)
210
- })
211
- })
212
- )
213
-
214
- const hasFilteredSelectItems = $derived(filteredItems.some((item) => !('type' in item)))
215
-
216
- // ---- Create item ----
217
- const trimmedSearch = $derived(searchTerm.trim())
218
- const exactMatchExists = $derived.by(() => {
219
- if (!trimmedSearch) return false
220
- const query = trimmedSearch.toLowerCase()
221
- for (const i of combinedItems) {
222
- if ('type' in i) continue
223
- if (i.value.toLowerCase() === query || (i.label ?? i.value).toLowerCase() === query) {
224
- return true
225
- }
226
- }
227
- return false
228
- })
229
- const showCreateItem = $derived.by(() => {
230
- if (!createItem) return false
231
- if (!trimmedSearch) return false
232
- const mode = createItem === true ? 'lazy' : createItem
233
- if (mode === 'always') return true
234
- return !exactMatchExists
235
- })
236
- const resolvedCreateLabel = $derived(
237
- typeof createItemLabel === 'function' ? createItemLabel(trimmedSearch) : createItemLabel
238
- )
239
-
240
- function findItemByCaseInsensitive(query: string): SelectMenuItem | undefined {
241
- const q = query.toLowerCase()
242
- for (const it of itemsMap.values()) {
243
- if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) {
244
- return it
245
- }
246
- }
247
- return undefined
248
- }
249
-
250
- function selectValue(val: string) {
251
- if (multiple) {
252
- if (!selectedValues.includes(val)) {
253
- value = [...selectedValues, val]
254
- }
255
- } else {
256
- value = val
257
- }
258
- }
259
-
260
- function handleCreate() {
261
- if (!showCreateItem) return
262
- const newValue = trimmedSearch
263
- if (!newValue) return
264
-
265
- const existing = findItemByCaseInsensitive(newValue)
266
- if (existing) {
267
- selectValue(existing.value)
268
- } else {
269
- createdItems = [...createdItems, { value: newValue, label: newValue }]
270
- selectValue(newValue)
271
- onCreate?.(newValue)
272
- }
273
-
274
- emit.onChange()
275
- resetSearch()
276
- }
277
-
278
- // ---- Leading / trailing ----
279
- const displayAvatar = $derived(multiple ? avatar : (singleSelectedItem?.avatar ?? avatar))
280
- const displayIcon = $derived(
281
- multiple ? (leadingIcon ?? icon) : (singleSelectedItem?.icon ?? leadingIcon ?? icon)
282
- )
283
- const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
284
- const leadingIconName = $derived(
285
- loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
286
- )
287
-
288
- // ---- Trailing icon ----
289
- const trailingIconName = $derived(loading && !isLeading ? loadingIcon : trailingIcon)
290
- const trailingIconClass = $derived(
291
- `${loading && !isLeading ? 'animate-spin' : 'transition-transform'} ${open && !loading ? 'rotate-180' : ''}`
292
- )
293
-
294
- // ---- Variant slots ----
295
- const variantSlots = $derived(
296
- selectMenuVariants({
297
- variant,
298
- color: resolvedColor,
299
- size: resolvedSize,
300
- leading: isLeading,
301
- trailing: true,
302
- loading,
303
- highlight: resolvedHighlight,
304
- side,
305
- transition
306
- })
307
- )
308
-
309
- // ---- Trigger classes ----
310
- const rootClass = $derived(
311
- variantSlots.root({
312
- class: [config.slots.root, fieldGroupClass?.root, className, ui?.root]
313
- })
314
- )
315
- const baseClass = $derived(
316
- variantSlots.base({
317
- class: [config.slots.base, fieldGroupClass?.base, ui?.base]
318
- })
319
- )
320
- const leadingClass = $derived(
321
- variantSlots.leading({ class: [config.slots.leading, ui?.leading] })
322
- )
323
- const leadingIconStyleClass = $derived(
324
- variantSlots.leadingIcon({ class: [config.slots.leadingIcon, ui?.leadingIcon] })
325
- )
326
- const leadingAvatarClass = $derived(
327
- variantSlots.leadingAvatar({ class: [config.slots.leadingAvatar, ui?.leadingAvatar] })
328
- )
329
- const leadingAvatarSizeClass = $derived(variantSlots.leadingAvatarSize() as AvatarSize)
330
- const trailingStyleClass = $derived(
331
- variantSlots.trailing({ class: [config.slots.trailing, ui?.trailing] })
332
- )
333
- const trailingIconBaseClass = $derived(
334
- variantSlots.trailingIcon({ class: [config.slots.trailingIcon, ui?.trailingIcon] })
335
- )
336
- const valueClass = $derived(variantSlots.value({ class: [config.slots.value, ui?.value] }))
337
- const placeholderClass = $derived(
338
- variantSlots.placeholder({ class: [config.slots.placeholder, ui?.placeholder] })
339
- )
340
-
341
- // ---- Content classes ----
342
- const contentClass = $derived(
343
- variantSlots.content({ class: [config.slots.content, ui?.content] })
344
- )
345
- const inputClass = $derived(variantSlots.input({ class: [config.slots.input, ui?.input] }))
346
- const viewportClass = $derived(
347
- variantSlots.viewport({ class: [config.slots.viewport, ui?.viewport] })
348
- )
349
- const groupLabelClass = $derived(
350
- variantSlots.groupLabel({ class: [config.slots.groupLabel, ui?.groupLabel] })
351
- )
352
- const separatorClass = $derived(
353
- variantSlots.separator({ class: [config.slots.separator, ui?.separator] })
354
- )
355
- const emptyClass = $derived(variantSlots.empty({ class: [config.slots.empty, ui?.empty] }))
356
- const createItemClass = $derived(
357
- variantSlots.createItem({ class: [config.slots.createItem, ui?.createItem] })
358
- )
359
- const createItemIconClass = $derived(
360
- variantSlots.createItemIcon({ class: [config.slots.createItemIcon, ui?.createItemIcon] })
361
- )
362
- const createItemLabelClass = $derived(
363
- variantSlots.createItemLabel({
364
- class: [config.slots.createItemLabel, ui?.createItemLabel]
365
- })
366
- )
367
-
368
- // ---- Item classes ----
369
- const itemClass = $derived(variantSlots.item({ class: [config.slots.item, ui?.item] }))
370
- const itemIconClass = $derived(
371
- variantSlots.itemIcon({ class: [config.slots.itemIcon, ui?.itemIcon] })
372
- )
373
- const itemAvatarClass = $derived(
374
- variantSlots.itemAvatar({ class: [config.slots.itemAvatar, ui?.itemAvatar] })
375
- )
376
- const itemAvatarSizeClass = $derived(variantSlots.itemAvatarSize() as AvatarSize)
377
- const itemLabelClass = $derived(
378
- variantSlots.itemLabel({ class: [config.slots.itemLabel, ui?.itemLabel] })
379
- )
380
- const itemDescriptionClass = $derived(
381
- variantSlots.itemDescription({
382
- class: [config.slots.itemDescription, ui?.itemDescription]
383
- })
384
- )
385
- const itemIndicatorClass = $derived(
386
- variantSlots.itemIndicator({ class: [config.slots.itemIndicator, ui?.itemIndicator] })
387
- )
388
-
389
- // ---- Type guards ----
390
- function isSelectItem(item: SelectMenuItemType): item is SelectMenuItem {
391
- return !('type' in item)
392
- }
393
-
394
- function isSeparator(item: SelectMenuItemType): item is { type: 'separator' } {
395
- return 'type' in item && item.type === 'separator'
396
- }
397
-
398
- function isLabel(item: SelectMenuItemType): item is { type: 'label'; label: string } {
399
- return 'type' in item && item.type === 'label'
400
- }
401
-
402
- // ---- Event handlers (Nuxt UI v4 pattern) ----
403
- function onUpdateOpen(val: boolean) {
404
- if (!val) {
405
- resetSearch()
406
- emit.onBlur()
407
- } else {
408
- emit.onFocus()
409
- }
410
- onOpenChange?.(val)
411
- }
5
+ <script lang="ts">import { Combobox } from "bits-ui";
6
+ import { getContext } from "svelte";
7
+ import Avatar from "../Avatar/Avatar.svelte";
8
+ import { getComponentConfig, iconsDefaults } from "../config.js";
9
+ import { fieldGroupVariantWithRoot } from "../FieldGroup/field-group.variants.js";
10
+ import { useDebounce } from "../hooks/useDebounce.svelte.js";
11
+ import { useFormField, useFormFieldEmit } from "../hooks/useFormField.svelte.js";
12
+ import Icon from "../Icon/Icon.svelte";
13
+ import { selectMenuDefaults, selectMenuVariants } from "./select-menu.variants.js";
14
+ const config = getComponentConfig("selectMenu", selectMenuDefaults);
15
+ const icons = getComponentConfig("icons", iconsDefaults);
16
+ let { ref = $bindable(null), value = $bindable(), open = $bindable(false), onOpenChange, items = [], placeholder, searchPlaceholder = "Search...", name, required = false, disabled = false, multiple = false, separator = ", ", ui, id, color = config.defaultVariants.color, variant = config.defaultVariants.variant, size, highlight, loading = false, loadingIcon = icons.loading, icon, leadingIcon, trailingIcon = icons.chevronDown, selectedIcon = icons.check, avatar, filterFields = ["label", "value"], ignoreFilter = false, emptyText = "No results found.", createItem = false, createItemLabel = (value) => `Create "${value}"`, createItemIcon, onCreate, transition = config.defaultVariants.transition ?? true, portal = true, side = config.defaultVariants.side ?? "bottom", sideOffset = 8, align = "start", alignOffset = 0, avoidCollisions = true, collisionBoundary, collisionPadding = 8, onEscapeKeydown, onInteractOutside, forceMount, class: className, leadingSlot, trailingSlot, item: itemSlot, itemLeading, itemLabel: itemLabelSlot, itemTrailing, selected: selectedSlot, empty: emptySlot, content: contentSlot, ...restProps } = $props();
17
+ // ---- Form context ----
18
+ const formFieldContext = useFormField();
19
+ const emit = useFormFieldEmit();
20
+ const fieldGroupContext = getContext("fieldGroup");
21
+ const hasError = $derived(formFieldContext?.error !== undefined && formFieldContext?.error !== false);
22
+ const resolvedSize = $derived(size ?? formFieldContext?.size ?? fieldGroupContext?.size ?? config.defaultVariants.size);
23
+ const resolvedColor = $derived(hasError ? "error" : color);
24
+ const resolvedHighlight = $derived(highlight ?? hasError);
25
+ const fieldGroupClass = $derived(fieldGroupContext ? fieldGroupVariantWithRoot.fieldGroup[fieldGroupContext.orientation ?? "horizontal"] : undefined);
26
+ const resolvedId = $derived(id ?? formFieldContext?.ariaId);
27
+ const resolvedName = $derived(name ?? formFieldContext?.name);
28
+ // ---- ARIA ----
29
+ const ariaDescribedBy = $derived(!formFieldContext ? undefined : hasError ? `${formFieldContext.ariaId}-error` : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`);
30
+ // ---- Created items (internal state for createItem) ----
31
+ let createdItems = $state([]);
32
+ const combinedItems = $derived.by(() => {
33
+ const propValues = new Set(items.filter((i) => !("type" in i)).map((i) => i.value));
34
+ const extras = createdItems.filter((c) => !propValues.has(c.value));
35
+ return [...items, ...extras];
36
+ });
37
+ // ---- Items lookup (O(1) via Map) ----
38
+ const itemsMap = $derived(new Map(combinedItems.filter((i) => !("type" in i)).map((i) => [i.value, i])));
39
+ // ---- Selection (single + multiple) ----
40
+ const selectedValues = $derived(multiple ? Array.isArray(value) ? value : [] : typeof value === "string" && value !== "" ? [value] : []);
41
+ const selectedItems = $derived(selectedValues.map((v) => itemsMap.get(v)).filter((i) => i !== undefined));
42
+ const hasSelection = $derived(selectedValues.length > 0);
43
+ const singleSelectedItem = $derived(multiple ? undefined : selectedItems[0]);
44
+ const displayLabel = $derived(multiple ? selectedItems.map((i) => i.label ?? i.value).join(separator) : singleSelectedItem?.label ?? singleSelectedItem?.value ?? "");
45
+ function removeValue(val) {
46
+ if (!multiple) return;
47
+ value = selectedValues.filter((v) => v !== val);
48
+ emit.onChange();
49
+ }
50
+ function clearSelection() {
51
+ if (!multiple) return;
52
+ value = [];
53
+ emit.onChange();
54
+ }
55
+ // ---- Search & filtering ----
56
+ let searchTerm = $state("");
57
+ let debouncedSearch = $state("");
58
+ const searchDebounce = useDebounce({ delay: 200 });
59
+ function setSearch(term) {
60
+ searchTerm = term;
61
+ searchDebounce.run(() => {
62
+ debouncedSearch = term;
63
+ });
64
+ }
65
+ function resetSearch() {
66
+ searchDebounce.cancel();
67
+ searchTerm = "";
68
+ debouncedSearch = "";
69
+ }
70
+ const filteredItems = $derived(ignoreFilter || !debouncedSearch.trim() ? combinedItems : combinedItems.filter((item) => {
71
+ if ("type" in item) return true;
72
+ const query = debouncedSearch.toLowerCase();
73
+ return filterFields.some((field) => {
74
+ const val = item[field];
75
+ return typeof val === "string" && val.toLowerCase().includes(query);
76
+ });
77
+ }));
78
+ const hasFilteredSelectItems = $derived(filteredItems.some((item) => !("type" in item)));
79
+ // ---- Create item ----
80
+ const trimmedSearch = $derived(searchTerm.trim());
81
+ const exactMatchExists = $derived.by(() => {
82
+ if (!trimmedSearch) return false;
83
+ const query = trimmedSearch.toLowerCase();
84
+ for (const i of combinedItems) {
85
+ if ("type" in i) continue;
86
+ if (i.value.toLowerCase() === query || (i.label ?? i.value).toLowerCase() === query) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ });
92
+ const showCreateItem = $derived.by(() => {
93
+ if (!createItem) return false;
94
+ if (!trimmedSearch) return false;
95
+ const mode = createItem === true ? "lazy" : createItem;
96
+ if (mode === "always") return true;
97
+ return !exactMatchExists;
98
+ });
99
+ const resolvedCreateLabel = $derived(typeof createItemLabel === "function" ? createItemLabel(trimmedSearch) : createItemLabel);
100
+ function findItemByCaseInsensitive(query) {
101
+ const q = query.toLowerCase();
102
+ for (const it of itemsMap.values()) {
103
+ if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) {
104
+ return it;
105
+ }
106
+ }
107
+ return undefined;
108
+ }
109
+ function selectValue(val) {
110
+ if (multiple) {
111
+ if (!selectedValues.includes(val)) {
112
+ value = [...selectedValues, val];
113
+ }
114
+ } else {
115
+ value = val;
116
+ }
117
+ }
118
+ function handleCreate() {
119
+ if (!showCreateItem) return;
120
+ const newValue = trimmedSearch;
121
+ if (!newValue) return;
122
+ const existing = findItemByCaseInsensitive(newValue);
123
+ if (existing) {
124
+ selectValue(existing.value);
125
+ } else {
126
+ createdItems = [...createdItems, {
127
+ value: newValue,
128
+ label: newValue
129
+ }];
130
+ selectValue(newValue);
131
+ onCreate?.(newValue);
132
+ }
133
+ emit.onChange();
134
+ resetSearch();
135
+ }
136
+ // ---- Leading / trailing ----
137
+ const displayAvatar = $derived(multiple ? avatar : singleSelectedItem?.avatar ?? avatar);
138
+ const displayIcon = $derived(multiple ? leadingIcon ?? icon : singleSelectedItem?.icon ?? leadingIcon ?? icon);
139
+ const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon);
140
+ const leadingIconName = $derived(loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined);
141
+ // ---- Trailing icon ----
142
+ const trailingIconName = $derived(loading && !isLeading ? loadingIcon : trailingIcon);
143
+ const trailingIconClass = $derived(`${loading && !isLeading ? "animate-spin" : "transition-transform"} ${open && !loading ? "rotate-180" : ""}`);
144
+ // ---- Variant slots ----
145
+ const variantSlots = $derived(selectMenuVariants({
146
+ variant,
147
+ color: resolvedColor,
148
+ size: resolvedSize,
149
+ leading: isLeading,
150
+ trailing: true,
151
+ loading,
152
+ highlight: resolvedHighlight,
153
+ side,
154
+ transition
155
+ }));
156
+ // ---- Trigger classes ----
157
+ const rootClass = $derived(variantSlots.root({ class: [
158
+ config.slots.root,
159
+ fieldGroupClass?.root,
160
+ className,
161
+ ui?.root
162
+ ] }));
163
+ const baseClass = $derived(variantSlots.base({ class: [
164
+ config.slots.base,
165
+ fieldGroupClass?.base,
166
+ ui?.base
167
+ ] }));
168
+ const leadingClass = $derived(variantSlots.leading({ class: [config.slots.leading, ui?.leading] }));
169
+ const leadingIconStyleClass = $derived(variantSlots.leadingIcon({ class: [config.slots.leadingIcon, ui?.leadingIcon] }));
170
+ const leadingAvatarClass = $derived(variantSlots.leadingAvatar({ class: [config.slots.leadingAvatar, ui?.leadingAvatar] }));
171
+ const leadingAvatarSizeClass = $derived(variantSlots.leadingAvatarSize());
172
+ const trailingStyleClass = $derived(variantSlots.trailing({ class: [config.slots.trailing, ui?.trailing] }));
173
+ const trailingIconBaseClass = $derived(variantSlots.trailingIcon({ class: [config.slots.trailingIcon, ui?.trailingIcon] }));
174
+ const valueClass = $derived(variantSlots.value({ class: [config.slots.value, ui?.value] }));
175
+ const placeholderClass = $derived(variantSlots.placeholder({ class: [config.slots.placeholder, ui?.placeholder] }));
176
+ // ---- Content classes ----
177
+ const contentClass = $derived(variantSlots.content({ class: [config.slots.content, ui?.content] }));
178
+ const inputClass = $derived(variantSlots.input({ class: [config.slots.input, ui?.input] }));
179
+ const viewportClass = $derived(variantSlots.viewport({ class: [config.slots.viewport, ui?.viewport] }));
180
+ const groupLabelClass = $derived(variantSlots.groupLabel({ class: [config.slots.groupLabel, ui?.groupLabel] }));
181
+ const separatorClass = $derived(variantSlots.separator({ class: [config.slots.separator, ui?.separator] }));
182
+ const emptyClass = $derived(variantSlots.empty({ class: [config.slots.empty, ui?.empty] }));
183
+ const createItemClass = $derived(variantSlots.createItem({ class: [config.slots.createItem, ui?.createItem] }));
184
+ const createItemIconClass = $derived(variantSlots.createItemIcon({ class: [config.slots.createItemIcon, ui?.createItemIcon] }));
185
+ const createItemLabelClass = $derived(variantSlots.createItemLabel({ class: [config.slots.createItemLabel, ui?.createItemLabel] }));
186
+ // ---- Item classes ----
187
+ const itemClass = $derived(variantSlots.item({ class: [config.slots.item, ui?.item] }));
188
+ const itemIconClass = $derived(variantSlots.itemIcon({ class: [config.slots.itemIcon, ui?.itemIcon] }));
189
+ const itemAvatarClass = $derived(variantSlots.itemAvatar({ class: [config.slots.itemAvatar, ui?.itemAvatar] }));
190
+ const itemAvatarSizeClass = $derived(variantSlots.itemAvatarSize());
191
+ const itemLabelClass = $derived(variantSlots.itemLabel({ class: [config.slots.itemLabel, ui?.itemLabel] }));
192
+ const itemDescriptionClass = $derived(variantSlots.itemDescription({ class: [config.slots.itemDescription, ui?.itemDescription] }));
193
+ const itemIndicatorClass = $derived(variantSlots.itemIndicator({ class: [config.slots.itemIndicator, ui?.itemIndicator] }));
194
+ // ---- Type guards ----
195
+ function isSelectItem(item) {
196
+ return !("type" in item);
197
+ }
198
+ function isSeparator(item) {
199
+ return "type" in item && item.type === "separator";
200
+ }
201
+ function isLabel(item) {
202
+ return "type" in item && item.type === "label";
203
+ }
204
+ // ---- Event handlers (Nuxt UI v4 pattern) ----
205
+ function onUpdateOpen(val) {
206
+ if (!val) {
207
+ resetSearch();
208
+ emit.onBlur();
209
+ } else {
210
+ emit.onFocus();
211
+ }
212
+ onOpenChange?.(val);
213
+ }
412
214
  </script>
413
215
 
414
216
  {#snippet renderItem(item: SelectMenuItem, index: number)}
@@ -1,5 +1,5 @@
1
1
  import type { SelectMenuProps } from './select-menu.types.js';
2
2
  export type Props = SelectMenuProps;
3
- declare const SelectMenu: import("svelte").Component<SelectMenuProps, {}, "value" | "ref" | "open">;
3
+ declare const SelectMenu: import("svelte").Component<SelectMenuProps, {}, "open" | "ref" | "value">;
4
4
  type SelectMenu = ReturnType<typeof SelectMenu>;
5
5
  export default SelectMenu;