jsonisch 0.1.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.
@@ -0,0 +1,476 @@
1
+ import { C as FieldErrors, G as JsonSchema, K as Path, M as ControlKind, S as FieldElement, a as InternalFormStore, i as FormValidator, r as FormConfig } from "../form-ref-D_xHSaqL.js";
2
+ import { t as SubmitHandler } from "../handle-submit-Dk7QW3_q.js";
3
+ import { ComponentType, FormHTMLAttributes, ReactElement, ReactNode } from "react";
4
+
5
+ //#region src/react/types.d.ts
6
+
7
+ /**
8
+ * The public form store returned by `useForm`/`useAppForm`: an immutable
9
+ * snapshot whose identity changes when any observed form-level value
10
+ * changes — reactivity rides on the object, not on property reads, so it
11
+ * composes with React Compiler memoization. The internal store is
12
+ * reachable for the methods layer — every method accepts this wrapper
13
+ * directly.
14
+ */
15
+ interface FormStore {
16
+ /**
17
+ * The internal form store (the methods layer unwraps it).
18
+ */
19
+ readonly internal: InternalFormStore;
20
+ /**
21
+ * Whether the form is currently submitting.
22
+ */
23
+ readonly isSubmitting: boolean;
24
+ /**
25
+ * Whether the form has been submitted.
26
+ */
27
+ readonly isSubmitted: boolean;
28
+ /**
29
+ * Whether the form is currently validating.
30
+ */
31
+ readonly isValidating: boolean;
32
+ /**
33
+ * Whether any field in the form has been touched.
34
+ */
35
+ readonly isTouched: boolean;
36
+ /**
37
+ * Whether any field in the form has been edited.
38
+ */
39
+ readonly isEdited: boolean;
40
+ /**
41
+ * Whether any field in the form differs from its start input.
42
+ */
43
+ readonly isDirty: boolean;
44
+ /**
45
+ * Whether no field in the form has errors.
46
+ */
47
+ readonly isValid: boolean;
48
+ /**
49
+ * The form-level (root) errors. Use `getDeepErrors` for all field errors.
50
+ */
51
+ readonly errors: FieldErrors;
52
+ }
53
+ /**
54
+ * The props a field store provides for binding a DOM element.
55
+ */
56
+ interface FieldElementProps {
57
+ /**
58
+ * The name attribute of the field element.
59
+ */
60
+ readonly name: string;
61
+ /**
62
+ * Whether to autofocus the element (set while the field has errors).
63
+ */
64
+ readonly autoFocus: boolean;
65
+ /**
66
+ * The ref callback registering the element on the field store.
67
+ */
68
+ readonly ref: (element: FieldElement | null) => void;
69
+ /**
70
+ * The focus handler (marks the field touched). Takes no arguments so it
71
+ * assigns to any element's focus handler type.
72
+ */
73
+ readonly onFocus: () => void;
74
+ /**
75
+ * The blur handler (triggers blur-mode validation). Takes no arguments so
76
+ * it assigns to any element's blur handler type.
77
+ */
78
+ readonly onBlur: () => void;
79
+ }
80
+ /**
81
+ * The plugin-contributed members of `FieldStore` — an empty marker each
82
+ * plugin merges its `fieldSnapshot` keys into via `declare module`
83
+ * augmentation IN ITS OWN FILE (fastify's decorate pattern), so widgets
84
+ * type-check flat members (`field.mode`, `field.setEntryMode(...)`) with
85
+ * no generics anywhere and ownership stays greppable. The react adapter
86
+ * itself knows no plugin's vocabulary; the runtime twin of this contract
87
+ * is `dispatchFieldSnapshot`'s collision throw.
88
+ */
89
+ interface FieldStoreSlots {}
90
+ /**
91
+ * The public field store returned by `useField`: an immutable snapshot —
92
+ * a new object identity whenever any observed value changes, stable
93
+ * otherwise. Widgets are plain controlled components that render `input`
94
+ * and call `onChange` with the new value; no signal hook, no directive.
95
+ * Plugin-contributed members ride `FieldStoreSlots`.
96
+ */
97
+ interface FieldStore extends FieldStoreSlots {
98
+ /**
99
+ * The path to the field within the form.
100
+ */
101
+ readonly path: Path;
102
+ /**
103
+ * The property name of the field (last path segment).
104
+ */
105
+ readonly name: string;
106
+ /**
107
+ * The JSON-Schema node of the field (title, enum options, formats, …).
108
+ */
109
+ readonly schema: JsonSchema;
110
+ /**
111
+ * The widget kind the field renders as (resolved at walk time).
112
+ */
113
+ readonly control: ControlKind;
114
+ /**
115
+ * The current input value of the field.
116
+ */
117
+ readonly input: unknown;
118
+ /**
119
+ * The current error messages of the field.
120
+ */
121
+ readonly errors: FieldErrors;
122
+ /**
123
+ * Whether the field (or a descendant) has been touched.
124
+ */
125
+ readonly isTouched: boolean;
126
+ /**
127
+ * Whether the field (or a descendant) has been edited.
128
+ */
129
+ readonly isEdited: boolean;
130
+ /**
131
+ * Whether the field (or a descendant) differs from its start input.
132
+ */
133
+ readonly isDirty: boolean;
134
+ /**
135
+ * Whether the field and its descendants have no errors.
136
+ */
137
+ readonly isValid: boolean;
138
+ /**
139
+ * Whether the field currently renders: `false` for a `hidden` control or
140
+ * while a conditional-visibility rule (`visibleWhen`) is unsatisfied.
141
+ * Registry dispatch skips invisible fields; headless layouts decide
142
+ * themselves. A hidden field keeps its state, dirtiness, and place in
143
+ * the payload.
144
+ */
145
+ readonly visible: boolean;
146
+ /**
147
+ * Sets the field input (controlled-component change handler).
148
+ */
149
+ readonly onChange: (value: unknown) => void;
150
+ /**
151
+ * The props to spread onto the field element.
152
+ */
153
+ readonly props: FieldElementProps;
154
+ }
155
+ /**
156
+ * The public field array store returned by `useFieldArray`.
157
+ */
158
+ interface FieldArrayStore {
159
+ /**
160
+ * The path to the array field within the form.
161
+ */
162
+ readonly path: Path;
163
+ /**
164
+ * The stable item IDs, one per row — use them as React keys so state
165
+ * follows its row across reorders.
166
+ */
167
+ readonly items: string[];
168
+ /**
169
+ * The current error messages of the array field itself.
170
+ */
171
+ readonly errors: FieldErrors;
172
+ /**
173
+ * Whether the array (or a descendant) has been touched.
174
+ */
175
+ readonly isTouched: boolean;
176
+ /**
177
+ * Whether the array (or a descendant) has been edited.
178
+ */
179
+ readonly isEdited: boolean;
180
+ /**
181
+ * Whether the array (or a descendant) differs from its start input.
182
+ */
183
+ readonly isDirty: boolean;
184
+ /**
185
+ * Whether the array and its descendants have no errors.
186
+ */
187
+ readonly isValid: boolean;
188
+ }
189
+ /**
190
+ * The props every registry widget receives.
191
+ */
192
+ interface WidgetProps {
193
+ /**
194
+ * The field store the widget renders.
195
+ */
196
+ readonly field: FieldStore;
197
+ /**
198
+ * The form store the field belongs to.
199
+ */
200
+ readonly form: FormStore;
201
+ }
202
+ /**
203
+ * The configuration of `createFormHook`: the registry mapping widget kinds
204
+ * to components, plus the injected validator compiler.
205
+ */
206
+ interface FormHookConfig {
207
+ /**
208
+ * The widget registry, keyed by `ControlKind`. A kind without an entry
209
+ * renders the `fallback`.
210
+ */
211
+ readonly widgets: Partial<Record<ControlKind, ComponentType<WidgetProps>>>;
212
+ /**
213
+ * The widget rendered for a kind without a registry entry. Without one, a
214
+ * built-in placeholder names the unsupported kind visibly.
215
+ */
216
+ readonly fallback?: ComponentType<WidgetProps> | undefined;
217
+ /**
218
+ * Compiles the injected validator for a schema, called ONCE per form
219
+ * (memoized by `useAppForm`). Omit for forms without enforcement.
220
+ */
221
+ readonly validate?: ((schema: JsonSchema) => FormValidator) | undefined;
222
+ }
223
+ /**
224
+ * The configuration of `useAppForm`: the store config minus the validator,
225
+ * which the hook compiles itself via the registry's `validate`.
226
+ */
227
+ type UseAppFormConfig = Omit<FormConfig, "validator">;
228
+ //#endregion
229
+ //#region src/react/components.d.ts
230
+ /**
231
+ * Props of the headless `Form` component.
232
+ */
233
+ interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit" | "noValidate"> {
234
+ /**
235
+ * The form store instance.
236
+ */
237
+ readonly of: FormStore;
238
+ /**
239
+ * The submit handler called with the validated output when validation
240
+ * succeeds.
241
+ */
242
+ readonly onSubmit: SubmitHandler;
243
+ /**
244
+ * The form contents.
245
+ */
246
+ readonly children?: ReactNode;
247
+ }
248
+ /**
249
+ * Headless form component: a native `<form noValidate>` wired to
250
+ * `handleSubmit` — an invalid submit blocks the handler and focuses the
251
+ * first erroring field. The registry-aware `Form` from `createFormHook`
252
+ * builds on this and adds whole-form auto-rendering.
253
+ *
254
+ * @param props The form component props.
255
+ *
256
+ * @returns A native form element.
257
+ */
258
+ declare function Form({
259
+ of,
260
+ onSubmit,
261
+ ...other
262
+ }: FormProps): ReactElement;
263
+ /**
264
+ * Props of the headless `Field` component.
265
+ */
266
+ interface FieldProps {
267
+ /**
268
+ * The form store the field belongs to.
269
+ */
270
+ readonly of: FormStore;
271
+ /**
272
+ * The path to the field.
273
+ */
274
+ readonly path: Path;
275
+ /**
276
+ * The render function receiving the field store.
277
+ */
278
+ readonly children: (field: FieldStore) => ReactNode;
279
+ }
280
+ /**
281
+ * Headless field component — the escape hatch for custom layouts: takes a
282
+ * form store and a path, and calls the render function with the reactive
283
+ * field store.
284
+ *
285
+ * @param props The field component props.
286
+ *
287
+ * @returns The rendered field UI.
288
+ */
289
+ declare function Field({
290
+ of,
291
+ path,
292
+ children
293
+ }: FieldProps): ReactNode;
294
+ /**
295
+ * Props of the headless `FieldArray` component.
296
+ */
297
+ interface FieldArrayProps {
298
+ /**
299
+ * The form store the array field belongs to.
300
+ */
301
+ readonly of: FormStore;
302
+ /**
303
+ * The path to the array field.
304
+ */
305
+ readonly path: Path;
306
+ /**
307
+ * The render function receiving the field array store.
308
+ */
309
+ readonly children: (fieldArray: FieldArrayStore) => ReactNode;
310
+ }
311
+ /**
312
+ * Headless field array component: calls the render function with the
313
+ * reactive field array store (stable item IDs for React keys).
314
+ *
315
+ * @param props The field array component props.
316
+ *
317
+ * @returns The rendered field array UI.
318
+ */
319
+ declare function FieldArray({
320
+ of,
321
+ path,
322
+ children
323
+ }: FieldArrayProps): ReactNode;
324
+ //#endregion
325
+ //#region src/react/create-form-hook.d.ts
326
+ /**
327
+ * Props of the registry-aware `Field` component returned by
328
+ * `createFormHook`.
329
+ */
330
+ interface AppFieldProps {
331
+ /**
332
+ * The form store the field belongs to.
333
+ */
334
+ readonly of: FormStore;
335
+ /**
336
+ * The path to the field.
337
+ */
338
+ readonly path: Path;
339
+ /**
340
+ * Optional render function for a custom layout (headless mode). Without
341
+ * it the field renders its registry widget.
342
+ */
343
+ readonly children?: ((field: FieldStore) => ReactNode) | undefined;
344
+ }
345
+ /**
346
+ * The bound form API returned by `createFormHook`.
347
+ */
348
+ interface FormHook {
349
+ /**
350
+ * Creates a form store, compiling the injected validator once for the
351
+ * form's schema.
352
+ */
353
+ readonly useAppForm: (config: UseAppFormConfig) => FormStore;
354
+ /**
355
+ * The form component. Without children it renders the WHOLE form from
356
+ * the schema via the widget registry — no hand-written field components.
357
+ */
358
+ readonly Form: (props: FormProps) => ReactElement;
359
+ /**
360
+ * The field component: registry-dispatched without children, headless
361
+ * with a render function.
362
+ */
363
+ readonly Field: (props: AppFieldProps) => ReactNode;
364
+ /**
365
+ * The auto-rendered field list (every non-hidden root field through its
366
+ * registry widget). `Form` renders it when given no children; use it
367
+ * directly to compose auto fields with custom chrome (e.g. a footer).
368
+ */
369
+ readonly Fields: (props: {
370
+ readonly of: FormStore;
371
+ }) => ReactElement;
372
+ }
373
+ /**
374
+ * Creates the app's form API around a widget registry (the TanStack
375
+ * `createFormHook` borrow): widgets are registered ONCE at module level,
376
+ * and every form derives its fields from the schema through them.
377
+ *
378
+ * @param config The registry and injected validator compiler.
379
+ *
380
+ * @returns The bound `useAppForm`, `Form` and `Field`.
381
+ */
382
+ declare function createFormHook(config: FormHookConfig): FormHook;
383
+ //#endregion
384
+ //#region src/react/use-field.d.ts
385
+ /**
386
+ * Creates a reactive field store for the field at the given path. Widgets
387
+ * are controlled components: render `field.input`, call
388
+ * `field.onChange(value)`, and spread `field.props` onto the DOM element so
389
+ * focus/blur validation modes and focus-on-error work.
390
+ *
391
+ * The returned store is an immutable SNAPSHOT: its identity changes when
392
+ * any observed value changes and is stable otherwise, so it composes with
393
+ * React Compiler memoization instead of fighting it (the LOS-567/LOS-602
394
+ * rewrite). Callbacks and DOM plumbing keep a stable identity for the
395
+ * field's lifetime.
396
+ *
397
+ * @param form The form store the field belongs to.
398
+ * @param path The path to the field.
399
+ *
400
+ * @returns The field store snapshot.
401
+ */
402
+ declare function useField(form: FormStore, path: Path): FieldStore;
403
+ //#endregion
404
+ //#region src/react/use-field-array.d.ts
405
+ /**
406
+ * Creates a reactive field array store for the array field at the given
407
+ * path. Render one row per `items` entry and use the item ID as the React
408
+ * key so row state and DOM follow their row across reorders.
409
+ *
410
+ * The returned store is an immutable SNAPSHOT (see `useField` for the
411
+ * model).
412
+ *
413
+ * @param form The form store the array field belongs to.
414
+ * @param path The path to the array field.
415
+ *
416
+ * @returns The field array store snapshot.
417
+ */
418
+ declare function useFieldArray(form: FormStore, path: Path): FieldArrayStore;
419
+ //#endregion
420
+ //#region src/react/use-form.d.ts
421
+ /**
422
+ * Creates a reactive form store from a form configuration. The store is
423
+ * created once for the component's lifetime — config changes after mount
424
+ * are ignored (`applyBaseline` rebases on a fresh server record; `reset`
425
+ * with a new `initialInput` discards in-flight edits with it).
426
+ *
427
+ * The returned store is an immutable SNAPSHOT over the stable `internal`
428
+ * store: its identity changes when any observed form-level value changes
429
+ * (see `useField` for the model).
430
+ *
431
+ * @param config The form configuration.
432
+ *
433
+ * @returns The form store snapshot.
434
+ */
435
+ declare function useForm(config: FormConfig): FormStore;
436
+ //#endregion
437
+ //#region src/react/use-signal-snapshot.d.ts
438
+ /**
439
+ * Value-level equality for snapshot objects: top-level keys compared with
440
+ * `Object.is`, with ONE extra level for plain arrays and plain objects —
441
+ * computed results (a `DerivedState`, an errors array) are rebuilt with a
442
+ * fresh identity on every recompute, and without the value compare every
443
+ * notification would produce a render even when nothing visible changed.
444
+ * Deeper nesting falls back to "not equal" (re-render), never the other
445
+ * way — a false negative costs one render, a false positive would cost a
446
+ * stale UI.
447
+ */
448
+ declare function snapshotEqual(a: unknown, b: unknown): boolean;
449
+ /**
450
+ * Subscribes the component to exactly the signals `compute` reads and
451
+ * returns the computed snapshot. THE reactive primitive of the react
452
+ * adapter — every jsonisch hook reads through it, and it is the public
453
+ * escape hatch for a component that needs a narrower subscription than
454
+ * `useField` provides.
455
+ *
456
+ * React-Compiler-safe by construction: the tracked reads happen inside a
457
+ * closure the library invokes — the compiler can only elide expressions
458
+ * whose call sites it memoized, never a call made from a hook's internals —
459
+ * and the returned snapshot is an immutable value whose identity changes
460
+ * when any observed value changes, so compiler memo caches keyed on it
461
+ * invalidate correctly instead of freezing.
462
+ *
463
+ * `useMemo` semantics apply to `deps`: the compute closure is captured when
464
+ * `deps` change, so everything it reads must be a signal (tracked) or
465
+ * listed in `deps` (recreates the store). `isEqual` gates re-renders by
466
+ * value; it defaults to `snapshotEqual`.
467
+ *
468
+ * @param compute The tracked read producing the snapshot.
469
+ * @param deps The non-signal inputs of `compute` (useMemo contract).
470
+ * @param isEqual The snapshot equality gate.
471
+ *
472
+ * @returns The current snapshot.
473
+ */
474
+ declare function useSignalSnapshot<T>(compute: () => T, deps: readonly unknown[], isEqual?: (a: T, b: T) => boolean): T;
475
+ //#endregion
476
+ export { type AppFieldProps, Field, FieldArray, type FieldArrayProps, type FieldArrayStore, type FieldElementProps, type FieldProps, type FieldStore, type FieldStoreSlots, Form, type FormHook, type FormHookConfig, type FormProps, type FormStore, type UseAppFormConfig, type WidgetProps, createFormHook, snapshotEqual, useField, useFieldArray, useForm, useSignalSnapshot };