sv5ui 1.5.0 → 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/README.md +60 -183
- 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/Table/Table.svelte +11 -0
- package/dist/Table/table.types.d.ts +3 -0
- package/dist/Table/table.variants.js +5 -5
- 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
|
@@ -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}
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
striped = false,
|
|
90
90
|
hoverable = config.defaultVariants.hoverable ?? true,
|
|
91
91
|
sticky = false,
|
|
92
|
+
action,
|
|
92
93
|
|
|
93
94
|
// Callbacks
|
|
94
95
|
onRowClick,
|
|
@@ -112,6 +113,16 @@
|
|
|
112
113
|
...restProps
|
|
113
114
|
}: TableProps<T> = $props()
|
|
114
115
|
|
|
116
|
+
// =========================================================================
|
|
117
|
+
// Apply Svelte action on root element
|
|
118
|
+
// =========================================================================
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (ref && action) {
|
|
121
|
+
const result = action(ref)
|
|
122
|
+
return () => result?.destroy?.()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
115
126
|
// =========================================================================
|
|
116
127
|
// Change notifications — skip initial mount, fire only on subsequent changes
|
|
117
128
|
// =========================================================================
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Action } from 'svelte/action';
|
|
2
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
4
5
|
import type { TableVariantProps, TableSlots } from './table.variants.js';
|
|
@@ -167,6 +168,8 @@ export interface TableProps<T extends Record<string, any> = Record<string, any>>
|
|
|
167
168
|
hoverable?: boolean;
|
|
168
169
|
/** Sticky header/footer. @default false */
|
|
169
170
|
sticky?: boolean | 'header' | 'footer';
|
|
171
|
+
/** Svelte action to apply on the root scroll container (e.g. infinite scroll). */
|
|
172
|
+
action?: Action<HTMLElement>;
|
|
170
173
|
/** Callback fired when a row is clicked. */
|
|
171
174
|
onRowClick?: (row: T, index: number, event: MouseEvent) => void;
|
|
172
175
|
/** Callback fired when pointer enters/leaves a row. Passes `null` on leave. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from 'tailwind-variants';
|
|
2
2
|
export const tableVariants = tv({
|
|
3
3
|
slots: {
|
|
4
|
-
root: 'relative w-full overflow-x-auto rounded-xl border border-outline-variant/50 bg-surface [contain:inline-size]',
|
|
4
|
+
root: 'relative w-full overflow-x-auto scrollbar-thin rounded-xl border border-outline-variant/50 bg-surface [contain:inline-size]',
|
|
5
5
|
base: 'min-w-full',
|
|
6
6
|
caption: 'sr-only',
|
|
7
7
|
thead: 'relative bg-surface-container-low',
|
|
@@ -40,14 +40,14 @@ export const tableVariants = tv({
|
|
|
40
40
|
},
|
|
41
41
|
sticky: {
|
|
42
42
|
true: {
|
|
43
|
-
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10',
|
|
44
|
-
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10'
|
|
43
|
+
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-t-xl',
|
|
44
|
+
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-b-xl'
|
|
45
45
|
},
|
|
46
46
|
header: {
|
|
47
|
-
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10'
|
|
47
|
+
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-t-xl'
|
|
48
48
|
},
|
|
49
49
|
footer: {
|
|
50
|
-
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10'
|
|
50
|
+
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-b-xl'
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
striped: {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
type FieldGroupVariantProps
|
|
14
14
|
} from '../FieldGroup/field-group.variants.js'
|
|
15
15
|
import Icon from '../Icon/Icon.svelte'
|
|
16
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
16
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
17
17
|
|
|
18
18
|
const config = getComponentConfig('textarea', textareaDefaults)
|
|
19
19
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -41,10 +41,32 @@
|
|
|
41
41
|
rows = 3,
|
|
42
42
|
maxrows = 0,
|
|
43
43
|
class: className,
|
|
44
|
+
onblur,
|
|
45
|
+
oninput,
|
|
46
|
+
onchange,
|
|
47
|
+
onfocus,
|
|
44
48
|
...restProps
|
|
45
49
|
}: Props = $props()
|
|
46
50
|
|
|
47
51
|
const formFieldContext = useFormField()
|
|
52
|
+
const emit = useFormFieldEmit()
|
|
53
|
+
|
|
54
|
+
function handleBlur(event: FocusEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
55
|
+
emit.onBlur()
|
|
56
|
+
onblur?.(event)
|
|
57
|
+
}
|
|
58
|
+
function handleInput(event: Event & { currentTarget: HTMLTextAreaElement }) {
|
|
59
|
+
emit.onInput()
|
|
60
|
+
oninput?.(event)
|
|
61
|
+
}
|
|
62
|
+
function handleChange(event: Event & { currentTarget: HTMLTextAreaElement }) {
|
|
63
|
+
emit.onChange()
|
|
64
|
+
onchange?.(event)
|
|
65
|
+
}
|
|
66
|
+
function handleFocus(event: FocusEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
67
|
+
emit.onFocus()
|
|
68
|
+
onfocus?.(event)
|
|
69
|
+
}
|
|
48
70
|
|
|
49
71
|
const fieldGroupContext = getContext<
|
|
50
72
|
| {
|
|
@@ -175,6 +197,10 @@
|
|
|
175
197
|
aria-describedby={ariaDescribedBy}
|
|
176
198
|
aria-invalid={resolvedHighlight ? true : undefined}
|
|
177
199
|
class={classes.base}
|
|
200
|
+
onblur={handleBlur}
|
|
201
|
+
oninput={handleInput}
|
|
202
|
+
onchange={handleChange}
|
|
203
|
+
onfocus={handleFocus}
|
|
178
204
|
></textarea>
|
|
179
205
|
|
|
180
206
|
{#if trailingSlot}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { useMediaQuery } from './useMediaQuery.svelte.js';
|
|
|
2
2
|
export type { UseMediaQueryOptions } from './useMediaQuery.svelte.js';
|
|
3
3
|
export { useClipboard } from './useClipboard.svelte.js';
|
|
4
4
|
export type { UseClipboardOptions } from './useClipboard.svelte.js';
|
|
5
|
-
export { useFormField } from './useFormField.svelte.js';
|
|
5
|
+
export { useFormField, useFormFieldEmit, wireFormEvents, FORM_FIELD_CONTEXT_KEY } from './useFormField.svelte.js';
|
|
6
6
|
export type { FormFieldContext } from './useFormField.svelte.js';
|
|
7
7
|
export { useClickOutside } from './useClickOutside.svelte.js';
|
|
8
8
|
export type { UseClickOutsideOptions } from './useClickOutside.svelte.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { useMediaQuery } from './useMediaQuery.svelte.js';
|
|
2
2
|
export { useClipboard } from './useClipboard.svelte.js';
|
|
3
|
-
export { useFormField } from './useFormField.svelte.js';
|
|
3
|
+
export { useFormField, useFormFieldEmit, wireFormEvents, FORM_FIELD_CONTEXT_KEY } from './useFormField.svelte.js';
|
|
4
4
|
export { useClickOutside } from './useClickOutside.svelte.js';
|
|
5
5
|
export { useInfiniteScroll } from './useInfiniteScroll.svelte.js';
|
|
6
6
|
export { useEscapeKeydown } from './useEscapeKeydown.svelte.js';
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { FormFieldProps } from '../FormField/form-field.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol key for the FormField context. Using a Symbol instead of a string
|
|
4
|
+
* prevents collisions with unrelated `getContext('formField')` calls from
|
|
5
|
+
* user code or other libraries.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FORM_FIELD_CONTEXT_KEY: unique symbol;
|
|
2
8
|
export interface FormFieldContext {
|
|
3
9
|
name?: string;
|
|
4
10
|
size: NonNullable<FormFieldProps['size']>;
|
|
5
11
|
error?: string | boolean;
|
|
6
12
|
ariaId: string;
|
|
13
|
+
/** Whether input events should validate this field before first blur. */
|
|
14
|
+
eagerValidation?: boolean;
|
|
15
|
+
/** Per-field override for the input debounce delay. */
|
|
16
|
+
validateOnInputDelay?: number;
|
|
17
|
+
/** Regex pattern used to match form errors for this field (in addition to exact name). */
|
|
18
|
+
errorPattern?: RegExp;
|
|
7
19
|
}
|
|
8
20
|
/**
|
|
9
21
|
* Access the nearest FormField context. Returns `undefined` when used outside a FormField.
|
|
@@ -19,3 +31,55 @@ export interface FormFieldContext {
|
|
|
19
31
|
* ```
|
|
20
32
|
*/
|
|
21
33
|
export declare function useFormField(): FormFieldContext | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Event emitter helpers for inputs nested inside a `<FormField>` within a `<Form>`.
|
|
36
|
+
* Each returned function is a safe no-op when used outside a Form, so inputs can
|
|
37
|
+
* unconditionally wire them to their native events.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```svelte
|
|
41
|
+
* <script>
|
|
42
|
+
* import { useFormFieldEmit } from 'sv5ui'
|
|
43
|
+
* const emit = useFormFieldEmit()
|
|
44
|
+
* </script>
|
|
45
|
+
*
|
|
46
|
+
* <input onblur={emit.onBlur} oninput={emit.onInput} onchange={emit.onChange} onfocus={emit.onFocus} />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function useFormFieldEmit(): {
|
|
50
|
+
onBlur(): void;
|
|
51
|
+
onFocus(): void;
|
|
52
|
+
onChange(): void;
|
|
53
|
+
onInput(): void;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Wires native DOM input events to the parent Form's event emitters while
|
|
57
|
+
* preserving any user-supplied handlers. Reduces boilerplate in wrapper
|
|
58
|
+
* components (Input, Textarea, etc.) from ~20 lines of handler definitions
|
|
59
|
+
* to 4 lines:
|
|
60
|
+
*
|
|
61
|
+
* ```svelte
|
|
62
|
+
* <script>
|
|
63
|
+
* const events = wireFormEvents({ onblur, oninput, onchange, onfocus })
|
|
64
|
+
* </script>
|
|
65
|
+
*
|
|
66
|
+
* <input {...events} />
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Each handler fires the Form emitter first, then calls the user handler
|
|
70
|
+
* (if any) with the original event.
|
|
71
|
+
*/
|
|
72
|
+
type InputEventHandler<E extends Event = Event> = (event: E) => void;
|
|
73
|
+
type FocusEventHandler = InputEventHandler<FocusEvent>;
|
|
74
|
+
export declare function wireFormEvents(userHandlers: {
|
|
75
|
+
onblur?: FocusEventHandler | null;
|
|
76
|
+
oninput?: InputEventHandler | null;
|
|
77
|
+
onchange?: InputEventHandler | null;
|
|
78
|
+
onfocus?: FocusEventHandler | null;
|
|
79
|
+
}): {
|
|
80
|
+
onblur(event: FocusEvent): void;
|
|
81
|
+
oninput(event: Event): void;
|
|
82
|
+
onchange(event: Event): void;
|
|
83
|
+
onfocus(event: FocusEvent): void;
|
|
84
|
+
};
|
|
85
|
+
export {};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { getContext } from 'svelte';
|
|
2
|
+
import { getFormContext } from '../Form/form.context.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* Symbol key for the FormField context. Using a Symbol instead of a string
|
|
5
|
+
* prevents collisions with unrelated `getContext('formField')` calls from
|
|
6
|
+
* user code or other libraries.
|
|
7
|
+
*/
|
|
8
|
+
export const FORM_FIELD_CONTEXT_KEY = Symbol('sv5ui:form-field');
|
|
2
9
|
/**
|
|
3
10
|
* Access the nearest FormField context. Returns `undefined` when used outside a FormField.
|
|
4
11
|
*
|
|
@@ -13,5 +20,67 @@ import { getContext } from 'svelte';
|
|
|
13
20
|
* ```
|
|
14
21
|
*/
|
|
15
22
|
export function useFormField() {
|
|
16
|
-
return getContext(
|
|
23
|
+
return getContext(FORM_FIELD_CONTEXT_KEY);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Event emitter helpers for inputs nested inside a `<FormField>` within a `<Form>`.
|
|
27
|
+
* Each returned function is a safe no-op when used outside a Form, so inputs can
|
|
28
|
+
* unconditionally wire them to their native events.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```svelte
|
|
32
|
+
* <script>
|
|
33
|
+
* import { useFormFieldEmit } from 'sv5ui'
|
|
34
|
+
* const emit = useFormFieldEmit()
|
|
35
|
+
* </script>
|
|
36
|
+
*
|
|
37
|
+
* <input onblur={emit.onBlur} oninput={emit.onInput} onchange={emit.onChange} onfocus={emit.onFocus} />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useFormFieldEmit() {
|
|
41
|
+
const fieldCtx = getContext(FORM_FIELD_CONTEXT_KEY);
|
|
42
|
+
const formCtx = getFormContext();
|
|
43
|
+
return {
|
|
44
|
+
onBlur() {
|
|
45
|
+
const n = fieldCtx?.name;
|
|
46
|
+
if (n)
|
|
47
|
+
formCtx?.onBlur(n);
|
|
48
|
+
},
|
|
49
|
+
onFocus() {
|
|
50
|
+
const n = fieldCtx?.name;
|
|
51
|
+
if (n)
|
|
52
|
+
formCtx?.onFocus(n);
|
|
53
|
+
},
|
|
54
|
+
onChange() {
|
|
55
|
+
const n = fieldCtx?.name;
|
|
56
|
+
if (n)
|
|
57
|
+
formCtx?.onChange(n);
|
|
58
|
+
},
|
|
59
|
+
onInput() {
|
|
60
|
+
const n = fieldCtx?.name;
|
|
61
|
+
if (n)
|
|
62
|
+
formCtx?.onInput(n, fieldCtx?.eagerValidation);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function wireFormEvents(userHandlers) {
|
|
67
|
+
const emit = useFormFieldEmit();
|
|
68
|
+
return {
|
|
69
|
+
onblur(event) {
|
|
70
|
+
emit.onBlur();
|
|
71
|
+
userHandlers.onblur?.(event);
|
|
72
|
+
},
|
|
73
|
+
oninput(event) {
|
|
74
|
+
emit.onInput();
|
|
75
|
+
userHandlers.oninput?.(event);
|
|
76
|
+
},
|
|
77
|
+
onchange(event) {
|
|
78
|
+
emit.onChange();
|
|
79
|
+
userHandlers.onchange?.(event);
|
|
80
|
+
},
|
|
81
|
+
onfocus(event) {
|
|
82
|
+
emit.onFocus();
|
|
83
|
+
userHandlers.onfocus?.(event);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
17
86
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from './Tabs/index.js';
|
|
|
31
31
|
export * from './Pagination/index.js';
|
|
32
32
|
export * from './FieldGroup/index.js';
|
|
33
33
|
export * from './FormField/index.js';
|
|
34
|
+
export * from './Form/index.js';
|
|
34
35
|
export * from './Input/index.js';
|
|
35
36
|
export * from './Textarea/index.js';
|
|
36
37
|
export * from './Select/index.js';
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export * from './Tabs/index.js';
|
|
|
32
32
|
export * from './Pagination/index.js';
|
|
33
33
|
export * from './FieldGroup/index.js';
|
|
34
34
|
export * from './FormField/index.js';
|
|
35
|
+
export * from './Form/index.js';
|
|
35
36
|
export * from './Input/index.js';
|
|
36
37
|
export * from './Textarea/index.js';
|
|
37
38
|
export * from './Select/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv5ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A modern Svelte 5 UI component library with Tailwind CSS",
|
|
5
5
|
"author": "ndlabdev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,9 +48,27 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
+
"joi": "^17.0.0 || ^18.0.0",
|
|
51
52
|
"mode-watcher": "^1.0.0",
|
|
52
53
|
"svelte": "^5.0.0",
|
|
53
|
-
"tailwindcss": "^4.0.0"
|
|
54
|
+
"tailwindcss": "^4.0.0",
|
|
55
|
+
"valibot": "^1.0.0",
|
|
56
|
+
"yup": "^1.7.0",
|
|
57
|
+
"zod": "^3.24.0 || ^4.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"joi": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"valibot": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"yup": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"zod": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
54
72
|
},
|
|
55
73
|
"devDependencies": {
|
|
56
74
|
"@eslint/compat": "^1.4.0",
|
|
@@ -66,6 +84,7 @@
|
|
|
66
84
|
"eslint-config-prettier": "^10.1.8",
|
|
67
85
|
"eslint-plugin-svelte": "^3.13.1",
|
|
68
86
|
"globals": "^16.5.0",
|
|
87
|
+
"joi": "^18.1.2",
|
|
69
88
|
"playwright": "^1.57.0",
|
|
70
89
|
"prettier": "^3.7.4",
|
|
71
90
|
"prettier-plugin-svelte": "^3.4.0",
|
|
@@ -76,9 +95,12 @@
|
|
|
76
95
|
"tailwindcss": "^4.1.17",
|
|
77
96
|
"typescript": "^5.9.3",
|
|
78
97
|
"typescript-eslint": "^8.48.1",
|
|
98
|
+
"valibot": "^1.3.1",
|
|
79
99
|
"vite": "^7.2.6",
|
|
80
100
|
"vitest": "^4.0.15",
|
|
81
|
-
"vitest-browser-svelte": "^2.0.1"
|
|
101
|
+
"vitest-browser-svelte": "^2.0.1",
|
|
102
|
+
"yup": "^1.7.1",
|
|
103
|
+
"zod": "^4.3.6"
|
|
82
104
|
},
|
|
83
105
|
"keywords": [
|
|
84
106
|
"svelte",
|
|
@@ -98,6 +120,7 @@
|
|
|
98
120
|
"dependencies": {
|
|
99
121
|
"@iconify/svelte": "^5.2.1",
|
|
100
122
|
"@internationalized/date": "^3.11.0",
|
|
123
|
+
"@standard-schema/spec": "^1.1.0",
|
|
101
124
|
"bits-ui": "^2.15.4",
|
|
102
125
|
"svelte-sonner": "^1.1.0",
|
|
103
126
|
"tailwind-merge": "^3.4.0",
|