@cleverbrush/react-form 4.4.3 → 5.0.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (C) 2024 by Andrew Zolotuhkin <andrew_zol@cleverbrush.com>
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -3,11 +3,122 @@
3
3
  [![CI](https://github.com/cleverbrush/framework/actions/workflows/ci.yml/badge.svg)](https://github.com/cleverbrush/framework/actions/workflows/ci.yml)
4
4
  [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](../../LICENSE)
5
5
  <!-- coverage-badge-start -->
6
- ![Coverage](https://img.shields.io/badge/coverage-98.7%25-brightgreen)
6
+ ![Unit coverage](https://img.shields.io/badge/unit_coverage-99.5%25-brightgreen)
7
7
  <!-- coverage-badge-end -->
8
8
 
9
9
  A headless, schema-driven form system for React based on `@cleverbrush/schema`. Uses PropertyDescriptors for type-safe field binding, supports global UI renderer configuration via a provider, and is completely UI-agnostic — works with plain HTML, MUI, Ant Design, or any component library.
10
10
 
11
+ ## Server validation issues
12
+
13
+ Return `{ ok: false, error: string, issues }` from `handleSubmit` to apply a
14
+ submission message and field issues together. An issue is plain
15
+ `{ pointer: string, detail: string }` data: `pointer` is a **form-relative JSON
16
+ Pointer**, not a dotted path. `''` means a form-level error. No HTTP, client, or
17
+ Next.js dependency is needed by this package.
18
+
19
+ ### A multi-file client/action/form example
20
+
21
+ The API contract, server handler, action and component remain separate. The
22
+ following assumes your application exports a configured typed `client` for
23
+ `api`; authentication, persistence and safe fallback messages stay application-owned.
24
+
25
+ ```ts
26
+ // profile-contract.ts (shared)
27
+ import { object, string } from '@cleverbrush/schema';
28
+ import { defineApi, endpoint } from '@cleverbrush/server/contract';
29
+ export const Profile = object({ name: string().minLength(2) });
30
+ export const api = defineApi({ profiles: {
31
+ save: endpoint.post('/profiles').body(Profile).responses({ 200: Profile })
32
+ } });
33
+ ```
34
+
35
+ ```ts
36
+ // profile-handler.ts (server)
37
+ import type { Handler } from '@cleverbrush/server';
38
+ import { api } from './profile-contract';
39
+ export const saveProfileHandler: Handler<typeof api.profiles.save> =
40
+ async ({ body }) => body; // Replace with application persistence.
41
+ // Register this handler with your application's implementation scope.
42
+ ```
43
+
44
+ ```ts
45
+ // save-profile.ts (server action / application boundary)
46
+ import { decodeValidationIssues } from '@cleverbrush/client';
47
+ import type { InferType } from '@cleverbrush/schema';
48
+ import { Profile } from './profile-contract';
49
+ import { client } from './api-client';
50
+ export async function saveProfile(values: InferType<typeof Profile>) {
51
+ try {
52
+ return { ok: true as const, data: await client.profiles.save({ body: values }) };
53
+ } catch (error) {
54
+ const issues = decodeValidationIssues(error, { source: 'body' });
55
+ if (issues) return { ok: false as const, error: 'Check your input.', issues };
56
+ // Add application-owned handling for expected general errors here.
57
+ throw error;
58
+ }
59
+ }
60
+ ```
61
+
62
+ ```tsx
63
+ // ProfileForm.tsx (browser; add your framework's client directive if needed)
64
+ import { useSchemaForm } from '@cleverbrush/react-form';
65
+ import { Profile } from './profile-contract';
66
+ import { saveProfile } from './save-profile';
67
+ export function ProfileForm() {
68
+ const form = useSchemaForm(Profile);
69
+ const name = form.useField(t => t.name);
70
+ return <form onSubmit={form.handleSubmit(saveProfile)}>
71
+ <label>Name <input value={name.value ?? ''}
72
+ onChange={e => name.onChange(e.target.value)} onBlur={name.onBlur}
73
+ aria-invalid={name.touched && !!name.error} aria-describedby="name-error" /></label>
74
+ <span id="name-error">{name.touched && name.error}</span>
75
+ {form.error && <p role="alert">{form.error}</p>}
76
+ <button disabled={form.submitting}>Save</button>
77
+ </form>;
78
+ }
79
+ ```
80
+
81
+ Ordinary shared-schema errors are already caught locally. This bridge handles
82
+ structured server rejections (including schema-version differences); it does
83
+ not manufacture field errors from business-error strings. Never serialize an
84
+ `ApiError` instance to the browser. Only its decoded issue data crosses the action
85
+ boundary. A form with different property names must explicitly map the returned
86
+ pointers to its own paths before returning the failure result.
87
+
88
+ ### Manual issues, indexed fields and lifecycle
89
+
90
+ Use `form.setIssues(issues)` for custom flows; it replaces the external issue set.
91
+ `form.setIssues([])` clears only external errors, not local schema errors. Prefer
92
+ returned issues for asynchronous submissions: `handleSubmit` ignores stale issues
93
+ after any value update, reset or unmount. Manual callers own that race protection.
94
+
95
+ ```tsx
96
+ const form = useSchemaForm(object({ addresses: array(object({ city: string() })) }));
97
+ const city = form.useField(t => t.addresses[0].city); // string | undefined
98
+ form.setIssues([{ pointer: '/addresses/0/city', detail: 'Choose another city.' }]);
99
+ // Equivalent with Field / a typed form system:
100
+ // <Field form={form} forProperty={t => t.addresses[0].city} />
101
+ ```
102
+
103
+ - Matched fields become touched and show their first external message through
104
+ `field.error`. External messages take precedence until cleared.
105
+ - Local validation, including debounced validation, does not erase external
106
+ issues on unchanged fields. Editing clears related ancestor/descendant issues,
107
+ not unrelated fields. Root issues clear when values change.
108
+ - `setValue` clears issues for changed values. Replacing/reordering an array
109
+ clears its indexed issues: indices are positions, not stable item identities.
110
+ - Reset and the next submission clear all external issues. Existing string-only
111
+ submission errors continue to work and clear on reset/resubmission.
112
+ - Root, unknown, and currently unbound field issues are included in `form.error`
113
+ alongside the application message, joined by newlines. Render that summary even
114
+ when using inline messages. Binding/unbinding a field updates the summary.
115
+ - Indexed selectors support object, primitive and nested arrays, but are not
116
+ native arrays: no `push`, negative indices, or add/remove/reorder UI helpers.
117
+
118
+ JSON Pointer escaping preserves distinct names: `/a.b`, `/a/b`, `/a~1b`, and
119
+ `/a~0b` target a dotted name, a nested field, a slash in a name, and a tilde in a
120
+ name respectively. Unknown paths never create values or mutate the form.
121
+
11
122
  ## Why @cleverbrush/react-form?
12
123
 
13
124
  **The problem:** Every popular React form library — React Hook Form, Formik, React Final Form — requires you to reference fields by **string names**: `register("email")`, `<Field name="address.city" />`. The moment you pass a field name as a string, you lose TypeScript's type safety. Rename a property in your data model and the compiler stays silent — your form just silently breaks at runtime. The larger your codebase, the more of these invisible string references you accumulate, and the more fragile every refactor becomes.
@@ -54,6 +165,21 @@ npm install @cleverbrush/react-form
54
165
 
55
166
  ## Quick Start
56
167
 
168
+ For typed renderer props/variants and managed submission, see the new
169
+ [consumer examples and migration guide](../../docs/cache-form-migration.md).
170
+ The provider-based APIs below remain supported.
171
+
172
+ Typed fields and headless `form.useField()` accept properties with schema
173
+ defaults, including enums, booleans, arrays, and nullable values. A default
174
+ does not erase the property's inferred type. Defaults are applied during
175
+ validation; call `form.reset(values)` to establish a visible clean baseline.
176
+ `reset()` still clears the store rather than restoring schema defaults.
177
+
178
+ Shared UI packages can directly export `createFormSystem(...)` results and
179
+ `system.Field` while emitting TypeScript declarations. The named
180
+ `TypedFormSystem` and `TypedFieldComponent` types preserve the registry's
181
+ field/variant/props checks across package boundaries.
182
+
57
183
  ```tsx
58
184
  import { object, string, number } from '@cleverbrush/schema';
59
185
  import { useSchemaForm, FormSystemProvider, Field } from '@cleverbrush/react-form';
@@ -316,9 +442,21 @@ const form = useSchemaForm(UserSchema, {
316
442
  | `form.useField(forProperty)` | Bind a field by PropertyDescriptor selector |
317
443
  | `form.submit()` | Validate and return `ValidationResult` (includes `result.object` on success) |
318
444
  | `form.validate()` | Run validation, propagate errors to fields |
319
- | `form.reset(values?)` | Reset all fields; optionally set new initial values |
445
+ | `form.reset(values?)` | Clear values, or establish supplied values as a clean baseline; synchronize mounted fields and clear errors/touched/dirty/validation |
446
+ | `form.handleSubmit(onValid, options?)` | Awaitable event handler: validate, prevent duplicate submits, handle success/failure results |
447
+ | `form.submitting` | Reactive, read-only pending state from validation through callbacks |
448
+ | `form.error` | Reactive, read-only submission error; cleared on reset or the next attempt |
320
449
  | `form.getValue()` | Get current form values as plain object |
321
- | `form.setValue(values)` | Merge values into form state |
450
+ | `form.setValue(values)` | Shallow-merge values and synchronize mounted fields without marking touched |
451
+
452
+ Form snapshots and dirty checks use `deepClone` and `deepEqual` from
453
+ [`@cleverbrush/deep`](../deep/README.md). Plain objects, arrays and Dates are cloned
454
+ on reset/setters; changing caller-owned input cannot change their baseline. Cycles,
455
+ shared references, sparse arrays and null prototypes are preserved. Replacing a
456
+ value with structurally equal data clears dirty, even with different sharing of
457
+ child references. Files and other opaque objects retain identity: a different File
458
+ is dirty even if its name/content match. Treat returned snapshots and opaque values
459
+ as read-only; update through setters rather than mutating them in place.
322
460
 
323
461
  ## useField
324
462
 
@@ -1,17 +1,29 @@
1
- import type { FieldState } from './types.js';
2
- /**
3
- * Internal form store — manages field state and notification.
4
- * Does not depend on React; used by hooks for state management.
5
- */
1
+ import type { FieldState, FormIssue, FormSubmissionState } from './types.js';
2
+ type Binding = {
3
+ getValue: (values: any) => {
4
+ success: boolean;
5
+ value?: any;
6
+ };
7
+ setValue: (values: any, value: any, options: any) => boolean;
8
+ };
9
+ /** Internal immutable snapshots shared by all bindings of a form. */
6
10
  export declare function createFormStore(initialValues: any): {
11
+ registerField: (path: string, binding: Binding) => FieldState;
7
12
  getFieldState: (path: string) => FieldState;
8
13
  updateFieldState: (path: string, patch: Partial<FieldState>) => void;
9
14
  subscribe: (path: string, listener: () => void) => () => void;
10
15
  subscribeGlobal: (listener: () => void) => () => void;
11
16
  getValues: () => any;
12
- setValues: (newValues: any) => void;
17
+ setValues: (newValues: any, changedPath?: string) => void;
18
+ setFieldValue: (path: string, value: any, createMissingStructure: boolean) => void;
13
19
  resetAll: (newInitialValues?: any) => void;
14
- notifyAll: () => void;
15
20
  getAllFieldPaths: () => string[];
21
+ getRevision: () => number;
22
+ getSubmissionState: () => FormSubmissionState;
23
+ setSubmissionState: (patch: Partial<FormSubmissionState>) => void;
24
+ setIssues: (next: readonly FormIssue[]) => void;
25
+ setSubmissionFailure: (error: string, next?: readonly FormIssue[]) => void;
26
+ notifyAll: () => void;
16
27
  };
17
28
  export type FormStore = ReturnType<typeof createFormStore>;
29
+ export {};
@@ -45,7 +45,7 @@ export declare function FormProvider<TSchema extends ObjectSchemaBuilder<any, an
45
45
  * const name = useField<typeof MySchema>((t) => t.name);
46
46
  * ```
47
47
  */
48
- export declare function useField<TSchema extends ObjectSchemaBuilder<any, any, any>, TPropertySchema extends SchemaBuilder<any, any, any> = SchemaBuilder<any, any, any>>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>): UseFieldResult<InferType<TPropertySchema>>;
48
+ export declare function useField<TSchema extends ObjectSchemaBuilder<any, any, any>, TPropertySchema extends SchemaBuilder<any, any, any, any> = any>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>): UseFieldResult<InferType<TPropertySchema>>;
49
49
  /**
50
50
  * Hook to access FormSystem configuration.
51
51
  */
@@ -15,6 +15,7 @@ export type FormContextValue = {
15
15
  schema: ObjectSchemaBuilder<any, any, any>;
16
16
  options: UseSchemaFormOptions;
17
17
  pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;
18
+ triggerValidation?: (markTouched: boolean) => Promise<unknown>;
18
19
  };
19
20
  /**
20
21
  * Context for per-form data — allows useField to work via context.
package/dist/helpers.d.ts CHANGED
@@ -12,7 +12,7 @@ export declare function getDescriptorPath(inner: PropertyDescriptorInner<any, an
12
12
  /**
13
13
  * Returns the schema type string (e.g. "string", "number", "object").
14
14
  */
15
- export declare function getSchemaType(schema: SchemaBuilder<any, any, any>): string;
15
+ export declare function getSchemaType(schema: SchemaBuilder<any, any, any, any>): string;
16
16
  /**
17
17
  * Builds a selector function from a dot-separated path string.
18
18
  * The selector traverses the PropertyDescriptorTree by property access.
package/dist/hooks.d.ts CHANGED
@@ -1,36 +1,33 @@
1
1
  import type { InferType, PropertyDescriptor, PropertyDescriptorTree, SchemaBuilder, ValidationResult } from '@cleverbrush/schema';
2
2
  import { ObjectSchemaBuilder } from '@cleverbrush/schema';
3
3
  import type { FormContextValue } from './contexts.js';
4
- import type { FieldRenderer, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
5
- /**
6
- * Return type for useSchemaForm — fully typed for IntelliSense.
7
- * The `useField` method infers the field value type from the schema via PropertyDescriptor.
8
- */
4
+ import type { FieldRenderer, FormIssue, FormSubmissionState, FormSubmitHandler, FormSubmitOptions, FormSubmitResult, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
5
+ /** A stable form controller with reactive field and submission subscriptions. */
9
6
  export type SchemaFormInstance<TSchema extends ObjectSchemaBuilder<any, any, any>> = {
10
- useField: <TPropertySchema extends SchemaBuilder<any, any, any>>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>) => UseFieldResult<InferType<TPropertySchema>>;
7
+ useField: <TPropertySchema extends SchemaBuilder<any, any, any, any>>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>) => UseFieldResult<InferType<TPropertySchema>>;
11
8
  submit: () => Promise<ValidationResult<InferType<TSchema>>>;
12
9
  validate: () => Promise<ValidationResult<InferType<TSchema>>>;
10
+ /** Clear values, or establish supplied values as the new clean baseline. */
13
11
  reset: (values?: Partial<InferType<TSchema>>) => void;
12
+ /** Read the current snapshot. Use setters rather than mutating it. */
14
13
  getValue: () => InferType<TSchema>;
14
+ /** Shallow-merge values without marking fields touched. */
15
15
  setValue: (values: Partial<InferType<TSchema>>) => void;
16
- /** @internal — Used by FormProvider and Field to access internal context */
16
+ /**
17
+ * Replace external issues without changing values. Empty pointers and unbound
18
+ * fields appear in form.error; bound fields receive the first issue and are
19
+ * marked touched. Prefer returned submission issues for async race protection.
20
+ * Pass [] to clear external issues without clearing local validation errors.
21
+ */
22
+ setIssues: (issues: readonly FormIssue[]) => void;
23
+ /** Validate and submit once; repeated calls while pending are ignored. */
24
+ handleSubmit: <TData = void>(onValid: (values: InferType<TSchema>) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>, options?: FormSubmitOptions<InferType<TSchema>, TData>) => FormSubmitHandler;
25
+ /** @internal — Used by FormProvider and Field to access internal context. */
17
26
  _getFormContext: () => FormContextValue;
18
- };
19
- /**
20
- * Hook that binds a schema to a form instance.
21
- * Provides field binding API, form-level validation, submit, reset.
22
- */
27
+ } & FormSubmissionState;
28
+ /** Bind a schema to independently subscribed fields and a stable form instance. */
23
29
  export declare function useSchemaForm<TSchema extends ObjectSchemaBuilder<any, any, any>>(schema: TSchema, options?: UseSchemaFormOptions): SchemaFormInstance<TSchema>;
