sv5ui 1.5.1 → 1.6.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/Checkbox/Checkbox.svelte +8 -2
- package/dist/CheckboxGroup/CheckboxGroup.svelte +15 -2
- 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/PinInput/PinInput.svelte +9 -2
- package/dist/RadioGroup/RadioGroup.svelte +17 -3
- package/dist/Select/Select.svelte +14 -3
- package/dist/SelectMenu/SelectMenu.svelte +9 -2
- 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 +1 -0
- package/dist/index.js +1 -0
- package/package.json +26 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
/**
|
|
3
|
+
* Form component variants.
|
|
4
|
+
*
|
|
5
|
+
* The Form component renders a near-styleless `<form>` (or `<div>` for nested forms) —
|
|
6
|
+
* matching Nuxt UI v4's minimal form theme. This exists primarily so users can inject
|
|
7
|
+
* shared form spacing via `defineConfig({ form: { slots: { root: 'space-y-4' } } })`.
|
|
8
|
+
*/
|
|
9
|
+
export const formVariants = tv({
|
|
10
|
+
slots: {
|
|
11
|
+
root: ''
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
export const formDefaults = {
|
|
15
|
+
defaultVariants: {},
|
|
16
|
+
slots: {}
|
|
17
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as Form } from './Form.svelte';
|
|
2
|
+
export type { FormProps, FormApi, FormError, FormErrorWithId, FormSubmitEvent, FormErrorEvent, FormSchema, FormInputEvents, FormValidateOpts, InferInput, InferOutput, FormData, StandardSchemaV1, JoiSchema } from './form.types.js';
|
|
3
|
+
export { FormValidationException } from './form.types.js';
|
|
4
|
+
export { getFormContext } from './form.context.svelte.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as Form } from './Form.svelte';
|
|
2
|
+
export { FormValidationException } from './form.types.js';
|
|
3
|
+
// Exposed so users can build custom input components that integrate with a parent Form
|
|
4
|
+
// (read errors, emit blur/input/change/focus events). Internal implementation details
|
|
5
|
+
// like FormContext, validateSchema, getAtPath, etc. are intentionally not re-exported.
|
|
6
|
+
export { getFormContext } from './form.context.svelte.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { FormError, FormSchema, JoiSchema } from './form.types.js';
|
|
3
|
+
export declare function isStandardSchema(schema: unknown): schema is StandardSchemaV1;
|
|
4
|
+
export declare function isJoiSchema(schema: unknown): schema is JoiSchema;
|
|
5
|
+
export interface ValidateResult<T = unknown> {
|
|
6
|
+
errors: FormError[] | null;
|
|
7
|
+
value: T | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateStandardSchema<T>(state: unknown, schema: StandardSchemaV1<unknown, T>): Promise<ValidateResult<T>>;
|
|
10
|
+
export declare function validateJoiSchema<T>(state: unknown, schema: JoiSchema): Promise<ValidateResult<T>>;
|
|
11
|
+
export declare function validateSchema<T>(state: unknown, schema: FormSchema): Promise<ValidateResult<T>>;
|
|
12
|
+
export declare function getAtPath<T extends Record<string, unknown>>(data: T, path?: string): unknown;
|
|
13
|
+
export declare function setAtPath<T extends Record<string, unknown>>(data: T, path: string, value: unknown): T;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ==================== TYPE GUARDS ====================
|
|
2
|
+
export function isStandardSchema(schema) {
|
|
3
|
+
return (typeof schema === 'object' &&
|
|
4
|
+
schema !== null &&
|
|
5
|
+
'~standard' in schema &&
|
|
6
|
+
typeof schema['~standard'] === 'object');
|
|
7
|
+
}
|
|
8
|
+
export function isJoiSchema(schema) {
|
|
9
|
+
if (typeof schema !== 'object' || schema === null)
|
|
10
|
+
return false;
|
|
11
|
+
const s = schema;
|
|
12
|
+
return (typeof s.validate === 'function' &&
|
|
13
|
+
typeof s.validateAsync === 'function' &&
|
|
14
|
+
'$_root' in s &&
|
|
15
|
+
typeof s.type === 'string');
|
|
16
|
+
}
|
|
17
|
+
// ==================== NORMALIZE STANDARD SCHEMA PATH ====================
|
|
18
|
+
function normalizePath(path) {
|
|
19
|
+
if (!path || path.length === 0)
|
|
20
|
+
return '';
|
|
21
|
+
return path
|
|
22
|
+
.map((segment) => {
|
|
23
|
+
if (typeof segment === 'object' && segment !== null && 'key' in segment) {
|
|
24
|
+
return String(segment.key);
|
|
25
|
+
}
|
|
26
|
+
return String(segment);
|
|
27
|
+
})
|
|
28
|
+
.join('.');
|
|
29
|
+
}
|
|
30
|
+
// ==================== STANDARD SCHEMA VALIDATOR ====================
|
|
31
|
+
// Handles Zod 3.24+, Valibot 1.0+, Yup 1.7+ (and any other library
|
|
32
|
+
// implementing the Standard Schema spec).
|
|
33
|
+
export async function validateStandardSchema(state, schema) {
|
|
34
|
+
let result = schema['~standard'].validate(state);
|
|
35
|
+
if (result instanceof Promise)
|
|
36
|
+
result = await result;
|
|
37
|
+
if ('issues' in result && result.issues) {
|
|
38
|
+
return {
|
|
39
|
+
errors: result.issues.map((issue) => ({
|
|
40
|
+
name: normalizePath(issue.path),
|
|
41
|
+
message: issue.message
|
|
42
|
+
})),
|
|
43
|
+
value: null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
errors: null,
|
|
48
|
+
value: result.value
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ==================== JOI VALIDATOR ====================
|
|
52
|
+
export async function validateJoiSchema(state, schema) {
|
|
53
|
+
// abortEarly: false → collect all errors, not just the first.
|
|
54
|
+
const result = schema.validate(state, { abortEarly: false });
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return {
|
|
57
|
+
errors: result.error.details.map((detail) => ({
|
|
58
|
+
name: detail.path.map(String).join('.'),
|
|
59
|
+
message: detail.message
|
|
60
|
+
})),
|
|
61
|
+
value: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
errors: null,
|
|
66
|
+
value: (result.value ?? null)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ==================== DISPATCH ====================
|
|
70
|
+
export function validateSchema(state, schema) {
|
|
71
|
+
// Joi is checked first, even though Joi 18+ also implements Standard Schema:
|
|
72
|
+
// Joi's Standard Schema impl defaults to `abortEarly: true` (stopping at the
|
|
73
|
+
// first error), whereas we want to surface ALL errors per field so the user
|
|
74
|
+
// can see every invalid field at once. The dedicated Joi adapter passes
|
|
75
|
+
// `{ abortEarly: false }` explicitly.
|
|
76
|
+
if (isJoiSchema(schema)) {
|
|
77
|
+
return validateJoiSchema(state, schema);
|
|
78
|
+
}
|
|
79
|
+
if (isStandardSchema(schema)) {
|
|
80
|
+
return validateStandardSchema(state, schema);
|
|
81
|
+
}
|
|
82
|
+
throw new Error('Form validation failed: Unsupported form schema');
|
|
83
|
+
}
|
|
84
|
+
// ==================== PATH UTILITIES (nested form support) ====================
|
|
85
|
+
export function getAtPath(data, path) {
|
|
86
|
+
if (!path)
|
|
87
|
+
return data;
|
|
88
|
+
return path.split('.').reduce((value, key) => {
|
|
89
|
+
if (value === null || value === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
return value[key];
|
|
92
|
+
}, data);
|
|
93
|
+
}
|
|
94
|
+
export function setAtPath(data, path, value) {
|
|
95
|
+
if (!path) {
|
|
96
|
+
return Object.assign(data, value);
|
|
97
|
+
}
|
|
98
|
+
if (!data)
|
|
99
|
+
return data;
|
|
100
|
+
const keys = path.split('.');
|
|
101
|
+
let current = data;
|
|
102
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
103
|
+
const key = keys[i];
|
|
104
|
+
if (current[key] === undefined || current[key] === null) {
|
|
105
|
+
const nextKey = keys[i + 1];
|
|
106
|
+
current[key] = !Number.isNaN(Number(nextKey)) ? [] : {};
|
|
107
|
+
}
|
|
108
|
+
current = current[key];
|
|
109
|
+
}
|
|
110
|
+
const lastKey = keys[keys.length - 1];
|
|
111
|
+
current[lastKey] = value;
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import { Label, useId } from 'bits-ui'
|
|
9
9
|
import { formFieldVariants, formFieldDefaults } from './form-field.variants.js'
|
|
10
10
|
import { getComponentConfig } from '../config.js'
|
|
11
|
-
import { setContext } from 'svelte'
|
|
12
|
-
import type
|
|
11
|
+
import { setContext, untrack } from 'svelte'
|
|
12
|
+
import { type FormFieldContext, FORM_FIELD_CONTEXT_KEY } from '../hooks/useFormField.svelte.js'
|
|
13
|
+
import { getFormContext } from '../Form/form.context.svelte.js'
|
|
13
14
|
|
|
14
15
|
const config = getComponentConfig('formField', formFieldDefaults)
|
|
15
16
|
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
size = config.defaultVariants.size ?? 'md',
|
|
26
27
|
required = false,
|
|
27
28
|
orientation = config.defaultVariants.orientation ?? 'vertical',
|
|
29
|
+
eagerValidation,
|
|
30
|
+
validateOnInputDelay,
|
|
31
|
+
errorPattern,
|
|
28
32
|
class: className,
|
|
29
33
|
children,
|
|
30
34
|
labelSlot,
|
|
@@ -38,6 +42,56 @@
|
|
|
38
42
|
const id = useId()
|
|
39
43
|
const ariaId = $derived(name ? `form-field-${name}` : id)
|
|
40
44
|
|
|
45
|
+
// Form-level context (undefined when FormField is used standalone).
|
|
46
|
+
const formCtx = getFormContext()
|
|
47
|
+
|
|
48
|
+
// Resolve error: explicit `error` prop always wins; otherwise ask the Form for
|
|
49
|
+
// errors matching this field's name (or errorPattern regex, if provided).
|
|
50
|
+
const resolvedError = $derived.by<string | boolean | undefined>(() => {
|
|
51
|
+
if (error !== undefined) return error
|
|
52
|
+
if (!formCtx || !name) return undefined
|
|
53
|
+
const errs = errorPattern ? formCtx.getErrors(errorPattern) : formCtx.getErrors(name)
|
|
54
|
+
return errs[0]?.message
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Register this field with the form. Split into two effects:
|
|
58
|
+
//
|
|
59
|
+
// 1. Lifecycle effect — tracks only `name`. Handles the initial registration
|
|
60
|
+
// and cleanup on unmount / rename. Reads other entry fields via `untrack`
|
|
61
|
+
// so they don't retrigger the lifecycle.
|
|
62
|
+
// 2. Update effect — tracks ariaId / errorPattern / eagerValidation /
|
|
63
|
+
// validateOnInputDelay and rewrites the entry via `.set()`. No cleanup —
|
|
64
|
+
// a single atomic overwrite, so no window where the registry is empty.
|
|
65
|
+
//
|
|
66
|
+
// Without this split, changing an unrelated prop (e.g. errorPattern) would
|
|
67
|
+
// cause cleanup→re-register, briefly leaving `#fieldRegistry.get(name)`
|
|
68
|
+
// undefined for any concurrent `#resolveErrorIds` call.
|
|
69
|
+
$effect(() => {
|
|
70
|
+
if (!formCtx || !name) return
|
|
71
|
+
const registeredName = name
|
|
72
|
+
untrack(() => {
|
|
73
|
+
formCtx.registerField(registeredName, {
|
|
74
|
+
id: ariaId,
|
|
75
|
+
pattern: errorPattern,
|
|
76
|
+
eagerValidation,
|
|
77
|
+
validateOnInputDelay
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
return () => {
|
|
81
|
+
formCtx.unregisterField(registeredName)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (!formCtx || !name) return
|
|
87
|
+
formCtx.registerField(name, {
|
|
88
|
+
id: ariaId,
|
|
89
|
+
pattern: errorPattern,
|
|
90
|
+
eagerValidation,
|
|
91
|
+
validateOnInputDelay
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
41
95
|
const variantSlots = $derived(formFieldVariants({ size, required, orientation }))
|
|
42
96
|
const classes = $derived({
|
|
43
97
|
root: variantSlots.root({ class: [config.slots.root, className, ui?.root] }),
|
|
@@ -55,10 +109,10 @@
|
|
|
55
109
|
help: variantSlots.help({ class: [config.slots.help, ui?.help] })
|
|
56
110
|
})
|
|
57
111
|
|
|
58
|
-
const hasError = $derived(
|
|
59
|
-
const errorMessage = $derived(typeof
|
|
112
|
+
const hasError = $derived(resolvedError !== undefined && resolvedError !== false)
|
|
113
|
+
const errorMessage = $derived(typeof resolvedError === 'string' ? resolvedError : undefined)
|
|
60
114
|
|
|
61
|
-
setContext<FormFieldContext>(
|
|
115
|
+
setContext<FormFieldContext>(FORM_FIELD_CONTEXT_KEY, {
|
|
62
116
|
get name() {
|
|
63
117
|
return name
|
|
64
118
|
},
|
|
@@ -66,10 +120,19 @@
|
|
|
66
120
|
return size
|
|
67
121
|
},
|
|
68
122
|
get error() {
|
|
69
|
-
return
|
|
123
|
+
return resolvedError
|
|
70
124
|
},
|
|
71
125
|
get ariaId() {
|
|
72
126
|
return ariaId
|
|
127
|
+
},
|
|
128
|
+
get eagerValidation() {
|
|
129
|
+
return eagerValidation
|
|
130
|
+
},
|
|
131
|
+
get validateOnInputDelay() {
|
|
132
|
+
return validateOnInputDelay
|
|
133
|
+
},
|
|
134
|
+
get errorPattern() {
|
|
135
|
+
return errorPattern
|
|
73
136
|
}
|
|
74
137
|
})
|
|
75
138
|
</script>
|
|
@@ -103,12 +166,12 @@
|
|
|
103
166
|
|
|
104
167
|
<div class={classes.container}>
|
|
105
168
|
{#if children}
|
|
106
|
-
{@render children({ error })}
|
|
169
|
+
{@render children({ error: resolvedError })}
|
|
107
170
|
{/if}
|
|
108
171
|
|
|
109
172
|
{#if hasError && (errorMessage || errorSlot)}
|
|
110
173
|
{#if errorSlot}
|
|
111
|
-
{@render errorSlot({ error })}
|
|
174
|
+
{@render errorSlot({ error: resolvedError })}
|
|
112
175
|
{:else if errorMessage}
|
|
113
176
|
<p id="{ariaId}-error" class={classes.error}>{errorMessage}</p>
|
|
114
177
|
{/if}
|
|
@@ -47,6 +47,21 @@ export type FormFieldProps = Omit<HTMLAttributes<HTMLDivElement>, 'class'> & {
|
|
|
47
47
|
* @default 'vertical'
|
|
48
48
|
*/
|
|
49
49
|
orientation?: NonNullable<FormFieldVariantProps['orientation']>;
|
|
50
|
+
/**
|
|
51
|
+
* When `true`, input events validate this field before first blur.
|
|
52
|
+
* By default, `input`-triggered validation only runs after the field has
|
|
53
|
+
* been blurred at least once.
|
|
54
|
+
*/
|
|
55
|
+
eagerValidation?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Per-field override for the form's `validateOnInputDelay` (debounce in ms).
|
|
58
|
+
*/
|
|
59
|
+
validateOnInputDelay?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Regex pattern to match form errors for this field (in addition to exact name match).
|
|
62
|
+
* Useful for array-style fields like `items.0.name`, `items.1.name`, etc.
|
|
63
|
+
*/
|
|
64
|
+
errorPattern?: RegExp;
|
|
50
65
|
/**
|
|
51
66
|
* Additional CSS classes for the root element.
|
|
52
67
|
*/
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
import type { InputProps } from './input.types.js'
|
|
2
|
+
import type { InputProps, InputValue } from './input.types.js'
|
|
3
3
|
|
|
4
|
-
export type Props = InputProps
|
|
4
|
+
export type Props<T extends InputValue = InputValue> = InputProps<T>
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
|
-
<script lang="ts">
|
|
7
|
+
<script lang="ts" generics="T extends InputValue = InputValue">
|
|
8
8
|
import { inputVariants, inputDefaults } from './input.variants.js'
|
|
9
9
|
import { getComponentConfig, iconsDefaults } from '../config.js'
|
|
10
10
|
import { getContext } from 'svelte'
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import Icon from '../Icon/Icon.svelte'
|
|
16
16
|
import Avatar from '../Avatar/Avatar.svelte'
|
|
17
17
|
import type { AvatarSize } from '../Avatar/avatar.types.js'
|
|
18
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
18
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
19
19
|
|
|
20
20
|
const config = getComponentConfig('input', inputDefaults)
|
|
21
21
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -42,10 +42,32 @@
|
|
|
42
42
|
leadingSlot,
|
|
43
43
|
trailingSlot,
|
|
44
44
|
class: className,
|
|
45
|
+
onblur,
|
|
46
|
+
oninput,
|
|
47
|
+
onchange,
|
|
48
|
+
onfocus,
|
|
45
49
|
...restProps
|
|
46
|
-
}: Props = $props()
|
|
50
|
+
}: Props<T> = $props()
|
|
47
51
|
|
|
48
52
|
const formFieldContext = useFormField()
|
|
53
|
+
const emit = useFormFieldEmit()
|
|
54
|
+
|
|
55
|
+
function handleBlur(event: FocusEvent & { currentTarget: HTMLInputElement }) {
|
|
56
|
+
emit.onBlur()
|
|
57
|
+
onblur?.(event)
|
|
58
|
+
}
|
|
59
|
+
function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
60
|
+
emit.onInput()
|
|
61
|
+
oninput?.(event)
|
|
62
|
+
}
|
|
63
|
+
function handleChange(event: Event & { currentTarget: HTMLInputElement }) {
|
|
64
|
+
emit.onChange()
|
|
65
|
+
onchange?.(event)
|
|
66
|
+
}
|
|
67
|
+
function handleFocus(event: FocusEvent & { currentTarget: HTMLInputElement }) {
|
|
68
|
+
emit.onFocus()
|
|
69
|
+
onfocus?.(event)
|
|
70
|
+
}
|
|
49
71
|
|
|
50
72
|
const fieldGroupContext = getContext<
|
|
51
73
|
| {
|
|
@@ -153,6 +175,10 @@
|
|
|
153
175
|
aria-describedby={ariaDescribedBy}
|
|
154
176
|
aria-invalid={resolvedHighlight ? true : undefined}
|
|
155
177
|
class={classes.base}
|
|
178
|
+
onblur={handleBlur}
|
|
179
|
+
oninput={handleInput}
|
|
180
|
+
onchange={handleChange}
|
|
181
|
+
onfocus={handleFocus}
|
|
156
182
|
/>
|
|
157
183
|
|
|
158
184
|
{#if trailingSlot}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import type { InputProps } from './input.types.js';
|
|
2
|
-
export type Props = InputProps
|
|
3
|
-
declare
|
|
4
|
-
|
|
1
|
+
import type { InputProps, InputValue } from './input.types.js';
|
|
2
|
+
export type Props<T extends InputValue = InputValue> = InputProps<T>;
|
|
3
|
+
declare function $$render<T extends InputValue = InputValue>(): {
|
|
4
|
+
props: Props<T>;
|
|
5
|
+
exports: {};
|
|
6
|
+
bindings: "ref" | "value";
|
|
7
|
+
slots: {};
|
|
8
|
+
events: {};
|
|
9
|
+
};
|
|
10
|
+
declare class __sveltets_Render<T extends InputValue = InputValue> {
|
|
11
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
12
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
13
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
14
|
+
bindings(): "ref" | "value";
|
|
15
|
+
exports(): {};
|
|
16
|
+
}
|
|
17
|
+
interface $$IsomorphicComponent {
|
|
18
|
+
new <T extends InputValue = InputValue>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
19
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
20
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
21
|
+
<T extends InputValue = InputValue>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
22
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
23
|
+
}
|
|
24
|
+
declare const Input: $$IsomorphicComponent;
|
|
25
|
+
type Input<T extends InputValue = InputValue> = InstanceType<typeof Input<T>>;
|
|
5
26
|
export default Input;
|
|
@@ -3,15 +3,36 @@ import type { HTMLInputAttributes } from 'svelte/elements';
|
|
|
3
3
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
4
4
|
import type { InputVariantProps, InputSlots } from './input.variants.js';
|
|
5
5
|
import type { AvatarProps } from '../Avatar/avatar.types.js';
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Valid value types for `<Input bind:value>`.
|
|
8
|
+
*
|
|
9
|
+
* Covers every HTML input `type` except `"file"` (which uses `FileList` and is
|
|
10
|
+
* typically handled via `onchange` events, not `bind:value`). Pattern borrowed
|
|
11
|
+
* from Nuxt UI v4's `AcceptableValue` — it's the minimum union that covers
|
|
12
|
+
* real-world usage while still giving TypeScript enough to catch mistakes.
|
|
13
|
+
*
|
|
14
|
+
* Use with the component generic for strict typing:
|
|
15
|
+
* ```ts
|
|
16
|
+
* let age = $state<number | undefined>(undefined)
|
|
17
|
+
* <Input type="number" bind:value={age} /> // age stays number
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export type InputValue = string | number | bigint | boolean | null | undefined;
|
|
21
|
+
export type InputProps<T extends InputValue = InputValue> = Omit<HTMLInputAttributes, 'class' | 'size' | 'value'> & {
|
|
7
22
|
/**
|
|
8
23
|
* Bind a reference to the underlying HTMLInputElement.
|
|
9
24
|
*/
|
|
10
25
|
ref?: HTMLInputElement | null;
|
|
11
26
|
/**
|
|
12
27
|
* The current value of the input. Supports two-way binding with `bind:value`.
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
*
|
|
29
|
+
* The type is inferred from the bound variable — Svelte's `bind:value`
|
|
30
|
+
* coerces based on the `type` attribute automatically:
|
|
31
|
+
* - `string` for `text | email | password | url | search | tel | date | time | color` etc.
|
|
32
|
+
* - `number` for `type="number" | "range"`
|
|
33
|
+
* - `boolean` for hidden boolean state (rare)
|
|
34
|
+
*/
|
|
35
|
+
value?: T;
|
|
15
36
|
/**
|
|
16
37
|
* Controls the visual style of the input.
|
|
17
38
|
* @default 'outline'
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { PinInput, useId } from 'bits-ui'
|
|
9
9
|
import { pinInputVariants, pinInputDefaults } from './pin-input.variants.js'
|
|
10
10
|
import { getComponentConfig } from '../config.js'
|
|
11
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
11
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
12
12
|
|
|
13
13
|
const config = getComponentConfig('pinInput', pinInputDefaults)
|
|
14
14
|
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
}: Props = $props()
|
|
44
44
|
|
|
45
45
|
const formFieldContext = useFormField()
|
|
46
|
+
const emit = useFormFieldEmit()
|
|
46
47
|
|
|
47
48
|
const autoInputId = useId()
|
|
48
49
|
const hasError = $derived(
|
|
@@ -69,9 +70,15 @@
|
|
|
69
70
|
function handleValueChange(v: string) {
|
|
70
71
|
const filtered = type === 'number' ? v.replace(/\D/g, '') : v
|
|
71
72
|
value = filtered
|
|
73
|
+
emit.onInput()
|
|
72
74
|
onValueChange?.(filtered)
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function handleComplete(v: string) {
|
|
78
|
+
emit.onChange()
|
|
79
|
+
onComplete?.(v)
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
const slots = $derived(
|
|
76
83
|
pinInputVariants({
|
|
77
84
|
variant,
|
|
@@ -110,7 +117,7 @@
|
|
|
110
117
|
maxlength={length}
|
|
111
118
|
{disabled}
|
|
112
119
|
{textalign}
|
|
113
|
-
{
|
|
120
|
+
onComplete={handleComplete}
|
|
114
121
|
pasteTransformer={resolvedPasteTransformer}
|
|
115
122
|
{pushPasswordManagerStrategy}
|
|
116
123
|
inputId={resolvedInputId}
|
|
@@ -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)
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
// ---- Form context ----
|
|
71
71
|
const formFieldContext = useFormField()
|
|
72
|
+
const emit = useFormFieldEmit()
|
|
72
73
|
|
|
73
74
|
const fieldGroupContext = getContext<
|
|
74
75
|
| {
|
|
@@ -317,12 +318,22 @@
|
|
|
317
318
|
<Select.Root
|
|
318
319
|
type="single"
|
|
319
320
|
bind:open
|
|
320
|
-
onOpenChange={(val) =>
|
|
321
|
+
onOpenChange={(val) => {
|
|
322
|
+
if (val) {
|
|
323
|
+
emit.onFocus()
|
|
324
|
+
} else {
|
|
325
|
+
emit.onBlur()
|
|
326
|
+
}
|
|
327
|
+
onOpenChange?.(val)
|
|
328
|
+
}}
|
|
321
329
|
{disabled}
|
|
322
330
|
{required}
|
|
323
331
|
items={bitsItems}
|
|
324
332
|
{value}
|
|
325
|
-
onValueChange={(val) =>
|
|
333
|
+
onValueChange={(val) => {
|
|
334
|
+
value = val
|
|
335
|
+
emit.onChange()
|
|
336
|
+
}}
|
|
326
337
|
>
|
|
327
338
|
<div bind:this={ref} class={rootClass}>
|
|
328
339
|
{#if leadingSlot}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import Avatar from '../Avatar/Avatar.svelte'
|
|
22
22
|
import Input from '../Input/Input.svelte'
|
|
23
23
|
import type { AvatarSize } from '../Avatar/avatar.types.js'
|
|
24
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
24
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
25
25
|
|
|
26
26
|
const config = getComponentConfig('selectMenu', selectMenuDefaults)
|
|
27
27
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
|
|
79
79
|
// ---- Form context ----
|
|
80
80
|
const formFieldContext = useFormField()
|
|
81
|
+
const emit = useFormFieldEmit()
|
|
81
82
|
|
|
82
83
|
const fieldGroupContext = getContext<
|
|
83
84
|
| {
|
|
@@ -257,6 +258,9 @@
|
|
|
257
258
|
function onUpdateOpen(val: boolean) {
|
|
258
259
|
if (!val) {
|
|
259
260
|
searchTerm = ''
|
|
261
|
+
emit.onBlur()
|
|
262
|
+
} else {
|
|
263
|
+
emit.onFocus()
|
|
260
264
|
}
|
|
261
265
|
onOpenChange?.(val)
|
|
262
266
|
}
|
|
@@ -366,7 +370,10 @@
|
|
|
366
370
|
{disabled}
|
|
367
371
|
{required}
|
|
368
372
|
{value}
|
|
369
|
-
onValueChange={(val) =>
|
|
373
|
+
onValueChange={(val) => {
|
|
374
|
+
value = val
|
|
375
|
+
emit.onChange()
|
|
376
|
+
}}
|
|
370
377
|
name={resolvedName}
|
|
371
378
|
>
|
|
372
379
|
<div bind:this={ref} class={rootClass}>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Slider, useId } from 'bits-ui'
|
|
9
9
|
import { sliderVariants, sliderDefaults } from './slider.variants.js'
|
|
10
10
|
import { getComponentConfig } from '../config.js'
|
|
11
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
11
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
12
12
|
|
|
13
13
|
const config = getComponentConfig('slider', sliderDefaults)
|
|
14
14
|
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
}: Props = $props()
|
|
38
38
|
|
|
39
39
|
const formFieldContext = useFormField()
|
|
40
|
+
const emit = useFormFieldEmit()
|
|
40
41
|
|
|
41
42
|
const autoId = useId()
|
|
42
43
|
const hasError = $derived(
|
|
@@ -59,10 +60,12 @@
|
|
|
59
60
|
|
|
60
61
|
function handleValueChange(v: number[]) {
|
|
61
62
|
value = isMultiple ? v : (v[0] ?? min)
|
|
63
|
+
emit.onInput()
|
|
62
64
|
onValueChange?.(value)
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function handleValueCommit(v: number[]) {
|
|
68
|
+
emit.onChange()
|
|
66
69
|
onValueCommit?.(isMultiple ? v : (v[0] ?? min))
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { switchVariants, switchDefaults } from './switch.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
|
|
|
14
14
|
const config = getComponentConfig('switch', switchDefaults)
|
|
15
15
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
}: Props = $props()
|
|
40
40
|
|
|
41
41
|
const formFieldContext = useFormField()
|
|
42
|
+
const emit = useFormFieldEmit()
|
|
42
43
|
|
|
43
44
|
const hasError = $derived(
|
|
44
45
|
formFieldContext?.error !== undefined && formFieldContext?.error !== false
|
|
@@ -108,7 +109,12 @@
|
|
|
108
109
|
<div class={classes.container}>
|
|
109
110
|
<Switch.Root
|
|
110
111
|
bind:checked
|
|
111
|
-
{
|
|
112
|
+
onCheckedChange={(val) => {
|
|
113
|
+
emit.onChange()
|
|
114
|
+
onCheckedChange?.(val)
|
|
115
|
+
}}
|
|
116
|
+
onblur={() => emit.onBlur()}
|
|
117
|
+
onfocus={() => emit.onFocus()}
|
|
112
118
|
id={resolvedId}
|
|
113
119
|
name={resolvedName}
|
|
114
120
|
{value}
|