sv5ui 1.5.1 → 1.7.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/dist/Calendar/Calendar.svelte +48 -6
- package/dist/Calendar/calendar.types.d.ts +19 -0
- package/dist/Calendar/calendar.variants.js +2 -1
- package/dist/Carousel/Carousel.svelte +279 -0
- package/dist/Carousel/Carousel.svelte.d.ts +26 -0
- package/dist/Carousel/carousel.types.d.ts +242 -0
- package/dist/Carousel/carousel.types.js +1 -0
- package/dist/Carousel/carousel.variants.d.ts +408 -0
- package/dist/Carousel/carousel.variants.js +88 -0
- package/dist/Carousel/index.d.ts +2 -0
- package/dist/Carousel/index.js +1 -0
- package/dist/Checkbox/Checkbox.svelte +8 -2
- package/dist/CheckboxGroup/CheckboxGroup.svelte +15 -2
- package/dist/FileUpload/FileUpload.svelte +81 -10
- package/dist/FileUpload/file-upload.types.d.ts +39 -0
- package/dist/FileUpload/index.d.ts +1 -1
- package/dist/Form/Form.svelte +203 -0
- package/dist/Form/Form.svelte.d.ts +26 -0
- package/dist/Form/form.context.svelte.d.ts +64 -0
- package/dist/Form/form.context.svelte.js +478 -0
- package/dist/Form/form.types.d.ts +164 -0
- package/dist/Form/form.types.js +12 -0
- package/dist/Form/form.variants.d.ts +39 -0
- package/dist/Form/form.variants.js +17 -0
- package/dist/Form/index.d.ts +4 -0
- package/dist/Form/index.js +6 -0
- package/dist/Form/validate-schema.d.ts +13 -0
- package/dist/Form/validate-schema.js +113 -0
- package/dist/FormField/FormField.svelte +71 -8
- package/dist/FormField/form-field.types.d.ts +15 -0
- package/dist/Input/Input.svelte +31 -5
- package/dist/Input/Input.svelte.d.ts +25 -4
- package/dist/Input/input.types.d.ts +24 -3
- package/dist/Modal/Modal.svelte +14 -3
- package/dist/Modal/modal.types.d.ts +15 -4
- package/dist/Modal/modal.variants.d.ts +110 -20
- package/dist/Modal/modal.variants.js +27 -9
- package/dist/PinInput/PinInput.svelte +27 -6
- package/dist/PinInput/pin-input.types.d.ts +11 -0
- package/dist/RadioGroup/RadioGroup.svelte +17 -3
- package/dist/Select/Select.svelte +100 -19
- package/dist/Select/select.types.d.ts +44 -2
- package/dist/SelectMenu/SelectMenu.svelte +215 -23
- package/dist/SelectMenu/select-menu.types.d.ts +62 -1
- package/dist/SelectMenu/select-menu.variants.d.ts +26 -0
- package/dist/SelectMenu/select-menu.variants.js +34 -6
- package/dist/Slideover/Slideover.svelte +13 -2
- package/dist/Slideover/slideover.types.d.ts +14 -3
- package/dist/Slideover/slideover.variants.d.ts +85 -5
- package/dist/Slideover/slideover.variants.js +42 -12
- package/dist/Slider/Slider.svelte +4 -1
- package/dist/Switch/Switch.svelte +8 -2
- package/dist/Textarea/Textarea.svelte +27 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFormField.svelte.d.ts +64 -0
- package/dist/hooks/useFormField.svelte.js +70 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +31 -3
|
@@ -3,6 +3,12 @@ import type { ClassNameValue } from 'tailwind-merge';
|
|
|
3
3
|
import type { SelectVariantProps, SelectSlots } from './select.variants.js';
|
|
4
4
|
import type { AvatarProps } from '../Avatar/avatar.types.js';
|
|
5
5
|
import type { SelectRootPropsWithoutHTML, SelectContentPropsWithoutHTML } from 'bits-ui';
|
|
6
|
+
/**
|
|
7
|
+
* The value shape for the Select.
|
|
8
|
+
* - When `multiple` is `false` (or omitted), the value is a single string (or undefined).
|
|
9
|
+
* - When `multiple` is `true`, the value is an array of strings.
|
|
10
|
+
*/
|
|
11
|
+
export type SelectValue<Multiple extends boolean = false> = Multiple extends true ? string[] : string | undefined;
|
|
6
12
|
/**
|
|
7
13
|
* A single selectable option within the Select.
|
|
8
14
|
*/
|
|
@@ -67,6 +73,17 @@ export interface SelectItemSlotProps {
|
|
|
67
73
|
/** Whether the item is currently selected */
|
|
68
74
|
selected?: boolean;
|
|
69
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Props passed to the `selected` snippet when `multiple` is true.
|
|
78
|
+
*/
|
|
79
|
+
export interface SelectSelectedSlotProps {
|
|
80
|
+
/** The full list of currently selected items resolved from values. */
|
|
81
|
+
items: SelectItem[];
|
|
82
|
+
/** Remove a value from the current selection. */
|
|
83
|
+
remove: (value: string) => void;
|
|
84
|
+
/** Clear all selected values. */
|
|
85
|
+
clear: () => void;
|
|
86
|
+
}
|
|
70
87
|
type RootProps = Pick<SelectRootPropsWithoutHTML, 'open' | 'onOpenChange' | 'name' | 'required' | 'disabled'>;
|
|
71
88
|
type ContentProps = Pick<SelectContentPropsWithoutHTML, 'side' | 'sideOffset' | 'align' | 'alignOffset' | 'avoidCollisions' | 'collisionBoundary' | 'collisionPadding' | 'onEscapeKeydown' | 'onInteractOutside' | 'forceMount' | 'loop'>;
|
|
72
89
|
/**
|
|
@@ -92,12 +109,31 @@ export interface SelectProps extends RootProps, ContentProps {
|
|
|
92
109
|
ref?: HTMLElement | null;
|
|
93
110
|
/**
|
|
94
111
|
* The currently selected value. Supports two-way binding with `bind:value`.
|
|
112
|
+
*
|
|
113
|
+
* - When `multiple` is `false`/omitted, this is a `string`.
|
|
114
|
+
* - When `multiple` is `true`, this is a `string[]`.
|
|
95
115
|
*/
|
|
96
|
-
value?: string;
|
|
116
|
+
value?: string | string[];
|
|
97
117
|
/**
|
|
98
118
|
* The default selected value when uncontrolled.
|
|
119
|
+
*
|
|
120
|
+
* - When `multiple` is `false`/omitted, this is a `string`.
|
|
121
|
+
* - When `multiple` is `true`, this is a `string[]`.
|
|
122
|
+
*/
|
|
123
|
+
defaultValue?: string | string[];
|
|
124
|
+
/**
|
|
125
|
+
* Whether multiple items can be selected at once.
|
|
126
|
+
* When `true`, `value` becomes a `string[]` and the trigger displays
|
|
127
|
+
* a comma-separated list of selected labels by default.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
multiple?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Separator used to join selected labels in the trigger when `multiple` is `true`.
|
|
133
|
+
* Ignored when the `selected` snippet is provided.
|
|
134
|
+
* @default ', '
|
|
99
135
|
*/
|
|
100
|
-
|
|
136
|
+
separator?: string;
|
|
101
137
|
/**
|
|
102
138
|
* Array of items to display in the select dropdown.
|
|
103
139
|
*/
|
|
@@ -194,6 +230,12 @@ export interface SelectProps extends RootProps, ContentProps {
|
|
|
194
230
|
* Takes precedence over `trailingIcon`.
|
|
195
231
|
*/
|
|
196
232
|
trailingSlot?: Snippet;
|
|
233
|
+
/**
|
|
234
|
+
* Custom rendering for the selected value(s) displayed in the trigger.
|
|
235
|
+
* Useful in `multiple` mode to render chips/tags instead of the default
|
|
236
|
+
* comma-separated label list.
|
|
237
|
+
*/
|
|
238
|
+
selected?: Snippet<[SelectSelectedSlotProps]>;
|
|
197
239
|
/**
|
|
198
240
|
* Custom snippet for rendering individual items in the dropdown.
|
|
199
241
|
* When provided, replaces the default item rendering.
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
} from './select-menu.types.js'
|
|
7
7
|
|
|
8
8
|
export type Props = SelectMenuProps
|
|
9
|
+
|
|
10
|
+
const CREATE_ITEM_VALUE = '@@sv5ui/select-menu/create-item'
|
|
9
11
|
</script>
|
|
10
12
|
|
|
11
13
|
<script lang="ts">
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
import Avatar from '../Avatar/Avatar.svelte'
|
|
22
24
|
import Input from '../Input/Input.svelte'
|
|
23
25
|
import type { AvatarSize } from '../Avatar/avatar.types.js'
|
|
24
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
26
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
25
27
|
|
|
26
28
|
const config = getComponentConfig('selectMenu', selectMenuDefaults)
|
|
27
29
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -37,6 +39,8 @@
|
|
|
37
39
|
name,
|
|
38
40
|
required = false,
|
|
39
41
|
disabled = false,
|
|
42
|
+
multiple = false,
|
|
43
|
+
separator = ', ',
|
|
40
44
|
ui,
|
|
41
45
|
id,
|
|
42
46
|
color = config.defaultVariants.color,
|
|
@@ -53,6 +57,10 @@
|
|
|
53
57
|
filterFields = ['label', 'value'] as string[],
|
|
54
58
|
ignoreFilter = false,
|
|
55
59
|
emptyText = 'No results found.',
|
|
60
|
+
createItem = false,
|
|
61
|
+
createItemLabel = (value: string) => `Create "${value}"`,
|
|
62
|
+
createItemIcon,
|
|
63
|
+
onCreate,
|
|
56
64
|
transition = config.defaultVariants.transition ?? true,
|
|
57
65
|
portal = true,
|
|
58
66
|
side = config.defaultVariants.side ?? 'bottom',
|
|
@@ -72,12 +80,14 @@
|
|
|
72
80
|
itemLeading,
|
|
73
81
|
itemLabel: itemLabelSlot,
|
|
74
82
|
itemTrailing,
|
|
83
|
+
selected: selectedSlot,
|
|
75
84
|
empty: emptySlot,
|
|
76
85
|
content: contentSlot
|
|
77
86
|
}: Props = $props()
|
|
78
87
|
|
|
79
88
|
// ---- Form context ----
|
|
80
89
|
const formFieldContext = useFormField()
|
|
90
|
+
const emit = useFormFieldEmit()
|
|
81
91
|
|
|
82
92
|
const fieldGroupContext = getContext<
|
|
83
93
|
| {
|
|
@@ -112,25 +122,70 @@
|
|
|
112
122
|
: `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`
|
|
113
123
|
)
|
|
114
124
|
|
|
125
|
+
// ---- Created items (internal state for createItem) ----
|
|
126
|
+
let createdItems = $state<SelectMenuItem[]>([])
|
|
127
|
+
|
|
128
|
+
const combinedItems = $derived.by(() => {
|
|
129
|
+
const propValues = new Set(
|
|
130
|
+
(items as SelectMenuItemType[])
|
|
131
|
+
.filter((i): i is SelectMenuItem => !('type' in i))
|
|
132
|
+
.map((i) => i.value)
|
|
133
|
+
)
|
|
134
|
+
const extras = createdItems.filter((c) => !propValues.has(c.value))
|
|
135
|
+
return [...(items as SelectMenuItemType[]), ...extras]
|
|
136
|
+
})
|
|
137
|
+
|
|
115
138
|
// ---- Items lookup (O(1) via Map) ----
|
|
116
139
|
const itemsMap = $derived(
|
|
117
140
|
new Map(
|
|
118
|
-
|
|
141
|
+
combinedItems
|
|
119
142
|
.filter((i): i is SelectMenuItem => !('type' in i))
|
|
120
143
|
.map((i) => [i.value, i])
|
|
121
144
|
)
|
|
122
145
|
)
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
const
|
|
147
|
+
// ---- Selection (single + multiple) ----
|
|
148
|
+
const selectedValues = $derived(
|
|
149
|
+
multiple
|
|
150
|
+
? Array.isArray(value)
|
|
151
|
+
? (value as string[])
|
|
152
|
+
: []
|
|
153
|
+
: typeof value === 'string' && value !== ''
|
|
154
|
+
? [value]
|
|
155
|
+
: []
|
|
156
|
+
)
|
|
157
|
+
const selectedItems = $derived(
|
|
158
|
+
selectedValues
|
|
159
|
+
.map((v) => itemsMap.get(v))
|
|
160
|
+
.filter((i): i is SelectMenuItem => i !== undefined)
|
|
161
|
+
)
|
|
162
|
+
const hasSelection = $derived(selectedValues.length > 0)
|
|
163
|
+
const singleSelectedItem = $derived(multiple ? undefined : selectedItems[0])
|
|
164
|
+
const displayLabel = $derived(
|
|
165
|
+
multiple
|
|
166
|
+
? selectedItems.map((i) => i.label ?? i.value).join(separator)
|
|
167
|
+
: (singleSelectedItem?.label ?? singleSelectedItem?.value ?? '')
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
function removeValue(val: string) {
|
|
171
|
+
if (!multiple) return
|
|
172
|
+
value = selectedValues.filter((v) => v !== val)
|
|
173
|
+
emit.onChange()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function clearSelection() {
|
|
177
|
+
if (!multiple) return
|
|
178
|
+
value = []
|
|
179
|
+
emit.onChange()
|
|
180
|
+
}
|
|
126
181
|
|
|
127
182
|
// ---- Search & filtering ----
|
|
128
183
|
let searchTerm = $state('')
|
|
129
184
|
|
|
130
185
|
const filteredItems = $derived(
|
|
131
186
|
ignoreFilter || !searchTerm.trim()
|
|
132
|
-
?
|
|
133
|
-
:
|
|
187
|
+
? combinedItems
|
|
188
|
+
: combinedItems.filter((item) => {
|
|
134
189
|
if ('type' in item) return true
|
|
135
190
|
const query = searchTerm.toLowerCase()
|
|
136
191
|
return filterFields.some((field) => {
|
|
@@ -142,9 +197,73 @@
|
|
|
142
197
|
|
|
143
198
|
const hasFilteredSelectItems = $derived(filteredItems.some((item) => !('type' in item)))
|
|
144
199
|
|
|
200
|
+
// ---- Create item ----
|
|
201
|
+
const trimmedSearch = $derived(searchTerm.trim())
|
|
202
|
+
const exactMatchExists = $derived.by(() => {
|
|
203
|
+
if (!trimmedSearch) return false
|
|
204
|
+
const query = trimmedSearch.toLowerCase()
|
|
205
|
+
for (const i of combinedItems) {
|
|
206
|
+
if ('type' in i) continue
|
|
207
|
+
if (i.value.toLowerCase() === query || (i.label ?? i.value).toLowerCase() === query) {
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return false
|
|
212
|
+
})
|
|
213
|
+
const showCreateItem = $derived.by(() => {
|
|
214
|
+
if (!createItem) return false
|
|
215
|
+
if (!trimmedSearch) return false
|
|
216
|
+
const mode = createItem === true ? 'lazy' : createItem
|
|
217
|
+
if (mode === 'always') return true
|
|
218
|
+
return !exactMatchExists
|
|
219
|
+
})
|
|
220
|
+
const resolvedCreateLabel = $derived(
|
|
221
|
+
typeof createItemLabel === 'function' ? createItemLabel(trimmedSearch) : createItemLabel
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
function findItemByCaseInsensitive(query: string): SelectMenuItem | undefined {
|
|
225
|
+
const q = query.toLowerCase()
|
|
226
|
+
for (const it of itemsMap.values()) {
|
|
227
|
+
if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) {
|
|
228
|
+
return it
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function selectValue(val: string) {
|
|
235
|
+
if (multiple) {
|
|
236
|
+
if (!selectedValues.includes(val)) {
|
|
237
|
+
value = [...selectedValues, val]
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
value = val
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleCreate() {
|
|
245
|
+
if (!showCreateItem) return
|
|
246
|
+
const newValue = trimmedSearch
|
|
247
|
+
if (!newValue) return
|
|
248
|
+
|
|
249
|
+
const existing = findItemByCaseInsensitive(newValue)
|
|
250
|
+
if (existing) {
|
|
251
|
+
selectValue(existing.value)
|
|
252
|
+
} else {
|
|
253
|
+
createdItems = [...createdItems, { value: newValue, label: newValue }]
|
|
254
|
+
selectValue(newValue)
|
|
255
|
+
onCreate?.(newValue)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
emit.onChange()
|
|
259
|
+
searchTerm = ''
|
|
260
|
+
}
|
|
261
|
+
|
|
145
262
|
// ---- Leading / trailing ----
|
|
146
|
-
const displayAvatar = $derived(
|
|
147
|
-
const displayIcon = $derived(
|
|
263
|
+
const displayAvatar = $derived(multiple ? avatar : (singleSelectedItem?.avatar ?? avatar))
|
|
264
|
+
const displayIcon = $derived(
|
|
265
|
+
multiple ? (leadingIcon ?? icon) : (singleSelectedItem?.icon ?? leadingIcon ?? icon)
|
|
266
|
+
)
|
|
148
267
|
const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
|
|
149
268
|
const leadingIconName = $derived(
|
|
150
269
|
loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
|
|
@@ -218,6 +337,17 @@
|
|
|
218
337
|
variantSlots.separator({ class: [config.slots.separator, ui?.separator] })
|
|
219
338
|
)
|
|
220
339
|
const emptyClass = $derived(variantSlots.empty({ class: [config.slots.empty, ui?.empty] }))
|
|
340
|
+
const createItemClass = $derived(
|
|
341
|
+
variantSlots.createItem({ class: [config.slots.createItem, ui?.createItem] })
|
|
342
|
+
)
|
|
343
|
+
const createItemIconClass = $derived(
|
|
344
|
+
variantSlots.createItemIcon({ class: [config.slots.createItemIcon, ui?.createItemIcon] })
|
|
345
|
+
)
|
|
346
|
+
const createItemLabelClass = $derived(
|
|
347
|
+
variantSlots.createItemLabel({
|
|
348
|
+
class: [config.slots.createItemLabel, ui?.createItemLabel]
|
|
349
|
+
})
|
|
350
|
+
)
|
|
221
351
|
|
|
222
352
|
// ---- Item classes ----
|
|
223
353
|
const itemClass = $derived(variantSlots.item({ class: [config.slots.item, ui?.item] }))
|
|
@@ -257,13 +387,16 @@
|
|
|
257
387
|
function onUpdateOpen(val: boolean) {
|
|
258
388
|
if (!val) {
|
|
259
389
|
searchTerm = ''
|
|
390
|
+
emit.onBlur()
|
|
391
|
+
} else {
|
|
392
|
+
emit.onFocus()
|
|
260
393
|
}
|
|
261
394
|
onOpenChange?.(val)
|
|
262
395
|
}
|
|
263
396
|
</script>
|
|
264
397
|
|
|
265
398
|
{#snippet renderItem(item: SelectMenuItem, index: number)}
|
|
266
|
-
{@const isSelected =
|
|
399
|
+
{@const isSelected = selectedValues.includes(item.value)}
|
|
267
400
|
<Combobox.Item
|
|
268
401
|
value={item.value}
|
|
269
402
|
label={item.label ?? item.value}
|
|
@@ -316,6 +449,12 @@
|
|
|
316
449
|
placeholder={searchPlaceholder}
|
|
317
450
|
value={searchTerm}
|
|
318
451
|
oninput={(e) => (searchTerm = (e.currentTarget as HTMLInputElement).value)}
|
|
452
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
453
|
+
if (e.key !== 'Enter') return
|
|
454
|
+
if (!showCreateItem) return
|
|
455
|
+
e.preventDefault()
|
|
456
|
+
handleCreate()
|
|
457
|
+
}}
|
|
319
458
|
variant="none"
|
|
320
459
|
size={resolvedSize}
|
|
321
460
|
class={inputClass}
|
|
@@ -339,7 +478,7 @@
|
|
|
339
478
|
{@render itemSlot({
|
|
340
479
|
item: selectItem,
|
|
341
480
|
index,
|
|
342
|
-
selected:
|
|
481
|
+
selected: selectedValues.includes(selectItem.value)
|
|
343
482
|
})}
|
|
344
483
|
{:else}
|
|
345
484
|
{@render renderItem(selectItem, index)}
|
|
@@ -347,28 +486,33 @@
|
|
|
347
486
|
{/if}
|
|
348
487
|
{/each}
|
|
349
488
|
|
|
350
|
-
{#if !hasFilteredSelectItems}
|
|
489
|
+
{#if !hasFilteredSelectItems && !showCreateItem}
|
|
351
490
|
{#if emptySlot}
|
|
352
491
|
{@render emptySlot({ searchTerm })}
|
|
353
492
|
{:else}
|
|
354
493
|
<div class={emptyClass}>{emptyText}</div>
|
|
355
494
|
{/if}
|
|
356
495
|
{/if}
|
|
496
|
+
|
|
497
|
+
{#if showCreateItem}
|
|
498
|
+
<Combobox.Item
|
|
499
|
+
value={CREATE_ITEM_VALUE}
|
|
500
|
+
label={resolvedCreateLabel}
|
|
501
|
+
{disabled}
|
|
502
|
+
class={createItemClass}
|
|
503
|
+
>
|
|
504
|
+
{#if createItemIcon}
|
|
505
|
+
<Icon name={createItemIcon} class={createItemIconClass} />
|
|
506
|
+
{/if}
|
|
507
|
+
<span class={createItemLabelClass}>{resolvedCreateLabel}</span>
|
|
508
|
+
</Combobox.Item>
|
|
509
|
+
{/if}
|
|
357
510
|
</div>
|
|
358
511
|
{/if}
|
|
359
512
|
</Combobox.Content>
|
|
360
513
|
{/snippet}
|
|
361
514
|
|
|
362
|
-
|
|
363
|
-
type="single"
|
|
364
|
-
bind:open
|
|
365
|
-
onOpenChange={onUpdateOpen}
|
|
366
|
-
{disabled}
|
|
367
|
-
{required}
|
|
368
|
-
{value}
|
|
369
|
-
onValueChange={(val) => (value = val)}
|
|
370
|
-
name={resolvedName}
|
|
371
|
-
>
|
|
515
|
+
{#snippet rootChildren()}
|
|
372
516
|
<div bind:this={ref} class={rootClass}>
|
|
373
517
|
{#if leadingSlot}
|
|
374
518
|
<span class={leadingClass}>
|
|
@@ -400,7 +544,13 @@
|
|
|
400
544
|
aria-invalid={resolvedHighlight ? true : undefined}
|
|
401
545
|
class={baseClass}
|
|
402
546
|
>
|
|
403
|
-
{#if
|
|
547
|
+
{#if selectedSlot && hasSelection}
|
|
548
|
+
{@render selectedSlot({
|
|
549
|
+
items: selectedItems,
|
|
550
|
+
remove: removeValue,
|
|
551
|
+
clear: clearSelection
|
|
552
|
+
})}
|
|
553
|
+
{:else if hasSelection && displayLabel}
|
|
404
554
|
<span class={valueClass}>{displayLabel}</span>
|
|
405
555
|
{:else if placeholder}
|
|
406
556
|
<span class={placeholderClass}>{placeholder}</span>
|
|
@@ -425,4 +575,46 @@
|
|
|
425
575
|
{:else}
|
|
426
576
|
{@render contentEl()}
|
|
427
577
|
{/if}
|
|
428
|
-
|
|
578
|
+
{/snippet}
|
|
579
|
+
|
|
580
|
+
{#if multiple}
|
|
581
|
+
<Combobox.Root
|
|
582
|
+
type="multiple"
|
|
583
|
+
bind:open
|
|
584
|
+
onOpenChange={onUpdateOpen}
|
|
585
|
+
{disabled}
|
|
586
|
+
{required}
|
|
587
|
+
value={selectedValues}
|
|
588
|
+
onValueChange={(val) => {
|
|
589
|
+
if (Array.isArray(val) && val.includes(CREATE_ITEM_VALUE)) {
|
|
590
|
+
handleCreate()
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
value = val
|
|
594
|
+
emit.onChange()
|
|
595
|
+
}}
|
|
596
|
+
name={resolvedName}
|
|
597
|
+
>
|
|
598
|
+
{@render rootChildren()}
|
|
599
|
+
</Combobox.Root>
|
|
600
|
+
{:else}
|
|
601
|
+
<Combobox.Root
|
|
602
|
+
type="single"
|
|
603
|
+
bind:open
|
|
604
|
+
onOpenChange={onUpdateOpen}
|
|
605
|
+
{disabled}
|
|
606
|
+
{required}
|
|
607
|
+
value={selectedValues[0] ?? ''}
|
|
608
|
+
onValueChange={(val) => {
|
|
609
|
+
if (val === CREATE_ITEM_VALUE) {
|
|
610
|
+
handleCreate()
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
value = val
|
|
614
|
+
emit.onChange()
|
|
615
|
+
}}
|
|
616
|
+
name={resolvedName}
|
|
617
|
+
>
|
|
618
|
+
{@render rootChildren()}
|
|
619
|
+
</Combobox.Root>
|
|
620
|
+
{/if}
|
|
@@ -67,6 +67,17 @@ export interface SelectMenuItemSlotProps {
|
|
|
67
67
|
/** Whether the item is currently selected */
|
|
68
68
|
selected?: boolean;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Props passed to the `selected` snippet when `multiple` is true.
|
|
72
|
+
*/
|
|
73
|
+
export interface SelectMenuSelectedSlotProps {
|
|
74
|
+
/** The full list of currently selected items resolved from values. */
|
|
75
|
+
items: SelectMenuItem[];
|
|
76
|
+
/** Remove a value from the current selection. */
|
|
77
|
+
remove: (value: string) => void;
|
|
78
|
+
/** Clear all selected values. */
|
|
79
|
+
clear: () => void;
|
|
80
|
+
}
|
|
70
81
|
type ContentProps = Pick<ComboboxContentPropsWithoutHTML, 'side' | 'sideOffset' | 'align' | 'alignOffset' | 'avoidCollisions' | 'collisionBoundary' | 'collisionPadding' | 'onEscapeKeydown' | 'onInteractOutside' | 'forceMount'>;
|
|
71
82
|
/**
|
|
72
83
|
* Props for the SelectMenu component.
|
|
@@ -83,8 +94,25 @@ export interface SelectMenuProps extends ContentProps {
|
|
|
83
94
|
ref?: HTMLElement | null;
|
|
84
95
|
/**
|
|
85
96
|
* The currently selected value. Supports two-way binding with `bind:value`.
|
|
97
|
+
*
|
|
98
|
+
* - When `multiple` is `false`/omitted, this is a `string`.
|
|
99
|
+
* - When `multiple` is `true`, this is a `string[]`.
|
|
100
|
+
*/
|
|
101
|
+
value?: string | string[];
|
|
102
|
+
/**
|
|
103
|
+
* Whether multiple items can be selected at once.
|
|
104
|
+
* When `true`, `value` becomes a `string[]` and the dropdown stays open
|
|
105
|
+
* after each selection. The trigger displays a comma-separated list of
|
|
106
|
+
* selected labels by default; use the `selected` snippet for chips/tags.
|
|
107
|
+
* @default false
|
|
108
|
+
*/
|
|
109
|
+
multiple?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Separator used to join selected labels in the trigger when `multiple` is `true`.
|
|
112
|
+
* Ignored when the `selected` snippet is provided.
|
|
113
|
+
* @default ', '
|
|
86
114
|
*/
|
|
87
|
-
|
|
115
|
+
separator?: string;
|
|
88
116
|
/**
|
|
89
117
|
* Whether the dropdown is open. Supports two-way binding with `bind:open`.
|
|
90
118
|
* @default false
|
|
@@ -192,6 +220,33 @@ export interface SelectMenuProps extends ContentProps {
|
|
|
192
220
|
* @default 'No results found.'
|
|
193
221
|
*/
|
|
194
222
|
emptyText?: string;
|
|
223
|
+
/**
|
|
224
|
+
* Allow the user to create a new item by typing in the search input.
|
|
225
|
+
* - `false` (default): the feature is disabled.
|
|
226
|
+
* - `true` / `'lazy'`: the create option only appears when no existing item
|
|
227
|
+
* matches the current search term (case-insensitive on `value` or `label`).
|
|
228
|
+
* - `'always'`: the create option is shown whenever the search term is
|
|
229
|
+
* non-empty, regardless of existing matches.
|
|
230
|
+
* @default false
|
|
231
|
+
*/
|
|
232
|
+
createItem?: boolean | 'always' | 'lazy';
|
|
233
|
+
/**
|
|
234
|
+
* Label rendered on the "create" option. Receives the trimmed search term.
|
|
235
|
+
* @default (value) => `Create "${value}"`
|
|
236
|
+
*/
|
|
237
|
+
createItemLabel?: string | ((value: string) => string);
|
|
238
|
+
/**
|
|
239
|
+
* Icon shown before the create option label. Pass `false` (or omit) to
|
|
240
|
+
* render no icon.
|
|
241
|
+
* @default undefined
|
|
242
|
+
*/
|
|
243
|
+
createItemIcon?: string | false;
|
|
244
|
+
/**
|
|
245
|
+
* Called when the user picks the "create" option. The new value is also
|
|
246
|
+
* tracked internally so the trigger can render its label even if the
|
|
247
|
+
* caller does not push it into `items`.
|
|
248
|
+
*/
|
|
249
|
+
onCreate?: (value: string) => void;
|
|
195
250
|
/**
|
|
196
251
|
* Animate the dropdown on open and close.
|
|
197
252
|
* @default true
|
|
@@ -224,6 +279,12 @@ export interface SelectMenuProps extends ContentProps {
|
|
|
224
279
|
* Takes precedence over `trailingIcon`.
|
|
225
280
|
*/
|
|
226
281
|
trailingSlot?: Snippet;
|
|
282
|
+
/**
|
|
283
|
+
* Custom rendering for the selected value(s) displayed in the trigger.
|
|
284
|
+
* Useful in `multiple` mode to render chips/tags instead of the default
|
|
285
|
+
* comma-separated label list.
|
|
286
|
+
*/
|
|
287
|
+
selected?: Snippet<[SelectMenuSelectedSlotProps]>;
|
|
227
288
|
/**
|
|
228
289
|
* Custom snippet for rendering individual items in the dropdown.
|
|
229
290
|
* When provided, replaces the default item rendering.
|
|
@@ -3,18 +3,28 @@ export declare const selectMenuVariants: import("tailwind-variants").TVReturnTyp
|
|
|
3
3
|
size: {
|
|
4
4
|
xs: {
|
|
5
5
|
empty: string;
|
|
6
|
+
createItem: string;
|
|
7
|
+
createItemIcon: string;
|
|
6
8
|
};
|
|
7
9
|
sm: {
|
|
8
10
|
empty: string;
|
|
11
|
+
createItem: string;
|
|
12
|
+
createItemIcon: string;
|
|
9
13
|
};
|
|
10
14
|
md: {
|
|
11
15
|
empty: string;
|
|
16
|
+
createItem: string;
|
|
17
|
+
createItemIcon: string;
|
|
12
18
|
};
|
|
13
19
|
lg: {
|
|
14
20
|
empty: string;
|
|
21
|
+
createItem: string;
|
|
22
|
+
createItemIcon: string;
|
|
15
23
|
};
|
|
16
24
|
xl: {
|
|
17
25
|
empty: string;
|
|
26
|
+
createItem: string;
|
|
27
|
+
createItemIcon: string;
|
|
18
28
|
};
|
|
19
29
|
};
|
|
20
30
|
}, {
|
|
@@ -22,6 +32,9 @@ export declare const selectMenuVariants: import("tailwind-variants").TVReturnTyp
|
|
|
22
32
|
input: string;
|
|
23
33
|
viewport: string;
|
|
24
34
|
empty: string;
|
|
35
|
+
createItem: string[];
|
|
36
|
+
createItemIcon: string;
|
|
37
|
+
createItemLabel: string;
|
|
25
38
|
}, undefined, {
|
|
26
39
|
variant: {
|
|
27
40
|
outline: string;
|
|
@@ -594,18 +607,28 @@ export declare const selectMenuDefaults: {
|
|
|
594
607
|
size: {
|
|
595
608
|
xs: {
|
|
596
609
|
empty: string;
|
|
610
|
+
createItem: string;
|
|
611
|
+
createItemIcon: string;
|
|
597
612
|
};
|
|
598
613
|
sm: {
|
|
599
614
|
empty: string;
|
|
615
|
+
createItem: string;
|
|
616
|
+
createItemIcon: string;
|
|
600
617
|
};
|
|
601
618
|
md: {
|
|
602
619
|
empty: string;
|
|
620
|
+
createItem: string;
|
|
621
|
+
createItemIcon: string;
|
|
603
622
|
};
|
|
604
623
|
lg: {
|
|
605
624
|
empty: string;
|
|
625
|
+
createItem: string;
|
|
626
|
+
createItemIcon: string;
|
|
606
627
|
};
|
|
607
628
|
xl: {
|
|
608
629
|
empty: string;
|
|
630
|
+
createItem: string;
|
|
631
|
+
createItemIcon: string;
|
|
609
632
|
};
|
|
610
633
|
};
|
|
611
634
|
}, {
|
|
@@ -613,6 +636,9 @@ export declare const selectMenuDefaults: {
|
|
|
613
636
|
input: string;
|
|
614
637
|
viewport: string;
|
|
615
638
|
empty: string;
|
|
639
|
+
createItem: string[];
|
|
640
|
+
createItemIcon: string;
|
|
641
|
+
createItemLabel: string;
|
|
616
642
|
}, {
|
|
617
643
|
variant: {
|
|
618
644
|
outline: string;
|
|
@@ -15,15 +15,43 @@ export const selectMenuVariants = tv({
|
|
|
15
15
|
],
|
|
16
16
|
input: 'border-b border-outline-variant',
|
|
17
17
|
viewport: 'p-1 flex-1 overflow-y-auto scrollbar-thin',
|
|
18
|
-
empty: 'text-center text-on-surface-variant'
|
|
18
|
+
empty: 'text-center text-on-surface-variant',
|
|
19
|
+
createItem: [
|
|
20
|
+
'group relative flex items-center gap-2 w-full rounded-sm px-2 cursor-pointer select-none',
|
|
21
|
+
'focus:outline-none',
|
|
22
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
23
|
+
'data-[highlighted]:bg-surface-container-highest'
|
|
24
|
+
],
|
|
25
|
+
createItemIcon: 'shrink-0 text-primary',
|
|
26
|
+
createItemLabel: 'flex-1 truncate text-on-surface'
|
|
19
27
|
},
|
|
20
28
|
variants: {
|
|
21
29
|
size: {
|
|
22
|
-
xs: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
xs: {
|
|
31
|
+
empty: 'p-2 text-xs',
|
|
32
|
+
createItem: 'py-1 text-xs',
|
|
33
|
+
createItemIcon: 'size-3'
|
|
34
|
+
},
|
|
35
|
+
sm: {
|
|
36
|
+
empty: 'p-2.5 text-xs',
|
|
37
|
+
createItem: 'py-1.5 text-xs',
|
|
38
|
+
createItemIcon: 'size-3.5'
|
|
39
|
+
},
|
|
40
|
+
md: {
|
|
41
|
+
empty: 'p-2.5 text-sm',
|
|
42
|
+
createItem: 'py-1.5 text-sm',
|
|
43
|
+
createItemIcon: 'size-4'
|
|
44
|
+
},
|
|
45
|
+
lg: {
|
|
46
|
+
empty: 'p-3 text-sm',
|
|
47
|
+
createItem: 'py-2 text-sm',
|
|
48
|
+
createItemIcon: 'size-5'
|
|
49
|
+
},
|
|
50
|
+
xl: {
|
|
51
|
+
empty: 'p-3 text-base',
|
|
52
|
+
createItem: 'py-2.5 text-base',
|
|
53
|
+
createItemIcon: 'size-5'
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
56
|
}
|
|
29
57
|
});
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
description,
|
|
29
29
|
side = config.defaultVariants.side ?? 'right',
|
|
30
30
|
overlay: showOverlay = config.defaultVariants.overlay ?? true,
|
|
31
|
-
transition = config.defaultVariants.transition ??
|
|
31
|
+
transition = config.defaultVariants.transition ?? 'slide',
|
|
32
|
+
size = config.defaultVariants.size ?? 'md',
|
|
32
33
|
inset = config.defaultVariants.inset ?? false,
|
|
33
34
|
portal = true,
|
|
34
35
|
close: closeProp = true,
|
|
@@ -56,8 +57,18 @@
|
|
|
56
57
|
!!headerSlot || hasHeading || !!actionsSlot || showClose || !!closeSlot
|
|
57
58
|
)
|
|
58
59
|
|
|
60
|
+
const resolvedTransition = $derived(
|
|
61
|
+
transition === false ? 'none' : transition === true ? 'slide' : transition
|
|
62
|
+
)
|
|
63
|
+
|
|
59
64
|
const variantSlots = $derived(
|
|
60
|
-
slideoverVariants({
|
|
65
|
+
slideoverVariants({
|
|
66
|
+
transition: resolvedTransition,
|
|
67
|
+
side,
|
|
68
|
+
size,
|
|
69
|
+
inset,
|
|
70
|
+
overlay: showOverlay
|
|
71
|
+
})
|
|
61
72
|
)
|
|
62
73
|
|
|
63
74
|
const classes = $derived({
|