24
- /**
25
- * Internal useField implementation, requires FormContextValue.
26
- * Returns UseFieldResult with untyped values — callers should cast to the proper generic type.
27
- */
28
- export declare function useFieldFromContext(formContext: FormContextValue, forProperty: (tree: any) => any, triggerValidation?: (markTouched: boolean) => Promise<any>): UseFieldResult;
29
- /**
30
- * Resolves a renderer from the FormSystem config based on schema type and optional variant.
31
- *
32
- * When `variant` is provided the registry is checked for `"type:variant"` first
33
- * (e.g. `"string:password"`). If no match is found it falls back to the base
34
- * `"type"` key (e.g. `"string"`).
35
- */
36
- export declare function resolveRenderer(config: FormSystemConfig | null, schema: SchemaBuilder<any, any, any>, variant?: string): FieldRenderer | undefined;
30
+ /** Internal field binding shared by direct and context-based hooks. */
31
+ export declare function useFieldFromContext(formContext: FormContextValue, forProperty: (tree: any) => any, triggerValidation?: ((markTouched: boolean) => Promise<unknown>) | undefined): UseFieldResult;
32
+ /** Resolve type:variant first, falling back to the base type. */
33
+ export declare function resolveRenderer(config: FormSystemConfig | null, schema: SchemaBuilder<any, any, any, any>, variant?: string): FieldRenderer | undefined;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export type { FieldProps, FormProviderProps, FormSystemProviderProps } from './c
2
2
  export { Field, FormProvider, FormSystemProvider, useField, useFormSystem } from './components.js';
3
3
  export type { SchemaFormInstance } from './hooks.js';
4
4
  export { useSchemaForm } from './hooks.js';
