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
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -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 ??
|
|
32
|
-
|
|
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({
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
18
|
+
size: {
|
|
19
|
+
sm: {
|
|
11
20
|
content: string;
|
|
12
21
|
};
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
77
|
+
size: {
|
|
78
|
+
sm: {
|
|
52
79
|
content: string;
|
|
53
80
|
};
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
136
|
+
size: {
|
|
137
|
+
sm: {
|
|
93
138
|
content: string;
|
|
94
139
|
};
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
199
|
+
size: {
|
|
200
|
+
sm: {
|
|
138
201
|
content: string;
|
|
139
202
|
};
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
258
|
+
size: {
|
|
259
|
+
sm: {
|
|
179
260
|
content: string;
|
|
180
261
|
};
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
content: '
|
|
31
|
+
size: {
|
|
32
|
+
sm: {
|
|
33
|
+
content: 'w-[calc(100vw-2rem)] max-w-md rounded-lg shadow-lg ring ring-outline-variant'
|
|
25
34
|
},
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
64
|
-
|
|
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="
|
|
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
|
-
{
|
|
132
|
+
disabled={isDisabled}
|
|
112
133
|
{textalign}
|
|
113
|
-
{
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
116
|
-
const
|
|
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(
|
|
120
|
-
const displayIcon = $derived(
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|