sv5ui 1.6.0 → 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 (33) 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/FileUpload/FileUpload.svelte +81 -10
  13. package/dist/FileUpload/file-upload.types.d.ts +39 -0
  14. package/dist/FileUpload/index.d.ts +1 -1
  15. package/dist/Modal/Modal.svelte +14 -3
  16. package/dist/Modal/modal.types.d.ts +15 -4
  17. package/dist/Modal/modal.variants.d.ts +110 -20
  18. package/dist/Modal/modal.variants.js +27 -9
  19. package/dist/PinInput/PinInput.svelte +18 -4
  20. package/dist/PinInput/pin-input.types.d.ts +11 -0
  21. package/dist/Select/Select.svelte +98 -28
  22. package/dist/Select/select.types.d.ts +44 -2
  23. package/dist/SelectMenu/SelectMenu.svelte +210 -25
  24. package/dist/SelectMenu/select-menu.types.d.ts +62 -1
  25. package/dist/SelectMenu/select-menu.variants.d.ts +26 -0
  26. package/dist/SelectMenu/select-menu.variants.js +34 -6
  27. package/dist/Slideover/Slideover.svelte +13 -2
  28. package/dist/Slideover/slideover.types.d.ts +14 -3
  29. package/dist/Slideover/slideover.variants.d.ts +85 -5
  30. package/dist/Slideover/slideover.variants.js +42 -12
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/package.json +6 -1
@@ -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">
@@ -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,6 +80,7 @@
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()
@@ -113,25 +122,70 @@
113
122
  : `${formFieldContext.ariaId}-description ${formFieldContext.ariaId}-help`
114
123
  )
115
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
+
116
138
  // ---- Items lookup (O(1) via Map) ----
117
139
  const itemsMap = $derived(
118
140
  new Map(
119
- (items as SelectMenuItemType[])
141
+ combinedItems
120
142
  .filter((i): i is SelectMenuItem => !('type' in i))
121
143
  .map((i) => [i.value, i])
122
144
  )
123
145
  )
124
146
 
125
- const selectedItem = $derived(value ? itemsMap.get(value) : undefined)
126
- const displayLabel = $derived(selectedItem?.label ?? selectedItem?.value ?? '')
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
+ }
127
181
 
128
182
  // ---- Search & filtering ----
129
183
  let searchTerm = $state('')
130
184
 
