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
@@ -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'
10
+ import { getComponentConfig, iconsDefaults } from '../config.js'
11
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,
@@ -45,6 +49,8 @@
45
49
  const formFieldContext = useFormField()
46
50
  const emit = useFormFieldEmit()
47
51
 
52
+ const isDisabled = $derived(disabled || loading)
53
+
48
54
  const autoInputId = useId()
49
55
  const hasError = $derived(
50
56
  formFieldContext?.error !== undefined && formFieldContext?.error !== false
@@ -86,7 +92,7 @@
86
92
  size: resolvedSize,
87
93
  highlight: resolvedHighlight,
88
94
  fixed,
89
- disabled
95
+ disabled: isDisabled
90
96
  })
91
97
  )
92
98
 
@@ -107,15 +113,23 @@
107
113
  })
108
114
  </script>
109
115
 
110
- <div class="contents" {...restProps}>
116
+ <div class="relative inline-flex" {...restProps}>
111
117
  {#if resolvedName}
112
118
  <input type="hidden" name={resolvedName} {value} />
113
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}
114
128
  <PinInput.Root
115
129
  bind:ref
116
130
  {value}
117
131
  maxlength={length}
118
- {disabled}
132
+ disabled={isDisabled}
119
133
  {textalign}
120
134
  onComplete={handleComplete}
121
135
  pasteTransformer={resolvedPasteTransformer}
@@ -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
@@ -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,6 +66,7 @@
64
66
  itemLeading,
65
67
  itemLabel: itemLabelSlot,
66
68
  itemTrailing,
69
+ selected: selectedSlot,
67
70
  content: contentSlot
68
71
  }: Props = $props()
69
72
 
@@ -113,12 +116,44 @@
113
116
  )
114
117
  )
115
118
 
116
- const selectedItem = $derived(value ? itemsMap.get(value) : undefined)
117
- 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
+ }
118
151
 
119
152
  // ---- Leading / trailing ----
120
- const displayAvatar = $derived(selectedItem?.avatar ?? avatar)
121
- 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
+ )
122
157
  const isLeading = $derived(!!leadingSlot || !!displayAvatar || !!displayIcon)
123
158
  const leadingIconName = $derived(
124
159
  loading && isLeading ? loadingIcon : !displayAvatar ? displayIcon : undefined
@@ -236,7 +271,7 @@
236
271
  </script>
237
272
 
238
273
  {#snippet renderItem(item: SelectItem, index: number)}
239
- {@const isSelected = value === item.value}
274
+ {@const isSelected = selectedValues.includes(item.value)}
240
275
  <Select.Item
241
276
  value={item.value}
242
277
  label={item.label ?? item.value}
@@ -303,7 +338,7 @@
303
338
  {@render itemSlot({
304
339
  item: selectItem,
305
340
  index,
306
- selected: value === selectItem.value
341
+ selected: selectedValues.includes(selectItem.value)
307
342
  })}
308
343
  {:else}
309
344
  {@render renderItem(selectItem, index)}
@@ -315,26 +350,7 @@
315
350
  </Select.Content>
316
351
  {/snippet}
317
352
 
318
- <Select.Root
319
- type="single"
320
- bind:open
321
- onOpenChange={(val) => {
322
- if (val) {
323
- emit.onFocus()
324
- } else {
325
- emit.onBlur()
326
- }
327
- onOpenChange?.(val)
328
- }}
329
- {disabled}
330
- {required}
331
- items={bitsItems}
332
- {value}
333
- onValueChange={(val) => {
334
- value = val
335
- emit.onChange()
336
- }}
337
- >
353
+ {#snippet rootChildren()}
338
354
  <div bind:this={ref} class={rootClass}>
339
355
  {#if leadingSlot}
340
356
  <span class={leadingClass}>
@@ -361,7 +377,13 @@
361
377
  aria-invalid={resolvedHighlight ? true : undefined}
362
378
  class={baseClass}
363
379
  >
364
- {#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}
365
387
  <span class={valueClass}>{displayLabel}</span>
366
388
  {:else if placeholder}
367
389
  <span class={placeholderClass}>{placeholder}</span>
@@ -386,4 +408,52 @@
386
408
  {:else}
387
409
  {@render selectContentEl()}
388
410
  {/if}
389
- </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}
@@ -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
- defaultValue?: string;
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.