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.
- package/README.md +6 -0
- package/dist/Alert/Alert.svelte +33 -22
- package/dist/Alert/Alert.svelte.d.ts +1 -1
- package/dist/Alert/alert.types.d.ts +4 -0
- package/dist/Avatar/Avatar.svelte +72 -46
- package/dist/Avatar/avatar.types.d.ts +36 -3
- package/dist/Avatar/avatar.variants.d.ts +138 -0
- package/dist/Avatar/avatar.variants.js +23 -12
- package/dist/Avatar/index.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroup.svelte +11 -6
- package/dist/AvatarGroup/AvatarGroup.svelte.d.ts +1 -1
- package/dist/AvatarGroup/avatar-group.types.d.ts +18 -3
- package/dist/AvatarGroup/avatar-group.variants.d.ts +85 -0
- package/dist/AvatarGroup/avatar-group.variants.js +19 -29
- package/dist/Badge/Badge.svelte +4 -3
- package/dist/Badge/Badge.svelte.d.ts +1 -1
- package/dist/Badge/badge.types.d.ts +9 -0
- package/dist/Breadcrumb/Breadcrumb.svelte +20 -7
- package/dist/Breadcrumb/Breadcrumb.svelte.d.ts +1 -1
- package/dist/Breadcrumb/breadcrumb.types.d.ts +5 -1
- package/dist/Breadcrumb/breadcrumb.variants.d.ts +15 -5
- package/dist/Breadcrumb/breadcrumb.variants.js +7 -3
- package/dist/Button/Button.svelte +71 -16
- package/dist/Button/Button.svelte.d.ts +0 -1
- package/dist/Button/button.types.d.ts +61 -2
- package/dist/Calendar/Calendar.svelte +4 -0
- package/dist/Calendar/Calendar.svelte.d.ts +1 -1
- package/dist/Calendar/calendar.types.d.ts +4 -0
- package/dist/Card/Card.svelte +5 -4
- package/dist/Card/Card.svelte.d.ts +1 -1
- package/dist/Card/card.types.d.ts +5 -1
- package/dist/Checkbox/Checkbox.svelte +37 -11
- package/dist/Checkbox/Checkbox.svelte.d.ts +1 -1
- package/dist/Checkbox/checkbox.types.d.ts +16 -1
- package/dist/Checkbox/checkbox.variants.d.ts +90 -0
- package/dist/Checkbox/checkbox.variants.js +73 -4
- package/dist/CheckboxGroup/CheckboxGroup.svelte +215 -0
- package/dist/CheckboxGroup/CheckboxGroup.svelte.d.ts +5 -0
- package/dist/CheckboxGroup/checkbox-group.types.d.ts +130 -0
- package/dist/CheckboxGroup/checkbox-group.types.js +1 -0
- package/dist/CheckboxGroup/checkbox-group.variants.d.ts +553 -0
- package/dist/CheckboxGroup/checkbox-group.variants.js +231 -0
- package/dist/CheckboxGroup/index.d.ts +2 -0
- package/dist/CheckboxGroup/index.js +1 -0
- package/dist/Chip/Chip.svelte +3 -2
- package/dist/Chip/Chip.svelte.d.ts +1 -1
- package/dist/Chip/chip.types.d.ts +5 -1
- package/dist/Chip/chip.variants.d.ts +135 -45
- package/dist/Chip/chip.variants.js +9 -9
- package/dist/ContextMenu/ContextMenu.svelte +87 -77
- package/dist/ContextMenu/ContextMenu.svelte.d.ts +1 -1
- package/dist/ContextMenu/context-menu.types.d.ts +9 -3
- package/dist/ContextMenu/context-menu.types.js +1 -1
- package/dist/ContextMenu/context-menu.variants.d.ts +74 -160
- package/dist/ContextMenu/context-menu.variants.js +63 -95
- package/dist/DropdownMenu/DropdownMenu.svelte +37 -43
- package/dist/DropdownMenu/DropdownMenu.svelte.d.ts +1 -1
- package/dist/DropdownMenu/dropdown-menu.types.d.ts +9 -3
- package/dist/DropdownMenu/dropdown-menu.types.js +1 -1
- package/dist/DropdownMenu/dropdown-menu.variants.d.ts +79 -230
- package/dist/DropdownMenu/dropdown-menu.variants.js +68 -111
- package/dist/DropdownMenu/index.d.ts +1 -1
- package/dist/Empty/Empty.svelte +68 -33
- package/dist/Empty/Empty.svelte.d.ts +1 -1
- package/dist/Empty/empty.types.d.ts +26 -9
- package/dist/Empty/empty.variants.d.ts +150 -130
- package/dist/Empty/empty.variants.js +33 -324
- package/dist/FieldGroup/FieldGroup.svelte +11 -6
- package/dist/FieldGroup/FieldGroup.svelte.d.ts +1 -1
- package/dist/FieldGroup/field-group.types.d.ts +4 -0
- package/dist/FileUpload/FileUpload.svelte +561 -0
- package/dist/FileUpload/FileUpload.svelte.d.ts +8 -0
- package/dist/FileUpload/file-upload.types.d.ts +164 -0
- package/dist/FileUpload/file-upload.types.js +1 -0
- package/dist/FileUpload/file-upload.variants.d.ts +397 -0
- package/dist/FileUpload/file-upload.variants.js +224 -0
- package/dist/FileUpload/index.d.ts +2 -0
- package/dist/FileUpload/index.js +1 -0
- package/dist/FormField/FormField.svelte +17 -18
- package/dist/FormField/FormField.svelte.d.ts +1 -1
- package/dist/FormField/form-field.types.d.ts +4 -0
- package/dist/Icon/Icon.svelte +13 -7
- package/dist/Icon/icon.types.d.ts +18 -9
- package/dist/Input/Input.svelte +30 -29
- package/dist/Kbd/Kbd.svelte +13 -3
- package/dist/Kbd/Kbd.svelte.d.ts +1 -1
- package/dist/Kbd/index.d.ts +1 -1
- package/dist/Kbd/kbd.types.d.ts +15 -1
- package/dist/Kbd/kbd.variants.d.ts +92 -30
- package/dist/Kbd/kbd.variants.js +55 -35
- package/dist/Kbd/useKbd.svelte.d.ts +2 -2
- package/dist/Kbd/useKbd.svelte.js +34 -41
- package/dist/Link/Link.svelte +69 -24
- package/dist/Link/Link.svelte.d.ts +1 -1
- package/dist/Link/link.types.d.ts +26 -8
- package/dist/Link/link.variants.d.ts +35 -60
- package/dist/Link/link.variants.js +8 -110
- package/dist/Modal/Modal.svelte +9 -1
- package/dist/Modal/modal.types.d.ts +5 -0
- package/dist/Modal/modal.variants.d.ts +5 -0
- package/dist/Modal/modal.variants.js +1 -0
- package/dist/Pagination/Pagination.svelte +143 -94
- package/dist/Pagination/Pagination.svelte.d.ts +1 -1
- package/dist/Pagination/index.d.ts +1 -1
- package/dist/Pagination/pagination.types.d.ts +21 -2
- package/dist/Pagination/pagination.variants.d.ts +21 -387
- package/dist/Pagination/pagination.variants.js +63 -59
- package/dist/PinInput/PinInput.svelte +150 -0
- package/dist/PinInput/PinInput.svelte.d.ts +6 -0
- package/dist/PinInput/index.d.ts +2 -0
- package/dist/PinInput/index.js +1 -0
- package/dist/PinInput/pin-input.types.d.ts +99 -0
- package/dist/PinInput/pin-input.types.js +1 -0
- package/dist/PinInput/pin-input.variants.d.ts +303 -0
- package/dist/PinInput/pin-input.variants.js +196 -0
- package/dist/Popover/Popover.svelte +9 -12
- package/dist/Popover/Popover.svelte.d.ts +1 -1
- package/dist/Popover/popover.types.d.ts +4 -0
- package/dist/Popover/popover.variants.d.ts +5 -75
- package/dist/Popover/popover.variants.js +6 -16
- package/dist/Progress/Progress.svelte +58 -30
- package/dist/Progress/progress.types.d.ts +9 -1
- package/dist/Progress/progress.variants.d.ts +55 -25
- package/dist/Progress/progress.variants.js +34 -28
- package/dist/RadioGroup/RadioGroup.svelte +105 -61
- package/dist/RadioGroup/RadioGroup.svelte.d.ts +1 -1
- package/dist/RadioGroup/radio-group.types.d.ts +16 -1
- package/dist/RadioGroup/radio-group.variants.d.ts +90 -0
- package/dist/RadioGroup/radio-group.variants.js +73 -4
- package/dist/Select/Select.svelte +9 -6
- package/dist/Select/Select.svelte.d.ts +1 -1
- package/dist/Select/select.types.d.ts +4 -0
- package/dist/SelectMenu/SelectMenu.svelte +436 -0
- package/dist/SelectMenu/SelectMenu.svelte.d.ts +5 -0
- package/dist/SelectMenu/index.d.ts +2 -0
- package/dist/SelectMenu/index.js +1 -0
- package/dist/SelectMenu/select-menu.types.d.ts +262 -0
- package/dist/SelectMenu/select-menu.types.js +1 -0
- package/dist/SelectMenu/select-menu.variants.d.ts +759 -0
- package/dist/SelectMenu/select-menu.variants.js +33 -0
- package/dist/Separator/Separator.svelte +1 -2
- package/dist/Separator/separator.variants.d.ts +1 -5
- package/dist/Separator/separator.variants.js +2 -2
- package/dist/Skeleton/Skeleton.svelte +18 -2
- package/dist/Skeleton/Skeleton.svelte.d.ts +1 -1
- package/dist/Skeleton/skeleton.types.d.ts +10 -1
- package/dist/Slideover/Slideover.svelte +9 -1
- package/dist/Slideover/slideover.types.d.ts +5 -0
- package/dist/Slideover/slideover.variants.d.ts +20 -5
- package/dist/Slideover/slideover.variants.js +4 -29
- package/dist/Slider/Slider.svelte +135 -0
- package/dist/Slider/Slider.svelte.d.ts +6 -0
- package/dist/Slider/index.d.ts +2 -0
- package/dist/Slider/index.js +1 -0
- package/dist/Slider/slider.types.d.ts +55 -0
- package/dist/Slider/slider.types.js +1 -0
- package/dist/Slider/slider.variants.d.ts +383 -0
- package/dist/Slider/slider.variants.js +102 -0
- package/dist/Switch/Switch.svelte +32 -31
- package/dist/Switch/Switch.svelte.d.ts +1 -1
- package/dist/Switch/switch.types.d.ts +6 -1
- package/dist/Switch/switch.variants.js +6 -6
- package/dist/Tabs/Tabs.svelte +6 -9
- package/dist/Tabs/Tabs.svelte.d.ts +1 -1
- package/dist/Tabs/tabs.types.d.ts +4 -0
- package/dist/Tabs/tabs.variants.js +2 -0
- package/dist/Textarea/Textarea.svelte +26 -25
- package/dist/ThemeModeButton/theme-mode-button.types.d.ts +7 -2
- package/dist/Timeline/Timeline.svelte +62 -19
- package/dist/Timeline/Timeline.svelte.d.ts +1 -1
- package/dist/Timeline/index.d.ts +1 -1
- package/dist/Timeline/timeline.types.d.ts +8 -0
- package/dist/Tooltip/Tooltip.svelte +12 -10
- package/dist/Tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/Tooltip/tooltip.types.d.ts +8 -4
- package/dist/Tooltip/tooltip.variants.d.ts +10 -75
- package/dist/Tooltip/tooltip.variants.js +8 -17
- package/dist/User/User.svelte +13 -9
- package/dist/User/User.svelte.d.ts +1 -1
- package/dist/User/user.types.d.ts +4 -0
- package/dist/User/user.variants.d.ts +60 -0
- package/dist/User/user.variants.js +13 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +9 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- 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 @@
|
|
|
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 {};
|