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.
Files changed (60) hide show
  1. package/dist/Calendar/Calendar.svelte +48 -6
  2. package/dist/Calendar/calendar.types.d.ts +19 -0
  3. package/dist/Calendar/calendar.variants.js +2 -1
  4. package/dist/Carousel/Carousel.svelte +279 -0
  5. package/dist/Carousel/Carousel.svelte.d.ts +26 -0
  6. package/dist/Carousel/carousel.types.d.ts +242 -0
  7. package/dist/Carousel/carousel.types.js +1 -0
  8. package/dist/Carousel/carousel.variants.d.ts +408 -0
  9. package/dist/Carousel/carousel.variants.js +88 -0
  10. package/dist/Carousel/index.d.ts +2 -0
  11. package/dist/Carousel/index.js +1 -0
  12. package/dist/Checkbox/Checkbox.svelte +8 -2
  13. package/dist/CheckboxGroup/CheckboxGroup.svelte +15 -2
  14. package/dist/FileUpload/FileUpload.svelte +81 -10
  15. package/dist/FileUpload/file-upload.types.d.ts +39 -0
  16. package/dist/FileUpload/index.d.ts +1 -1
  17. package/dist/Form/Form.svelte +203 -0
  18. package/dist/Form/Form.svelte.d.ts +26 -0
  19. package/dist/Form/form.context.svelte.d.ts +64 -0
  20. package/dist/Form/form.context.svelte.js +478 -0
  21. package/dist/Form/form.types.d.ts +164 -0
  22. package/dist/Form/form.types.js +12 -0
  23. package/dist/Form/form.variants.d.ts +39 -0
  24. package/dist/Form/form.variants.js +17 -0
  25. package/dist/Form/index.d.ts +4 -0
  26. package/dist/Form/index.js +6 -0
  27. package/dist/Form/validate-schema.d.ts +13 -0
  28. package/dist/Form/validate-schema.js +113 -0
  29. package/dist/FormField/FormField.svelte +71 -8
  30. package/dist/FormField/form-field.types.d.ts +15 -0
  31. package/dist/Input/Input.svelte +31 -5
  32. package/dist/Input/Input.svelte.d.ts +25 -4
  33. package/dist/Input/input.types.d.ts +24 -3
  34. package/dist/Modal/Modal.svelte +14 -3
  35. package/dist/Modal/modal.types.d.ts +15 -4
  36. package/dist/Modal/modal.variants.d.ts +110 -20
  37. package/dist/Modal/modal.variants.js +27 -9
  38. package/dist/PinInput/PinInput.svelte +27 -6
  39. package/dist/PinInput/pin-input.types.d.ts +11 -0
  40. package/dist/RadioGroup/RadioGroup.svelte +17 -3
  41. package/dist/Select/Select.svelte +100 -19
  42. package/dist/Select/select.types.d.ts +44 -2
  43. package/dist/SelectMenu/SelectMenu.svelte +215 -23
  44. package/dist/SelectMenu/select-menu.types.d.ts +62 -1
  45. package/dist/SelectMenu/select-menu.variants.d.ts +26 -0
  46. package/dist/SelectMenu/select-menu.variants.js +34 -6
  47. package/dist/Slideover/Slideover.svelte +13 -2
  48. package/dist/Slideover/slideover.types.d.ts +14 -3
  49. package/dist/Slideover/slideover.variants.d.ts +85 -5
  50. package/dist/Slideover/slideover.variants.js +42 -12
  51. package/dist/Slider/Slider.svelte +4 -1
  52. package/dist/Switch/Switch.svelte +8 -2
  53. package/dist/Textarea/Textarea.svelte +27 -1
  54. package/dist/hooks/index.d.ts +1 -1
  55. package/dist/hooks/index.js +1 -1
  56. package/dist/hooks/useFormField.svelte.d.ts +64 -0
  57. package/dist/hooks/useFormField.svelte.js +70 -1
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2 -0
  60. package/package.json +31 -3
@@ -28,8 +28,9 @@
28
28
  description,
29
29
  overlay: showOverlay = config.defaultVariants.overlay ?? true,
30
30
  scrollable = config.defaultVariants.scrollable ?? false,
31
- transition = config.defaultVariants.transition ?? true,
32
- fullscreen = config.defaultVariants.fullscreen ?? false,
31
+ transition = config.defaultVariants.transition ?? 'scale',
32
+ size = config.defaultVariants.size ?? 'md',
33
+ fullscreen = false,
33
34
  portal = true,
