@cleverbrush/react-form 0.0.0-beta-20260410073748

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,624 @@
1
+ # @cleverbrush/react-form
2
+
3
+ [![CI](https://github.com/cleverbrush/framework/actions/workflows/ci.yml/badge.svg)](https://github.com/cleverbrush/framework/actions/workflows/ci.yml)
4
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](../../LICENSE)
5
+ <!-- coverage-badge-start -->
6
+ ![Coverage](https://img.shields.io/badge/coverage-96.1%25-brightgreen)
7
+ <!-- coverage-badge-end -->
8
+
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
+
11
+ ## Why @cleverbrush/react-form?
12
+
13
+ **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.
14
+
15
+ ```tsx
16
+ // React Hook Form — field names are plain strings
17
+ const { register } = useForm<User>();
18
+ <input {...register("name")} /> // ← no compiler error if "name" is renamed
19
+ <input {...register("emial")} /> // ← typo: silently fails at runtime
20
+
21
+ // Formik — same problem
22
+ <Field name="address.city" /> // ← rename "city" → "town" and nothing warns you
23
+ ```
24
+
25
+ **The solution:** `@cleverbrush/react-form` binds fields via **PropertyDescriptor selectors** — actual TypeScript expressions like `(t) => t.address.city` — instead of strings. The compiler knows the exact shape of your schema, so a renamed or mistyped property is a **compile-time error**, not a runtime surprise. On top of that, the schema **IS** the validation, the type definition, **AND** the form field configuration. One source of truth.
26
+
27
+ ```tsx
28
+ // @cleverbrush/react-form — fully type-safe selectors
29
+ <Field forProperty={(t) => t.name} form={form} /> // ✓ checked at compile time
30
+ <Field forProperty={(t) => t.address.city} form={form} /> // ✓ rename "city" → compiler error
31
+ <Field forProperty={(t) => t.emial} form={form} /> // ✗ compile error: "emial" doesn't exist
32
+ ```
33
+
34
+ **What makes it different:**
35
+
36
+ | Feature | @cleverbrush/react-form | React Hook Form | Formik | React Final Form |
37
+ | --- | --- | --- | --- | --- |
38
+ | Schema-driven validation | ✓ built-in | ~ via resolver | ~ via plugin | ✗ |
39
+ | Single source of truth (types + validation) | ✓ | ✗ | ✗ | ✗ |
40
+ | Type-safe field selectors | ✓ | ~ | ✗ | ✗ |
41
+ | Headless / UI-agnostic | ✓ | ✓ | ~ | ✓ |
42
+ | Global renderer system | ✓ | ✗ | ✗ | ✗ |
43
+ | Auto-field rendering by type + variant | ✓ | ✗ | ✗ | ✗ |
44
+ | Nested objects | ✓ | ✓ | ✓ | ✓ |
45
+ | Async validation | ✓ | ✓ | ✓ | ✓ |
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ npm install @cleverbrush/react-form
51
+ ```
52
+
53
+ **Peer dependencies:** `react >=18`, `@cleverbrush/schema ^2.0.0`
54
+
55
+ ## Quick Start
56
+
57
+ ```tsx
58
+ import { object, string, number } from '@cleverbrush/schema';
59
+ import { useSchemaForm, FormSystemProvider, Field } from '@cleverbrush/react-form';
60
+
61
+ // 1. Define schema — reuse across forms, API validation, mapping, etc.
62
+ const ContactSchema = object({
63
+ name: string().required('Name is required').minLength(2, 'Name must be at least 2 characters'),
64
+ email: string().required('Email is required'),
65
+ age: number().required('Age is required').min(18, 'Must be at least 18')
66
+ });
67
+
68
+ // 2. Define renderers once per app — maps schema types to UI components
69
+ const renderers = {
70
+ string: ({ value, onChange, onBlur, error, touched }) => (
71
+ <div>
72
+ <input
73
+ type="text"
74
+ value={value ?? ''}
75
+ onChange={(e) => onChange(e.target.value)}
76
+ onBlur={onBlur}
77
+ />
78
+ {touched && error && <span className="error">{error}</span>}
79
+ </div>
80
+ ),
81
+ number: ({ value, onChange, onBlur, error, touched }) => (
82
+ <div>
83
+ <input
84
+ type="number"
85
+ value={value ?? ''}
86
+ onChange={(e) => onChange(Number(e.target.value))}
87
+ onBlur={onBlur}
88
+ />
89
+ {touched && error && <span className="error">{error}</span>}
90
+ </div>
91
+ )
92
+ };
93
+
94
+ // 3. Each form component only picks which fields to show — no boilerplate
95
+ function ContactForm() {
96
+ const form = useSchemaForm(ContactSchema);
97
+
98
+ const handleSubmit = async () => {
99
+ const result = await form.submit();
100
+ if (result.valid) {
101
+ console.log('Submitted:', result.object);
102
+ }
103
+ };
104
+
105
+ return (
106
+ <div>
107
+ <Field forProperty={(t) => t.name} form={form} />
108
+ <Field forProperty={(t) => t.email} form={form} />
109
+ <Field forProperty={(t) => t.age} form={form} />
110
+ <button onClick={handleSubmit}>Submit</button>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ // 4. Wrap once at the app root — all forms below share the renderers
116
+ function App() {
117
+ return (
118
+ <FormSystemProvider renderers={renderers}>
119
+ <ContactForm />
120
+ </FormSystemProvider>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## How It Works — Step by Step
126
+
127
+ 1. **Define a schema** using `@cleverbrush/schema` — this is your single source of truth for types, validation rules, and field metadata
128
+ 2. **Register renderers** via `FormSystemProvider` — plain functions that map schema types (`"string"`, `"number"`, `"boolean"`) to your UI components (plain HTML, MUI, Ant Design, etc.)
129
+ 3. **Create a form instance** via `useSchemaForm(schema)` — returns state management, validation, submit/reset lifecycle
130
+ 4. **Render fields** via `<Field forProperty={(t) => t.name} form={form} />` — the component looks up the registered renderer for the field's schema type
131
+ 5. **Submit** — `form.submit()` runs the schema's full validation and returns a typed result
132
+
133
+ ## Core Concepts
134
+
135
+ | Part | Responsibility | When to Use |
136
+ |------|---------------|-------------|
137
+ | **FormSystemProvider** | Global renderer registry via React Context | Once at the app root |
138
+ | **useSchemaForm** | Per-schema form instance (state, validation, lifecycle) | In every component that needs a form |
139
+ | **useField** | Descriptor-based field binding (value, dirty, touched, error) | When you want fine-grained control |
140
+ | **Field** | UI-agnostic component that resolves renderers by schema type | For most form fields — quick and declarative |
141
+
142
+ ## Registering Renderers
143
+
144
+ Renderers are plain functions that receive field state and return React nodes. Define a renderer map keyed by schema type (`string`, `number`, `boolean`, etc.):
145
+
146
+ ### Plain HTML
147
+
148
+ ```tsx
149
+ import { FieldRenderProps } from '@cleverbrush/react-form';
150
+
151
+ const htmlRenderers = {
152
+ string: ({ value, onChange, onBlur, error, touched, label, name, fieldProps }: FieldRenderProps) => (
153
+ <div>
154
+ {label && <label>{label}</label>}
155
+ <input
156
+ type="text"
157
+ name={name}
158
+ value={value ?? ''}
159
+ onChange={(e) => onChange(e.target.value)}
160
+ onBlur={onBlur}
161
+ {...fieldProps}
162
+ />
163
+ {touched && error && <span className="error">{error}</span>}
164
+ </div>
165
+ ),
166
+ number: ({ value, onChange, onBlur, error, touched, label, name, fieldProps }: FieldRenderProps) => (
167
+ <div>
168
+ {label && <label>{label}</label>}
169
+ <input
170
+ type="number"
171
+ name={name}
172
+ value={value ?? ''}
173
+ onChange={(e) => onChange(Number(e.target.value))}
174
+ onBlur={onBlur}
175
+ {...fieldProps}
176
+ />
177
+ {touched && error && <span className="error">{error}</span>}
178
+ </div>
179
+ ),
180
+ boolean: ({ value, onChange, label }: FieldRenderProps) => (
181
+ <label>
182
+ <input
183
+ type="checkbox"
184
+ checked={value ?? false}
185
+ onChange={(e) => onChange(e.target.checked)}
186
+ />
187
+ {label}
188
+ </label>
189
+ )
190
+ };
191
+ ```
192
+
193
+ ### Variant Renderers
194
+
195
+ You can register renderers for specific variants using a `"type:variant"` key.
196
+ When `<Field variant="password" />` is rendered on a `string` field, the
197
+ registry is checked for `"string:password"` first, then falls back to `"string"`:
198
+
199
+ ```tsx
200
+ const renderers = {
201
+ // Default string renderer
202
+ string: ({ value, onChange, onBlur, error, touched, label, name, fieldProps }: FieldRenderProps) => (
203
+ <div>
204
+ {label && <label>{label}</label>}
205
+ <input type="text" name={name} value={value ?? ''}
206
+ onChange={(e) => onChange(e.target.value)} onBlur={onBlur}
207
+ {...fieldProps} />
208
+ {touched && error && <span className="error">{error}</span>}
209
+ </div>
210
+ ),
211
+ // Password variant — rendered when <Field variant="password" /> is used on a string field
212
+ 'string:password': ({ value, onChange, onBlur, error, touched, label, name, fieldProps }: FieldRenderProps) => (
213
+ <div>
214
+ {label && <label>{label}</label>}
215
+ <input type="password" name={name} value={value ?? ''}
216
+ onChange={(e) => onChange(e.target.value)} onBlur={onBlur}
217
+ {...fieldProps} />
218
+ {touched && error && <span className="error">{error}</span>}
219
+ </div>
220
+ ),
221
+ // Textarea variant
222
+ 'string:textarea': ({ value, onChange, onBlur, error, touched, label, name, fieldProps }: FieldRenderProps) => (
223
+ <div>
224
+ {label && <label>{label}</label>}
225
+ <textarea name={name} value={value ?? ''}
226
+ onChange={(e) => onChange(e.target.value)} onBlur={onBlur}
227
+ {...(fieldProps as any)} />
228
+ {touched && error && <span className="error">{error}</span>}
229
+ </div>
230
+ )
231
+ };
232
+ ```
233
+
234
+ ### MUI (Material UI)
235
+
236
+ ```tsx
237
+ import { TextField, Checkbox } from '@mui/material';
238
+
239
+ const muiRenderers = {
240
+ string: ({ value, onChange, onBlur, error, touched }: FieldRenderProps) => (
241
+ <TextField
242
+ value={value ?? ''}
243
+ onChange={(e) => onChange(e.target.value)}
244
+ onBlur={onBlur}
245
+ error={touched && !!error}
246
+ helperText={touched ? error : undefined}
247
+ />
248
+ ),
249
+ number: ({ value, onChange, onBlur, error, touched }: FieldRenderProps) => (
250
+ <TextField
251
+ type="number"
252
+ value={value ?? ''}
253
+ onChange={(e) => onChange(Number(e.target.value))}
254
+ onBlur={onBlur}
255
+ error={touched && !!error}
256
+ helperText={touched ? error : undefined}
257
+ />
258
+ ),
259
+ boolean: ({ value, onChange }: FieldRenderProps) => (
260
+ <Checkbox
261
+ checked={value ?? false}
262
+ onChange={(e) => onChange(e.target.checked)}
263
+ />
264
+ )
265
+ };
266
+ ```
267
+
268
+ ## FormSystemProvider
269
+
270
+ Register renderers once at the top of your app. All `<Field>` components below resolve renderers by schema type automatically:
271
+
272
+ ```tsx
273
+ import { FormSystemProvider } from '@cleverbrush/react-form';
274
+
275
+ // Public website with plain HTML inputs
276
+ <FormSystemProvider renderers={htmlRenderers}>
277
+ <PublicApp />
278
+ </FormSystemProvider>
279
+
280
+ // Admin panel using MUI
281
+ <FormSystemProvider renderers={muiRenderers}>
282
+ <AdminApp />
283
+ </FormSystemProvider>
284
+ ```
285
+
286
+ ### Nesting
287
+
288
+ Inner providers override/extend outer providers:
289
+
290
+ ```tsx
291
+ <FormSystemProvider renderers={muiRenderers}>
292
+ <MainApp />
293
+ {/* Override just the string renderer in this section */}
294
+ <FormSystemProvider renderers={{ string: customStringRenderer }}>
295
+ <SpecialSection />
296
+ </FormSystemProvider>
297
+ </FormSystemProvider>
298
+ ```
299
+
300
+ ## useSchemaForm
301
+
302
+ Creates a form instance bound to a schema. Returns field binding and form lifecycle methods:
303
+
304
+ ```tsx
305
+ const form = useSchemaForm(UserSchema, {
306
+ createMissingStructure: true, // default: true — auto-create parent objects when setting nested values
307
+ validateOnMount: false, // default: false — set to true to show errors immediately on mount
308
+ validationDebounceMs: 300 // optional — debounce onChange validation (ms); validate()/submit() are always immediate
309
+ });
310
+ ```
311
+
312
+ ### Returned API
313
+
314
+ | Method | Description |
315
+ |--------|-------------|
316
+ | `form.useField(forProperty)` | Bind a field by PropertyDescriptor selector |
317
+ | `form.submit()` | Validate and return `ValidationResult` (includes `result.object` on success) |
318
+ | `form.validate()` | Run validation, propagate errors to fields |
319
+ | `form.reset(values?)` | Reset all fields; optionally set new initial values |
320
+ | `form.getValue()` | Get current form values as plain object |
321
+ | `form.setValue(values)` | Merge values into form state |
322
+
323
+ ## useField
324
+
325
+ Binds a single field via PropertyDescriptor selector. Can be used via `form.useField()` or the context-based standalone `useField()`:
326
+
327
+ ```tsx
328
+ // Via form instance
329
+ const name = form.useField((t) => t.name);
330
+ const city = form.useField((t) => t.address.city);
331
+
332
+ // Or via context (inside a FormProvider)
333
+ const name = useField((t) => t.name);
334
+ ```
335
+
336
+ ### Returned State & API
337
+
338
+ | Property | Type | Description |
339
+ |----------|------|-------------|
340
+ | `value` | `T \| undefined` | Current field value |
341
+ | `initialValue` | `T \| undefined` | Value at form init / last reset |
342
+ | `dirty` | `boolean` | `true` if value differs from initialValue |
343
+ | `touched` | `boolean` | `true` after `onBlur` has been called |
344
+ | `error` | `string \| undefined` | Validation error message from schema |
345
+ | `validating` | `boolean` | `true` during async validation |
346
+ | `onChange(value)` | `(T) => void` | Update field value |
347
+ | `onBlur()` | `() => void` | Mark field as touched |
348
+ | `setValue(value)` | `(T) => void` | Alias for `onChange` |
349
+ | `schema` | `SchemaBuilder` | The field's schema builder |
350
+
351
+ ## Field Component
352
+
353
+ Resolves the renderer from the `FormSystemProvider` registry by schema type (and optional variant), or uses an explicit `renderer` prop:
354
+
355
+ ```tsx
356
+ // Auto-resolved from FormSystemProvider (string schema → string renderer)
357
+ <Field forProperty={(t) => t.name} form={form} />
358
+
359
+ // Variant-based resolution: looks up "string:password", falls back to "string"
360
+ <Field forProperty={(t) => t.password} form={form} variant="password" />
361
+
362
+ // With label, name, and extra props for the renderer
363
+ <Field
364
+ forProperty={(t) => t.email}
365
+ form={form}
366
+ label="Email address"
367
+ name="email"
368
+ fieldProps={{ placeholder: 'you@example.com', autoComplete: 'email' }}
369
+ />
370
+
371
+ // Explicit renderer override
372
+ <Field forProperty={(t) => t.name} form={form} renderer={customRenderer} />
373
+ ```
374
+
375
+ ### Props
376
+
377
+ | Prop | Type | Description |
378
+ |------|------|-------------|
379
+ | `forProperty` | `(tree) => PropertyDescriptor` | PropertyDescriptor selector for the field |
380
+ | `form` | `SchemaFormInstance` | Form instance from `useSchemaForm` |
381
+ | `renderer?` | `FieldRenderer` | Optional explicit renderer (overrides provider) |
382
+ | `variant?` | `string` | Variant hint for renderer resolution and forwarded to the renderer |
383
+ | `label?` | `string` | Visible label text forwarded to the renderer |
384
+ | `name?` | `string` | HTML `name` attribute forwarded to the renderer |
385
+ | `fieldProps?` | `Record<string, unknown>` | Extra renderer-specific props (e.g. `placeholder`, `autoComplete`) |
386
+
387
+ ## Headless Usage (without Field component)
388
+
389
+ For full control over rendering, use `form.useField()` directly:
390
+
391
+ ```tsx
392
+ function UserForm() {
393
+ const form = useSchemaForm(UserSchema);
394
+ const name = form.useField((t) => t.name);
395
+ const email = form.useField((t) => t.email);
396
+
397
+ return (
398
+ <>
399
+ <input
400
+ value={name.value ?? ''}
401
+ onChange={(e) => name.onChange(e.target.value)}
402
+ onBlur={name.onBlur}
403
+ />
404
+ {name.touched && name.error && <span>{name.error}</span>}
405
+
406
+ <input
407
+ value={email.value ?? ''}
408
+ onChange={(e) => email.onChange(e.target.value)}
409
+ onBlur={email.onBlur}
410
+ />
411
+ <button onClick={() => form.submit()}>Submit</button>
412
+ </>
413
+ );
414
+ }
415
+ ```
416
+
417
+ ## FormProvider
418
+
419
+ Context bridge that allows standalone `useField()` usage outside of `form.useField()`:
420
+
421
+ ```tsx
422
+ import { FormProvider, useField } from '@cleverbrush/react-form';
423
+
424
+ function NameInput() {
425
+ const name = useField((t) => t.name);
426
+ return <input value={name.value ?? ''} onChange={(e) => name.onChange(e.target.value)} />;
427
+ }
428
+
429
+ function UserForm() {
430
+ const form = useSchemaForm(UserSchema);
431
+
432
+ return (
433
+ <FormProvider form={form}>
434
+ <NameInput />
435
+ </FormProvider>
436
+ );
437
+ }
438
+ ```
439
+
440
+ ## Nested Object Schemas
441
+
442
+ PropertyDescriptor selectors support nested paths:
443
+
444
+ ```tsx
445
+ const UserSchema = object({
446
+ name: string(),
447
+ address: object({
448
+ city: string(),
449
+ zip: number()
450
+ })
451
+ });
452
+
453
+ function UserForm() {
454
+ const form = useSchemaForm(UserSchema);
455
+
456
+ return (
457
+ <>
458
+ <Field forProperty={(t) => t.name} form={form} />
459
+ <Field forProperty={(t) => t.address.city} form={form} />
460
+ <Field forProperty={(t) => t.address.zip} form={form} />
461
+ </>
462
+ );
463
+ }
464
+ ```
465
+
466
+ ## Validation
467
+
468
+ Validation uses `@cleverbrush/schema` validators. Errors are automatically propagated to the corresponding field state:
469
+
470
+ ```tsx
471
+ const SignupSchema = object({
472
+ username: string().addValidator(async (val) => {
473
+ if (val.length < 3) {
474
+ return {
475
+ valid: false,
476
+ errors: [{ message: 'Username must be at least 3 characters' }]
477
+ };
478
+ }
479
+ return { valid: true };
480
+ }),
481
+ email: string()
482
+ });
483
+
484
+ function SignupForm() {
485
+ const form = useSchemaForm(SignupSchema);
486
+
487
+ return (
488
+ <FormSystemProvider renderers={htmlRenderers}>
489
+ <Field forProperty={(t) => t.username} form={form} />
490
+ <Field forProperty={(t) => t.email} form={form} />
491
+ <button onClick={async () => {
492
+ const result = await form.submit();
493
+ if (result.valid) {
494
+ console.log('Success:', result.object);
495
+ }
496
+ }}>Submit</button>
497
+ </FormSystemProvider>
498
+ );
499
+ }
500
+ ```
501
+
502
+ ### How validation works
503
+
504
+ 1. `form.validate()` or `form.submit()` runs the schema's full validation
505
+ 2. Per-property errors are resolved via `getErrorsFor()` using PropertyDescriptors
506
+ 3. Each field's `error` state is updated automatically
507
+ 4. Renderers receive the `error` string and `touched` boolean to decide how/when to display errors
508
+
509
+ ## End-to-End Example
510
+
511
+ Here's a complete, realistic example showing all pieces together — a registration form with nested address, custom validation, and MUI renderers:
512
+
513
+ ```tsx
514
+ import { object, string, number } from '@cleverbrush/schema';
515
+ import { useSchemaForm, FormSystemProvider, Field } from '@cleverbrush/react-form';
516
+ import { TextField } from '@mui/material';
517
+
518
+ // Schema — single source of truth for types, validation, and form fields
519
+ const RegistrationSchema = object({
520
+ name: string().required('Name is required').minLength(2, 'Too short'),
521
+ email: string().required('Email is required').matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email'),
522
+ age: number().required('Age is required').min(18, 'Must be 18+'),
523
+ address: object({
524
+ city: string().required('City is required'),
525
+ zip: string().required('ZIP is required').minLength(5, 'Invalid ZIP')
526
+ })
527
+ });
528
+
529
+ // Renderers — define once, reuse everywhere
530
+ const renderers = {
531
+ string: ({ value, onChange, onBlur, error, touched }) => (
532
+ <TextField
533
+ value={value ?? ''}
534
+ onChange={(e) => onChange(e.target.value)}
535
+ onBlur={onBlur}
536
+ error={touched && !!error}
537
+ helperText={touched ? error : undefined}
538
+ fullWidth
539
+ margin="normal"
540
+ />
541
+ ),
542
+ number: ({ value, onChange, onBlur, error, touched }) => (
543
+ <TextField
544
+ type="number"
545
+ value={value ?? ''}
546
+ onChange={(e) => onChange(Number(e.target.value))}
547
+ onBlur={onBlur}
548
+ error={touched && !!error}
549
+ helperText={touched ? error : undefined}
550
+ fullWidth
551
+ margin="normal"
552
+ />
553
+ )
554
+ };
555
+
556
+ // Form component — just declare which fields to show
557
+ function RegistrationForm() {
558
+ const form = useSchemaForm(RegistrationSchema);
559
+
560
+ return (
561
+ <div>
562
+ <Field forProperty={(t) => t.name} form={form} />
563
+ <Field forProperty={(t) => t.email} form={form} />
564
+ <Field forProperty={(t) => t.age} form={form} />
565
+ <Field forProperty={(t) => t.address.city} form={form} />
566
+ <Field forProperty={(t) => t.address.zip} form={form} />
567
+ <button onClick={async () => {
568
+ const result = await form.submit();
569
+ if (result.valid) {
570
+ console.log('Registered:', result.object);
571
+ }
572
+ }}>Register</button>
573
+ </div>
574
+ );
575
+ }
576
+
577
+ // App — wrap with provider
578
+ function App() {
579
+ return (
580
+ <FormSystemProvider renderers={renderers}>
581
+ <RegistrationForm />
582
+ </FormSystemProvider>
583
+ );
584
+ }
585
+ ```
586
+
587
+ ## API Reference
588
+
589
+ ### Exports
590
+
591
+ | Export | Type | Description |
592
+ |--------|------|-------------|
593
+ | `FormSystemProvider` | Component | Global renderer registry provider |
594
+ | `FormProvider` | Component | Form context bridge for standalone `useField` |
595
+ | `Field` | Component | Auto-rendered field by schema type |
596
+ | `useSchemaForm` | Hook | Create a form instance from schema |
597
+ | `useField` | Hook | Context-based field binding (use inside `FormProvider`) |
598
+ | `useFormSystem` | Hook | Access `FormSystemProvider` config |
599
+
600
+ ### Types
601
+
602
+ | Type | Description |
603
+ |------|-------------|
604
+ | `FieldRenderer` | `(props: FieldRenderProps) => ReactNode` |
605
+ | `FieldRenderProps` | Props passed to renderers: `value`, `initialValue`, `dirty`, `touched`, `error`, `validating`, `onChange`, `onBlur`, `setValue`, `schema`, `variant?`, `label?`, `name?`, `fieldProps?` |
606
+ | `FormSystemConfig` | `{ renderers?: Record<string, FieldRenderer> }` — keys can be `"type"` or `"type:variant"` |
607
+ | `FieldState` | `{ value, initialValue, dirty, touched, error, validating }` |
608
+ | `UseFieldResult` | `FieldState & { onChange, onBlur, setValue, schema }` |
609
+ | `UseSchemaFormOptions` | `{ createMissingStructure?: boolean; validateOnMount?: boolean; validationDebounceMs?: number }` |
610
+ | `SchemaFormInstance` | Return type of `useSchemaForm` |
611
+ | `FormSystemProviderProps` | Props for `FormSystemProvider` |
612
+ | `FormProviderProps` | Props for `FormProvider` |
613
+ | `FieldProps` | Props for `Field` |
614
+
615
+ ## Code Quality
616
+
617
+ - **Linting:** [Biome](https://biomejs.dev/) — enforced on every PR via CI
618
+ - **Type checking:** TypeScript strict mode — field selectors and form state are fully typed end-to-end
619
+ - **Unit tests:** [Vitest](https://vitest.dev/) + [Testing Library](https://testing-library.com/) — covering form state management, validation, async validators, field rendering, and provider configuration
620
+ - **CI:** Every pull request must pass lint + build + test before merge — see [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)
621
+
622
+ ## License
623
+
624
+ BSD-3-Clause
@@ -0,0 +1,17 @@
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
+ */
6
+ export declare function createFormStore(initialValues: any): {
7
+ getFieldState: (path: string) => FieldState;
8
+ updateFieldState: (path: string, patch: Partial<FieldState>) => void;
9
+ subscribe: (path: string, listener: () => void) => () => void;
10
+ subscribeGlobal: (listener: () => void) => () => void;
11
+ getValues: () => any;
12
+ setValues: (newValues: any) => void;
13
+ resetAll: (newInitialValues?: any) => void;
14
+ notifyAll: () => void;
15
+ getAllFieldPaths: () => string[];
16
+ };
17
+ export type FormStore = ReturnType<typeof createFormStore>;
@@ -0,0 +1,92 @@
1
+ import type { InferType, ObjectSchemaBuilder, PropertyDescriptor, PropertyDescriptorTree, SchemaBuilder } from '@cleverbrush/schema';
2
+ import type React from 'react';
3
+ import type { SchemaFormInstance } from './hooks.js';
4
+ import type { FieldRenderer, FormSystemConfig, UseFieldResult } from './types.js';
5
+ export type FormSystemProviderProps = {
6
+ /**
7
+ * Renderer registry mapping schema types to renderer functions.
8
+ * Example: `{ string: (props) => <input .../>, number: (props) => <input type="number" .../> }`
9
+ */
10
+ renderers?: Record<string, FieldRenderer>;
11
+ /**
12
+ * Full configuration object (for future extensibility).
13
+ * If both `renderers` and `config.renderers` are provided, `renderers` takes precedence.
14
+ */
15
+ config?: FormSystemConfig;
16
+ children: React.ReactNode;
17
+ };
18
+ /**
19
+ * Provides global renderer configuration via React Context.
20
+ * Supports nesting — inner provider overrides outer.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <FormSystemProvider renderers={htmlRenderers}>
25
+ * <App />
26
+ * </FormSystemProvider>
27
+ * ```
28
+ */
29
+ export declare function FormSystemProvider({ renderers, config, children }: FormSystemProviderProps): React.ReactNode;
30
+ export type FormProviderProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {
31
+ form: SchemaFormInstance<TSchema>;
32
+ children: React.ReactNode;
33
+ };
34
+ /**
35
+ * Provides form context to children so they can use the context-based useField.
36
+ */
37
+ export declare function FormProvider<TSchema extends ObjectSchemaBuilder<any, any, any>>({ form, children }: FormProviderProps<TSchema>): React.ReactNode;
38
+ /**
39
+ * Context-based useField — can be used inside a FormProvider.
40
+ * For best IntelliSense, prefer `form.useField()` which infers field types from the schema.
41
+ * When using this context-based version, specify the schema type explicitly:
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const name = useField<typeof MySchema>((t) => t.name);
46
+ * ```
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>>;
49
+ /**
50
+ * Hook to access FormSystem configuration.
51
+ */
52
+ export declare function useFormSystem(): FormSystemConfig;
53
+ export type FieldProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {
54
+ forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, any, any>;
55
+ form: SchemaFormInstance<TSchema>;
56
+ renderer?: FieldRenderer;
57
+ /**
58
+ * Rendering variant hint. Participates in renderer resolution:
59
+ * the registry is checked for `"type:variant"` (e.g. `"string:password"`)
60
+ * before falling back to the base `"type"` key.
61
+ * Also forwarded to the renderer via `FieldRenderProps.variant`.
62
+ */
63
+ variant?: string;
64
+ /** Visible label text forwarded to the renderer via `FieldRenderProps.label`. */
65
+ label?: string;
66
+ /** HTML `name` attribute forwarded to the renderer via `FieldRenderProps.name`. */
67
+ name?: string;
68
+ /**
69
+ * Bag of extra renderer-specific props forwarded to the renderer via
70
+ * `FieldRenderProps.fieldProps` (e.g. `placeholder`, `autoComplete`).
71
+ */
72
+ fieldProps?: Record<string, unknown>;
73
+ };
74
+ /**
75
+ * UI-agnostic Field component.
76
+ *
77
+ * Resolves a renderer from:
78
+ * 1. Explicit `renderer` prop
79
+ * 2. FormSystemProvider registry — checked for `"type:variant"` first,
80
+ * then for the base `"type"`
81
+ *
82
+ * All rendering hints (`variant`, `label`, `name`, `fieldProps`) are
83
+ * forwarded to the resolved renderer via `FieldRenderProps`.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * <Field forProperty={(t) => t.password} form={form}
88
+ * variant="password" label="Password"
89
+ * fieldProps={{ placeholder: "Enter password", autoComplete: "current-password" }} />
90
+ * ```
91
+ */
92
+ export declare function Field<TSchema extends ObjectSchemaBuilder<any, any, any>>({ forProperty, form, renderer, variant, label, name, fieldProps }: FieldProps<TSchema>): React.ReactNode;
@@ -0,0 +1,22 @@
1
+ import type { ObjectSchemaBuilder, PropertyDescriptorInner, PropertyDescriptorTree } from '@cleverbrush/schema';
2
+ import type { FormStore } from './FormStore.js';
3
+ import type { FormSystemConfig, UseSchemaFormOptions } from './types.js';
4
+ /**
5
+ * Context for global form system configuration (renderers, etc).
6
+ */
7
+ export declare const FormSystemContext: import("react").Context<FormSystemConfig | null>;
8
+ /**
9
+ * Internal form context value — used to bridge useSchemaForm store
10
+ * to the context-based useField and Field component.
11
+ */
12
+ export type FormContextValue = {
13
+ store: FormStore;
14
+ descriptorTree: PropertyDescriptorTree<any, any>;
15
+ schema: ObjectSchemaBuilder<any, any, any>;
16
+ options: UseSchemaFormOptions;
17
+ pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;
18
+ };
19
+ /**
20
+ * Context for per-form data — allows useField to work via context.
21
+ */
22
+ export declare const FormContext: import("react").Context<FormContextValue | null>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A debounce function ensures that a function is not called too frequently.
3
+ * It only allows the function to be executed after a specified delay has passed since the last call.
4
+ * @param func The function to debounce.
5
+ * @param wait The delay in ms before the function is called.
6
+ */
7
+ export declare function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void;
@@ -0,0 +1,38 @@
1
+ import type { PropertyDescriptorInner, PropertyDescriptorTree, SchemaBuilder } from '@cleverbrush/schema';
2
+ import { ObjectSchemaBuilder } from '@cleverbrush/schema';
3
+ /**
4
+ * Builds a map from PropertyDescriptorInner identity → dot-separated path
5
+ * by traversing the descriptor tree recursively.
6
+ */
7
+ export declare function buildDescriptorPathMap(tree: PropertyDescriptorTree<any, any>, schema: ObjectSchemaBuilder<any, any, any>): Map<PropertyDescriptorInner<any, any, any>, string>;
8
+ /**
9
+ * Looks up the path for a given descriptor from the pre-built map.
10
+ */
11
+ export declare function getDescriptorPath(inner: PropertyDescriptorInner<any, any, any>, pathMap: Map<PropertyDescriptorInner<any, any, any>, string>): string;
12
+ /**
13
+ * Returns the schema type string (e.g. "string", "number", "object").
14
+ */
15
+ export declare function getSchemaType(schema: SchemaBuilder<any, any, any>): string;
16
+ /**
17
+ * Builds a selector function from a dot-separated path string.
18
+ * The selector traverses the PropertyDescriptorTree by property access.
19
+ * Example: "customer.address.city" → (tree) => tree.customer.address.city
20
+ */
21
+ export declare function buildSelectorFromPath(path: string): (tree: any) => any;
22
+ /**
23
+ * Checks whether a validation error path matches a field's expected error path.
24
+ * Matches exact path, path with nested suffix, or path with validator suffix.
25
+ * Example: isErrorPathMatch("$.user.name", "$.user.name") → true
26
+ * isErrorPathMatch("$.user.name($validators[0])", "$.user.name") → true
27
+ */
28
+ export declare function isErrorPathMatch(errPath: string, fieldErrorPath: string): boolean;
29
+ /**
30
+ * Ensures all nested object structures exist in the values object
31
+ * by traversing the schema and creating empty objects where needed.
32
+ * This prevents ObjectSchemaBuilder.validate() from throwing when
33
+ * nested object properties are undefined.
34
+ *
35
+ * Example: ensureNestedStructure({}, OrderSchema)
36
+ * → { customer: { address: {} } }
37
+ */
38
+ export declare function ensureNestedStructure(values: any, schema: ObjectSchemaBuilder<any, any, any>): any;
@@ -0,0 +1,36 @@
1
+ import type { InferType, PropertyDescriptor, PropertyDescriptorTree, SchemaBuilder, ValidationResult } from '@cleverbrush/schema';
2
+ import { ObjectSchemaBuilder } from '@cleverbrush/schema';
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
+ */
9
+ 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>>;
11
+ submit: () => Promise<ValidationResult<InferType<TSchema>>>;
12
+ validate: () => Promise<ValidationResult<InferType<TSchema>>>;
13
+ reset: (values?: Partial<InferType<TSchema>>) => void;
14
+ getValue: () => InferType<TSchema>;
15
+ setValue: (values: Partial<InferType<TSchema>>) => void;
16
+ /** @internal — Used by FormProvider and Field to access internal context */
17
+ _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
+ */
23
+ 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;
@@ -0,0 +1,5 @@
1
+ export type { FieldProps, FormProviderProps, FormSystemProviderProps } from './components.js';
2
+ export { Field, FormProvider, FormSystemProvider, useField, useFormSystem } from './components.js';
3
+ export type { SchemaFormInstance } from './hooks.js';
4
+ export { useSchemaForm } from './hooks.js';
5
+ export type { FieldRenderer, FieldRenderProps, FieldState, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
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};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"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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","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"]}
@@ -0,0 +1,132 @@
1
+ import type { SchemaBuilder } from '@cleverbrush/schema';
2
+ import type { ReactNode } from 'react';
3
+ /**
4
+ * A renderer function that receives field state and returns a React node.
5
+ */
6
+ export type FieldRenderer = (props: FieldRenderProps) => ReactNode;
7
+ /**
8
+ * Props passed to a field renderer.
9
+ */
10
+ export type FieldRenderProps = {
11
+ value: any;
12
+ initialValue: any;
13
+ dirty: boolean;
14
+ touched: boolean;
15
+ error: string | undefined;
16
+ validating: boolean;
17
+ onChange: (value: any) => void;
18
+ onBlur: () => void;
19
+ setValue: (value: any) => void;
20
+ schema: SchemaBuilder<any, any, any>;
21
+ /**
22
+ * Rendering variant hint passed from the `Field` component.
23
+ * Used by renderers to select a sub-variant of the base schema type
24
+ * (e.g. `"password"` for a string field rendered as a password input).
25
+ *
26
+ * Also participates in renderer resolution: when set, the renderer
27
+ * registry is first checked for `"type:variant"` (e.g. `"string:password"`)
28
+ * before falling back to the base `"type"` key.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <Field forProperty={(t) => t.secret} form={form} variant="password" />
33
+ * ```
34
+ */
35
+ variant?: string;
36
+ /**
37
+ * Visible label text forwarded from the `Field` component.
38
+ * Renderers can use this to render a `<label>` element.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * <Field forProperty={(t) => t.name} form={form} label="Full name" />
43
+ * ```
44
+ */
45
+ label?: string;
46
+ /**
47
+ * HTML `name` attribute forwarded from the `Field` component.
48
+ * Renderers can apply this to the underlying input for `FormData` submission.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * <Field forProperty={(t) => t.email} form={form} name="email" />
53
+ * ```
54
+ */
55
+ name?: string;
56
+ /**
57
+ * Bag of extra renderer-specific props forwarded from the `Field` component.
58
+ * Useful for passing HTML attributes (`placeholder`, `autoComplete`, `type`)
59
+ * or UI-library-specific options without extending `FieldRenderProps` itself.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * <Field
64
+ * forProperty={(t) => t.email}
65
+ * form={form}
66
+ * fieldProps={{ placeholder: "you@example.com", autoComplete: "email" }}
67
+ * />
68
+ * ```
69
+ */
70
+ fieldProps?: Record<string, unknown>;
71
+ };
72
+ /**
73
+ * Configuration for FormSystemProvider.
74
+ */
75
+ export type FormSystemConfig = {
76
+ renderers?: Record<string, FieldRenderer>;
77
+ };
78
+ /**
79
+ * Field state tracked per-field.
80
+ */
81
+ export type FieldState = {
82
+ value: any;
83
+ initialValue: any;
84
+ dirty: boolean;
85
+ touched: boolean;
86
+ error: string | undefined;
87
+ validating: boolean;
88
+ };
89
+ /**
90
+ * Return type for useField — strongly typed with the inferred property value type.
91
+ * When the value type cannot be inferred, falls back to `any`.
92
+ */
93
+ export type UseFieldResult<T = any> = {
94
+ value: T | undefined;
95
+ initialValue: T | undefined;
96
+ dirty: boolean;
97
+ touched: boolean;
98
+ error: string | undefined;
99
+ validating: boolean;
100
+ onChange: (value: T) => void;
101
+ onBlur: () => void;
102
+ setValue: (value: T) => void;
103
+ schema: SchemaBuilder<any, any, any>;
104
+ };
105
+ /**
106
+ * Options for useSchemaForm.
107
+ */
108
+ export type UseSchemaFormOptions = {
109
+ createMissingStructure?: boolean;
110
+ /**
111
+ * When `true`, runs full schema validation on mount and marks all fields
112
+ * as touched so that error messages (e.g. for required fields) are visible
113
+ * immediately without waiting for user interaction.
114
+ *
115
+ * @default false
116
+ */
117
+ validateOnMount?: boolean;
118
+ /**
119
+ * Debounce delay in milliseconds for onChange-triggered validation.
120
+ * When set, rapid field changes only trigger one validation run after
121
+ * the user stops typing for the specified duration.
122
+ *
123
+ * Explicit calls to `form.validate()`, `form.submit()`, and
124
+ * `validateOnMount` are **not** debounced — they always run immediately.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * const form = useSchemaForm(Schema, { validationDebounceMs: 300 });
129
+ * ```
130
+ */
131
+ validationDebounceMs?: number;
132
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
3
+ "bugs": {
4
+ "url": "https://github.com/cleverbrush/framework/issues",
5
+ "email": "andrew_zol@cleverbrush.com"
6
+ },
7
+ "dependencies": {
8
+ "@cleverbrush/schema": "0.0.0-beta-20260410073748"
9
+ },
10
+ "devDependencies": {
11
+ "@types/react": "^19.0.0",
12
+ "react": "^19.0.0"
13
+ },
14
+ "peerDependencies": {
15
+ "react": ">=18.0.0"
16
+ },
17
+ "description": "Headless, schema-driven form system for React based on @cleverbrush/schema",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "homepage": "https://docs.cleverbrush.com/modules/_cleverbrush_react_form.html",
22
+ "keywords": [
23
+ "react forms",
24
+ "schema validation",
25
+ "form system",
26
+ "cleverbrush"
27
+ ],
28
+ "license": "BSD 3-Clause",
29
+ "main": "./dist/index.js",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "sideEffects": false,
37
+ "name": "@cleverbrush/react-form",
38
+ "readme": "https://github.com/cleverbrush/framework/tree/master/libs/react-form#readme",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "github:cleverbrush/framework"
42
+ },
43
+ "scripts": {
44
+ "watch": "tsc --build --watch",
45
+ "build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
46
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
47
+ },
48
+ "type": "module",
49
+ "types": "./dist/index.d.ts",
50
+ "version": "0.0.0-beta-20260410073748"
51
+ }