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.
@@ -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}
@@ -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';
@@ -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('formField');
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.5.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",