131
185
  const filteredItems = $derived(
132
186
  ignoreFilter || !searchTerm.trim()
133
- ? items
134
- : items.filter((item) => {
187
+ ? combinedItems
188
+ : combinedItems.filter((item) => {
135
189
  if ('type' in item) return true
136
190
  const query = searchTerm.toLowerCase()
137
191
  return filterFields.some((field) => {
@@ -143,9 +197,73 @@
143
197
 
144
198
  const hasFilteredSelectItems = $derived(filteredItems.some((item) => !('type' in item)))
145
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
+
146
262
  // ---- Leading / trailing ----
147
- const displayAvatar = $derived(selectedItem?.avatar ?? avatar)
148
- const displayIcon = $derived(selectedItem?.icon ?? leadingIcon ?? icon)
263
+ const displayAvatar = $derived(multiple ? avatar : (singleSelectedItem?.avatar ?? avatar))
264
+ const displayIcon = $derived(
265
+ multiple ? (leadingIcon ?? icon) : (singleSelectedItem?.icon ?? leadingIcon ?? icon)
266
+ )
149
267
  const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
150
268
  const leadingIconName = $derived(
151
269
  loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
@@ -219,6 +337,17 @@
219
337
  variantSlots.separator({ class: [config.slots.separator, ui?.separator] })
220
338
  )
221
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
+ )
222
351
 
223
352
  // ---- Item classes ----
224
353
  const itemClass = $derived(variantSlots.item({ class: [config.slots.item, ui?.item] }))
@@ -267,7 +396,7 @@
267
396
  </script>
268
397
 
269
398
  {#snippet renderItem(item: SelectMenuItem, index: number)}
270
- {@const isSelected = value === item.value}
399
+ {@const isSelected = selectedValues.includes(item.value)}
271
400
  <Combobox.Item
272
401
  value={item.value}
273
402
  label={item.label ?? item.value}
@@ -320,6 +449,12 @@
320
449
  placeholder={searchPlaceholder}
321
450
  value={searchTerm}
322
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
+ }}
323
458
  variant="none"
324
459
  size={resolvedSize}
325
460
  class={inputClass}
@@ -343,7 +478,7 @@
343
478
  {@render itemSlot({
344
479
  item: selectItem,
345
480
  index,
346
- selected: value === selectItem.value
481
+ selected: selectedValues.includes(selectItem.value)
347
482
  })}
348
483
  {:else}
349
484
  {@render renderItem(selectItem, index)}
@@ -351,31 +486,33 @@
351
486
  {/if}
352
487
  {/each}
353
488
 
354
- {#if !hasFilteredSelectItems}
489
+ {#if !hasFilteredSelectItems && !showCreateItem}
355
490
  {#if emptySlot}
356
491
  {@render emptySlot({ searchTerm })}
357
492
  {:else}
358
493
  <div class={emptyClass}>{emptyText}</div>
359
494
  {/if}
360
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}
361
510
  </div>
362
511
  {/if}
363
512
  </Combobox.Content>
364
513
  {/snippet}
365
514
 
366
- <Combobox.Root
367
- type="single"
368
- bind:open
369
- onOpenChange={onUpdateOpen}
370
- {disabled}
371
- {required}
372
- {value}
373
- onValueChange={(val) => {
374
- value = val
375
- emit.onChange()
376
- }}
377
- name={resolvedName}
378
- >
515
+ {#snippet rootChildren()}
379
516
  <div bind:this={ref} class={rootClass}>
380
517
  {#if leadingSlot}
381
518
  <span class={leadingClass}>
@@ -407,7 +544,13 @@
407
544
  aria-invalid={resolvedHighlight ? true : undefined}
408
545
  class={baseClass}
409
546
  >
410
- {#if value && displayLabel}
547
+ {#if selectedSlot && hasSelection}
548
+ {@render selectedSlot({
549
+ items: selectedItems,
550
+ remove: removeValue,
551
+ clear: clearSelection
552
+ })}
553
+ {:else if hasSelection && displayLabel}
411
554
  <span class={valueClass}>{displayLabel}</span>
412
555
  {:else if placeholder}
413
556
  <span class={placeholderClass}>{placeholder}</span>
@@ -432,4 +575,46 @@
432
575
  {:else}
433
576
  {@render contentEl()}
434
577
  {/if}
435
- </Combobox.Root>
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
- value?: string;
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: { empty: 'p-2 text-xs' },
23
- sm: { empty: 'p-2.5 text-xs' },
24
- md: { empty: 'p-2.5 text-sm' },
25
- lg: { empty: 'p-3 text-sm' },
26
- xl: { empty: 'p-3 text-base' }
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 ?? true,
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({ transition, side, inset, overlay: showOverlay })
65
+ slideoverVariants({
66
+ transition: resolvedTransition,
67
+ side,
68
+ size,
69
+ inset,
70
+ overlay: showOverlay
71
+ })
61
72
  )
62
73
 
63
74
  const classes = $derived({
@@ -42,10 +42,21 @@ export interface SlideoverProps extends RootProps, ContentProps {
42
42
  */
43
43
  overlay?: SlideoverVariantProps['overlay'];
44
44
  /**
45
- * Animate the slideover on open and close.
46
- * @default true
45
+ * Controls the entrance/exit animation.
46
+ * - `'none'` / `false`: no animation
47
+ * - `'fade'`: overlay + content fade
48
+ * - `'slide'` / `true`: overlay fade + content slide-in from the chosen side (default)
49
+ * - `'scale'`: overlay fade + content scale-in
50
+ * @default 'slide'
51
+ */
52
+ transition?: SlideoverVariantProps['transition'] | boolean;
53
+ /**
54
+ * Controls the panel dimension along its axis. For `side="left"` /
55
+ * `side="right"` this sets `max-width`; for `side="top"` / `side="bottom"`
56
+ * this sets `max-height`.
57
+ * @default 'md'
47
58
  */
48
- transition?: SlideoverVariantProps['transition'];
59
+ size?: SlideoverVariantProps['size'];
49
60
  /**
50
61
  * Display the slideover with inset margins and rounded corners.
51
62
  * @default false