5
- export type { FieldRenderer, FieldRenderProps, FieldState, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
5
+ export type { TypedFieldComponent, TypedFieldProps, TypedFieldRenderer, TypedFormSystem, TypedRendererRegistry } from './system.js';
6
+ export { createFormSystem, defineFieldRenderer } from './system.js';
7
+ export type { FieldRenderer, FieldRenderProps, FieldState, FormIssue, FormSubmissionState, FormSubmitHandler, FormSubmitOptions, FormSubmitResult, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- import{useContext as B,useMemo as ce}from"react";import{createContext as W}from"react";var b=W(null),E=W(null);import{ObjectSchemaBuilder as H,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as Y}from"@cleverbrush/schema";function U(t,s){let r=new Map,e=s.introspect();if(!e.properties)return r;for(let n of Object.keys(e.properties)){let i=t[n];if(!i||!(Y in i))continue;let f=i[Y];r.set(f,n);let c=e.properties[n];if(c instanceof H){let l=U(i,c);for(let[a,p]of l)r.set(a,`${n}.${p}`)}}return r}function G(t,s){return s.get(t)??""}function M(t){return t.introspect()?.type??"unknown"}function z(t){let s=t.split(".");return r=>{let e=r;for(let n of s){if(e==null)return;e=e[n]}return e}}function A(t,s){let r=s.introspect();if(!r.properties)return t??{};let e=t!=null?{...t}:{};for(let n of Object.keys(r.properties)){let i=r.properties[n];i instanceof H&&(e[n]=A(e[n],i))}return e}import{ObjectSchemaBuilder as oe,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as se}from"@cleverbrush/schema";import{useCallback as y,useEffect as Q,useMemo as K,useRef as h,useState as ae}from"react";function q(t,s){let r=null;return(...e)=>{r!==null&&clearTimeout(r),r=setTimeout(()=>{t(...e)},s)}}function J(t){let s=t!=null?{...t}:{},r=new Map,e=new Map,n=new Set;function i(o){return r.has(o)||r.set(o,{value:void 0,initialValue:void 0,dirty:!1,touched:!1,error:void 0,validating:!1}),r.get(o)}function f(o){return i(o)}function c(o,u){let g={...i(o),...u};r.set(o,g),l(o)}function l(o){let u=e.get(o);if(u)for(let d of u)d()}function a(){for(let[,o]of e)for(let u of o)u();for(let o of n)o()}function p(o,u){return e.has(o)||e.set(o,new Set),e.get(o).add(u),()=>{e.get(o)?.delete(u)}}function F(o){return n.add(o),()=>{n.delete(o)}}function R(){return s}function T(o){s=o!=null?{...o}:{}}function S(o){s=o!=null?{...o}:{},r.clear(),a()}function P(){return Array.from(r.keys())}return{getFieldState:f,updateFieldState:c,subscribe:p,subscribeGlobal:F,getValues:R,setValues:T,resetAll:S,notifyAll:a,getAllFieldPaths:P}}function ie(t,s){let r={createMissingStructure:!0,...s},e=h(null);e.current||(e.current=J({}));let n=e.current,i=h(null);i.current||(i.current=oe.getPropertiesFor(t));let f=i.current,c=h(null);c.current||(c.current=U(f,t));let l=c.current,a=h(t),p=h(r);p.current=r;let F=K(()=>({store:n,descriptorTree:f,schema:a.current,options:p.current,pathMap:l}),[n,f,l]),R=h(F);R.current=F;let T=h(0),S=y(async m=>{let I=++T.current,j=n.getValues(),ee=A(j,a.current),D;try{D=await a.current.validateAsync(ee,{doNotStopOnFirstError:!0})}catch{return{valid:!1}}if(I!==T.current)return D;let re=n.getAllFieldPaths();for(let w of re){let v={error:void 0};m&&(v.touched=!0),n.updateFieldState(w,v)}let $=D,te=new Set;if(typeof $.getErrorsFor=="function"){let w=$.getErrorsFor;for(let[,v]of l)try{let ne=z(v),O=w(ne);if(O&&Array.isArray(O.errors)&&O.errors.length>0){let L={error:O.errors[0]};m&&(L.touched=!0),n.updateFieldState(v,L),te.add(v)}}catch{}}return D},[n,l]),P=y(async()=>S(!0),[S]),o=y(async()=>P(),[P]),u=y(m=>{let I=m??{};n.resetAll(I)},[n]),d=y(()=>n.getValues(),[n]),g=y(m=>{let j={...n.getValues(),...m};n.setValues(j),n.notifyAll()},[n]),x=y(()=>R.current,[]),V=h(r.validateOnMount);Q(()=>{V.current&&S(!0)},[]);let C=h(null);r.validationDebounceMs!=null&&r.validationDebounceMs>0&&!C.current&&(C.current=q(m=>{S(m)},r.validationDebounceMs));let N=y(m=>C.current?(C.current(m),Promise.resolve(void 0)):S(m),[S]),_=y(m=>k(R.current,m,N),[N]);return K(()=>({useField:_,submit:o,validate:P,reset:u,getValue:d,setValue:g,_getFormContext:x}),[_,o,P,u,d,g,x])}function k(t,s,r){let{store:e,descriptorTree:n,options:i,pathMap:f}=t,l=s(n)[se],a=G(l,f),p=l.getSchema(),F=h(null);if(F.current!==a){let u=e.getValues(),{success:d,value:g}=l.getValue(u),x=e.getFieldState(a);if(x.initialValue===void 0&&!x.touched){let V=d?g:void 0;e.updateFieldState(a,{value:V,initialValue:V,dirty:!1})}F.current=a}let[,R]=ae(0);Q(()=>e.subscribe(a,()=>{R(d=>d+1)}),[e,a]);let T=y(u=>{let d=e.getValues();l.setValue(d,u,{createMissingStructure:i.createMissingStructure!==!1}),e.setValues(d);let g=e.getFieldState(a);e.updateFieldState(a,{value:u,dirty:u!==g.initialValue}),r&&r(!1)},[e,l,a,i,r]),S=y(()=>{e.updateFieldState(a,{touched:!0})},[e,a]),P=y(u=>{T(u)},[T]),o=e.getFieldState(a);return{value:o.value,initialValue:o.initialValue,dirty:o.dirty,touched:o.touched,error:o.error,validating:o.validating,onChange:T,onBlur:S,setValue:P,schema:p}}function X(t,s,r){if(!t?.renderers)return;let e=M(s);if(r){let n=t.renderers[`${e}:${r}`];if(n)return n}return t.renderers[e]}import{jsx as Z}from"react/jsx-runtime";function ue({renderers:t,config:s,children:r}){let e=B(b),n=ce(()=>{let i={...s,renderers:{...s?.renderers,...t}};return e?{...e,...i,renderers:{...e.renderers,...i.renderers}}:i},[e,s,t]);return Z(b.Provider,{value:n,children:r})}function le({form:t,children:s}){let r=t._getFormContext();return Z(E.Provider,{value:r,children:s})}function me(t){let s=B(E);if(!s)throw new Error("useField must be used within a FormProvider. Wrap your component tree with <FormProvider form={form}>.");return k(s,t)}function pe(){let t=B(b);if(!t)throw new Error("useFormSystem must be used within a FormSystemProvider");return t}function de({forProperty:t,form:s,renderer:r,variant:e,label:n,name:i,fieldProps:f}){let c=s.useField(t),l=B(b),a=r??X(l,c.schema,e);if(!a){let p=M(c.schema),F=e?`"${p}:${e}" or "${p}"`:`"${p}"`;throw new Error(`No renderer found for schema type ${F}. Provide a renderer prop or configure one in FormSystemProvider.`)}return a({value:c.value,initialValue:c.initialValue,dirty:c.dirty,touched:c.touched,error:c.error,validating:c.validating,onChange:c.onChange,onBlur:c.onBlur,setValue:c.setValue,schema:c.schema,variant:e,label:n,name:i,fieldProps:f})}export{de as Field,le as FormProvider,ue as FormSystemProvider,me as useField,pe as useFormSystem,ie as useSchemaForm};
1
+ import{useContext as Q,useMemo as Re}from"react";import{createContext as ie}from"react";var W=ie(null),Z=ie(null);import{ObjectSchemaBuilder as ce,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as ae}from"@cleverbrush/schema";function ee(s,n){let c=new Map,e=n.introspect();if(!e.properties)return c;for(let a of Object.keys(e.properties)){let u=s[a];if(!u||!(ae in u))continue;let p=u[ae];c.set(p,a);let o=e.properties[a];if(o instanceof ce){let d=ee(u,o);for(let[f,l]of d)c.set(f,`${a}.${l}`)}}return c}function U(s){return s.introspect()?.type??"unknown"}function re(s,n){let c=n.introspect();if(!c.properties)return s??{};let e=s!=null?{...s}:{};for(let a of Object.keys(c.properties)){let u=c.properties[a];u instanceof ce&&(e[a]=re(e[a],u))}return e}import{ObjectSchemaBuilder as Fe,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as he}from"@cleverbrush/schema";import{useCallback as S,useEffect as de,useMemo as le,useRef as D,useSyncExternalStore as me}from"react";import{deepClone as L,deepEqual as G}from"@cleverbrush/deep";function ue(s){let n=L(s??{}),c=n,e=0,a=[],u,p=new Map,o={submitting:!1,error:void 0},d=new Map,f=new Map,l=new Map,F=new Set,P=0,O=new Set,j=!1;function h(r){P++;try{r()}finally{if(P--,P===0){let t=[...O];O.clear();let i=j;j=!1;for(let m of t)k(m);i&&g()}}}function g(){if(P){j=!0;return}for(let r of F)r()}function B(r,t){let i=r?.getValue(t);return i?.success?i.value:void 0}function E(r){if(!f.has(r)){let t=d.get(r),i=B(t,n),m=B(t,c);f.set(r,{value:i,initialValue:m,dirty:!G(i,m),touched:a.some(v=>v.pointer===r),error:M(r),validating:!1})}return f.get(r)}function z(r,t){return d.has(r)||d.set(r,t),E(r)}function k(r){if(P){O.add(r);return}for(let t of l.get(r)??[])t()}function q(){for(let r of l.keys())k(r);g()}function $(r,t){let i=E(r);Object.entries(t).every(([m,v])=>Object.is(i[m],v))||(f.set(r,{...i,...t}),k(r))}function M(r){return r===""?void 0:a.find(t=>t.pointer===r)?.detail}function H(r,t){Object.hasOwn(t,"error")&&p.set(r,t.error),$(r,{...t,error:M(r)??p.get(r)})}function N(r){h(()=>{a=r.map(({pointer:t,detail:i})=>({pointer:t,detail:i}));for(let t of d.keys())$(t,{error:M(t)??p.get(t),...M(t)!==void 0?{touched:!0}:{}});I({})})}function J(r,t){h(()=>{t&&N(t),I({error:r})})}function Y(r){for(let[t,i]of d){let m=B(i,n),v=B(i,c);H(t,{value:m,initialValue:v,dirty:!G(m,v),validating:!1,...r?{touched:!1,error:void 0}:{}})}}function X(r,t){let i=l.get(r);return i||(i=new Set,l.set(r,i)),i.add(t),I({}),()=>{i.delete(t),i.size===0&&l.delete(r),I({})}}function y(r){return F.add(r),()=>{F.delete(r)}}function V(){return o}function I(r){Object.hasOwn(r,"error")&&(u=r.error);let t=[u,...a.filter(m=>m.pointer===""||!l.get(m.pointer)?.size).map(m=>m.detail)].filter(m=>!!m),i={...o,...r,error:[...new Set(t)].join(`
2
+ `)||void 0};i.submitting===o.submitting&&i.error===o.error||(o=i,g())}function K(){return n}function b(){return e}function w(r,t){let i=n;if(n=L(r??{}),e++,a.length>0&&!G(i,n)){let v=(C,R)=>{if(R==="")return C;if(!(!R.startsWith("/")||/~(?![01])/.test(R)))return R.slice(1).split("/").reduce((A,Te)=>{let se=Te.replace(/~1/g,"/").replace(/~0/g,"~");return A!=null&&Object.hasOwn(A,se)?A[se]:void 0},C)},Se=[...d.keys()].filter(C=>Array.isArray(v(i,C))&&!G(v(i,C),v(n,C))&&(!t||t===C||C.startsWith(t+"/")));N(a.filter(C=>{let R=C.pointer;return R===""||t&&(R===t||R.startsWith(t+"/")||t.startsWith(R+"/"))||Se.some(A=>R===A||R.startsWith(A+"/"))?!1:G(v(i,R),v(n,R))}))}Y(!1)}function x(r,t,i){let m=L(n);d.get(r)?.setValue(m,L(t),{createMissingStructure:i})&&w(m,r)}function _(r){n=L(r??{}),c=n,e++,a=[],p.clear(),Y(!0),I({error:void 0})}function T(){return Array.from(d.keys())}return{registerField:z,getFieldState:E,updateFieldState:H,subscribe:X,subscribeGlobal:y,getValues:K,setValues:w,setFieldValue:x,resetAll:_,getAllFieldPaths:T,getRevision:b,getSubmissionState:V,setSubmissionState:I,setIssues:N,setSubmissionFailure:J,notifyAll:q}}function ge(s,n){let c=D(null);c.current||(c.current=ue({}));let e=c.current,a=D(null);a.current||(a.current=Fe.getPropertiesFor(s));let u=a.current,p=D(null);p.current||(p.current=ee(u,s));let o=p.current;for(let[y]of o)e.registerField(y.toJsonPointer(),y);let d=D(s),f=D({});f.current={createMissingStructure:!0,...n};let l=D(!0),F=D(0),P=D(0),O=D(!1),j=D(null),h=S(()=>{j.current!==null&&clearTimeout(j.current),j.current=null},[]);de(()=>(l.current=!0,()=>{l.current=!1,F.current++,P.current++,h()}),[h]);let g=S(async y=>{h();let V=++P.current,I=e.getRevision();for(let x of e.getAllFieldPaths())e.updateFieldState(x,{validating:!0});let K=re(e.getValues(),d.current),b;try{b=await d.current.validateAsync(K,{doNotStopOnFirstError:!0})}catch{b={valid:!1}}if(!l.current||V!==P.current||I!==e.getRevision())return b;for(let x of e.getAllFieldPaths())e.updateFieldState(x,{error:void 0,validating:!1,...y?{touched:!0}:{}});let w=b.getErrorsFor;if(typeof w=="function")for(let x of e.getAllFieldPaths()){let _=x.slice(1).split("/").map(T=>T.replace(/~1/g,"/").replace(/~0/g,"~"));try{let T=w(r=>_.reduce((t,i)=>t?.[i],r));Array.isArray(T?.errors)&&T.errors.length&&e.updateFieldState(x,{error:T.errors[0]})}catch{}}return b},[h,e]),B=S(()=>g(!0),[g]),E=S(()=>B(),[B]),z=S(y=>{F.current++,P.current++,h(),e.resetAll(y)},[e,h]),k=S(()=>e.getValues(),[e]),q=S(y=>{h(),e.setValues({...e.getValues(),...y})},[e,h]),$=S(y=>{h();let V=f.current.validationDebounceMs;return V!=null&&V>0?(j.current=setTimeout(()=>{j.current=null,g(y)},V),Promise.resolve()):g(y)},[g,h]),M=S((y,V)=>async I=>{if(I?.preventDefault(),O.current||!l.current)return;O.current=!0;let K=F.current,b=()=>l.current&&K===F.current;e.setIssues([]),e.setSubmissionState({submitting:!0,error:void 0});try{let w=e.getRevision(),x=await g(!0);if(!b()||w!==e.getRevision()||!x.valid||x.object===void 0)return;let _=x.object,T;try{T=await y(_)}catch(r){if(!b())return;if(!V?.onError)throw r;let t=await V.onError(r);b()&&e.setSubmissionState({error:t});return}if(!b())return;if(T&&!T.ok){e.setSubmissionFailure(T.error,w===e.getRevision()?T.issues:void 0);return}await V?.onSuccess?.(T?.data,_)}finally{O.current=!1,l.current&&e.setSubmissionState({submitting:!1})}},[g,e]),H=le(()=>({store:e,descriptorTree:u,schema:d.current,pathMap:o,get options(){return f.current},triggerValidation:$}),[e,u,o,$]),N=D(H);N.current=H;let J=S(()=>N.current,[]),Y=S(y=>te(N.current,y),[]),X=D(n?.validateOnMount);return de(()=>{X.current&&g(!0)},[g]),me(e.subscribeGlobal,e.getSubmissionState,e.getSubmissionState),le(()=>({useField:Y,submit:E,validate:B,reset:z,getValue:k,setValue:q,setIssues:e.setIssues,handleSubmit:M,_getFormContext:J,get submitting(){return e.getSubmissionState().submitting},get error(){return e.getSubmissionState().error}}),[Y,E,B,z,k,q,M,J,e])}function te(s,n,c=s.triggerValidation){let{store:e,descriptorTree:a,options:u}=s,p=n(a)[he],o=p.toJsonPointer();e.registerField(o,p);let d=S(O=>e.subscribe(o,O),[e,o]),f=S(()=>e.getFieldState(o),[e,o]),l=me(d,f,f),F=S(O=>{e.setFieldValue(o,O,u.createMissingStructure!==!1),c?.(!1)},[e,o,u.createMissingStructure,c]),P=S(()=>e.updateFieldState(o,{touched:!0}),[e,o]);return{...l,onChange:F,onBlur:P,setValue:F,schema:p.getSchema()}}function pe(s,n,c){if(!s?.renderers)return;let e=U(n);return(c?s.renderers[`${e}:${c}`]:void 0)??s.renderers[e]}import{jsx as ye}from"react/jsx-runtime";function ne({renderers:s,config:n,children:c}){let e=Q(W),a=Re(()=>{let u={...n,renderers:{...n?.renderers,...s}};return e?{...e,...u,renderers:{...e.renderers,...u.renderers}}:u},[e,n,s]);return ye(W.Provider,{value:a,children:c})}function Pe({form:s,children:n}){let c=s._getFormContext();return ye(Z.Provider,{value:c,children:n})}function be(s){let n=Q(Z);if(!n)throw new Error("useField must be used within a FormProvider. Wrap your component tree with <FormProvider form={form}>.");return te(n,s)}function xe(){let s=Q(W);if(!s)throw new Error("useFormSystem must be used within a FormSystemProvider");return s}function oe({forProperty:s,form:n,renderer:c,variant:e,label:a,name:u,fieldProps:p}){let o=n.useField(s),d=Q(W),f=c??pe(d,o.schema,e);if(!f){let l=U(o.schema),F=e?`"${l}:${e}" or "${l}"`:`"${l}"`;throw new Error(`No renderer found for schema type ${F}. Provide a renderer prop or configure one in FormSystemProvider.`)}return f({value:o.value,initialValue:o.initialValue,dirty:o.dirty,touched:o.touched,error:o.error,validating:o.validating,onChange:o.onChange,onBlur:o.onBlur,setValue:o.setValue,schema:o.schema,variant:e,label:a,name:u,fieldProps:p})}import{SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as ve}from"@cleverbrush/schema";import{jsx as fe}from"react/jsx-runtime";function De(s){return s}function Oe(s){let n=Object.freeze({...s.renderers});function c(a){let u=a.forProperty(a.form._getFormContext().descriptorTree),p=U(u[ve].getSchema()),o=a.variant?`${p}:${a.variant}`:p,d=n[o];if(!d)throw new Error(`No renderer registered for "${o}" in this form system.`);return fe(oe,{...a,renderer:d})}function e({children:a}){return fe(ne,{renderers:n,children:a})}return{Field:c,Provider:e,renderers:n}}export{oe as Field,Pe as FormProvider,ne as FormSystemProvider,Oe as createFormSystem,De as defineFieldRenderer,be as useField,xe as useFormSystem,ge as useSchemaForm};
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components.tsx","../src/contexts.ts","../src/helpers.ts","../src/hooks.ts","../src/debounce.ts","../src/FormStore.ts"],"sourcesContent":["import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptor,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type React from 'react';\nimport { useContext, useMemo } from 'react';\nimport { FormContext, FormSystemContext } from './contexts.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport { resolveRenderer, useFieldFromContext } from './hooks.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult\n} from './types.js';\n\n// ─── FormSystemProvider ──────────────────────────────────────────────────────\n\nexport type FormSystemProviderProps = {\n /**\n * Renderer registry mapping schema types to renderer functions.\n * Example: `{ string: (props) => <input .../>, number: (props) => <input type=\"number\" .../> }`\n */\n renderers?: Record<string, FieldRenderer>;\n /**\n * Full configuration object (for future extensibility).\n * If both `renderers` and `config.renderers` are provided, `renderers` takes precedence.\n */\n config?: FormSystemConfig;\n children: React.ReactNode;\n};\n\n/**\n * Provides global renderer configuration via React Context.\n * Supports nesting — inner provider overrides outer.\n *\n * @example\n * ```tsx\n * <FormSystemProvider renderers={htmlRenderers}>\n * <App />\n * </FormSystemProvider>\n * ```\n */\nexport function FormSystemProvider({\n renderers,\n config,\n children\n}: FormSystemProviderProps): React.ReactNode {\n const parentConfig = useContext(FormSystemContext);\n\n const resolvedConfig: FormSystemConfig = useMemo(() => {\n const current: FormSystemConfig = {\n ...config,\n renderers: {\n ...config?.renderers,\n ...renderers\n }\n };\n if (!parentConfig) return current;\n return {\n ...parentConfig,\n ...current,\n renderers: {\n ...parentConfig.renderers,\n ...current.renderers\n }\n };\n }, [parentConfig, config, renderers]);\n\n return (\n <FormSystemContext.Provider value={resolvedConfig}>\n {children}\n </FormSystemContext.Provider>\n );\n}\n\n// ─── FormProvider ────────────────────────────────────────────────────────────\n\nexport type FormProviderProps<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n form: SchemaFormInstance<TSchema>;\n children: React.ReactNode;\n};\n\n/**\n * Provides form context to children so they can use the context-based useField.\n */\nexport function FormProvider<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>({ form, children }: FormProviderProps<TSchema>): React.ReactNode {\n const formContext = form._getFormContext();\n return (\n <FormContext.Provider value={formContext}>\n {children}\n </FormContext.Provider>\n );\n}\n\n// ─── Context-based useField ──────────────────────────────────────────────────\n\n/**\n * Context-based useField — can be used inside a FormProvider.\n * For best IntelliSense, prefer `form.useField()` which infers field types from the schema.\n * When using this context-based version, specify the schema type explicitly:\n *\n * @example\n * ```tsx\n * const name = useField<typeof MySchema>((t) => t.name);\n * ```\n */\nexport function useField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TPropertySchema extends SchemaBuilder<any, any, any> = SchemaBuilder<\n any,\n any,\n any\n >\n>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n): UseFieldResult<InferType<TPropertySchema>> {\n const formContext = useContext(FormContext);\n if (!formContext) {\n throw new Error(\n 'useField must be used within a FormProvider. ' +\n 'Wrap your component tree with <FormProvider form={form}>.'\n );\n }\n return useFieldFromContext(formContext, forProperty) as UseFieldResult<\n InferType<TPropertySchema>\n >;\n}\n\n// ─── useFormSystem ───────────────────────────────────────────────────────────\n\n/**\n * Hook to access FormSystem configuration.\n */\nexport function useFormSystem(): FormSystemConfig {\n const config = useContext(FormSystemContext);\n if (!config) {\n throw new Error(\n 'useFormSystem must be used within a FormSystemProvider'\n );\n }\n return config;\n}\n\n// ─── Field Component ─────────────────────────────────────────────────────────\n\nexport type FieldProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, any, any>;\n form: SchemaFormInstance<TSchema>;\n renderer?: FieldRenderer;\n /**\n * Rendering variant hint. Participates in renderer resolution:\n * the registry is checked for `\"type:variant\"` (e.g. `\"string:password\"`)\n * before falling back to the base `\"type\"` key.\n * Also forwarded to the renderer via `FieldRenderProps.variant`.\n */\n variant?: string;\n /** Visible label text forwarded to the renderer via `FieldRenderProps.label`. */\n label?: string;\n /** HTML `name` attribute forwarded to the renderer via `FieldRenderProps.name`. */\n name?: string;\n /**\n * Bag of extra renderer-specific props forwarded to the renderer via\n * `FieldRenderProps.fieldProps` (e.g. `placeholder`, `autoComplete`).\n */\n fieldProps?: Record<string, unknown>;\n};\n\n/**\n * UI-agnostic Field component.\n *\n * Resolves a renderer from:\n * 1. Explicit `renderer` prop\n * 2. FormSystemProvider registry — checked for `\"type:variant\"` first,\n * then for the base `\"type\"`\n *\n * All rendering hints (`variant`, `label`, `name`, `fieldProps`) are\n * forwarded to the resolved renderer via `FieldRenderProps`.\n *\n * @example\n * ```tsx\n * <Field forProperty={(t) => t.password} form={form}\n * variant=\"password\" label=\"Password\"\n * fieldProps={{ placeholder: \"Enter password\", autoComplete: \"current-password\" }} />\n * ```\n */\nexport function Field<TSchema extends ObjectSchemaBuilder<any, any, any>>({\n forProperty,\n form,\n renderer,\n variant,\n label,\n name,\n fieldProps\n}: FieldProps<TSchema>): React.ReactNode {\n const fieldResult = form.useField(forProperty);\n const systemConfig = useContext(FormSystemContext);\n\n const resolvedRenderer =\n renderer ?? resolveRenderer(systemConfig, fieldResult.schema, variant);\n\n if (!resolvedRenderer) {\n const schemaType = getSchemaType(fieldResult.schema);\n const tried = variant\n ? `\"${schemaType}:${variant}\" or \"${schemaType}\"`\n : `\"${schemaType}\"`;\n throw new Error(\n `No renderer found for schema type ${tried}. ` +\n 'Provide a renderer prop or configure one in FormSystemProvider.'\n );\n }\n\n return resolvedRenderer({\n value: fieldResult.value,\n initialValue: fieldResult.initialValue,\n dirty: fieldResult.dirty,\n touched: fieldResult.touched,\n error: fieldResult.error,\n validating: fieldResult.validating,\n onChange: fieldResult.onChange,\n onBlur: fieldResult.onBlur,\n setValue: fieldResult.setValue,\n schema: fieldResult.schema,\n variant,\n label,\n name,\n fieldProps\n });\n}\n","import type {\n ObjectSchemaBuilder,\n PropertyDescriptorInner,\n PropertyDescriptorTree\n} from '@cleverbrush/schema';\nimport { createContext } from 'react';\nimport type { FormStore } from './FormStore.js';\nimport type { FormSystemConfig, UseSchemaFormOptions } from './types.js';\n\n/**\n * Context for global form system configuration (renderers, etc).\n */\nexport const FormSystemContext = createContext<FormSystemConfig | null>(null);\n\n/**\n * Internal form context value — used to bridge useSchemaForm store\n * to the context-based useField and Field component.\n */\nexport type FormContextValue = {\n store: FormStore;\n descriptorTree: PropertyDescriptorTree<any, any>;\n schema: ObjectSchemaBuilder<any, any, any>;\n options: UseSchemaFormOptions;\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;\n};\n\n/**\n * Context for per-form data — allows useField to work via context.\n */\nexport const FormContext = createContext<FormContextValue | null>(null);\n","import type {\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\n\n/**\n * Builds a map from PropertyDescriptorInner identity → dot-separated path\n * by traversing the descriptor tree recursively.\n */\nexport function buildDescriptorPathMap(\n tree: PropertyDescriptorTree<any, any>,\n schema: ObjectSchemaBuilder<any, any, any>\n): Map<PropertyDescriptorInner<any, any, any>, string> {\n const map = new Map<PropertyDescriptorInner<any, any, any>, string>();\n const introspected = schema.introspect();\n if (!introspected.properties) return map;\n\n for (const propName of Object.keys(introspected.properties)) {\n const propDescriptor = (tree as any)[propName];\n if (\n !propDescriptor ||\n !(SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR in propDescriptor)\n ) {\n continue;\n }\n const inner = propDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ] as PropertyDescriptorInner<any, any, any>;\n map.set(inner, propName);\n\n // Recurse into nested objects\n const propSchema = introspected.properties[propName];\n if (propSchema instanceof ObjectSchemaBuilder) {\n const childMap = buildDescriptorPathMap(propDescriptor, propSchema);\n for (const [childInner, childPath] of childMap) {\n map.set(childInner, `${propName}.${childPath}`);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Looks up the path for a given descriptor from the pre-built map.\n */\nexport function getDescriptorPath(\n inner: PropertyDescriptorInner<any, any, any>,\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>\n): string {\n return pathMap.get(inner) ?? '';\n}\n\n/**\n * Returns the schema type string (e.g. \"string\", \"number\", \"object\").\n */\nexport function getSchemaType(schema: SchemaBuilder<any, any, any>): string {\n const introspected = schema.introspect();\n return introspected?.type ?? 'unknown';\n}\n\n/**\n * Builds a selector function from a dot-separated path string.\n * The selector traverses the PropertyDescriptorTree by property access.\n * Example: \"customer.address.city\" → (tree) => tree.customer.address.city\n */\nexport function buildSelectorFromPath(path: string): (tree: any) => any {\n const parts = path.split('.');\n return (tree: any) => {\n let current = tree;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n };\n}\n\n/**\n * Checks whether a validation error path matches a field's expected error path.\n * Matches exact path, path with nested suffix, or path with validator suffix.\n * Example: isErrorPathMatch(\"$.user.name\", \"$.user.name\") → true\n * isErrorPathMatch(\"$.user.name($validators[0])\", \"$.user.name\") → true\n */\nexport function isErrorPathMatch(\n errPath: string,\n fieldErrorPath: string\n): boolean {\n return (\n errPath === fieldErrorPath ||\n errPath.startsWith(fieldErrorPath + '.') ||\n errPath.startsWith(fieldErrorPath + '(')\n );\n}\n\n/**\n * Ensures all nested object structures exist in the values object\n * by traversing the schema and creating empty objects where needed.\n * This prevents ObjectSchemaBuilder.validate() from throwing when\n * nested object properties are undefined.\n *\n * Example: ensureNestedStructure({}, OrderSchema)\n * → { customer: { address: {} } }\n */\nexport function ensureNestedStructure(\n values: any,\n schema: ObjectSchemaBuilder<any, any, any>\n): any {\n const introspected = schema.introspect();\n if (!introspected.properties) return values != null ? values : {};\n\n const result = values != null ? { ...values } : {};\n for (const key of Object.keys(introspected.properties)) {\n const propSchema = introspected.properties[key];\n if (propSchema instanceof ObjectSchemaBuilder) {\n result[key] = ensureNestedStructure(result[key], propSchema);\n }\n }\n return result;\n}\n","import type {\n InferType,\n PropertyDescriptor,\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder,\n ValidationResult\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { FormContextValue } from './contexts.js';\nimport { debounce } from './debounce.js';\nimport type { FormStore } from './FormStore.js';\nimport { createFormStore } from './FormStore.js';\nimport {\n buildDescriptorPathMap,\n buildSelectorFromPath,\n ensureNestedStructure,\n getDescriptorPath,\n getSchemaType\n} from './helpers.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult,\n UseSchemaFormOptions\n} from './types.js';\n\n// ─── SchemaFormInstance ──────────────────────────────────────────────────────\n\n/**\n * Return type for useSchemaForm — fully typed for IntelliSense.\n * The `useField` method infers the field value type from the schema via PropertyDescriptor.\n */\nexport type SchemaFormInstance<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n useField: <TPropertySchema extends SchemaBuilder<any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ) => UseFieldResult<InferType<TPropertySchema>>;\n submit: () => Promise<ValidationResult<InferType<TSchema>>>;\n validate: () => Promise<ValidationResult<InferType<TSchema>>>;\n reset: (values?: Partial<InferType<TSchema>>) => void;\n getValue: () => InferType<TSchema>;\n setValue: (values: Partial<InferType<TSchema>>) => void;\n /** @internal — Used by FormProvider and Field to access internal context */\n _getFormContext: () => FormContextValue;\n};\n\n// ─── useSchemaForm ───────────────────────────────────────────────────────────\n\n/**\n * Hook that binds a schema to a form instance.\n * Provides field binding API, form-level validation, submit, reset.\n */\nexport function useSchemaForm<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>(\n schema: TSchema,\n options?: UseSchemaFormOptions\n): SchemaFormInstance<TSchema> {\n const resolvedOptions: UseSchemaFormOptions = {\n createMissingStructure: true,\n ...options\n };\n\n const storeRef = useRef<FormStore | null>(null);\n if (!storeRef.current) {\n storeRef.current = createFormStore({});\n }\n const store = storeRef.current;\n\n const descriptorTreeRef = useRef<PropertyDescriptorTree<\n TSchema,\n TSchema\n > | null>(null);\n if (!descriptorTreeRef.current) {\n descriptorTreeRef.current = ObjectSchemaBuilder.getPropertiesFor(\n schema\n ) as PropertyDescriptorTree<TSchema, TSchema>;\n }\n const descriptorTree = descriptorTreeRef.current;\n\n const pathMapRef = useRef<Map<\n PropertyDescriptorInner<any, any, any>,\n string\n > | null>(null);\n if (!pathMapRef.current) {\n pathMapRef.current = buildDescriptorPathMap(descriptorTree, schema);\n }\n const pathMap = pathMapRef.current;\n\n const schemaRef = useRef(schema);\n const optionsRef = useRef(resolvedOptions);\n optionsRef.current = resolvedOptions;\n\n const formContextValue = useMemo<FormContextValue>(\n () => ({\n store,\n descriptorTree,\n schema: schemaRef.current,\n options: optionsRef.current,\n pathMap\n }),\n [store, descriptorTree, pathMap]\n );\n\n const formContextRef = useRef(formContextValue);\n formContextRef.current = formContextValue;\n\n // Generation counter to discard stale validation results from concurrent runs\n const validationGenRef = useRef(0);\n\n /**\n * Runs full schema validation using getErrorsFor to extract per-field errors.\n * Optionally marks all fields as touched (used by submit/explicit validate).\n */\n const runValidation = useCallback(\n async (\n markTouched: boolean\n ): Promise<ValidationResult<InferType<TSchema>>> => {\n const gen = ++validationGenRef.current;\n const values = store.getValues();\n // Ensure all nested object structures exist to prevent\n // ObjectSchemaBuilder.validateAsync() from throwing on undefined nested objects\n const safeValues = ensureNestedStructure(values, schemaRef.current);\n let result: ValidationResult<InferType<TSchema>>;\n try {\n result = (await schemaRef.current.validateAsync(safeValues, {\n doNotStopOnFirstError: true\n })) as ValidationResult<InferType<TSchema>>;\n } catch {\n // If validation itself throws, treat as invalid\n return { valid: false } as ValidationResult<InferType<TSchema>>;\n }\n\n // Discard results if a newer validation has started since this one began\n if (gen !== validationGenRef.current) {\n return result as ValidationResult<InferType<TSchema>>;\n }\n\n // Clear all existing field errors\n const allPaths = store.getAllFieldPaths();\n for (const p of allPaths) {\n const patch: Partial<{\n error: string | undefined;\n touched: boolean;\n }> = { error: undefined };\n if (markTouched) {\n patch.touched = true;\n }\n store.updateFieldState(p, patch);\n }\n\n // Use getErrorsFor to extract per-field errors via tree selectors\n const resultWithErrors = result as ValidationResult<\n InferType<TSchema>\n > & {\n getErrorsFor?: (selector: (t: any) => any) => {\n errors: ReadonlyArray<string>;\n isValid: boolean;\n };\n errors?: ReadonlyArray<{ message: string; path?: string }>;\n };\n\n // Build a map of errors found via getErrorsFor so we can detect gaps\n const fieldsWithErrors = new Set<string>();\n\n if (typeof resultWithErrors.getErrorsFor === 'function') {\n const getErrorsFor = resultWithErrors.getErrorsFor;\n\n // Extract per-field errors by building selectors from field paths\n for (const [, path] of pathMap) {\n try {\n const selector = buildSelectorFromPath(path);\n const fieldResult = getErrorsFor(selector);\n if (\n fieldResult &&\n Array.isArray(fieldResult.errors) &&\n fieldResult.errors.length > 0\n ) {\n const errorMessage = fieldResult.errors[0];\n const patch: Partial<{\n error: string | undefined;\n touched: boolean;\n }> = { error: errorMessage };\n if (markTouched) {\n patch.touched = true;\n }\n store.updateFieldState(path, patch);\n fieldsWithErrors.add(path);\n }\n } catch {\n // If getErrorsFor fails for this path, skip\n }\n }\n }\n\n return result as ValidationResult<InferType<TSchema>>;\n },\n [store, pathMap]\n );\n\n const validate = useCallback(async (): Promise<\n ValidationResult<InferType<TSchema>>\n > => {\n return runValidation(true);\n }, [runValidation]);\n\n const submit = useCallback(async (): Promise<\n ValidationResult<InferType<TSchema>>\n > => {\n return validate();\n }, [validate]);\n\n const reset = useCallback(\n (values?: Partial<InferType<TSchema>>) => {\n const newValues = values ?? {};\n store.resetAll(newValues);\n },\n [store]\n );\n\n const getValue = useCallback((): InferType<TSchema> => {\n return store.getValues();\n }, [store]);\n\n const setValueFn = useCallback(\n (values: Partial<InferType<TSchema>>) => {\n const currentValues = store.getValues();\n const merged = { ...currentValues, ...values };\n store.setValues(merged);\n store.notifyAll();\n },\n [store]\n );\n\n const _getFormContext = useCallback(() => formContextRef.current, []);\n\n // Validate on mount when requested — runs once after first render\n const validateOnMountRef = useRef(resolvedOptions.validateOnMount);\n // biome-ignore lint/correctness/useExhaustiveDependencies: We only want to check validateOnMount on the initial mount, ignoring changes to it after that\n useEffect(() => {\n if (validateOnMountRef.current) {\n runValidation(true);\n }\n }, []);\n\n // Create a debounced version of runValidation for onChange triggers.\n // validate(), submit(), and validateOnMount always use runValidation directly.\n const debouncedValidationRef = useRef<\n ((markTouched: boolean) => void) | null\n >(null);\n if (\n resolvedOptions.validationDebounceMs != null &&\n resolvedOptions.validationDebounceMs > 0 &&\n !debouncedValidationRef.current\n ) {\n debouncedValidationRef.current = debounce((markTouched: boolean) => {\n runValidation(markTouched);\n }, resolvedOptions.validationDebounceMs);\n }\n\n const triggerValidation = useCallback(\n (markTouched: boolean) => {\n if (debouncedValidationRef.current) {\n debouncedValidationRef.current(markTouched);\n return Promise.resolve(\n undefined as unknown as ValidationResult<InferType<TSchema>>\n );\n }\n return runValidation(markTouched);\n },\n [runValidation]\n );\n\n const useFieldHook = useCallback(\n <TPropertySchema extends SchemaBuilder<any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ): UseFieldResult<InferType<TPropertySchema>> => {\n return useFieldFromContext(\n formContextRef.current,\n forProperty,\n triggerValidation\n ) as UseFieldResult<InferType<TPropertySchema>>;\n },\n [triggerValidation]\n );\n\n return useMemo(\n () => ({\n useField: useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue: setValueFn,\n _getFormContext\n }),\n [\n useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValueFn,\n _getFormContext\n ]\n );\n}\n\n// ─── useField (from context) ─────────────────────────────────────────────────\n\n/**\n * Internal useField implementation, requires FormContextValue.\n * Returns UseFieldResult with untyped values — callers should cast to the proper generic type.\n */\nexport function useFieldFromContext(\n formContext: FormContextValue,\n forProperty: (tree: any) => any,\n triggerValidation?: (markTouched: boolean) => Promise<any>\n): UseFieldResult {\n const { store, descriptorTree, options, pathMap } = formContext;\n\n const descriptor = forProperty(descriptorTree as any);\n const inner = descriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR];\n const path = getDescriptorPath(inner, pathMap);\n const fieldSchema = inner.getSchema();\n\n // Initialize field state from current values on first access\n const initializedRef = useRef<string | null>(null);\n\n if (initializedRef.current !== path) {\n const values = store.getValues();\n const { success, value } = inner.getValue(values);\n const currentState = store.getFieldState(path);\n if (currentState.initialValue === undefined && !currentState.touched) {\n const resolvedValue = success ? value : undefined;\n store.updateFieldState(path, {\n value: resolvedValue,\n initialValue: resolvedValue,\n dirty: false\n });\n }\n initializedRef.current = path;\n }\n\n const [, setRenderTick] = useState(0);\n\n // Subscribe to field changes with proper cleanup on unmount/path change\n useEffect(() => {\n const unsub = store.subscribe(path, () => {\n setRenderTick(c => c + 1);\n });\n return unsub;\n }, [store, path]);\n\n const onChange = useCallback(\n (value: any) => {\n const values = store.getValues();\n inner.setValue(values, value, {\n createMissingStructure: options.createMissingStructure !== false\n });\n store.setValues(values);\n const currentState = store.getFieldState(path);\n store.updateFieldState(path, {\n value,\n dirty: value !== currentState.initialValue\n });\n // Run validation on every field change (without marking all fields touched)\n if (triggerValidation) {\n triggerValidation(false);\n }\n },\n [store, inner, path, options, triggerValidation]\n );\n\n const onBlur = useCallback(() => {\n store.updateFieldState(path, { touched: true });\n }, [store, path]);\n\n const setValue = useCallback(\n (value: any) => {\n onChange(value);\n },\n [onChange]\n );\n\n const fieldState = store.getFieldState(path);\n\n return {\n value: fieldState.value,\n initialValue: fieldState.initialValue,\n dirty: fieldState.dirty,\n touched: fieldState.touched,\n error: fieldState.error,\n validating: fieldState.validating,\n onChange,\n onBlur,\n setValue,\n schema: fieldSchema\n };\n}\n\n/**\n * Resolves a renderer from the FormSystem config based on schema type and optional variant.\n *\n * When `variant` is provided the registry is checked for `\"type:variant\"` first\n * (e.g. `\"string:password\"`). If no match is found it falls back to the base\n * `\"type\"` key (e.g. `\"string\"`).\n */\nexport function resolveRenderer(\n config: FormSystemConfig | null,\n schema: SchemaBuilder<any, any, any>,\n variant?: string\n): FieldRenderer | undefined {\n if (!config?.renderers) return undefined;\n const type = getSchemaType(schema);\n if (variant) {\n const variantRenderer = config.renderers[`${type}:${variant}`];\n if (variantRenderer) return variantRenderer;\n }\n return config.renderers[type];\n}\n","/**\n * A debounce function ensures that a function is not called too frequently.\n * It only allows the function to be executed after a specified delay has passed since the last call.\n * @param func The function to debounce.\n * @param wait The delay in ms before the function is called.\n */\nexport function debounce<T extends (...args: any[]) => void>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return (...args: Parameters<T>) => {\n if (timeout !== null) {\n clearTimeout(timeout);\n }\n timeout = setTimeout(() => {\n func(...args);\n }, wait);\n };\n}\n","import type { FieldState } from './types.js';\n\n/**\n * Internal form store — manages field state and notification.\n * Does not depend on React; used by hooks for state management.\n */\nexport function createFormStore(initialValues: any) {\n let values: any = initialValues != null ? { ...initialValues } : {};\n const fieldStates = new Map<string, FieldState>();\n const listeners = new Map<string, Set<() => void>>();\n const globalListeners = new Set<() => void>();\n\n function ensureFieldState(path: string): FieldState {\n if (!fieldStates.has(path)) {\n fieldStates.set(path, {\n value: undefined,\n initialValue: undefined,\n dirty: false,\n touched: false,\n error: undefined,\n validating: false\n });\n }\n return fieldStates.get(path)!;\n }\n\n function getFieldState(path: string): FieldState {\n return ensureFieldState(path);\n }\n\n function updateFieldState(path: string, patch: Partial<FieldState>) {\n const current = ensureFieldState(path);\n const updated = { ...current, ...patch };\n fieldStates.set(path, updated);\n notifyPath(path);\n }\n\n function notifyPath(path: string) {\n const pathListeners = listeners.get(path);\n if (pathListeners) {\n for (const listener of pathListeners) {\n listener();\n }\n }\n }\n\n function notifyAll() {\n for (const [, pathListeners] of listeners) {\n for (const listener of pathListeners) {\n listener();\n }\n }\n for (const listener of globalListeners) {\n listener();\n }\n }\n\n function subscribe(path: string, listener: () => void): () => void {\n if (!listeners.has(path)) {\n listeners.set(path, new Set());\n }\n listeners.get(path)!.add(listener);\n return () => {\n listeners.get(path)?.delete(listener);\n };\n }\n\n function subscribeGlobal(listener: () => void): () => void {\n globalListeners.add(listener);\n return () => {\n globalListeners.delete(listener);\n };\n }\n\n function getValues(): any {\n return values;\n }\n\n function setValues(newValues: any) {\n values = newValues != null ? { ...newValues } : {};\n }\n\n function resetAll(newInitialValues?: any) {\n values = newInitialValues != null ? { ...newInitialValues } : {};\n fieldStates.clear();\n notifyAll();\n }\n\n function getAllFieldPaths(): string[] {\n return Array.from(fieldStates.keys());\n }\n\n return {\n getFieldState,\n updateFieldState,\n subscribe,\n subscribeGlobal,\n getValues,\n setValues,\n resetAll,\n notifyAll,\n getAllFieldPaths\n };\n}\n\nexport type FormStore = ReturnType<typeof createFormStore>;\n"],"mappings":"AAQA,OAAS,cAAAA,EAAY,WAAAC,OAAe,QCHpC,OAAS,iBAAAC,MAAqB,QAOvB,IAAMC,EAAoBD,EAAuC,IAAI,EAiB/DE,EAAcF,EAAuC,IAAI,ECxBtE,OACI,uBAAAG,EACA,qCAAAC,MACG,sBAMA,SAASC,EACZC,EACAC,EACmD,CACnD,IAAMC,EAAM,IAAI,IACVC,EAAeF,EAAO,WAAW,EACvC,GAAI,CAACE,EAAa,WAAY,OAAOD,EAErC,QAAWE,KAAY,OAAO,KAAKD,EAAa,UAAU,EAAG,CACzD,IAAME,EAAkBL,EAAaI,CAAQ,EAC7C,GACI,CAACC,GACD,EAAEP,KAAqCO,GAEvC,SAEJ,IAAMC,EAAQD,EACVP,CACJ,EACAI,EAAI,IAAII,EAAOF,CAAQ,EAGvB,IAAMG,EAAaJ,EAAa,WAAWC,CAAQ,EACnD,GAAIG,aAAsBV,EAAqB,CAC3C,IAAMW,EAAWT,EAAuBM,EAAgBE,CAAU,EAClE,OAAW,CAACE,EAAYC,CAAS,IAAKF,EAClCN,EAAI,IAAIO,EAAY,GAAGL,CAAQ,IAAIM,CAAS,EAAE,CAEtD,CACJ,CAEA,OAAOR,CACX,CAKO,SAASS,EACZL,EACAM,EACM,CACN,OAAOA,EAAQ,IAAIN,CAAK,GAAK,EACjC,CAKO,SAASO,EAAcZ,EAA8C,CAExE,OADqBA,EAAO,WAAW,GAClB,MAAQ,SACjC,CAOO,SAASa,EAAsBC,EAAkC,CACpE,IAAMC,EAAQD,EAAK,MAAM,GAAG,EAC5B,OAAQf,GAAc,CAClB,IAAIiB,EAAUjB,EACd,QAAWkB,KAAQF,EAAO,CACtB,GAAIC,GAAW,KAAM,OACrBA,EAAUA,EAAQC,CAAI,CAC1B,CACA,OAAOD,CACX,CACJ,CA4BO,SAASE,EACZC,EACAC,EACG,CACH,IAAMC,EAAeD,EAAO,WAAW,EACvC,GAAI,CAACC,EAAa,WAAY,OAAOF,GAA0B,CAAC,EAEhE,IAAMG,EAASH,GAAU,KAAO,CAAE,GAAGA,CAAO,EAAI,CAAC,EACjD,QAAWI,KAAO,OAAO,KAAKF,EAAa,UAAU,EAAG,CACpD,IAAMG,EAAaH,EAAa,WAAWE,CAAG,EAC1CC,aAAsBC,IACtBH,EAAOC,CAAG,EAAIL,EAAsBI,EAAOC,CAAG,EAAGC,CAAU,EAEnE,CACA,OAAOF,CACX,CCpHA,OACI,uBAAAI,GACA,qCAAAC,OACG,sBACP,OAAS,eAAAC,EAAa,aAAAC,EAAW,WAAAC,EAAS,UAAAC,EAAQ,YAAAC,OAAgB,QCN3D,SAASC,EACZC,EACAC,EACgC,CAChC,IAAIC,EAAgD,KAEpD,MAAO,IAAIC,IAAwB,CAC3BD,IAAY,MACZ,aAAaA,CAAO,EAExBA,EAAU,WAAW,IAAM,CACvBF,EAAK,GAAGG,CAAI,CAChB,EAAGF,CAAI,CACX,CACJ,CCdO,SAASG,EAAgBC,EAAoB,CAChD,IAAIC,EAAcD,GAAiB,KAAO,CAAE,GAAGA,CAAc,EAAI,CAAC,EAC5DE,EAAc,IAAI,IAClBC,EAAY,IAAI,IAChBC,EAAkB,IAAI,IAE5B,SAASC,EAAiBC,EAA0B,CAChD,OAAKJ,EAAY,IAAII,CAAI,GACrBJ,EAAY,IAAII,EAAM,CAClB,MAAO,OACP,aAAc,OACd,MAAO,GACP,QAAS,GACT,MAAO,OACP,WAAY,EAChB,CAAC,EAEEJ,EAAY,IAAII,CAAI,CAC/B,CAEA,SAASC,EAAcD,EAA0B,CAC7C,OAAOD,EAAiBC,CAAI,CAChC,CAEA,SAASE,EAAiBF,EAAcG,EAA4B,CAEhE,IAAMC,EAAU,CAAE,GADFL,EAAiBC,CAAI,EACP,GAAGG,CAAM,EACvCP,EAAY,IAAII,EAAMI,CAAO,EAC7BC,EAAWL,CAAI,CACnB,CAEA,SAASK,EAAWL,EAAc,CAC9B,IAAMM,EAAgBT,EAAU,IAAIG,CAAI,EACxC,GAAIM,EACA,QAAWC,KAAYD,EACnBC,EAAS,CAGrB,CAEA,SAASC,GAAY,CACjB,OAAW,CAAC,CAAEF,CAAa,IAAKT,EAC5B,QAAWU,KAAYD,EACnBC,EAAS,EAGjB,QAAWA,KAAYT,EACnBS,EAAS,CAEjB,CAEA,SAASE,EAAUT,EAAcO,EAAkC,CAC/D,OAAKV,EAAU,IAAIG,CAAI,GACnBH,EAAU,IAAIG,EAAM,IAAI,GAAK,EAEjCH,EAAU,IAAIG,CAAI,EAAG,IAAIO,CAAQ,EAC1B,IAAM,CACTV,EAAU,IAAIG,CAAI,GAAG,OAAOO,CAAQ,CACxC,CACJ,CAEA,SAASG,EAAgBH,EAAkC,CACvD,OAAAT,EAAgB,IAAIS,CAAQ,EACrB,IAAM,CACTT,EAAgB,OAAOS,CAAQ,CACnC,CACJ,CAEA,SAASI,GAAiB,CACtB,OAAOhB,CACX,CAEA,SAASiB,EAAUC,EAAgB,CAC/BlB,EAASkB,GAAa,KAAO,CAAE,GAAGA,CAAU,EAAI,CAAC,CACrD,CAEA,SAASC,EAASC,EAAwB,CACtCpB,EAASoB,GAAoB,KAAO,CAAE,GAAGA,CAAiB,EAAI,CAAC,EAC/DnB,EAAY,MAAM,EAClBY,EAAU,CACd,CAEA,SAASQ,GAA6B,CAClC,OAAO,MAAM,KAAKpB,EAAY,KAAK,CAAC,CACxC,CAEA,MAAO,CACH,cAAAK,EACA,iBAAAC,EACA,UAAAO,EACA,gBAAAC,EACA,UAAAC,EACA,UAAAC,EACA,SAAAE,EACA,UAAAN,EACA,iBAAAQ,CACJ,CACJ,CF3CO,SAASC,GAGZC,EACAC,EAC2B,CAC3B,IAAMC,EAAwC,CAC1C,uBAAwB,GACxB,GAAGD,CACP,EAEME,EAAWC,EAAyB,IAAI,EACzCD,EAAS,UACVA,EAAS,QAAUE,EAAgB,CAAC,CAAC,GAEzC,IAAMC,EAAQH,EAAS,QAEjBI,EAAoBH,EAGhB,IAAI,EACTG,EAAkB,UACnBA,EAAkB,QAAUC,GAAoB,iBAC5CR,CACJ,GAEJ,IAAMS,EAAiBF,EAAkB,QAEnCG,EAAaN,EAGT,IAAI,EACTM,EAAW,UACZA,EAAW,QAAUC,EAAuBF,EAAgBT,CAAM,GAEtE,IAAMY,EAAUF,EAAW,QAErBG,EAAYT,EAAOJ,CAAM,EACzBc,EAAaV,EAAOF,CAAe,EACzCY,EAAW,QAAUZ,EAErB,IAAMa,EAAmBC,EACrB,KAAO,CACH,MAAAV,EACA,eAAAG,EACA,OAAQI,EAAU,QAClB,QAASC,EAAW,QACpB,QAAAF,CACJ,GACA,CAACN,EAAOG,EAAgBG,CAAO,CACnC,EAEMK,EAAiBb,EAAOW,CAAgB,EAC9CE,EAAe,QAAUF,EAGzB,IAAMG,EAAmBd,EAAO,CAAC,EAM3Be,EAAgBC,EAClB,MACIC,GACgD,CAChD,IAAMC,EAAM,EAAEJ,EAAiB,QACzBK,EAASjB,EAAM,UAAU,EAGzBkB,GAAaC,EAAsBF,EAAQV,EAAU,OAAO,EAC9Da,EACJ,GAAI,CACAA,EAAU,MAAMb,EAAU,QAAQ,cAAcW,GAAY,CACxD,sBAAuB,EAC3B,CAAC,CACL,MAAQ,CAEJ,MAAO,CAAE,MAAO,EAAM,CAC1B,CAGA,GAAIF,IAAQJ,EAAiB,QACzB,OAAOQ,EAIX,IAAMC,GAAWrB,EAAM,iBAAiB,EACxC,QAAWsB,KAAKD,GAAU,CACtB,IAAME,EAGD,CAAE,MAAO,MAAU,EACpBR,IACAQ,EAAM,QAAU,IAEpBvB,EAAM,iBAAiBsB,EAAGC,CAAK,CACnC,CAGA,IAAMC,EAAmBJ,EAWnBK,GAAmB,IAAI,IAE7B,GAAI,OAAOD,EAAiB,cAAiB,WAAY,CACrD,IAAME,EAAeF,EAAiB,aAGtC,OAAW,CAAC,CAAEG,CAAI,IAAKrB,EACnB,GAAI,CACA,IAAMsB,GAAWC,EAAsBF,CAAI,EACrCG,EAAcJ,EAAaE,EAAQ,EACzC,GACIE,GACA,MAAM,QAAQA,EAAY,MAAM,GAChCA,EAAY,OAAO,OAAS,EAC9B,CAEE,IAAMP,EAGD,CAAE,MAJcO,EAAY,OAAO,CAAC,CAId,EACvBf,IACAQ,EAAM,QAAU,IAEpBvB,EAAM,iBAAiB2B,EAAMJ,CAAK,EAClCE,GAAiB,IAAIE,CAAI,CAC7B,CACJ,MAAQ,CAER,CAER,CAEA,OAAOP,CACX,EACA,CAACpB,EAAOM,CAAO,CACnB,EAEMyB,EAAWjB,EAAY,SAGlBD,EAAc,EAAI,EAC1B,CAACA,CAAa,CAAC,EAEZmB,EAASlB,EAAY,SAGhBiB,EAAS,EACjB,CAACA,CAAQ,CAAC,EAEPE,EAAQnB,EACTG,GAAyC,CACtC,IAAMiB,EAAYjB,GAAU,CAAC,EAC7BjB,EAAM,SAASkC,CAAS,CAC5B,EACA,CAAClC,CAAK,CACV,EAEMmC,EAAWrB,EAAY,IAClBd,EAAM,UAAU,EACxB,CAACA,CAAK,CAAC,EAEJoC,EAAatB,EACdG,GAAwC,CAErC,IAAMoB,EAAS,CAAE,GADKrC,EAAM,UAAU,EACH,GAAGiB,CAAO,EAC7CjB,EAAM,UAAUqC,CAAM,EACtBrC,EAAM,UAAU,CACpB,EACA,CAACA,CAAK,CACV,EAEMsC,EAAkBxB,EAAY,IAAMH,EAAe,QAAS,CAAC,CAAC,EAG9D4B,EAAqBzC,EAAOF,EAAgB,eAAe,EAEjE4C,EAAU,IAAM,CACRD,EAAmB,SACnB1B,EAAc,EAAI,CAE1B,EAAG,CAAC,CAAC,EAIL,IAAM4B,EAAyB3C,EAE7B,IAAI,EAEFF,EAAgB,sBAAwB,MACxCA,EAAgB,qBAAuB,GACvC,CAAC6C,EAAuB,UAExBA,EAAuB,QAAUC,EAAU3B,GAAyB,CAChEF,EAAcE,CAAW,CAC7B,EAAGnB,EAAgB,oBAAoB,GAG3C,IAAM+C,EAAoB7B,EACrBC,GACO0B,EAAuB,SACvBA,EAAuB,QAAQ1B,CAAW,EACnC,QAAQ,QACX,MACJ,GAEGF,EAAcE,CAAW,EAEpC,CAACF,CAAa,CAClB,EAEM+B,EAAe9B,EAEb+B,GAIOC,EACHnC,EAAe,QACfkC,EACAF,CACJ,EAEJ,CAACA,CAAiB,CACtB,EAEA,OAAOjC,EACH,KAAO,CACH,SAAUkC,EACV,OAAAZ,EACA,SAAAD,EACA,MAAAE,EACA,SAAAE,EACA,SAAUC,EACV,gBAAAE,CACJ,GACA,CACIM,EACAZ,EACAD,EACAE,EACAE,EACAC,EACAE,CACJ,CACJ,CACJ,CAQO,SAASQ,EACZC,EACAF,EACAF,EACc,CACd,GAAM,CAAE,MAAA3C,EAAO,eAAAG,EAAgB,QAAAR,EAAS,QAAAW,CAAQ,EAAIyC,EAG9CC,EADaH,EAAY1C,CAAqB,EAC3B8C,EAAiC,EACpDtB,EAAOuB,EAAkBF,EAAO1C,CAAO,EACvC6C,EAAcH,EAAM,UAAU,EAG9BI,EAAiBtD,EAAsB,IAAI,EAEjD,GAAIsD,EAAe,UAAYzB,EAAM,CACjC,IAAMV,EAASjB,EAAM,UAAU,EACzB,CAAE,QAAAqD,EAAS,MAAAC,CAAM,EAAIN,EAAM,SAAS/B,CAAM,EAC1CsC,EAAevD,EAAM,cAAc2B,CAAI,EAC7C,GAAI4B,EAAa,eAAiB,QAAa,CAACA,EAAa,QAAS,CAClE,IAAMC,EAAgBH,EAAUC,EAAQ,OACxCtD,EAAM,iBAAiB2B,EAAM,CACzB,MAAO6B,EACP,aAAcA,EACd,MAAO,EACX,CAAC,CACL,CACAJ,EAAe,QAAUzB,CAC7B,CAEA,GAAM,CAAC,CAAE8B,CAAa,EAAIC,GAAS,CAAC,EAGpClB,EAAU,IACQxC,EAAM,UAAU2B,EAAM,IAAM,CACtC8B,EAAcE,GAAKA,EAAI,CAAC,CAC5B,CAAC,EAEF,CAAC3D,EAAO2B,CAAI,CAAC,EAEhB,IAAMiC,EAAW9C,EACZwC,GAAe,CACZ,IAAMrC,EAASjB,EAAM,UAAU,EAC/BgD,EAAM,SAAS/B,EAAQqC,EAAO,CAC1B,uBAAwB3D,EAAQ,yBAA2B,EAC/D,CAAC,EACDK,EAAM,UAAUiB,CAAM,EACtB,IAAMsC,EAAevD,EAAM,cAAc2B,CAAI,EAC7C3B,EAAM,iBAAiB2B,EAAM,CACzB,MAAA2B,EACA,MAAOA,IAAUC,EAAa,YAClC,CAAC,EAEGZ,GACAA,EAAkB,EAAK,CAE/B,EACA,CAAC3C,EAAOgD,EAAOrB,EAAMhC,EAASgD,CAAiB,CACnD,EAEMkB,EAAS/C,EAAY,IAAM,CAC7Bd,EAAM,iBAAiB2B,EAAM,CAAE,QAAS,EAAK,CAAC,CAClD,EAAG,CAAC3B,EAAO2B,CAAI,CAAC,EAEVmC,EAAWhD,EACZwC,GAAe,CACZM,EAASN,CAAK,CAClB,EACA,CAACM,CAAQ,CACb,EAEMG,EAAa/D,EAAM,cAAc2B,CAAI,EAE3C,MAAO,CACH,MAAOoC,EAAW,MAClB,aAAcA,EAAW,aACzB,MAAOA,EAAW,MAClB,QAASA,EAAW,QACpB,MAAOA,EAAW,MAClB,WAAYA,EAAW,WACvB,SAAAH,EACA,OAAAC,EACA,SAAAC,EACA,OAAQX,CACZ,CACJ,CASO,SAASa,EACZC,EACAvE,EACAwE,EACyB,CACzB,GAAI,CAACD,GAAQ,UAAW,OACxB,IAAME,EAAOC,EAAc1E,CAAM,EACjC,GAAIwE,EAAS,CACT,IAAMG,EAAkBJ,EAAO,UAAU,GAAGE,CAAI,IAAID,CAAO,EAAE,EAC7D,GAAIG,EAAiB,OAAOA,CAChC,CACA,OAAOJ,EAAO,UAAUE,CAAI,CAChC,CHrWQ,cAAAG,MAAA,oBA3BD,SAASC,GAAmB,CAC/B,UAAAC,EACA,OAAAC,EACA,SAAAC,CACJ,EAA6C,CACzC,IAAMC,EAAeC,EAAWC,CAAiB,EAE3CC,EAAmCC,GAAQ,IAAM,CACnD,IAAMC,EAA4B,CAC9B,GAAGP,EACH,UAAW,CACP,GAAGA,GAAQ,UACX,GAAGD,CACP,CACJ,EACA,OAAKG,EACE,CACH,GAAGA,EACH,GAAGK,EACH,UAAW,CACP,GAAGL,EAAa,UAChB,GAAGK,EAAQ,SACf,CACJ,EAR0BA,CAS9B,EAAG,CAACL,EAAcF,EAAQD,CAAS,CAAC,EAEpC,OACIF,EAACO,EAAkB,SAAlB,CAA2B,MAAOC,EAC9B,SAAAJ,EACL,CAER,CAcO,SAASO,GAEd,CAAE,KAAAC,EAAM,SAAAR,CAAS,EAAgD,CAC/D,IAAMS,EAAcD,EAAK,gBAAgB,EACzC,OACIZ,EAACc,EAAY,SAAZ,CAAqB,MAAOD,EACxB,SAAAT,EACL,CAER,CAcO,SAASW,GAQZC,EAG0C,CAC1C,IAAMH,EAAcP,EAAWQ,CAAW,EAC1C,GAAI,CAACD,EACD,MAAM,IAAI,MACN,wGAEJ,EAEJ,OAAOI,EAAoBJ,EAAaG,CAAW,CAGvD,CAOO,SAASE,IAAkC,CAC9C,IAAMf,EAASG,EAAWC,CAAiB,EAC3C,GAAI,CAACJ,EACD,MAAM,IAAI,MACN,wDACJ,EAEJ,OAAOA,CACX,CA8CO,SAASgB,GAA0D,CACtE,YAAAH,EACA,KAAAJ,EACA,SAAAQ,EACA,QAAAC,EACA,MAAAC,EACA,KAAAC,EACA,WAAAC,CACJ,EAAyC,CACrC,IAAMC,EAAcb,EAAK,SAASI,CAAW,EACvCU,EAAepB,EAAWC,CAAiB,EAE3CoB,EACFP,GAAYQ,EAAgBF,EAAcD,EAAY,OAAQJ,CAAO,EAEzE,GAAI,CAACM,EAAkB,CACnB,IAAME,EAAaC,EAAcL,EAAY,MAAM,EAC7CM,EAAQV,EACR,IAAIQ,CAAU,IAAIR,CAAO,SAASQ,CAAU,IAC5C,IAAIA,CAAU,IACpB,MAAM,IAAI,MACN,qCAAqCE,CAAK,mEAE9C,CACJ,CAEA,OAAOJ,EAAiB,CACpB,MAAOF,EAAY,MACnB,aAAcA,EAAY,aAC1B,MAAOA,EAAY,MACnB,QAASA,EAAY,QACrB,MAAOA,EAAY,MACnB,WAAYA,EAAY,WACxB,SAAUA,EAAY,SACtB,OAAQA,EAAY,OACpB,SAAUA,EAAY,SACtB,OAAQA,EAAY,OACpB,QAAAJ,EACA,MAAAC,EACA,KAAAC,EACA,WAAAC,CACJ,CAAC,CACL","names":["useContext","useMemo","createContext","FormSystemContext","FormContext","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","buildDescriptorPathMap","tree","schema","map","introspected","propName","propDescriptor","inner","propSchema","childMap","childInner","childPath","getDescriptorPath","pathMap","getSchemaType","buildSelectorFromPath","path","parts","current","part","ensureNestedStructure","values","schema","introspected","result","key","propSchema","ObjectSchemaBuilder","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","useCallback","useEffect","useMemo","useRef","useState","debounce","func","wait","timeout","args","createFormStore","initialValues","values","fieldStates","listeners","globalListeners","ensureFieldState","path","getFieldState","updateFieldState","patch","updated","notifyPath","pathListeners","listener","notifyAll","subscribe","subscribeGlobal","getValues","setValues","newValues","resetAll","newInitialValues","getAllFieldPaths","useSchemaForm","schema","options","resolvedOptions","storeRef","useRef","createFormStore","store","descriptorTreeRef","ObjectSchemaBuilder","descriptorTree","pathMapRef","buildDescriptorPathMap","pathMap","schemaRef","optionsRef","formContextValue","useMemo","formContextRef","validationGenRef","runValidation","useCallback","markTouched","gen","values","safeValues","ensureNestedStructure","result","allPaths","p","patch","resultWithErrors","fieldsWithErrors","getErrorsFor","path","selector","buildSelectorFromPath","fieldResult","validate","submit","reset","newValues","getValue","setValueFn","merged","_getFormContext","validateOnMountRef","useEffect","debouncedValidationRef","debounce","triggerValidation","useFieldHook","forProperty","useFieldFromContext","formContext","inner","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","getDescriptorPath","fieldSchema","initializedRef","success","value","currentState","resolvedValue","setRenderTick","useState","c","onChange","onBlur","setValue","fieldState","resolveRenderer","config","variant","type","getSchemaType","variantRenderer","jsx","FormSystemProvider","renderers","config","children","parentConfig","useContext","FormSystemContext","resolvedConfig","useMemo","current","FormProvider","form","formContext","FormContext","useField","forProperty","useFieldFromContext","useFormSystem","Field","renderer","variant","label","name","fieldProps","fieldResult","systemConfig","resolvedRenderer","resolveRenderer","schemaType","getSchemaType","tried"]}
1
+ {"version":3,"sources":["../src/components.tsx","../src/contexts.ts","../src/helpers.ts","../src/hooks.ts","../src/FormStore.ts","../src/system.tsx"],"sourcesContent":["import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptor,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type React from 'react';\nimport { useContext, useMemo } from 'react';\nimport { FormContext, FormSystemContext } from './contexts.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport { resolveRenderer, useFieldFromContext } from './hooks.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult\n} from './types.js';\n\n// ─── FormSystemProvider ──────────────────────────────────────────────────────\n\nexport type FormSystemProviderProps = {\n /**\n * Renderer registry mapping schema types to renderer functions.\n * Example: `{ string: (props) => <input .../>, number: (props) => <input type=\"number\" .../> }`\n */\n renderers?: Record<string, FieldRenderer>;\n /**\n * Full configuration object (for future extensibility).\n * If both `renderers` and `config.renderers` are provided, `renderers` takes precedence.\n */\n config?: FormSystemConfig;\n children: React.ReactNode;\n};\n\n/**\n * Provides global renderer configuration via React Context.\n * Supports nesting — inner provider overrides outer.\n *\n * @example\n * ```tsx\n * <FormSystemProvider renderers={htmlRenderers}>\n * <App />\n * </FormSystemProvider>\n * ```\n */\nexport function FormSystemProvider({\n renderers,\n config,\n children\n}: FormSystemProviderProps): React.ReactNode {\n const parentConfig = useContext(FormSystemContext);\n\n const resolvedConfig: FormSystemConfig = useMemo(() => {\n const current: FormSystemConfig = {\n ...config,\n renderers: {\n ...config?.renderers,\n ...renderers\n }\n };\n if (!parentConfig) return current;\n return {\n ...parentConfig,\n ...current,\n renderers: {\n ...parentConfig.renderers,\n ...current.renderers\n }\n };\n }, [parentConfig, config, renderers]);\n\n return (\n <FormSystemContext.Provider value={resolvedConfig}>\n {children}\n </FormSystemContext.Provider>\n );\n}\n\n// ─── FormProvider ────────────────────────────────────────────────────────────\n\nexport type FormProviderProps<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n form: SchemaFormInstance<TSchema>;\n children: React.ReactNode;\n};\n\n/**\n * Provides form context to children so they can use the context-based useField.\n */\nexport function FormProvider<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>({ form, children }: FormProviderProps<TSchema>): React.ReactNode {\n const formContext = form._getFormContext();\n return (\n <FormContext.Provider value={formContext}>\n {children}\n </FormContext.Provider>\n );\n}\n\n// ─── Context-based useField ──────────────────────────────────────────────────\n\n/**\n * Context-based useField — can be used inside a FormProvider.\n * For best IntelliSense, prefer `form.useField()` which infers field types from the schema.\n * When using this context-based version, specify the schema type explicitly:\n *\n * @example\n * ```tsx\n * const name = useField<typeof MySchema>((t) => t.name);\n * ```\n */\nexport function useField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TPropertySchema extends SchemaBuilder<any, any, any, any> = any\n>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n): UseFieldResult<InferType<TPropertySchema>> {\n const formContext = useContext(FormContext);\n if (!formContext) {\n throw new Error(\n 'useField must be used within a FormProvider. ' +\n 'Wrap your component tree with <FormProvider form={form}>.'\n );\n }\n return useFieldFromContext(formContext, forProperty) as UseFieldResult<\n InferType<TPropertySchema>\n >;\n}\n\n// ─── useFormSystem ───────────────────────────────────────────────────────────\n\n/**\n * Hook to access FormSystem configuration.\n */\nexport function useFormSystem(): FormSystemConfig {\n const config = useContext(FormSystemContext);\n if (!config) {\n throw new Error(\n 'useFormSystem must be used within a FormSystemProvider'\n );\n }\n return config;\n}\n\n// ─── Field Component ─────────────────────────────────────────────────────────\n\nexport type FieldProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, any, any>;\n form: SchemaFormInstance<TSchema>;\n renderer?: FieldRenderer;\n /**\n * Rendering variant hint. Participates in renderer resolution:\n * the registry is checked for `\"type:variant\"` (e.g. `\"string:password\"`)\n * before falling back to the base `\"type\"` key.\n * Also forwarded to the renderer via `FieldRenderProps.variant`.\n */\n variant?: string;\n /** Visible label text forwarded to the renderer via `FieldRenderProps.label`. */\n label?: string;\n /** HTML `name` attribute forwarded to the renderer via `FieldRenderProps.name`. */\n name?: string;\n /**\n * Bag of extra renderer-specific props forwarded to the renderer via\n * `FieldRenderProps.fieldProps` (e.g. `placeholder`, `autoComplete`).\n */\n fieldProps?: Record<string, unknown>;\n};\n\n/**\n * UI-agnostic Field component.\n *\n * Resolves a renderer from:\n * 1. Explicit `renderer` prop\n * 2. FormSystemProvider registry — checked for `\"type:variant\"` first,\n * then for the base `\"type\"`\n *\n * All rendering hints (`variant`, `label`, `name`, `fieldProps`) are\n * forwarded to the resolved renderer via `FieldRenderProps`.\n *\n * @example\n * ```tsx\n * <Field forProperty={(t) => t.password} form={form}\n * variant=\"password\" label=\"Password\"\n * fieldProps={{ placeholder: \"Enter password\", autoComplete: \"current-password\" }} />\n * ```\n */\nexport function Field<TSchema extends ObjectSchemaBuilder<any, any, any>>({\n forProperty,\n form,\n renderer,\n variant,\n label,\n name,\n fieldProps\n}: FieldProps<TSchema>): React.ReactNode {\n const fieldResult = form.useField(forProperty);\n const systemConfig = useContext(FormSystemContext);\n\n const resolvedRenderer =\n renderer ?? resolveRenderer(systemConfig, fieldResult.schema, variant);\n\n if (!resolvedRenderer) {\n const schemaType = getSchemaType(fieldResult.schema);\n const tried = variant\n ? `\"${schemaType}:${variant}\" or \"${schemaType}\"`\n : `\"${schemaType}\"`;\n throw new Error(\n `No renderer found for schema type ${tried}. ` +\n 'Provide a renderer prop or configure one in FormSystemProvider.'\n );\n }\n\n return resolvedRenderer({\n value: fieldResult.value,\n initialValue: fieldResult.initialValue,\n dirty: fieldResult.dirty,\n touched: fieldResult.touched,\n error: fieldResult.error,\n validating: fieldResult.validating,\n onChange: fieldResult.onChange,\n onBlur: fieldResult.onBlur,\n setValue: fieldResult.setValue,\n schema: fieldResult.schema,\n variant,\n label,\n name,\n fieldProps\n });\n}\n","import type {\n ObjectSchemaBuilder,\n PropertyDescriptorInner,\n PropertyDescriptorTree\n} from '@cleverbrush/schema';\nimport { createContext } from 'react';\nimport type { FormStore } from './FormStore.js';\nimport type { FormSystemConfig, UseSchemaFormOptions } from './types.js';\n\n/**\n * Context for global form system configuration (renderers, etc).\n */\nexport const FormSystemContext = createContext<FormSystemConfig | null>(null);\n\n/**\n * Internal form context value — used to bridge useSchemaForm store\n * to the context-based useField and Field component.\n */\nexport type FormContextValue = {\n store: FormStore;\n descriptorTree: PropertyDescriptorTree<any, any>;\n schema: ObjectSchemaBuilder<any, any, any>;\n options: UseSchemaFormOptions;\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;\n triggerValidation?: (markTouched: boolean) => Promise<unknown>;\n};\n\n/**\n * Context for per-form data — allows useField to work via context.\n */\nexport const FormContext = createContext<FormContextValue | null>(null);\n","import type {\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\n\n/**\n * Builds a map from PropertyDescriptorInner identity → dot-separated path\n * by traversing the descriptor tree recursively.\n */\nexport function buildDescriptorPathMap(\n tree: PropertyDescriptorTree<any, any>,\n schema: ObjectSchemaBuilder<any, any, any>\n): Map<PropertyDescriptorInner<any, any, any>, string> {\n const map = new Map<PropertyDescriptorInner<any, any, any>, string>();\n const introspected = schema.introspect();\n if (!introspected.properties) return map;\n\n for (const propName of Object.keys(introspected.properties)) {\n const propDescriptor = (tree as any)[propName];\n if (\n !propDescriptor ||\n !(SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR in propDescriptor)\n ) {\n continue;\n }\n const inner = propDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ] as PropertyDescriptorInner<any, any, any>;\n map.set(inner, propName);\n\n // Recurse into nested objects\n const propSchema = introspected.properties[propName];\n if (propSchema instanceof ObjectSchemaBuilder) {\n const childMap = buildDescriptorPathMap(propDescriptor, propSchema);\n for (const [childInner, childPath] of childMap) {\n map.set(childInner, `${propName}.${childPath}`);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Looks up the path for a given descriptor from the pre-built map.\n */\nexport function getDescriptorPath(\n inner: PropertyDescriptorInner<any, any, any>,\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>\n): string {\n return pathMap.get(inner) ?? '';\n}\n\n/**\n * Returns the schema type string (e.g. \"string\", \"number\", \"object\").\n */\nexport function getSchemaType(\n schema: SchemaBuilder<any, any, any, any>\n): string {\n const introspected = schema.introspect();\n return introspected?.type ?? 'unknown';\n}\n\n/**\n * Builds a selector function from a dot-separated path string.\n * The selector traverses the PropertyDescriptorTree by property access.\n * Example: \"customer.address.city\" → (tree) => tree.customer.address.city\n */\nexport function buildSelectorFromPath(path: string): (tree: any) => any {\n const parts = path.split('.');\n return (tree: any) => {\n let current = tree;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n };\n}\n\n/**\n * Checks whether a validation error path matches a field's expected error path.\n * Matches exact path, path with nested suffix, or path with validator suffix.\n * Example: isErrorPathMatch(\"$.user.name\", \"$.user.name\") → true\n * isErrorPathMatch(\"$.user.name($validators[0])\", \"$.user.name\") → true\n */\nexport function isErrorPathMatch(\n errPath: string,\n fieldErrorPath: string\n): boolean {\n return (\n errPath === fieldErrorPath ||\n errPath.startsWith(fieldErrorPath + '.') ||\n errPath.startsWith(fieldErrorPath + '(')\n );\n}\n\n/**\n * Ensures all nested object structures exist in the values object\n * by traversing the schema and creating empty objects where needed.\n * This prevents ObjectSchemaBuilder.validate() from throwing when\n * nested object properties are undefined.\n *\n * Example: ensureNestedStructure({}, OrderSchema)\n * → { customer: { address: {} } }\n */\nexport function ensureNestedStructure(\n values: any,\n schema: ObjectSchemaBuilder<any, any, any>\n): any {\n const introspected = schema.introspect();\n if (!introspected.properties) return values != null ? values : {};\n\n const result = values != null ? { ...values } : {};\n for (const key of Object.keys(introspected.properties)) {\n const propSchema = introspected.properties[key];\n if (propSchema instanceof ObjectSchemaBuilder) {\n result[key] = ensureNestedStructure(result[key], propSchema);\n }\n }\n return result;\n}\n","import type {\n InferType,\n PropertyDescriptor,\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder,\n ValidationResult\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore\n} from 'react';\nimport type { FormContextValue } from './contexts.js';\nimport type { FormStore } from './FormStore.js';\nimport { createFormStore } from './FormStore.js';\nimport {\n buildDescriptorPathMap,\n ensureNestedStructure,\n getSchemaType\n} from './helpers.js';\nimport type {\n FieldRenderer,\n FormIssue,\n FormSubmissionState,\n FormSubmitHandler,\n FormSubmitOptions,\n FormSubmitResult,\n FormSystemConfig,\n UseFieldResult,\n UseSchemaFormOptions\n} from './types.js';\n\n/** A stable form controller with reactive field and submission subscriptions. */\nexport type SchemaFormInstance<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n useField: <TPropertySchema extends SchemaBuilder<any, any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ) => UseFieldResult<InferType<TPropertySchema>>;\n submit: () => Promise<ValidationResult<InferType<TSchema>>>;\n validate: () => Promise<ValidationResult<InferType<TSchema>>>;\n /** Clear values, or establish supplied values as the new clean baseline. */\n reset: (values?: Partial<InferType<TSchema>>) => void;\n /** Read the current snapshot. Use setters rather than mutating it. */\n getValue: () => InferType<TSchema>;\n /** Shallow-merge values without marking fields touched. */\n setValue: (values: Partial<InferType<TSchema>>) => void;\n /**\n * Replace external issues without changing values. Empty pointers and unbound\n * fields appear in form.error; bound fields receive the first issue and are\n * marked touched. Prefer returned submission issues for async race protection.\n * Pass [] to clear external issues without clearing local validation errors.\n */\n setIssues: (issues: readonly FormIssue[]) => void;\n /** Validate and submit once; repeated calls while pending are ignored. */\n handleSubmit: <TData = void>(\n onValid: (\n values: InferType<TSchema>\n ) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>,\n options?: FormSubmitOptions<InferType<TSchema>, TData>\n ) => FormSubmitHandler;\n /** @internal — Used by FormProvider and Field to access internal context. */\n _getFormContext: () => FormContextValue;\n} & FormSubmissionState;\n\n/** Bind a schema to independently subscribed fields and a stable form instance. */\nexport function useSchemaForm<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>(\n schema: TSchema,\n options?: UseSchemaFormOptions\n): SchemaFormInstance<TSchema> {\n const storeRef = useRef<FormStore | null>(null);\n if (!storeRef.current) storeRef.current = createFormStore({});\n const store = storeRef.current;\n const descriptorTreeRef = useRef<PropertyDescriptorTree<\n TSchema,\n TSchema\n > | null>(null);\n if (!descriptorTreeRef.current) {\n descriptorTreeRef.current = ObjectSchemaBuilder.getPropertiesFor(\n schema\n ) as PropertyDescriptorTree<TSchema, TSchema>;\n }\n const descriptorTree = descriptorTreeRef.current;\n const pathMapRef = useRef<Map<\n PropertyDescriptorInner<any, any, any>,\n string\n > | null>(null);\n if (!pathMapRef.current) {\n pathMapRef.current = buildDescriptorPathMap(descriptorTree, schema);\n }\n const pathMap = pathMapRef.current;\n for (const [descriptor] of pathMap) {\n store.registerField(descriptor.toJsonPointer(), descriptor);\n }\n const schemaRef = useRef(schema);\n const optionsRef = useRef<UseSchemaFormOptions>({});\n optionsRef.current = { createMissingStructure: true, ...options };\n const mountedRef = useRef(true);\n const epochRef = useRef(0);\n const validationGenRef = useRef(0);\n const submissionLockRef = useRef(false);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const cancelScheduledValidation = useCallback(() => {\n if (timerRef.current !== null) clearTimeout(timerRef.current);\n timerRef.current = null;\n }, []);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n epochRef.current++;\n validationGenRef.current++;\n cancelScheduledValidation();\n };\n }, [cancelScheduledValidation]);\n\n const runValidation = useCallback(\n async (\n markTouched: boolean\n ): Promise<ValidationResult<InferType<TSchema>>> => {\n cancelScheduledValidation();\n const gen = ++validationGenRef.current;\n const revision = store.getRevision();\n for (const path of store.getAllFieldPaths()) {\n store.updateFieldState(path, { validating: true });\n }\n const safeValues = ensureNestedStructure(\n store.getValues(),\n schemaRef.current\n );\n let result: ValidationResult<InferType<TSchema>>;\n try {\n result = (await schemaRef.current.validateAsync(safeValues, {\n doNotStopOnFirstError: true\n })) as ValidationResult<InferType<TSchema>>;\n } catch {\n result = { valid: false } as ValidationResult<\n InferType<TSchema>\n >;\n }\n // A value update invalidates validation immediately, even while the next\n // validation is only scheduled (debounced) and has not started yet.\n if (\n !mountedRef.current ||\n gen !== validationGenRef.current ||\n revision !== store.getRevision()\n )\n return result;\n\n for (const path of store.getAllFieldPaths()) {\n store.updateFieldState(path, {\n error: undefined,\n validating: false,\n ...(markTouched ? { touched: true } : {})\n });\n }\n const getErrorsFor = (result as any).getErrorsFor;\n if (typeof getErrorsFor === 'function') {\n for (const path of store.getAllFieldPaths()) {\n const parts = path\n .slice(1)\n .split('/')\n .map(part =>\n part.replace(/~1/g, '/').replace(/~0/g, '~')\n );\n try {\n const field = getErrorsFor((tree: any) =>\n parts.reduce(\n (value: any, key) => value?.[key],\n tree\n )\n );\n if (\n Array.isArray(field?.errors) &&\n field.errors.length\n ) {\n store.updateFieldState(path, {\n error: field.errors[0]\n });\n }\n } catch {\n // A missing error selector must not prevent other fields\n // from receiving their validation results.\n }\n }\n }\n return result;\n },\n [cancelScheduledValidation, store]\n );\n\n const validate = useCallback(() => runValidation(true), [runValidation]);\n const submit = useCallback(() => validate(), [validate]);\n const reset = useCallback(\n (values?: Partial<InferType<TSchema>>) => {\n epochRef.current++;\n validationGenRef.current++;\n cancelScheduledValidation();\n store.resetAll(values);\n },\n [store, cancelScheduledValidation]\n );\n const getValue = useCallback(\n (): InferType<TSchema> => store.getValues(),\n [store]\n );\n const setValue = useCallback(\n (values: Partial<InferType<TSchema>>) => {\n cancelScheduledValidation();\n store.setValues({ ...store.getValues(), ...values });\n },\n [store, cancelScheduledValidation]\n );\n const triggerValidation = useCallback(\n (markTouched: boolean) => {\n cancelScheduledValidation();\n const delay = optionsRef.current.validationDebounceMs;\n if (delay != null && delay > 0) {\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n void runValidation(markTouched);\n }, delay);\n return Promise.resolve();\n }\n return runValidation(markTouched);\n },\n [runValidation, cancelScheduledValidation]\n );\n\n const handleSubmit = useCallback(\n <TData>(\n onValid: (\n values: InferType<TSchema>\n ) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>,\n submissionOptions?: FormSubmitOptions<InferType<TSchema>, TData>\n ): FormSubmitHandler =>\n async event => {\n event?.preventDefault();\n if (submissionLockRef.current || !mountedRef.current) return;\n submissionLockRef.current = true;\n const epoch = epochRef.current;\n const current = () =>\n mountedRef.current && epoch === epochRef.current;\n store.setIssues([]);\n store.setSubmissionState({\n submitting: true,\n error: undefined\n });\n try {\n const revision = store.getRevision();\n const result = await runValidation(true);\n if (\n !current() ||\n revision !== store.getRevision() ||\n !result.valid ||\n result.object === undefined\n )\n return;\n const values = result.object;\n let outcome: FormSubmitResult<TData>;\n try {\n outcome = await onValid(values);\n } catch (error) {\n if (!current()) return;\n if (!submissionOptions?.onError) throw error;\n const message = await submissionOptions.onError(error);\n if (current())\n store.setSubmissionState({ error: message });\n return;\n }\n if (!current()) return;\n if (outcome && !outcome.ok) {\n store.setSubmissionFailure(\n outcome.error,\n revision === store.getRevision()\n ? outcome.issues\n : undefined\n );\n return;\n }\n // Errors from success callbacks (including redirects) propagate;\n // they are not translated as failures of a completed submission.\n await submissionOptions?.onSuccess?.(outcome?.data, values);\n } finally {\n submissionLockRef.current = false;\n if (mountedRef.current)\n store.setSubmissionState({ submitting: false });\n }\n },\n [runValidation, store]\n );\n\n const formContextValue = useMemo<FormContextValue>(\n () => ({\n store,\n descriptorTree,\n schema: schemaRef.current,\n pathMap,\n get options() {\n return optionsRef.current;\n },\n triggerValidation\n }),\n [store, descriptorTree, pathMap, triggerValidation]\n );\n const formContextRef = useRef(formContextValue);\n formContextRef.current = formContextValue;\n const _getFormContext = useCallback(() => formContextRef.current, []);\n const useFieldHook = useCallback(\n <TPropertySchema extends SchemaBuilder<any, any, any, any>>(\n selector: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ): UseFieldResult<InferType<TPropertySchema>> =>\n useFieldFromContext(formContextRef.current, selector),\n []\n );\n const validateOnMountRef = useRef(options?.validateOnMount);\n useEffect(() => {\n if (validateOnMountRef.current) void runValidation(true);\n }, [runValidation]);\n\n // Subscribe to submission state without changing the controller's identity.\n useSyncExternalStore(\n store.subscribeGlobal,\n store.getSubmissionState,\n store.getSubmissionState\n );\n return useMemo(\n () => ({\n useField: useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue,\n setIssues: store.setIssues,\n handleSubmit,\n _getFormContext,\n get submitting() {\n return store.getSubmissionState().submitting;\n },\n get error() {\n return store.getSubmissionState().error;\n }\n }),\n [\n useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue,\n handleSubmit,\n _getFormContext,\n store\n ]\n );\n}\n\n/** Internal field binding shared by direct and context-based hooks. */\nexport function useFieldFromContext(\n formContext: FormContextValue,\n forProperty: (tree: any) => any,\n triggerValidation = formContext.triggerValidation\n): UseFieldResult {\n const { store, descriptorTree, options } = formContext;\n const inner =\n forProperty(descriptorTree)[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR];\n const path = inner.toJsonPointer();\n store.registerField(path, inner);\n const subscribe = useCallback(\n (listener: () => void) => store.subscribe(path, listener),\n [store, path]\n );\n const getSnapshot = useCallback(\n () => store.getFieldState(path),\n [store, path]\n );\n const fieldState = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getSnapshot\n );\n const onChange = useCallback(\n (value: any) => {\n store.setFieldValue(\n path,\n value,\n options.createMissingStructure !== false\n );\n void triggerValidation?.(false);\n },\n [store, path, options.createMissingStructure, triggerValidation]\n );\n const onBlur = useCallback(\n () => store.updateFieldState(path, { touched: true }),\n [store, path]\n );\n return {\n ...fieldState,\n onChange,\n onBlur,\n setValue: onChange,\n schema: inner.getSchema()\n };\n}\n\n/** Resolve type:variant first, falling back to the base type. */\nexport function resolveRenderer(\n config: FormSystemConfig | null,\n schema: SchemaBuilder<any, any, any, any>,\n variant?: string\n): FieldRenderer | undefined {\n if (!config?.renderers) return undefined;\n const type = getSchemaType(schema);\n return (\n (variant ? config.renderers[`${type}:${variant}`] : undefined) ??\n config.renderers[type]\n );\n}\n","import { deepClone, deepEqual } from '@cleverbrush/deep';\nimport type { FieldState, FormIssue, FormSubmissionState } from './types.js';\n\ntype Binding = {\n getValue: (values: any) => { success: boolean; value?: any };\n setValue: (values: any, value: any, options: any) => boolean;\n};\n\n/** Internal immutable snapshots shared by all bindings of a form. */\nexport function createFormStore(initialValues: any) {\n let values = deepClone(initialValues ?? {});\n let baseline = values;\n let revision = 0;\n let issues: readonly FormIssue[] = [];\n let submissionError: string | undefined;\n const localErrors = new Map<string, string | undefined>();\n let submission: FormSubmissionState = {\n submitting: false,\n error: undefined\n };\n const bindings = new Map<string, Binding>();\n const fieldStates = new Map<string, FieldState>();\n const listeners = new Map<string, Set<() => void>>();\n const globalListeners = new Set<() => void>();\n let batching = 0;\n const pendingPaths = new Set<string>();\n let pendingGlobal = false;\n function batch(callback: () => void) {\n batching++;\n try {\n callback();\n } finally {\n batching--;\n if (batching === 0) {\n const paths = [...pendingPaths];\n pendingPaths.clear();\n const global = pendingGlobal;\n pendingGlobal = false;\n for (const path of paths) notifyPath(path);\n if (global) notifyGlobal();\n }\n }\n }\n function notifyGlobal() {\n if (batching) {\n pendingGlobal = true;\n return;\n }\n for (const listener of globalListeners) listener();\n }\n\n function read(binding: Binding | undefined, source: any) {\n const result = binding?.getValue(source);\n return result?.success ? result.value : undefined;\n }\n function getFieldState(path: string): FieldState {\n if (!fieldStates.has(path)) {\n const binding = bindings.get(path);\n const value = read(binding, values);\n const initialValue = read(binding, baseline);\n fieldStates.set(path, {\n value,\n initialValue,\n dirty: !deepEqual(value, initialValue),\n touched: issues.some(issue => issue.pointer === path),\n error: externalError(path),\n validating: false\n });\n }\n return fieldStates.get(path)!;\n }\n function registerField(path: string, binding: Binding) {\n if (!bindings.has(path)) bindings.set(path, binding);\n return getFieldState(path);\n }\n function notifyPath(path: string) {\n if (batching) {\n pendingPaths.add(path);\n return;\n }\n for (const listener of listeners.get(path) ?? []) listener();\n }\n function notifyAll() {\n for (const path of listeners.keys()) notifyPath(path);\n notifyGlobal();\n }\n function commitFieldState(path: string, patch: Partial<FieldState>) {\n const current = getFieldState(path);\n if (\n Object.entries(patch).every(([key, value]) =>\n Object.is(current[key as keyof FieldState], value)\n )\n )\n return;\n fieldStates.set(path, { ...current, ...patch });\n notifyPath(path);\n }\n function externalError(path: string) {\n return path === ''\n ? undefined\n : issues.find(issue => issue.pointer === path)?.detail;\n }\n function updateFieldState(path: string, patch: Partial<FieldState>) {\n if (Object.hasOwn(patch, 'error')) localErrors.set(path, patch.error);\n commitFieldState(path, {\n ...patch,\n error: externalError(path) ?? localErrors.get(path)\n });\n }\n function setIssues(next: readonly FormIssue[]) {\n batch(() => {\n // Copy caller-owned data; later mutations must not change store snapshots.\n issues = next.map(({ pointer, detail }) => ({ pointer, detail }));\n for (const path of bindings.keys()) {\n commitFieldState(path, {\n error: externalError(path) ?? localErrors.get(path),\n ...(externalError(path) !== undefined\n ? { touched: true }\n : {})\n });\n }\n setSubmissionState({});\n });\n }\n function setSubmissionFailure(error: string, next?: readonly FormIssue[]) {\n batch(() => {\n if (next) setIssues(next);\n setSubmissionState({ error });\n });\n }\n function syncFields(reset: boolean) {\n for (const [path, binding] of bindings) {\n const value = read(binding, values);\n const initialValue = read(binding, baseline);\n updateFieldState(path, {\n value,\n initialValue,\n dirty: !deepEqual(value, initialValue),\n validating: false,\n ...(reset ? { touched: false, error: undefined } : {})\n });\n }\n }\n function subscribe(path: string, listener: () => void): () => void {\n let set = listeners.get(path);\n if (!set) {\n set = new Set();\n listeners.set(path, set);\n }\n set.add(listener);\n setSubmissionState({});\n return () => {\n set.delete(listener);\n if (set.size === 0) listeners.delete(path);\n setSubmissionState({});\n };\n }\n function subscribeGlobal(listener: () => void): () => void {\n globalListeners.add(listener);\n return () => {\n globalListeners.delete(listener);\n };\n }\n function getSubmissionState() {\n return submission;\n }\n function setSubmissionState(patch: Partial<FormSubmissionState>) {\n if (Object.hasOwn(patch, 'error')) submissionError = patch.error;\n const messages = [\n submissionError,\n ...issues\n .filter(\n issue =>\n issue.pointer === '' ||\n !listeners.get(issue.pointer)?.size\n )\n .map(issue => issue.detail)\n ].filter((message): message is string => !!message);\n const next = {\n ...submission,\n ...patch,\n error: [...new Set(messages)].join('\\n') || undefined\n };\n if (\n next.submitting === submission.submitting &&\n next.error === submission.error\n )\n return;\n submission = next;\n notifyGlobal();\n }\n function getValues() {\n return values;\n }\n function getRevision() {\n return revision;\n }\n function setValues(newValues: any, changedPath?: string) {\n const previous = values;\n values = deepClone(newValues ?? {});\n revision++;\n const changed = issues.length > 0 && !deepEqual(previous, values);\n if (changed) {\n const readPointer = (source: any, pointer: string) => {\n if (pointer === '') return source;\n if (!pointer.startsWith('/') || /~(?![01])/.test(pointer))\n return undefined;\n return pointer\n .slice(1)\n .split('/')\n .reduce((value, part) => {\n const key = part\n .replace(/~1/g, '/')\n .replace(/~0/g, '~');\n return value != null && Object.hasOwn(value, key)\n ? value[key]\n : undefined;\n }, source);\n };\n // Arrays are positional: replacing/reordering a container invalidates\n // all of its indexed issues, even when two items have equal values.\n const arrayChanges = [...bindings.keys()].filter(\n path =>\n Array.isArray(readPointer(previous, path)) &&\n !deepEqual(\n readPointer(previous, path),\n readPointer(values, path)\n ) &&\n (!changedPath ||\n changedPath === path ||\n path.startsWith(changedPath + '/'))\n );\n setIssues(\n issues.filter(issue => {\n const path = issue.pointer;\n if (path === '') return false;\n if (\n changedPath &&\n (path === changedPath ||\n path.startsWith(changedPath + '/') ||\n changedPath.startsWith(path + '/'))\n )\n return false;\n if (\n arrayChanges.some(\n array =>\n path === array || path.startsWith(array + '/')\n )\n )\n return false;\n return deepEqual(\n readPointer(previous, path),\n readPointer(values, path)\n );\n })\n );\n }\n syncFields(false);\n }\n function setFieldValue(\n path: string,\n value: any,\n createMissingStructure: boolean\n ) {\n const next = deepClone(values);\n if (\n bindings\n .get(path)\n ?.setValue(next, deepClone(value), { createMissingStructure })\n ) {\n setValues(next, path);\n }\n }\n function resetAll(newInitialValues?: any) {\n values = deepClone(newInitialValues ?? {});\n baseline = values;\n revision++;\n issues = [];\n localErrors.clear();\n syncFields(true);\n setSubmissionState({ error: undefined });\n }\n function getAllFieldPaths() {\n return Array.from(bindings.keys());\n }\n\n return {\n registerField,\n getFieldState,\n updateFieldState,\n subscribe,\n subscribeGlobal,\n getValues,\n setValues,\n setFieldValue,\n resetAll,\n getAllFieldPaths,\n getRevision,\n getSubmissionState,\n setSubmissionState,\n setIssues,\n setSubmissionFailure,\n notifyAll\n };\n}\n\nexport type FormStore = ReturnType<typeof createFormStore>;\n","import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport { SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR } from '@cleverbrush/schema';\nimport type { ReactNode } from 'react';\nimport { Field, type FieldProps, FormSystemProvider } from './components.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport type { FieldRenderer } from './types.js';\n\ndeclare const rendererTypes: unique symbol;\n\n/** A renderer whose accepted values and custom props survive registration. */\nexport type TypedFieldRenderer<TValue, TProps> = FieldRenderer<\n TValue,\n TProps\n> & {\n readonly [rendererTypes]: { value: TValue; props: TProps };\n};\n\n/**\n * Attach type information without adding a UI dependency or runtime wrapper.\n * Include null in TValue when the renderer supports nullable fields.\n */\nexport function defineFieldRenderer<TValue, TProps = Record<string, never>>(\n renderer: FieldRenderer<TValue, TProps>\n): TypedFieldRenderer<TValue, TProps> {\n return renderer as TypedFieldRenderer<TValue, TProps>;\n}\n\nexport type TypedRendererRegistry = Readonly<\n Record<string, TypedFieldRenderer<any, any>>\n>;\n\ntype ValueOf<T> = T extends TypedFieldRenderer<infer V, any> ? V : never;\ntype PropsOf<T> = T extends TypedFieldRenderer<any, infer P> ? P : never;\ntype KindOf<T> = 0 extends 1 & T\n ? string\n : [NonNullable<T>] extends [string]\n ? 'string'\n : [NonNullable<T>] extends [number]\n ? 'number'\n : [NonNullable<T>] extends [boolean]\n ? 'boolean'\n : [NonNullable<T>] extends [Date]\n ? 'date'\n : [NonNullable<T>] extends [readonly unknown[]]\n ? 'array'\n : [NonNullable<T>] extends [object]\n ? 'object'\n : never;\ntype CustomProps<P> = {} extends P ? { fieldProps?: P } : { fieldProps: P };\ntype RendererChoice<R extends TypedRendererRegistry, V> = {\n [K in keyof R & string]: [Exclude<V, undefined>] extends [ValueOf<R[K]>]\n ? K extends `${KindOf<V>}:${infer Variant}`\n ? { variant: Variant } & CustomProps<PropsOf<R[K]>>\n : K extends KindOf<V>\n ? { variant?: undefined } & CustomProps<PropsOf<R[K]>>\n : never\n : never;\n}[keyof R & string];\n\ntype FieldDescriptor = {\n readonly [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {\n getSchema: () => SchemaBuilder<any, any, any, any>;\n };\n};\ntype DescriptorValue<D extends FieldDescriptor> = InferType<\n ReturnType<D[typeof SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]['getSchema']>\n>;\n\n/** Props are inferred from the selected schema property and registered variant. */\nexport type TypedFieldProps<\n R extends TypedRendererRegistry,\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n> = {\n form: SchemaFormInstance<TSchema>;\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => TDescriptor;\n label?: string;\n name?: string;\n} & RendererChoice<R, NoInfer<DescriptorValue<TDescriptor>>>;\n\n/** Named callable type also supports exporting system.Field from UI packages. */\nexport type TypedFieldComponent<R extends TypedRendererRegistry> = <\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n>(\n props: TypedFieldProps<R, TSchema, TDescriptor>\n) => ReactNode;\n\n/** Named return type keeps exported consumer registries declaration-safe. */\nexport type TypedFormSystem<R extends TypedRendererRegistry> = {\n Field: TypedFieldComponent<R>;\n Provider: (props: { children: ReactNode }) => ReactNode;\n renderers: Readonly<R>;\n};\n\n/**\n * Create a typed Field and a provider for an application's renderer registry.\n * The typed Field resolves from this factory's closed registry, so an untyped\n * ancestor provider cannot silently replace a renderer with incompatible props.\n * The Provider also configures legacy Field consumers and supports nesting.\n * Extend a system with createFormSystem({ renderers: { ...system.renderers,\n * 'string:custom': customRenderer } }). Existing global APIs remain available.\n */\nexport function createFormSystem<\n const R extends TypedRendererRegistry\n>(config: { renderers: R }): TypedFormSystem<R> {\n const renderers = Object.freeze({ ...config.renderers });\n function TypedField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n >(props: TypedFieldProps<R, TSchema, TDescriptor>): ReactNode {\n const descriptor = props.forProperty(\n props.form._getFormContext()\n .descriptorTree as PropertyDescriptorTree<TSchema, TSchema>\n );\n const type = getSchemaType(\n descriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR].getSchema()\n );\n const key = props.variant ? `${type}:${props.variant}` : type;\n const renderer = renderers[key];\n if (!renderer)\n throw new Error(\n `No renderer registered for \"${key}\" in this form system.`\n );\n // Field's legacy signature erases descriptor parent types. The factory\n // has already checked the selected value and renderer-specific props.\n return (\n <Field\n {...(props as unknown as FieldProps<TSchema>)}\n renderer={renderer}\n />\n );\n }\n function Provider({ children }: { children: ReactNode }): ReactNode {\n return (\n <FormSystemProvider renderers={renderers}>\n {children}\n </FormSystemProvider>\n );\n }\n return { Field: TypedField, Provider, renderers };\n}\n"],"mappings":"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","names":["useContext","useMemo","createContext","FormSystemContext","FormContext","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","buildDescriptorPathMap","tree","schema","map","introspected","propName","propDescriptor","inner","propSchema","childMap","childInner","childPath","getSchemaType","schema","ensureNestedStructure","values","schema","introspected","result","key","propSchema","ObjectSchemaBuilder","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","useCallback","useEffect","useMemo","useRef","useSyncExternalStore","deepClone","deepEqual","createFormStore","initialValues","values","baseline","revision","issues","submissionError","localErrors","submission","bindings","fieldStates","listeners","globalListeners","batching","pendingPaths","pendingGlobal","batch","callback","paths","global","path","notifyPath","notifyGlobal","listener","read","binding","source","result","getFieldState","value","initialValue","issue","externalError","registerField","notifyAll","commitFieldState","patch","current","key","updateFieldState","setIssues","next","pointer","detail","setSubmissionState","setSubmissionFailure","error","syncFields","reset","subscribe","set","subscribeGlobal","getSubmissionState","messages","message","getValues","getRevision","setValues","newValues","changedPath","previous","readPointer","part","arrayChanges","array","setFieldValue","createMissingStructure","resetAll","newInitialValues","getAllFieldPaths","useSchemaForm","schema","options","storeRef","useRef","createFormStore","store","descriptorTreeRef","ObjectSchemaBuilder","descriptorTree","pathMapRef","buildDescriptorPathMap","pathMap","descriptor","schemaRef","optionsRef","mountedRef","epochRef","validationGenRef","submissionLockRef","timerRef","cancelScheduledValidation","useCallback","useEffect","runValidation","markTouched","gen","revision","path","safeValues","ensureNestedStructure","result","getErrorsFor","parts","part","field","tree","value","key","validate","submit","reset","values","getValue","setValue","triggerValidation","delay","handleSubmit","onValid","submissionOptions","event","epoch","current","outcome","error","message","formContextValue","useMemo","formContextRef","_getFormContext","useFieldHook","selector","useFieldFromContext","validateOnMountRef","useSyncExternalStore","formContext","forProperty","inner","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","subscribe","listener","getSnapshot","fieldState","onChange","onBlur","resolveRenderer","config","variant","type","getSchemaType","jsx","FormSystemProvider","renderers","config","children","parentConfig","useContext","FormSystemContext","resolvedConfig","useMemo","current","FormProvider","form","formContext","FormContext","useField","forProperty","useFieldFromContext","useFormSystem","Field","renderer","variant","label","name","fieldProps","fieldResult","systemConfig","resolvedRenderer","resolveRenderer","schemaType","getSchemaType","tried","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","jsx","defineFieldRenderer","renderer","createFormSystem","config","renderers","TypedField","props","descriptor","type","getSchemaType","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","key","Field","Provider","children","FormSystemProvider"]}
@@ -0,0 +1,69 @@
1
+ import type { InferType, ObjectSchemaBuilder, PropertyDescriptorTree, SchemaBuilder } from '@cleverbrush/schema';
2
+ import { SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR } from '@cleverbrush/schema';
3
+ import type { ReactNode } from 'react';
4
+ import type { SchemaFormInstance } from './hooks.js';
5
+ import type { FieldRenderer } from './types.js';
6
+ declare const rendererTypes: unique symbol;
7
+ /** A renderer whose accepted values and custom props survive registration. */
8
+ export type TypedFieldRenderer<TValue, TProps> = FieldRenderer<TValue, TProps> & {
9
+ readonly [rendererTypes]: {
10
+ value: TValue;
11
+ props: TProps;
12
+ };
13
+ };
14
+ /**
15
+ * Attach type information without adding a UI dependency or runtime wrapper.
16
+ * Include null in TValue when the renderer supports nullable fields.
17
+ */
18
+ export declare function defineFieldRenderer<TValue, TProps = Record<string, never>>(renderer: FieldRenderer<TValue, TProps>): TypedFieldRenderer<TValue, TProps>;
19
+ export type TypedRendererRegistry = Readonly<Record<string, TypedFieldRenderer<any, any>>>;
20
+ type ValueOf<T> = T extends TypedFieldRenderer<infer V, any> ? V : never;
21
+ type PropsOf<T> = T extends TypedFieldRenderer<any, infer P> ? P : never;
22
+ type KindOf<T> = 0 extends 1 & T ? string : [NonNullable<T>] extends [string] ? 'string' : [NonNullable<T>] extends [number] ? 'number' : [NonNullable<T>] extends [boolean] ? 'boolean' : [NonNullable<T>] extends [Date] ? 'date' : [NonNullable<T>] extends [readonly unknown[]] ? 'array' : [NonNullable<T>] extends [object] ? 'object' : never;
23
+ type CustomProps<P> = {} extends P ? {
24
+ fieldProps?: P;
25
+ } : {
26
+ fieldProps: P;
27
+ };
28
+ type RendererChoice<R extends TypedRendererRegistry, V> = {
29
+ [K in keyof R & string]: [Exclude<V, undefined>] extends [ValueOf<R[K]>] ? K extends `${KindOf<V>}:${infer Variant}` ? {
30
+ variant: Variant;
31
+ } & CustomProps<PropsOf<R[K]>> : K extends KindOf<V> ? {
32
+ variant?: undefined;
33
+ } & CustomProps<PropsOf<R[K]>> : never : never;
34
+ }[keyof R & string];
35
+ type FieldDescriptor = {
36
+ readonly [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {
37
+ getSchema: () => SchemaBuilder<any, any, any, any>;
38
+ };
39
+ };
40
+ type DescriptorValue<D extends FieldDescriptor> = InferType<ReturnType<D[typeof SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]['getSchema']>>;
41
+ /** Props are inferred from the selected schema property and registered variant. */
42
+ export type TypedFieldProps<R extends TypedRendererRegistry, TSchema extends ObjectSchemaBuilder<any, any, any>, TDescriptor extends FieldDescriptor> = {
43
+ form: SchemaFormInstance<TSchema>;
44
+ forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => TDescriptor;
45
+ label?: string;
46
+ name?: string;
47
+ } & RendererChoice<R, NoInfer<DescriptorValue<TDescriptor>>>;
48
+ /** Named callable type also supports exporting system.Field from UI packages. */
49
+ export type TypedFieldComponent<R extends TypedRendererRegistry> = <TSchema extends ObjectSchemaBuilder<any, any, any>, TDescriptor extends FieldDescriptor>(props: TypedFieldProps<R, TSchema, TDescriptor>) => ReactNode;
50
+ /** Named return type keeps exported consumer registries declaration-safe. */
51
+ export type TypedFormSystem<R extends TypedRendererRegistry> = {
52
+ Field: TypedFieldComponent<R>;
53
+ Provider: (props: {
54
+ children: ReactNode;
55
+ }) => ReactNode;
56
+ renderers: Readonly<R>;
57
+ };
58
+ /**
59
+ * Create a typed Field and a provider for an application's renderer registry.
60
+ * The typed Field resolves from this factory's closed registry, so an untyped
61
+ * ancestor provider cannot silently replace a renderer with incompatible props.
62
+ * The Provider also configures legacy Field consumers and supports nesting.
63
+ * Extend a system with createFormSystem({ renderers: { ...system.renderers,
64
+ * 'string:custom': customRenderer } }). Existing global APIs remain available.
65
+ */
66
+ export declare function createFormSystem<const R extends TypedRendererRegistry>(config: {
67
+ renderers: R;
68
+ }): TypedFormSystem<R>;
69
+ export {};
package/dist/types.d.ts CHANGED
@@ -1,23 +1,23 @@
1
1
  import type { SchemaBuilder } from '@cleverbrush/schema';
2
- import type { ReactNode } from 'react';
2
+ import type { FormEvent, ReactNode } from 'react';
3
3
  /**
4
4
  * A renderer function that receives field state and returns a React node.
5
5
  */
6
- export type FieldRenderer = (props: FieldRenderProps) => ReactNode;
6
+ export type FieldRenderer<TValue = any, TProps = Record<string, unknown>> = (props: FieldRenderProps<TValue, TProps>) => ReactNode;
7
7
  /**
8
8
  * Props passed to a field renderer.
9
9
  */
10
- export type FieldRenderProps = {
11
- value: any;
12
- initialValue: any;
10
+ export type FieldRenderProps<TValue = any, TProps = Record<string, unknown>> = {
11
+ value: TValue | undefined;
12
+ initialValue: TValue | undefined;
13
13
  dirty: boolean;
14
14
  touched: boolean;
15
15
  error: string | undefined;
16
16
  validating: boolean;
17
- onChange: (value: any) => void;
17
+ onChange: (value: TValue) => void;
18
18
  onBlur: () => void;
19
- setValue: (value: any) => void;
20
- schema: SchemaBuilder<any, any, any>;
19
+ setValue: (value: TValue) => void;
20
+ schema: SchemaBuilder<any, any, any, any>;
21
21
  /**
22
22
  * Rendering variant hint passed from the `Field` component.
23
23
  * Used by renderers to select a sub-variant of the base schema type
@@ -67,7 +67,7 @@ export type FieldRenderProps = {
67
67
  * />
68
68
  * ```
69
69
  */
70
- fieldProps?: Record<string, unknown>;
70
+ fieldProps?: TProps;
71
71
  };
72
72
  /**
73
73
  * Configuration for FormSystemProvider.
@@ -100,7 +100,7 @@ export type UseFieldResult<T = any> = {
100
100
  onChange: (value: T) => void;
101
101
  onBlur: () => void;
102
102
  setValue: (value: T) => void;
103
- schema: SchemaBuilder<any, any, any>;
103
+ schema: SchemaBuilder<any, any, any, any>;
104
104
  };
105
105
  /**
106
106
  * Options for useSchemaForm.
@@ -130,3 +130,33 @@ export type UseSchemaFormOptions = {
130
130
  */
131
131
  validationDebounceMs?: number;
132
132
  };
133
+ /**
134
+ * Plain external validation issue. `pointer` is a form-relative JSON Pointer
135
+ * (RFC 6901), or '' for a form-level issue. Safe to serialize through actions.
136
+ * The form does not decode HTTP responses or infer paths from message text.
137
+ */
138
+ export type FormIssue = {
139
+ readonly pointer: string;
140
+ readonly detail: string;
141
+ };
142
+ /** Reactive state for handleSubmit; independent of any UI kit. */
143
+ export type FormSubmissionState = {
144
+ readonly submitting: boolean;
145
+ readonly error: string | undefined;
146
+ };
147
+ /** Void denotes success without data; explicit failures preserve user input. */
148
+ export type FormSubmitResult<TData = void> = void | {
149
+ ok: true;
150
+ data?: TData;
151
+ } | {
152
+ ok: false;
153
+ error: string;
154
+ issues?: readonly FormIssue[];
155
+ };
156
+ /** Application-owned success UI and exception translation. Rethrows propagate. */
157
+ export type FormSubmitOptions<TValues, TData = void> = {
158
+ onSuccess?: (data: TData | undefined, values: TValues) => void | Promise<void>;
159
+ onError?: (error: unknown) => string | Promise<string>;
160
+ };
161
+ /** Awaitable handler for a form event or a programmatic invocation. */
162
+ export type FormSubmitHandler = (event?: Pick<FormEvent, 'preventDefault'>) => Promise<void>;
package/package.json CHANGED
@@ -5,7 +5,8 @@
5
5
  "email": "andrew_zol@cleverbrush.com"
6
6
  },
7
7
  "dependencies": {
8
- "@cleverbrush/schema": "^4.4.3"
8
+ "@cleverbrush/deep": "^5.0.0",
9
+ "@cleverbrush/schema": "^5.0.0"
9
10
  },
10
11
  "devDependencies": {
11
12
  "@types/react": "^19.0.0",
@@ -25,7 +26,7 @@
25
26
  "form system",
26
27
  "cleverbrush"
27
28
  ],
28
- "license": "BSD 3-Clause",
29
+ "license": "BSD-3-Clause",
29
30
  "main": "./dist/index.js",
30
31
  "exports": {
31
32
  ".": {
@@ -35,6 +36,9 @@
35
36
  },
36
37
  "sideEffects": false,
37
38
  "name": "@cleverbrush/react-form",
39
+ "engines": {
40
+ "node": ">=24"
41
+ },
38
42
  "readme": "https://github.com/cleverbrush/framework/tree/master/libs/react-form#readme",
39
43
  "repository": {
40
44
  "type": "git",
@@ -47,5 +51,5 @@
47
51
  },
48
52
  "type": "module",
49
53
  "types": "./dist/index.d.ts",
50
- "version": "4.4.3"
54
+ "version": "5.0.0"
51
55
  }