34
35
  close: closeProp = true,
35
36
  dismissible = true,
@@ -46,6 +47,11 @@
46
47
  closeSlot
47
48
  }: Props = $props()
48
49
 
50
+ const resolvedSize = $derived(fullscreen ? 'full' : size)
51
+ const resolvedTransition = $derived(
52
+ transition === false ? 'none' : transition === true ? 'scale' : transition
53
+ )
54
+
49
55
  const showClose = $derived(!!closeProp)
50
56
  const closeProps = $derived(typeof closeProp === 'object' ? closeProp : {})
51
57
 
@@ -57,7 +63,12 @@
57
63
  )
58
64
 
59
65
  const variantSlots = $derived(
60
- modalVariants({ transition, fullscreen, overlay: showOverlay, scrollable })
66
+ modalVariants({
67
+ transition: resolvedTransition,
68
+ size: resolvedSize,
69
+ overlay: showOverlay,
70
+ scrollable
71
+ })
61
72
  )
62
73
 
63
74
  const classes = $derived({
@@ -43,15 +43,26 @@ export interface ModalProps extends RootProps, ContentProps {
43
43
  */
44
44
  scrollable?: ModalVariantProps['scrollable'];
45
45
  /**
46
- * Animate the modal on open and close.
47
- * @default true
46
+ * Controls the entrance/exit animation.
47
+ * - `'none'` / `false`: no animation
48
+ * - `'fade'`: overlay + content fade
49
+ * - `'slide'`: overlay fade + content slide-in from top
50
+ * - `'scale'` / `true`: overlay fade + content scale-in (default)
51
+ * @default 'scale'
52
+ */
53
+ transition?: ModalVariantProps['transition'] | boolean;
54
+ /**
55
+ * Controls the modal width. The `'full'` value expands to fill the
56
+ * entire viewport (replaces the deprecated `fullscreen` prop).
57
+ * @default 'md'
48
58
  */
49
- transition?: ModalVariantProps['transition'];
59
+ size?: ModalVariantProps['size'];
50
60
  /**
51
61
  * Expand the modal to fill the entire viewport.
62
+ * @deprecated Use `size="full"` instead. Retained as an alias for backward compatibility.
52
63
  * @default false
53
64
  */
54
- fullscreen?: ModalVariantProps['fullscreen'];
65
+ fullscreen?: boolean;
55
66
  /**
56
67
  * Render the modal content in a portal appended to `<body>`.
57
68
  * @default true
@@ -1,16 +1,34 @@
1
1
  import { type VariantProps } from 'tailwind-variants';
2
2
  export declare const modalVariants: import("tailwind-variants").TVReturnType<{
3
3
  transition: {
4
- true: {
4
+ none: {};
5
+ fade: {
6
+ overlay: string;
7
+ content: string;
8
+ };
9
+ slide: {
10
+ overlay: string;
11
+ content: string;
12
+ };
13
+ scale: {
5
14
  overlay: string;
6
15
  content: string;
7
16
  };
8
17
  };
9
- fullscreen: {
10
- true: {
18
+ size: {
19
+ sm: {
11
20
  content: string;
12
21
  };
13
- false: {
22
+ md: {
23
+ content: string;
24
+ };
25
+ lg: {
26
+ content: string;
27
+ };
28
+ xl: {
29
+ content: string;
30
+ };
31
+ full: {
14
32
  content: string;
15
33
  };
16
34
  };
@@ -42,16 +60,34 @@ export declare const modalVariants: import("tailwind-variants").TVReturnType<{
42
60
  close: string;
43
61
  }, undefined, {
44
62
  transition: {
45
- true: {
63
+ none: {};
64
+ fade: {
65
+ overlay: string;
66
+ content: string;
67
+ };
68
+ slide: {
69
+ overlay: string;
70
+ content: string;
71
+ };
72
+ scale: {
46
73
  overlay: string;
47
74
  content: string;
48
75
  };
49
76
  };
50
- fullscreen: {
51
- true: {
77
+ size: {
78
+ sm: {
52
79
  content: string;
53
80
  };
54
- false: {
81
+ md: {
82
+ content: string;
83
+ };
84
+ lg: {
85
+ content: string;
86
+ };
87
+ xl: {
88
+ content: string;
89
+ };
90
+ full: {
55
91
  content: string;
56
92
  };
57
93
  };
@@ -83,16 +119,34 @@ export declare const modalVariants: import("tailwind-variants").TVReturnType<{
83
119
  close: string;
84
120
  }, import("tailwind-variants").TVReturnType<{
85
121
  transition: {
86
- true: {
122
+ none: {};
123
+ fade: {
124
+ overlay: string;
125
+ content: string;
126
+ };
127
+ slide: {
128
+ overlay: string;
129
+ content: string;
130
+ };
131
+ scale: {
87
132
  overlay: string;
88
133
  content: string;
89
134
  };
90
135
  };
91
- fullscreen: {
92
- true: {
136
+ size: {
137
+ sm: {
93
138
  content: string;
94
139
  };
95
- false: {
140
+ md: {
141
+ content: string;
142
+ };
143
+ lg: {
144
+ content: string;
145
+ };
146
+ xl: {
147
+ content: string;
148
+ };
149
+ full: {
96
150
  content: string;
97
151
  };
98
152
  };
@@ -128,16 +182,34 @@ export type ModalSlots = keyof ReturnType<typeof modalVariants>;
128
182
  export declare const modalDefaults: {
129
183
  defaultVariants: import("tailwind-variants").TVDefaultVariants<{
130
184
  transition: {
131
- true: {
185
+ none: {};
186
+ fade: {
187
+ overlay: string;
188
+ content: string;
189
+ };
190
+ slide: {
191
+ overlay: string;
192
+ content: string;
193
+ };
194
+ scale: {
132
195
  overlay: string;
133
196
  content: string;
134
197
  };
135
198
  };
136
- fullscreen: {
137
- true: {
199
+ size: {
200
+ sm: {
138
201
  content: string;
139
202
  };
140
- false: {
203
+ md: {
204
+ content: string;
205
+ };
206
+ lg: {
207
+ content: string;
208
+ };
209
+ xl: {
210
+ content: string;
211
+ };
212
+ full: {
141
213
  content: string;
142
214
  };
143
215
  };
@@ -169,16 +241,34 @@ export declare const modalDefaults: {
169
241
  close: string;
170
242
  }, {
171
243
  transition: {
172
- true: {
244
+ none: {};
245
+ fade: {
246
+ overlay: string;
247
+ content: string;
248
+ };
249
+ slide: {
250
+ overlay: string;
251
+ content: string;
252
+ };
253
+ scale: {
173
254
  overlay: string;
174
255
  content: string;
175
256
  };
176
257
  };
177
- fullscreen: {
178
- true: {
258
+ size: {
259
+ sm: {
179
260
  content: string;
180
261
  };
181
- false: {
262
+ md: {
263
+ content: string;
264
+ };
265
+ lg: {
266
+ content: string;
267
+ };
268
+ xl: {
269
+ content: string;
270
+ };
271
+ full: {
182
272
  content: string;
183
273
  };
184
274
  };
@@ -14,17 +14,35 @@ export const modalVariants = tv({
14
14
  },
15
15
  variants: {
16
16
  transition: {
17
- true: {
17
+ none: {},
18
+ fade: {
19
+ overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_150ms_ease-in]',
20
+ content: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_150ms_ease-in]'
21
+ },
22
+ slide: {
23
+ overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_150ms_ease-in]',
24
+ content: 'data-[state=open]:animate-[slide-in-from-top_200ms_ease-out] data-[state=closed]:animate-[slide-out-to-top_150ms_ease-in]'
25
+ },
26
+ scale: {
18
27
  overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_150ms_ease-in]',
19
28
  content: 'data-[state=open]:animate-[scale-in_200ms_cubic-bezier(0.32,0.72,0,1)] data-[state=closed]:animate-[scale-out_150ms_cubic-bezier(0.32,0.72,0,1)]'
20
29
  }
21
30
  },
22
- fullscreen: {
23
- true: {
24
- content: 'inset-0'
31
+ size: {
32
+ sm: {
33
+ content: 'w-[calc(100vw-2rem)] max-w-md rounded-lg shadow-lg ring ring-outline-variant'
25
34
  },
26
- false: {
35
+ md: {
27
36
  content: 'w-[calc(100vw-2rem)] max-w-lg rounded-lg shadow-lg ring ring-outline-variant'
37
+ },
38
+ lg: {
39
+ content: 'w-[calc(100vw-2rem)] max-w-2xl rounded-lg shadow-lg ring ring-outline-variant'
40
+ },
41
+ xl: {
42
+ content: 'w-[calc(100vw-2rem)] max-w-4xl rounded-lg shadow-lg ring ring-outline-variant'
43
+ },
44
+ full: {
45
+ content: 'inset-0'
28
46
  }
29
47
  },
30
48
  overlay: {
@@ -46,22 +64,22 @@ export const modalVariants = tv({
46
64
  compoundVariants: [
47
65
  {
48
66
  scrollable: true,
49
- fullscreen: false,
67
+ size: ['sm', 'md', 'lg', 'xl'],
50
68
  class: {
51
69
  overlay: 'grid place-items-center p-4 sm:py-8'
52
70
  }
53
71
  },
54
72
  {
55
73
  scrollable: false,
56
- fullscreen: false,
74
+ size: ['sm', 'md', 'lg', 'xl'],
57
75
  class: {
58
76
  content: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-4rem)] overflow-hidden'
59
77
  }
60
78
  }
61
79
  ],
62
80
  defaultVariants: {
63
- transition: true,
64
- fullscreen: false,
81
+ transition: 'scale',
82
+ size: 'md',
65
83
  overlay: true,
66
84
  scrollable: false
67
85
  }
@@ -7,10 +7,12 @@
7
7
  <script lang="ts">
8
8
  import { PinInput, useId } from 'bits-ui'
9
9
  import { pinInputVariants, pinInputDefaults } from './pin-input.variants.js'
10
- import { getComponentConfig } from '../config.js'
11
- import { useFormField } from '../hooks/useFormField.svelte.js'
10
+ import { getComponentConfig, iconsDefaults } from '../config.js'
11
+ import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
12
+ import Icon from '../Icon/Icon.svelte'
12
13
 
13
14
  const config = getComponentConfig('pinInput', pinInputDefaults)
15
+ const icons = getComponentConfig('icons', iconsDefaults)
14
16
 
15
17
  let {
16
18
  ref = $bindable(null),
@@ -33,6 +35,8 @@
33
35
  autofocus = false,
34
36
  autofocusDelay = 0,
35
37
  highlight = false,
38
+ loading = false,
39
+ loadingIcon = icons.loading,
36
40
  fixed = false,
37
41
  color = config.defaultVariants.color,
38
42
  size,
@@ -43,6 +47,9 @@
43
47
  }: Props = $props()
44
48
 
45
49
  const formFieldContext = useFormField()
50
+ const emit = useFormFieldEmit()
51
+
52
+ const isDisabled = $derived(disabled || loading)
46
53
 
47
54
  const autoInputId = useId()
48
55
  const hasError = $derived(
@@ -69,9 +76,15 @@
69
76
  function handleValueChange(v: string) {
70
77
  const filtered = type === 'number' ? v.replace(/\D/g, '') : v
71
78
  value = filtered
79
+ emit.onInput()
72
80
  onValueChange?.(filtered)
73
81
  }
74
82
 
83
+ function handleComplete(v: string) {
84
+ emit.onChange()
85
+ onComplete?.(v)
86
+ }
87
+
75
88
  const slots = $derived(
76
89
  pinInputVariants({
77
90
  variant,
@@ -79,7 +92,7 @@
79
92
  size: resolvedSize,
80
93
  highlight: resolvedHighlight,
81
94
  fixed,
82
- disabled
95
+ disabled: isDisabled
83
96
  })
84
97
  )
85
98
 
@@ -100,17 +113,25 @@
100
113
  })
101
114
  </script>
102
115
 
103
- <div class="contents" {...restProps}>
116
+ <div class="relative inline-flex" {...restProps}>
104
117
  {#if resolvedName}
105
118
  <input type="hidden" name={resolvedName} {value} />
106
119
  {/if}
120
+ {#if loading}
121
+ <span
122
+ class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-surface/60"
123
+ aria-hidden="true"
124
+ >
125
+ <Icon name={loadingIcon} class="size-5 animate-spin text-on-surface-variant" />
126
+ </span>
127
+ {/if}
107
128
  <PinInput.Root
108
129
  bind:ref
109
130
  {value}
110
131
  maxlength={length}
111
- {disabled}
132
+ disabled={isDisabled}
112
133
  {textalign}
113
- {onComplete}
134
+ onComplete={handleComplete}
114
135
  pasteTransformer={resolvedPasteTransformer}
115
136
  {pushPasswordManagerStrategy}
116
137
  inputId={resolvedInputId}
@@ -68,6 +68,17 @@ export type PinInputProps = Pick<PinInputPrimitive.RootProps, 'disabled' | 'text
68
68
  * @default false
69
69
  */
70
70
  highlight?: boolean;
71
+ /**
72
+ * Show a loading spinner over the cells and disable interaction.
73
+ * Useful when verifying an OTP code against a backend.
74
+ * @default false
75
+ */
76
+ loading?: boolean;
77
+ /**
78
+ * Icon displayed as the loading indicator. Defaults to `icons.loading`
79
+ * from the global app config (`lucide:loader-circle`).
80
+ */
81
+ loadingIcon?: string;
71
82
  /**
72
83
  * Prevent responsive text size changes on mobile.
73
84
  * @default false
@@ -9,7 +9,7 @@
9
9
  import { radioGroupVariants, radioGroupDefaults } from './radio-group.variants.js'
10
10
  import { getComponentConfig, iconsDefaults } from '../config.js'
11
11
  import Icon from '../Icon/Icon.svelte'
12
- import { useFormField } from '../hooks/useFormField.svelte.js'
12
+ import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
13
13
  import type { RadioGroupItem } from './radio-group.types.js'
14
14
 
15
15
  const config = getComponentConfig('radioGroup', radioGroupDefaults)
@@ -43,6 +43,7 @@
43
43
  }: Props = $props()
44
44
 
45
45
  const formFieldContext = useFormField()
46
+ const emit = useFormFieldEmit()
46
47
 
47
48
  const hasError = $derived(
48
49
  formFieldContext?.error !== undefined && formFieldContext?.error !== false
@@ -159,10 +160,23 @@
159
160
  {/if}
160
161
  {/snippet}
161
162
 
162
- <div {...restProps} bind:this={ref} class={layoutClasses.root}>
163
+ <div
164
+ {...restProps}
165
+ bind:this={ref}
166
+ class={layoutClasses.root}
167
+ onfocusin={() => emit.onFocus()}
168
+ onfocusout={(e) => {
169
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
170
+ emit.onBlur()
171
+ }
172
+ }}
173
+ >
163
174
  <RadioGroup.Root
164
175
  bind:value
165
- {onValueChange}
176
+ onValueChange={(val) => {
177
+ emit.onChange()
178
+ onValueChange?.(val)
179
+ }}
166
180
  id={resolvedId}
167
181
  name={resolvedName}
168
182
  disabled={isDisabled}
@@ -16,7 +16,7 @@
16
16
  import Icon from '../Icon/Icon.svelte'
17
17
  import Avatar from '../Avatar/Avatar.svelte'
18
18
  import type { AvatarSize } from '../Avatar/avatar.types.js'
19
- import { useFormField } from '../hooks/useFormField.svelte.js'
19
+ import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
20
20
 
21
21
  const config = getComponentConfig('select', selectDefaults)
22
22
  const icons = getComponentConfig('icons', iconsDefaults)
@@ -31,6 +31,8 @@
31
31
  name,
32
32
  required = false,
33
33
  disabled = false,
34
+ multiple = false,
35
+ separator = ', ',
34
36
  ui,
35
37
  id,
36
38
  color = config.defaultVariants.color,
@@ -64,11 +66,13 @@
64
66
  itemLeading,
65
67
  itemLabel: itemLabelSlot,
66
68
  itemTrailing,
69
+ selected: selectedSlot,
67
70
  content: contentSlot
68
71
  }: Props = $props()
69
72
 
70
73
  // ---- Form context ----
71
74
  const formFieldContext = useFormField()
75
+ const emit = useFormFieldEmit()
72
76
 
73
77
  const fieldGroupContext = getContext<
74
78
  | {
@@ -112,12 +116,44 @@
112
116
  )
113
117
  )
114
118
 
115
- const selectedItem = $derived(value ? itemsMap.get(value) : undefined)
116
- const displayLabel = $derived(selectedItem?.label ?? selectedItem?.value ?? '')
119
+ // ---- Selection (single + multiple) ----
120
+ const selectedValues = $derived(
121
+ multiple
122
+ ? Array.isArray(value)
123
+ ? (value as string[])
124
+ : []
125
+ : typeof value === 'string' && value !== ''
126
+ ? [value]
127
+ : []
128
+ )
129
+ const selectedItems = $derived(
130
+ selectedValues.map((v) => itemsMap.get(v)).filter((i): i is SelectItem => i !== undefined)
131
+ )
132
+ const hasSelection = $derived(selectedValues.length > 0)
133
+ const singleSelectedItem = $derived(multiple ? undefined : selectedItems[0])
134
+ const displayLabel = $derived(
135
+ multiple
136
+ ? selectedItems.map((i) => i.label ?? i.value).join(separator)
137
+ : (singleSelectedItem?.label ?? singleSelectedItem?.value ?? '')
138
+ )
139
+
140
+ function removeValue(val: string) {
141
+ if (!multiple) return
142
+ value = selectedValues.filter((v) => v !== val)
143
+ emit.onChange()
144
+ }
145
+
146
+ function clearSelection() {
147
+ if (!multiple) return
148
+ value = []
149
+ emit.onChange()
150
+ }
117
151
 
118
152
  // ---- Leading / trailing ----
119
- const displayAvatar = $derived(selectedItem?.avatar ?? avatar)
120
- const displayIcon = $derived(selectedItem?.icon ?? leadingIcon ?? icon)
153
+ const displayAvatar = $derived(multiple ? avatar : (singleSelectedItem?.avatar ?? avatar))
154
+ const displayIcon = $derived(
155
+ multiple ? (leadingIcon ?? icon) : (singleSelectedItem?.icon ?? leadingIcon ?? icon)
156
+ )
121
157
  const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
122
158
  const leadingIconName = $derived(
123
159
  loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
@@ -235,7 +271,7 @@
235
271
  </script>
236
272
 
237
273
  {#snippet renderItem(item: SelectItem, index: number)}
238
- {@const isSelected = value === item.value}
274
+ {@const isSelected = selectedValues.includes(item.value)}
239
275
  <Select.Item
240
276
  value={item.value}
241
277
  label={item.label ?? item.value}
@@ -302,7 +338,7 @@
302
338
  {@render itemSlot({
303
339
  item: selectItem,
304
340
  index,
305
- selected: value === selectItem.value
341
+ selected: selectedValues.includes(selectItem.value)
306
342
  })}
307
343
  {:else}
308
344
  {@render renderItem(selectItem, index)}
@@ -314,16 +350,7 @@
314
350
  </Select.Content>
315
351
  {/snippet}
316
352
 
317
- <Select.Root
318
- type="single"
319
- bind:open
320
- onOpenChange={(val) => onOpenChange?.(val)}
321
- {disabled}
322
- {required}
323
- items={bitsItems}
324
- {value}
325
- onValueChange={(val) => (value = val)}
326
- >
353
+ {#snippet rootChildren()}
327
354
  <div bind:this={ref} class={rootClass}>
328
355
  {#if leadingSlot}
329
356
  <span class={leadingClass}>
@@ -350,7 +377,13 @@
350
377
  aria-invalid={resolvedHighlight ? true : undefined}
351
378
  class={baseClass}
352
379
  >
353
- {#if value && displayLabel}
380
+ {#if selectedSlot && hasSelection}
381
+ {@render selectedSlot({
382
+ items: selectedItems,
383
+ remove: removeValue,
384
+ clear: clearSelection
385
+ })}
386
+ {:else if hasSelection && displayLabel}
354
387
  <span class={valueClass}>{displayLabel}</span>
355
388
  {:else if placeholder}
356
389
  <span class={placeholderClass}>{placeholder}</span>
@@ -375,4 +408,52 @@
375
408
  {:else}
376
409
  {@render selectContentEl()}
377
410
  {/if}
378
- </Select.Root>
411
+ {/snippet}
412
+
413
+ {#if multiple}
414
+ <Select.Root
415
+ type="multiple"
416
+ bind:open
417
+ onOpenChange={(val) => {
418
+ if (val) {
419
+ emit.onFocus()
420
+ } else {
421
+ emit.onBlur()
422
+ }
423
+ onOpenChange?.(val)
424
+ }}
425
+ {disabled}
426
+ {required}
427
+ items={bitsItems}
428
+ value={selectedValues}
429
+ onValueChange={(val) => {
430
+ value = val
431
+ emit.onChange()
432
+ }}
433
+ >
434
+ {@render rootChildren()}
435
+ </Select.Root>
436
+ {:else}
437
+ <Select.Root
438
+ type="single"
439
+ bind:open
440
+ onOpenChange={(val) => {
441
+ if (val) {
442
+ emit.onFocus()
443
+ } else {
444
+ emit.onBlur()
445
+ }
446
+ onOpenChange?.(val)
447
+ }}
448
+ {disabled}
449
+ {required}
450
+ items={bitsItems}
451
+ value={selectedValues[0] ?? ''}
452
+ onValueChange={(val) => {
453
+ value = val
454
+ emit.onChange()
455
+ }}
456
+ >
457
+ {@render rootChildren()}
458
+ </Select.Root>
459
+ {/if}