attaform 0.16.4 → 0.17.1

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.
Files changed (84) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/devtools.cjs +19 -12
  3. package/dist/chunks/devtools.cjs.map +1 -1
  4. package/dist/chunks/devtools.mjs +19 -12
  5. package/dist/chunks/devtools.mjs.map +1 -1
  6. package/dist/chunks/indexeddb.cjs +1 -1
  7. package/dist/chunks/indexeddb.mjs +1 -1
  8. package/dist/chunks/local-storage.cjs +1 -1
  9. package/dist/chunks/local-storage.mjs +1 -1
  10. package/dist/chunks/session-storage.cjs +1 -1
  11. package/dist/chunks/session-storage.mjs +1 -1
  12. package/dist/index.cjs +26 -7
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +52 -9
  15. package/dist/index.d.mts +52 -9
  16. package/dist/index.d.ts +52 -9
  17. package/dist/index.mjs +28 -9
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/nuxt.d.cts +1 -1
  20. package/dist/nuxt.d.mts +1 -1
  21. package/dist/nuxt.d.ts +1 -1
  22. package/dist/runtime/plugins/attaform.cjs +3 -3
  23. package/dist/runtime/plugins/attaform.cjs.map +1 -1
  24. package/dist/runtime/plugins/attaform.mjs +3 -3
  25. package/dist/runtime/plugins/attaform.mjs.map +1 -1
  26. package/dist/shared/{attaform.CMRmwGDt.d.cts → attaform.B1jvxsOF.d.mts} +1 -1
  27. package/dist/shared/{attaform.DyV1O4tI.mjs → attaform.B3ZaPIzS.mjs} +1436 -391
  28. package/dist/shared/attaform.B3ZaPIzS.mjs.map +1 -0
  29. package/dist/shared/{attaform.Dd_pWnmn.cjs → attaform.B5qiXQwN.cjs} +59 -10
  30. package/dist/shared/attaform.B5qiXQwN.cjs.map +1 -0
  31. package/dist/shared/{attaform.CIwZtbGV.cjs → attaform.BBM2muQ9.cjs} +2 -2
  32. package/dist/shared/{attaform.CIwZtbGV.cjs.map → attaform.BBM2muQ9.cjs.map} +1 -1
  33. package/dist/shared/{attaform.keLBaHB6.cjs → attaform.BV40t5y2.cjs} +240 -115
  34. package/dist/shared/attaform.BV40t5y2.cjs.map +1 -0
  35. package/dist/shared/attaform.C0iFnTN0.d.ts +165 -0
  36. package/dist/shared/{attaform.CXMOheyZ.d.mts → attaform.C6qzEdIM.d.cts} +1 -1
  37. package/dist/shared/{attaform.CJttVxRj.cjs → attaform.C8LVFVVe.cjs} +2 -2
  38. package/dist/shared/{attaform.CJttVxRj.cjs.map → attaform.C8LVFVVe.cjs.map} +1 -1
  39. package/dist/shared/attaform.CHorcsIU.d.cts +165 -0
  40. package/dist/shared/{attaform.BfMxsfmE.mjs → attaform.CIEQgJnM.mjs} +143 -78
  41. package/dist/shared/attaform.CIEQgJnM.mjs.map +1 -0
  42. package/dist/shared/{attaform.CCQkY4Ta.d.ts → attaform.CTwNcpLE.d.ts} +1 -1
  43. package/dist/shared/{attaform.UA19EF3J.mjs → attaform.CVCmBKZX.mjs} +59 -10
  44. package/dist/shared/attaform.CVCmBKZX.mjs.map +1 -0
  45. package/dist/shared/{attaform.CU3JperC.d.cts → attaform.C_5aB6EQ.d.cts} +657 -135
  46. package/dist/shared/{attaform.CU3JperC.d.mts → attaform.C_5aB6EQ.d.mts} +657 -135
  47. package/dist/shared/{attaform.CU3JperC.d.ts → attaform.C_5aB6EQ.d.ts} +657 -135
  48. package/dist/shared/{attaform.fegmBJaq.cjs → attaform.Cer8JO_P.cjs} +1435 -389
  49. package/dist/shared/attaform.Cer8JO_P.cjs.map +1 -0
  50. package/dist/shared/{attaform.g7rfuXdz.mjs → attaform.CpERWz3u.mjs} +240 -115
  51. package/dist/shared/attaform.CpERWz3u.mjs.map +1 -0
  52. package/dist/shared/attaform.CuE-bS1C.d.mts +165 -0
  53. package/dist/shared/{attaform.rIRYSUI1.cjs → attaform.Dee2rU1P.cjs} +145 -77
  54. package/dist/shared/attaform.Dee2rU1P.cjs.map +1 -0
  55. package/dist/shared/{attaform.CINUMjPq.mjs → attaform.Vo-Kft0t.mjs} +2 -2
  56. package/dist/shared/{attaform.CINUMjPq.mjs.map → attaform.Vo-Kft0t.mjs.map} +1 -1
  57. package/dist/shared/{attaform.DZRj9s0s.mjs → attaform.h1sq3BFu.mjs} +2 -2
  58. package/dist/shared/{attaform.DZRj9s0s.mjs.map → attaform.h1sq3BFu.mjs.map} +1 -1
  59. package/dist/zod-v3.cjs +3 -3
  60. package/dist/zod-v3.d.cts +27 -5
  61. package/dist/zod-v3.d.mts +27 -5
  62. package/dist/zod-v3.d.ts +27 -5
  63. package/dist/zod-v3.mjs +3 -3
  64. package/dist/zod-v4.cjs +3 -3
  65. package/dist/zod-v4.d.cts +16 -42
  66. package/dist/zod-v4.d.mts +16 -42
  67. package/dist/zod-v4.d.ts +16 -42
  68. package/dist/zod-v4.mjs +3 -3
  69. package/dist/zod.cjs +4 -4
  70. package/dist/zod.cjs.map +1 -1
  71. package/dist/zod.d.cts +7 -5
  72. package/dist/zod.d.mts +7 -5
  73. package/dist/zod.d.ts +7 -5
  74. package/dist/zod.mjs +5 -5
  75. package/dist/zod.mjs.map +1 -1
  76. package/package.json +6 -11
  77. package/dist/shared/attaform.BfMxsfmE.mjs.map +0 -1
  78. package/dist/shared/attaform.Dd_pWnmn.cjs.map +0 -1
  79. package/dist/shared/attaform.DyV1O4tI.mjs.map +0 -1
  80. package/dist/shared/attaform.UA19EF3J.mjs.map +0 -1
  81. package/dist/shared/attaform.fegmBJaq.cjs.map +0 -1
  82. package/dist/shared/attaform.g7rfuXdz.mjs.map +0 -1
  83. package/dist/shared/attaform.keLBaHB6.cjs.map +0 -1
  84. package/dist/shared/attaform.rIRYSUI1.cjs.map +0 -1
@@ -73,87 +73,6 @@ type ResolvedFieldMeta = {
73
73
  readonly meta: Readonly<FieldMetaPayload>;
74
74
  };
75
75
 
76
- /**
77
- * Path primitives for advanced integrations. The form library accepts
78
- * paths in dotted-string form (`'user.email'`) at every public API.
79
- * These primitives are exposed for adapter authors who need to
80
- * canonicalise user-provided paths.
81
- */
82
- declare const pathKeyBrand: unique symbol;
83
- /**
84
- * Branded string identifier for a canonicalised path. Useful as a
85
- * `Map` key — two paths that resolve to the same canonical form
86
- * produce the same `PathKey`. Treat as opaque; don't try to parse.
87
- */
88
- type PathKey = string & {
89
- readonly [pathKeyBrand]: 'PathKey';
90
- };
91
- /** A single path segment — a property name or array index. */
92
- type Segment = string | number;
93
- /** A structured path as a read-only sequence of segments. */
94
- type Path = readonly Segment[];
95
- /**
96
- * Parse a dotted-string path into structured segments.
97
- *
98
- * ```ts
99
- * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
100
- * parseDottedPath('items.0.name') // ['items', 0, 'name']
101
- * parseDottedPath('') // [''] (the empty-string key)
102
- * ```
103
- *
104
- * The empty-string input `''` is the **literal empty-key path**, not
105
- * the root. Use the array form `[]` for root. Form-level errors
106
- * (root `.refine()`) live at the empty-string path bucket so
107
- * `errors('')` returns them without sweeping every field error too.
108
- *
109
- * Throws `InvalidPathError` for paths with empty INTERNAL segments
110
- * (`'a..b'`, leading or trailing dots). For keys containing literal
111
- * dots, pass an array form (`['user.name']`) instead.
112
- */
113
- declare function parseDottedPath(path: string): Segment[];
114
- /**
115
- * Canonicalise a path into structured segments plus a stable string
116
- * key. Accepts either dotted-string or array form; integer-looking
117
- * segments normalise to numbers.
118
- *
119
- * ```ts
120
- * canonicalizePath('items.0.name')
121
- * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
122
- *
123
- * canonicalizePath(['items', 0, 'name'])
124
- * // → same result
125
- * ```
126
- *
127
- * The returned `key` is suitable as a `Map`/`Set` key — equal paths
128
- * produce equal keys regardless of input form.
129
- */
130
- declare function canonicalizePath(input: string | Path): {
131
- segments: readonly Segment[];
132
- key: PathKey;
133
- };
134
- /**
135
- * The root path — an empty segment tuple. Pass to APIs that accept
136
- * a `Path` to address the form value as a whole.
137
- */
138
- declare const ROOT_PATH: Path;
139
- /** Stable string key for the root path. */
140
- declare const ROOT_PATH_KEY: PathKey;
141
- /**
142
- * `true` when `path` starts with every segment of `prefix` (in order).
143
- * The empty `prefix` matches every path — ROOT prefix is universal.
144
- *
145
- * Walks segments rather than `PathKey` strings because the data this
146
- * helper operates on (e.g. `meta.errors[].path`) carries segment
147
- * arrays directly.
148
- *
149
- * ```ts
150
- * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
151
- * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
152
- * isPathPrefix([], ['anything']) // true (root prefix)
153
- * ```
154
- */
155
- declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
156
-
157
76
  /** Internal brand for the `Unset` type. Never exposed at runtime. */
158
77
  declare const _unsetBrand: unique symbol;
159
78
  /**
@@ -458,6 +377,98 @@ type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symb
458
377
  [K in keyof T]: DefaultValuesShape<T[K]>;
459
378
  } : T;
460
379
 
380
+ /**
381
+ * Per-form options threaded from `useForm` into the adapter factory.
382
+ * Today carries the resolved `maxRecursionDepth` so adapter walks can
383
+ * cap their descent through recursive schemas; future per-form runtime
384
+ * knobs land here too.
385
+ */
386
+ interface SchemaFactoryOptions {
387
+ /** Resolved recursion ceiling (per-form > app-default > library default). */
388
+ maxRecursionDepth: number;
389
+ }
390
+
391
+ /**
392
+ * Path primitives for advanced integrations. The form library accepts
393
+ * paths in dotted-string form (`'user.email'`) at every public API.
394
+ * These primitives are exposed for adapter authors who need to
395
+ * canonicalise user-provided paths.
396
+ */
397
+ declare const pathKeyBrand: unique symbol;
398
+ /**
399
+ * Branded string identifier for a canonicalised path. Useful as a
400
+ * `Map` key — two paths that resolve to the same canonical form
401
+ * produce the same `PathKey`. Treat as opaque; don't try to parse.
402
+ */
403
+ type PathKey = string & {
404
+ readonly [pathKeyBrand]: 'PathKey';
405
+ };
406
+ /** A single path segment — a property name or array index. */
407
+ type Segment = string | number;
408
+ /** A structured path as a read-only sequence of segments. */
409
+ type Path = readonly Segment[];
410
+ /**
411
+ * Parse a dotted-string path into structured segments.
412
+ *
413
+ * ```ts
414
+ * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
415
+ * parseDottedPath('items.0.name') // ['items', 0, 'name']
416
+ * parseDottedPath('') // [''] (the empty-string key)
417
+ * ```
418
+ *
419
+ * The empty-string input `''` is the **literal empty-key path**, not
420
+ * the root. Use the array form `[]` for root. Form-level errors
421
+ * (root `.refine()`) live at the empty-string path bucket so
422
+ * `errors('')` returns them without sweeping every field error too.
423
+ *
424
+ * Throws `InvalidPathError` for paths with empty INTERNAL segments
425
+ * (`'a..b'`, leading or trailing dots). For keys containing literal
426
+ * dots, pass an array form (`['user.name']`) instead.
427
+ */
428
+ declare function parseDottedPath(path: string): Segment[];
429
+ /**
430
+ * Canonicalise a path into structured segments plus a stable string
431
+ * key. Accepts either dotted-string or array form; integer-looking
432
+ * segments normalise to numbers.
433
+ *
434
+ * ```ts
435
+ * canonicalizePath('items.0.name')
436
+ * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
437
+ *
438
+ * canonicalizePath(['items', 0, 'name'])
439
+ * // → same result
440
+ * ```
441
+ *
442
+ * The returned `key` is suitable as a `Map`/`Set` key — equal paths
443
+ * produce equal keys regardless of input form.
444
+ */
445
+ declare function canonicalizePath(input: string | Path): {
446
+ segments: readonly Segment[];
447
+ key: PathKey;
448
+ };
449
+ /**
450
+ * The root path — an empty segment tuple. Pass to APIs that accept
451
+ * a `Path` to address the form value as a whole.
452
+ */
453
+ declare const ROOT_PATH: Path;
454
+ /** Stable string key for the root path. */
455
+ declare const ROOT_PATH_KEY: PathKey;
456
+ /**
457
+ * `true` when `path` starts with every segment of `prefix` (in order).
458
+ * The empty `prefix` matches every path — ROOT prefix is universal.
459
+ *
460
+ * Walks segments rather than `PathKey` strings because the data this
461
+ * helper operates on (e.g. `meta.errors[].path`) carries segment
462
+ * arrays directly.
463
+ *
464
+ * ```ts
465
+ * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
466
+ * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
467
+ * isPathPrefix([], ['anything']) // true (root prefix)
468
+ * ```
469
+ */
470
+ declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
471
+
461
472
  /**
462
473
  * Identifier for a form. A `FormKey` is the string passed via
463
474
  * `useForm({ key })`, used to look up a form by name from a distant
@@ -659,6 +670,66 @@ type AbstractSchema<Form, GetValueFormType> = {
659
670
  * callers treat that as "don't fill" and fall back to existing data.
660
671
  */
661
672
  getDefaultAtPath(path: Path): unknown;
673
+ /**
674
+ * Return the schema's "appropriate nullish value" at the given path
675
+ * — the underlying type's empty/falsy concrete, with `.default(x)`
676
+ * wrappers explicitly NOT honoured. Powers `form.clear(path)`:
677
+ * `clear` differs from `reset` precisely in that it ignores
678
+ * declared defaults and produces `false` / `0` / `''` / `[]` / a
679
+ * recursively-empty object instead.
680
+ *
681
+ * Semantics (mirrors `getDefaultAtPath`'s sub-path resolution,
682
+ * differs at leaves):
683
+ * - **Primitive leaf:** the primitive's falsy concrete
684
+ * (`''` / `0` / `false` / `0n` / `new Date(0)`, etc.).
685
+ * - **Array / Set / Record:** empty.
686
+ * - **Optional<T>:** `undefined` (the wrapper's "absent" marker).
687
+ * - **Nullable<T>:** `null` (the wrapper's "explicit empty").
688
+ * - **Default<T> / Prefault<T> / Catch<T>:** inner-schema empty
689
+ * — the declared default value is INTENTIONALLY skipped.
690
+ * - **Readonly<T> / preprocess(fn, T):** inner-schema empty.
691
+ * - **Object:** recursive — every property gets its own empty.
692
+ * - **Discriminated union:** first variant's recursive empty
693
+ * (parallels `getDefaultAtPath`'s first-success precedent).
694
+ * - **Path doesn't exist in schema:** `undefined`.
695
+ *
696
+ * Adapters may return `undefined` when the path can't be resolved;
697
+ * callers treat that as "don't write" and leave existing storage
698
+ * unchanged.
699
+ */
700
+ getEmptyValueAtPath(path: Path): unknown;
701
+ /**
702
+ * Give the schema a chance to normalize the consumer's write value
703
+ * before it lands in storage / hits the slim-primitive gate. Each
704
+ * schema library exposes this concept differently — Zod calls it
705
+ * `z.preprocess(fn, inner)`, Yup calls it `.transform()`, Valibot
706
+ * spells it `pipe(transform(fn), inner)` — but the shape is the
707
+ * same: "this input shape gets coerced into that storage shape at
708
+ * the boundary."
709
+ *
710
+ * Runs SYNCHRONOUSLY at the write boundary so storage holds the
711
+ * post-normalization shape. Without this, a schema like `notify:
712
+ * z.preprocess(v => v == null ? defaultVar : v, innerDU)` would
713
+ * let the consumer write `null` and lock storage into `null` —
714
+ * because the gate sees the raw input (which the preprocess wrapper
715
+ * accepts as `unknown`) and storage holds a shape no variant
716
+ * matches.
717
+ *
718
+ * Adapters MUST:
719
+ * - Return `value` unchanged when no normalization is declared at
720
+ * the path.
721
+ * - Return `value` unchanged when the user's normalization fn
722
+ * returns a `Promise` (async coercion can't run at write time —
723
+ * validation handles it during parse).
724
+ * - Let user-thrown errors propagate (the user wrote the fn; we
725
+ * just tag the path in the wrapper error for diagnostics).
726
+ *
727
+ * Normalization runs when `path` equals the wrapper's exact
728
+ * location. Writes deeper than the wrapper bypass it (a wrapper
729
+ * over the whole subtree can't be invoked from a partial leaf
730
+ * write).
731
+ */
732
+ normalizeWriteValueAtPath(value: unknown, path: Path): unknown;
662
733
  /**
663
734
  * Distinguish a tuple (fixed-length, position-typed) from an
664
735
  * unbounded array at `path`. The runtime calls this on every
@@ -719,7 +790,7 @@ type AbstractSchema<Form, GetValueFormType> = {
719
790
  * returned as a `success: false` response with a populated
720
791
  * `errors` array.
721
792
  */
722
- validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<Form>>;
793
+ validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<GetValueFormType>>;
723
794
  /**
724
795
  * Sync sister to `getSchemasAtPath` / `validateAtPath`. Returns the
725
796
  * set of primitive `typeof`-style kinds the path's leaf schema
@@ -931,6 +1002,14 @@ type UnionDiscriminatorContext = {
931
1002
  * skips the reshape and falls back to a plain write.
932
1003
  */
933
1004
  getVariantDefault(value: unknown): unknown;
1005
+ /**
1006
+ * Returns `true` iff `value` is a literal recognised by one of the
1007
+ * discriminator's variants. Used by reshape to decide whether to
1008
+ * seek a variant default or emit a stub state. NOT used at the
1009
+ * runtime write gate — consumer-side value validity is a
1010
+ * validation-time concern.
1011
+ */
1012
+ isVariantSelected(value: unknown): boolean;
934
1013
  };
935
1014
  /**
936
1015
  * The set of primitive "kinds" the slim-primitive write contract
@@ -1130,19 +1209,136 @@ type WriteMeta = {
1130
1209
  * an infinite loop. Don't set from consumer code.
1131
1210
  */
1132
1211
  readonly skipDiscriminatorReshape?: boolean;
1212
+ /**
1213
+ * Hint about an array structural mutation, set by `field-arrays.ts`
1214
+ * helpers so `setValueAtPath` can surgically clear variant memory
1215
+ * for indices the operation invalidated. Without this hint, a raw
1216
+ * whole-array `setValue(arrayPath, [...])` clears all memory under
1217
+ * the array (the runtime can't tell which indices stayed put).
1218
+ * Internal — don't set from consumer code.
1219
+ */
1220
+ readonly arrayOp?: {
1221
+ readonly kind: 'shift-from';
1222
+ readonly index: number;
1223
+ } | {
1224
+ readonly kind: 'shift-range';
1225
+ readonly fromIndex: number;
1226
+ readonly toIndex: number;
1227
+ } | {
1228
+ readonly kind: 'swap';
1229
+ readonly a: number;
1230
+ readonly b: number;
1231
+ } | {
1232
+ readonly kind: 'replace-at';
1233
+ readonly index: number;
1234
+ };
1235
+ /**
1236
+ * Per-instance config overrides threaded through writes so each
1237
+ * `useForm({ key })` callsite honors its own `validateOn` /
1238
+ * `debounceMs` / `rememberVariants` even when sharing a FormStore
1239
+ * with sibling calls (e.g., a modal and main form rendering the
1240
+ * same logical form). Internal — set by `buildFormApi` from
1241
+ * the per-instance options bag; the store reads each field with
1242
+ * fallback to its construction-time defaults.
1243
+ */
1244
+ readonly instance?: {
1245
+ readonly validateOn?: ValidateOn;
1246
+ readonly debounceMs?: number;
1247
+ readonly rememberVariants?: boolean;
1248
+ };
1249
+ /**
1250
+ * When `true`, marks this `applyFormReplacement` call as the
1251
+ * persistence hydration step. Modules that snapshot the form state
1252
+ * (notably the history module) treat hydration as the baseline:
1253
+ * stacks reset to a single seed of the post-hydration value, so a
1254
+ * subsequent `undo()` can't recover the transient pre-hydration
1255
+ * default. Internal — set by `wirePersistence`. Don't set from
1256
+ * consumer code.
1257
+ */
1258
+ readonly hydration?: boolean;
1259
+ /**
1260
+ * When `true`, this write originated from a sibling tab's
1261
+ * BroadcastChannel broadcast (the multi-tab sync module's inbound
1262
+ * apply). Listeners that initiate side effects check this flag to
1263
+ * avoid amplification loops and spurious side effects:
1264
+ *
1265
+ * - The multi-tab sync OUTBOUND broadcaster skips so a remote-driven
1266
+ * write doesn't echo back across the channel.
1267
+ * - The history module updates its diff anchor but does NOT push a
1268
+ * delta — remote writes aren't part of the local user's undo
1269
+ * timeline.
1270
+ * - The persistence writer skips so the receiving tab doesn't
1271
+ * double-persist a value the originating tab already wrote.
1272
+ *
1273
+ * Internal — set by `createMultiTabSyncModule`. Don't set from
1274
+ * consumer code.
1275
+ */
1276
+ readonly crossTab?: boolean;
1133
1277
  };
1134
1278
  /**
1135
1279
  * Undo/redo configuration passed via `useForm({ history })`.
1136
1280
  *
1137
- * - `true` — enable with the default snapshot cap (`max: 50`).
1138
- * - `{ max }` — enable and tune the bounded snapshot stack size.
1281
+ * - `true` — enable with the default position cap (`max: 128`).
1282
+ * - `{ max }` — enable and tune the bounded history size.
1139
1283
  *
1140
- * When enabled, every mutation pushes a snapshot; `undo()` /
1141
- * `redo()` walk the stacks. `reset()` clears history.
1284
+ * When enabled, every mutation records a forward delta; `form.history.undo()`
1285
+ * / `form.history.redo()` walk the chain. `reset()` is itself a mutation —
1286
+ * the pre-reset state stays one undo away. Persistence hydration is the
1287
+ * floor: after hydrate applies, the chain reseeds with the hydrated value
1288
+ * and `undo()` cannot reach the transient pre-hydration default.
1142
1289
  */
1143
1290
  type HistoryConfig = true | {
1144
1291
  max?: number;
1145
1292
  };
1293
+ /**
1294
+ * Consolidated undo/redo namespace at `form.history`. All history-related
1295
+ * surface lives here — methods and reactive flags both — so consumers
1296
+ * have one canonical address to read from.
1297
+ *
1298
+ * Always present on `useForm()` return whether or not `history` was
1299
+ * configured. When history isn't enabled, methods are no-ops returning
1300
+ * `false` (or `void`), `canUndo` / `canRedo` read `false`, and `size`
1301
+ * reads `0`. Consumer templates don't need conditional logic.
1302
+ *
1303
+ * Reactivity: built as `readonly(reactive({...}))`, so `canUndo` / `canRedo`
1304
+ * / `size` auto-unwrap on access (plain `boolean` / `number`, not refs).
1305
+ * Method fields (`undo`, `redo`, `clear`) pass through as plain functions.
1306
+ */
1307
+ type FormHistoryNamespace = {
1308
+ /**
1309
+ * Step back one position in the history chain. Returns `true` when a
1310
+ * step was taken, `false` when already at the oldest reachable
1311
+ * position (or when history isn't configured).
1312
+ */
1313
+ readonly undo: () => boolean;
1314
+ /**
1315
+ * Replay the next step forward in the chain. Returns `true` on
1316
+ * success, `false` when there's nothing queued (or history isn't
1317
+ * configured). The forward branch is dropped as soon as a new
1318
+ * mutation lands.
1319
+ */
1320
+ readonly redo: () => boolean;
1321
+ /**
1322
+ * Wipe the undo and redo branches; reseed the chain with the current
1323
+ * form state as the new baseline. The form value, errors, and
1324
+ * blankPaths all stay where they are — only the past/future history
1325
+ * resets. After `clear()`: `canUndo === false`, `canRedo === false`,
1326
+ * `size === 1`. No-op when history isn't configured.
1327
+ */
1328
+ readonly clear: () => void;
1329
+ /** `true` when there is at least one undo step available. */
1330
+ readonly canUndo: boolean;
1331
+ /** `true` when `undo()` has been called and a `redo()` would replay. */
1332
+ readonly canRedo: boolean;
1333
+ /**
1334
+ * Total reachable positions in the history chain (the current
1335
+ * position plus everything reachable via `undo()` / `redo()`).
1336
+ * Useful for debug overlays; UI driving undo/redo buttons should
1337
+ * gate on `canUndo` / `canRedo` instead. Reads `0` when history
1338
+ * isn't configured.
1339
+ */
1340
+ readonly size: number;
1341
+ };
1146
1342
  /**
1147
1343
  * Full options bag for `useForm({ persist })`. Use this when you need
1148
1344
  * to override defaults beyond picking the backend.
@@ -1240,10 +1436,15 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1240
1436
  * abstract entry point accepts any object implementing
1241
1437
  * `AbstractSchema`.
1242
1438
  *
1243
- * For schemas that depend on the form's identity, pass a factory
1244
- * `(key) => schema` instead — the library calls it once per form.
1439
+ * For schemas that depend on the form's identity or per-form
1440
+ * options, pass a factory `(key, options) => schema` instead — the
1441
+ * library calls it once per form, after `mergeWithDefaults` has
1442
+ * resolved the options bag (`maxRecursionDepth`, etc.). Most
1443
+ * adapters ignore the options argument; the typed Zod entry points
1444
+ * use it to thread the resolved recursion cap into the adapter
1445
+ * closure.
1245
1446
  */
1246
- schema: Schema | ((key: FormKey) => Schema);
1447
+ schema: Schema | ((key: FormKey, options: SchemaFactoryOptions) => Schema);
1247
1448
  /**
1248
1449
  * Optional identifier for this form. Omit for one-off forms; the
1249
1450
  * library allocates a unique key automatically (SSR-safe, stable
@@ -1353,13 +1554,14 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1353
1554
  */
1354
1555
  persist?: PersistConfig;
1355
1556
  /**
1356
- * Opt-in undo/redo. Off by default. `true` enables with a 50-snapshot
1557
+ * Opt-in undo/redo. Off by default. `true` enables with a 128-position
1357
1558
  * cap; `{ max: N }` tunes the cap.
1358
1559
  *
1359
- * Every mutation pushes a snapshot. `undo()` pops one; `redo()`
1360
- * replays it. `reset()` clears history. Reactive flags
1361
- * `state.canUndo` / `state.canRedo` / `state.historySize` reflect
1362
- * the current stack.
1560
+ * Every mutation records a forward delta. `form.history.undo()` walks
1561
+ * one step back; `form.history.redo()` walks one step forward.
1562
+ * `reset()` is itself a mutation, so the pre-reset state stays one
1563
+ * undo away. The consolidated `form.history` namespace also exposes
1564
+ * `clear()`, `canUndo`, `canRedo`, and `size`.
1363
1565
  */
1364
1566
  history?: HistoryConfig;
1365
1567
  /**
@@ -1416,6 +1618,71 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1416
1618
  * `false` → never show.
1417
1619
  */
1418
1620
  shouldShowErrors?: ShouldShowErrorsConfig;
1621
+ /**
1622
+ * Recursion ceiling for schema walks that descend through recursive
1623
+ * schemas (Zod's `z.lazy(...)` today). Default `64`. Per-form value
1624
+ * overrides `AttaformDefaults.maxRecursionDepth`, which overrides
1625
+ * the library default.
1626
+ *
1627
+ * Schemas that don't include a recursive boundary ignore this knob
1628
+ * entirely — it's read only at the descent step through a recursive
1629
+ * wrapper. Set it on the specific form whose schema is recursive
1630
+ * (a comment tree, a category tree, a nested-rule editor):
1631
+ *
1632
+ * ```ts
1633
+ * useForm({ schema: commentTreeSchema, maxRecursionDepth: 128 })
1634
+ * ```
1635
+ *
1636
+ * Past the cap, the slim-primitive type gate falls back to permissive
1637
+ * (write-time type checks skip; full schema validation still runs).
1638
+ * Storage and reads work at any depth; only the per-write type gate
1639
+ * stops short of the cap. Raise the cap if you regularly edit nodes
1640
+ * beyond the default depth.
1641
+ *
1642
+ * See `AttaformDefaults.maxRecursionDepth` for the resolution rules
1643
+ * and the broader description of where the cap is read.
1644
+ */
1645
+ maxRecursionDepth?: number;
1646
+ /**
1647
+ * Override the path-segment name stems treated as sensitive for this
1648
+ * form. Sensitive paths are excluded from persistence writes,
1649
+ * multi-tab sync broadcasts, AND the DevTools redact walk.
1650
+ *
1651
+ * Resolution: per-form value (this field) > global default
1652
+ * (`createAttaform({ defaults: { sensitiveNames } })`) > library
1653
+ * default (`DEFAULT_SENSITIVE_NAMES`).
1654
+ *
1655
+ * Pass an empty array `[]` as the explicit opt-out — "nothing is
1656
+ * sensitive on this form" — for fully-trusted internal tooling.
1657
+ * See `AttaformDefaults.sensitiveNames` for composition examples.
1658
+ */
1659
+ sensitiveNames?: readonly string[];
1660
+ /**
1661
+ * Cross-tab synchronisation via BroadcastChannel. Defaults to `true`
1662
+ * (when the browser supports it and the page is in a secure
1663
+ * context): a keyed `useForm` callsite auto-pairs with same-keyed
1664
+ * siblings in other same-origin tabs and mirrors their mutations
1665
+ * in near real-time.
1666
+ *
1667
+ * **Resolution order (per-register override > per-form > global > library):**
1668
+ *
1669
+ * register(path, { multiTab }) > useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
1670
+ *
1671
+ * **When to set `false`:** forms holding PII / PHI, contexts where
1672
+ * tab isolation is required by policy, or any flow where conflicting
1673
+ * tab edits could corrupt user intent. Sensitive-named paths (via
1674
+ * `sensitiveNames`) are always stripped from outbound broadcasts
1675
+ * regardless of this setting.
1676
+ *
1677
+ * **Secure-context requirement.** Multi-tab sync is silently disabled
1678
+ * outside `window.isSecureContext === true` (HTTPS or localhost). On
1679
+ * plain HTTP a one-shot dev warning fires and the module noops.
1680
+ *
1681
+ * **Anonymous (auto-keyed) forms skip sync entirely** — without a
1682
+ * consumer-supplied `key`, cross-tab identity is undefined and the
1683
+ * channel would be solo by construction.
1684
+ */
1685
+ multiTab?: boolean;
1419
1686
  };
1420
1687
  /**
1421
1688
  * App-level defaults applied to every `useForm` call. Set these once
@@ -1509,6 +1776,102 @@ type AttaformDefaults = {
1509
1776
  * predicate, so reading them inside would be a self-reference.
1510
1777
  */
1511
1778
  shouldShowErrors?: ShouldShowErrorsConfig;
1779
+ /**
1780
+ * Default for `useForm({ maxRecursionDepth })`. Recursion ceiling
1781
+ * for schema walks that descend through recursive schemas (Zod's
1782
+ * `z.lazy(...)` today, equivalent constructs in any future adapter).
1783
+ * Library default: `64`.
1784
+ *
1785
+ * Resolution order (per-form wins):
1786
+ *
1787
+ * useForm({ maxRecursionDepth }) > AttaformDefaults > library default (64)
1788
+ *
1789
+ * Read at every step of a schema walk that crosses a recursive
1790
+ * boundary — default-value derivation at construction, slim-primitive
1791
+ * type gates on each write, path-by-path schema resolution. Walks
1792
+ * track their descent depth and switch to a permissive fallback once
1793
+ * `depth > maxRecursionDepth`.
1794
+ *
1795
+ * "Permissive fallback" means storage and reads keep working at any
1796
+ * depth; only the per-write type gate stops checking past the cap.
1797
+ * Full schema validation (`validateAsync`, `handleSubmit`) still runs
1798
+ * against the real schema, so refinement errors at any depth still
1799
+ * surface — the cap only affects the *write-time gate*.
1800
+ *
1801
+ * Forms with no recursive schemas ignore this entirely — the cap is
1802
+ * read only at the descent step through a recursive wrapper. Setting
1803
+ * it app-wide is the right move when you have multiple recursive
1804
+ * forms that should share one ceiling:
1805
+ *
1806
+ * ```ts
1807
+ * createAttaform({
1808
+ * defaults: { maxRecursionDepth: 128 },
1809
+ * })
1810
+ * ```
1811
+ *
1812
+ * Per-form override stays available for the one tree-shaped form
1813
+ * whose depth is unusual:
1814
+ *
1815
+ * ```ts
1816
+ * useForm({ schema: deepCategoryTreeSchema, maxRecursionDepth: 256 })
1817
+ * ```
1818
+ *
1819
+ * Setting this app-wide costs nothing for non-recursive forms — the
1820
+ * walks that read the cap never run for them.
1821
+ *
1822
+ * Pass `Infinity` to disable the cap entirely. Walks will then
1823
+ * descend through recursive boundaries until they terminate
1824
+ * structurally; a schema with no structural terminator will exhaust
1825
+ * the JS call stack. Reserve for schemas whose authors are
1826
+ * confident the recursion is bounded by the actual data shape.
1827
+ */
1828
+ maxRecursionDepth?: number;
1829
+ /**
1830
+ * Override the path-segment name stems treated as sensitive.
1831
+ * Sensitive paths are excluded from persistence writes, multi-tab
1832
+ * sync broadcasts, AND the DevTools redact walk — one configurable
1833
+ * source of truth across every surface.
1834
+ *
1835
+ * Library default is `DEFAULT_SENSITIVE_NAMES` (exported from
1836
+ * `attaform`); compose to extend:
1837
+ *
1838
+ * ```ts
1839
+ * import { DEFAULT_SENSITIVE_NAMES, createAttaform } from 'attaform'
1840
+ *
1841
+ * createAttaform({
1842
+ * defaults: { sensitiveNames: [...DEFAULT_SENSITIVE_NAMES, 'mrn', 'tax_id'] }
1843
+ * })
1844
+ * ```
1845
+ *
1846
+ * Pass an empty array `[]` as the explicit opt-out — "nothing is
1847
+ * sensitive" — for fully-trusted internal tooling. When present at
1848
+ * the per-form level via `useForm({ sensitiveNames })`, the per-form
1849
+ * list REPLACES the global one (consumers compose their own
1850
+ * additive lists via the exported default).
1851
+ */
1852
+ sensitiveNames?: readonly string[];
1853
+ /**
1854
+ * App-wide default for `useForm({ multiTab })`. Default `true` when
1855
+ * the runtime supports `BroadcastChannel` AND `window.isSecureContext`
1856
+ * is true (HTTPS in production, localhost in development) — same gate
1857
+ * browsers apply to other sensitive APIs (clipboard, geolocation,
1858
+ * push, web crypto subtle).
1859
+ *
1860
+ * Set to `false` once at the plugin level for a multi-tenant
1861
+ * deployment that prefers tab-isolation by default; individual forms
1862
+ * can still opt back in via `useForm({ multiTab: true })`.
1863
+ *
1864
+ * **Resolution order (per-form wins):**
1865
+ *
1866
+ * useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
1867
+ *
1868
+ * **Secure-context gate.** Multi-tab sync only activates over HTTPS
1869
+ * or localhost. On plain HTTP, the module silently noops with a
1870
+ * one-shot dev-mode warning — production deployments MUST be served
1871
+ * over HTTPS for sync to function. See the multi-tab-sync recipe's
1872
+ * Security section for the threat model.
1873
+ */
1874
+ multiTab?: boolean;
1512
1875
  };
1513
1876
  /**
1514
1877
  * Callback invoked by `handleSubmit` after the form parses successfully.
@@ -1765,6 +2128,25 @@ type RegisterOptions = {
1765
2128
  * client-side storage for this user's session.
1766
2129
  */
1767
2130
  acknowledgeSensitive?: boolean;
2131
+ /**
2132
+ * Opt this field OUT of multi-tab sync. The form-level cascade
2133
+ * activates sync by default; passing `multiTab: false` on a single
2134
+ * register call keeps that path tab-local — outbound patches at
2135
+ * the path are stripped, and inbound patches at the path are
2136
+ * rejected (symmetric tab-local behaviour).
2137
+ *
2138
+ * The opt-out is downgrade-only — you cannot pass `multiTab: true`
2139
+ * to bring sync back on a form whose form-level `multiTab` is
2140
+ * `false` (in that case the sync module never instantiated; there's
2141
+ * no broadcaster to opt back into).
2142
+ *
2143
+ * Use for fields that hold transient per-tab UI state inside an
2144
+ * otherwise-synced form (e.g. an editor's cursor position field
2145
+ * mirrored into the form for save-on-blur), or for individual
2146
+ * paths the consumer wants to scope to the originating tab without
2147
+ * disabling sync globally.
2148
+ */
2149
+ multiTab?: boolean;
1768
2150
  /**
1769
2151
  * Sync transformation pipeline applied to user-typed values before
1770
2152
  * they reach form state. Composes left-to-right: each transform
@@ -2088,6 +2470,59 @@ type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
2088
2470
  * array elements as possibly-undefined to reflect runtime reality.
2089
2471
  */
2090
2472
  type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
2473
+ /**
2474
+ * Detect `any` distinctly from `unknown`. The trick: `1 & any` is `any`
2475
+ * and `0 extends any` is `true`; `1 & unknown` is `1` and `0 extends 1`
2476
+ * is `false`. Used to fork `PathSetValuePayload` so `z.any()` paths
2477
+ * resolve to `any` (matching the read-side surface) and `z.unknown()` /
2478
+ * preprocess paths resolve to `unknown` (matching Zod's input typing).
2479
+ */
2480
+ type IsAny<T> = 0 extends 1 & T ? true : false;
2481
+ /**
2482
+ * Resolves `setValue`'s `value` argument type at a single `Path` leaf.
2483
+ *
2484
+ * Three branches, one per Zod input-typing case:
2485
+ *
2486
+ * 1. **`any` leaf (`z.any()`)** — schema input type is `any`; the
2487
+ * whole form API surface (read, register, fields) is `any` at
2488
+ * this path. This branch returns raw `any` so `setValue` stays
2489
+ * consistent with the rest. Callsites that pass an unannotated
2490
+ * `(prev) => ...` may surface `noImplicitAny` under the
2491
+ * consumer's tsconfig — annotate `(prev: any) => ...` to opt
2492
+ * into the looser shape explicitly.
2493
+ *
2494
+ * 2. **`unknown` leaf (`z.unknown()`, `z.preprocess()` input)** —
2495
+ * schema input is unconstrained; consumers narrow before use.
2496
+ * The branch returns `({} | null | undefined) | ((prev: unknown)
2497
+ * => unknown)` instead of a `SetValuePayload<unknown, ...>`-style
2498
+ * union for three reasons:
2499
+ *
2500
+ * a. **Union absorption** — `unknown | X` collapses to `unknown`,
2501
+ * erasing the callback union member. With the callback shape
2502
+ * gone, TS has no contextual type for `prev` and decays it to
2503
+ * implicit `any` under `noImplicitAny`. The triple
2504
+ * `{} | null | undefined` is structurally equivalent to
2505
+ * `unknown` (covers the same value space) but is NOT subject
2506
+ * to absorption — the callback branch survives the union and
2507
+ * `prev` infers cleanly to `unknown`.
2508
+ *
2509
+ * b. **`NonNullable<unknown> = {}`** — applying `NonNullable` to
2510
+ * the read slot for an unknown leaf narrows `prev` to `{}`,
2511
+ * which is looser than `unknown` (allows ad-hoc property
2512
+ * access). This branch keeps the read slot as `unknown`
2513
+ * directly so the consumer is forced to narrow.
2514
+ *
2515
+ * c. **`Unset`-widening doesn't apply** — `DefaultValuesShape`
2516
+ * widens primitive leaves to admit `unset`; for an unknown
2517
+ * leaf there's no primitive to widen. The open-form triple
2518
+ * covers the same value space the runtime accepts (any
2519
+ * value, including `unset` — symbols are `{}`).
2520
+ *
2521
+ * 3. **All other leaves** — flow through unchanged via
2522
+ * `SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>`.
2523
+ */
2524
+ type PathSetValuePayload<Leaf> = IsAny<Leaf> extends true ? any : unknown extends Leaf ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
2525
+ ({} | null | undefined) | ((prev: unknown) => unknown) : SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>;
2091
2526
  /**
2092
2527
  * Per-field reactive shape returned by `form.fields.<leaf-path>` and
2093
2528
  * `form.fields(path)`. Slim, readonly across the board. The unified
@@ -2570,16 +3005,6 @@ type FormMeta<F = unknown> = FieldState<F> & {
2570
3005
  * `try { await onSubmit() }` instead.
2571
3006
  */
2572
3007
  readonly submitError: unknown;
2573
- /** `true` when there is at least one undo step available. Always present (false when history is disabled). */
2574
- readonly canUndo: boolean;
2575
- /** `true` when `undo()` has been called and a `redo()` would replay. Always present (false when history is disabled). */
2576
- readonly canRedo: boolean;
2577
- /**
2578
- * Total snapshots across the undo and redo stacks. Useful for
2579
- * debug overlays; UI driving undo/redo buttons should gate on
2580
- * `canUndo` / `canRedo` instead.
2581
- */
2582
- readonly historySize: number;
2583
3008
  /**
2584
3009
  * Per-`useForm()`-call identity. Stable for the lifetime of one
2585
3010
  * `useForm()` call; new on every fresh mount. Orthogonal to
@@ -2618,8 +3043,31 @@ type FormMeta<F = unknown> = FieldState<F> & {
2618
3043
  * form.handleSubmit(onSubmit) // returns a submit handler
2619
3044
  * form.meta.submitting // form-level reactive flag
2620
3045
  * ```
3046
+ *
3047
+ * Three generic slots split the write view, parse view, and read view:
3048
+ *
3049
+ * - `Form` — the **input / write shape** (`z.input<Schema>`). Used
3050
+ * by `setValue`, `defaultValues`, and `register`'s write side.
3051
+ * Loose: preprocess paths accept `unknown` at the write boundary,
3052
+ * defaulted fields accept their inner type optionally.
3053
+ *
3054
+ * - `GetValueFormType` — the **output / parsed shape**
3055
+ * (`z.output<Schema>`). Used by `handleSubmit`'s `onSubmit`
3056
+ * callback and by `form.process()`'s success payload. This is the
3057
+ * shape after refinements have fired and transforms have run.
3058
+ *
3059
+ * - `ReadForm` — the **read / storage shape**. Used by `values`,
3060
+ * `fields`, `register`'s read side, `toRef`. Per-key precise: at
3061
+ * the write-boundary wrappers (`default` / `prefault` / `catch` /
3062
+ * `readonly` / `preprocess`) the value is `z.output<Inner>`
3063
+ * (default has fired, preprocess has normalized); at transforms /
3064
+ * pipes the value stays `z.input<Inner>` (transforms are deferred
3065
+ * until parse). For schema-agnostic call sites defaults to `Form`.
3066
+ *
3067
+ * For schemas without write-boundary wrappers or transforms the three
3068
+ * shapes coincide.
2621
3069
  */
2622
- type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form> = {
3070
+ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form> = {
2623
3071
  /**
2624
3072
  * Wraps your submit logic with validation and error routing.
2625
3073
  *
@@ -2631,10 +3079,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2631
3079
  * ```
2632
3080
  *
2633
3081
  * `data` is the strictly-typed parsed value — refinements have
2634
- * fired, so every leaf is guaranteed to satisfy its schema-level
2635
- * format / range / membership constraints.
3082
+ * fired and `.transform()`s have run, so the payload matches
3083
+ * `z.output<Schema>` (the post-parse output shape). For schemas
3084
+ * where the input type differs from the output type (e.g.
3085
+ * `z.string().transform(v => v.length > 10)`), `data` is the
3086
+ * output shape while `form.values` stays the input shape.
2636
3087
  */
2637
- handleSubmit: HandleSubmit<Form>;
3088
+ handleSubmit: HandleSubmit<GetValueFormType>;
2638
3089
  /**
2639
3090
  * Reactive readonly proxy over the form's storage value. Read
2640
3091
  * identically in script and template — no `.value`, no auto-unwrap
@@ -2657,10 +3108,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2657
3108
  *
2658
3109
  * Reads reflect what's storable: enum-typed slots widen to their
2659
3110
  * primitive supertype (`string`), so refinement-invalid but
2660
- * structurally-valid values are visible. Use `handleSubmit` /
2661
- * `validateAsync()` when you need the post-validation strict type.
3111
+ * structurally-valid values are visible. Storage holds the
3112
+ * `z.input<Schema>` shape — `.transform()`s have NOT run, so for
3113
+ * a schema like `z.string().transform(v => v.length > 10)` the
3114
+ * value reads as `string`, not `boolean`. Use `handleSubmit` or
3115
+ * `form.process()` when you need the post-transform output shape.
2662
3116
  */
2663
- values: ValuesSurface<WriteShape<GetValueFormType>>;
3117
+ values: ValuesSurface<WriteShape<ReadForm>>;
2664
3118
  /**
2665
3119
  * Reactive per-field state proxy. Pinia-style nested object — read
2666
3120
  * leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
@@ -2678,8 +3132,9 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2678
3132
  * descends into the nested leaf.
2679
3133
  *
2680
3134
  * Leaf values follow the slim WriteShape contract: enum-typed leaves
2681
- * widen to their primitive supertype. The errors array, dirty flag,
2682
- * focus state, etc. are unaffected.
3135
+ * widen to their primitive supertype, and the leaf value reflects
3136
+ * the `z.input<Schema>` shape (transforms deferred until parse).
3137
+ * The errors array, dirty flag, focus state, etc. are unaffected.
2683
3138
  *
2684
3139
  * Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
2685
3140
  * `errors`, `blank`, `focused`, `blurred`, `value`,
@@ -2688,7 +3143,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2688
3143
  * Document edge case; rename the offending schema field if the
2689
3144
  * collision matters.
2690
3145
  */
2691
- fields: FieldStateMap<WriteShape<GetValueFormType>>;
3146
+ fields: FieldStateMap<WriteShape<ReadForm>>;
2692
3147
  /**
2693
3148
  * Write to the form programmatically. Two forms:
2694
3149
  *
@@ -2744,14 +3199,14 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2744
3199
  * it blank (storage holds the slim default; UI displays
2745
3200
  * empty; submit raises "No value supplied" for required schemas).
2746
3201
  */
2747
- <Path extends FlatPath<Form>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, Path>>, NonNullable<WriteShape<NestedType<Form, Path>>>>>(path: Path, value: Value): boolean;
3202
+ <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
2748
3203
  /**
2749
3204
  * Tuple-segment form. Equivalent to the dotted-string overload —
2750
3205
  * useful when paths are built from variables or arrays:
2751
3206
  * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
2752
3207
  * type is exact, matching the dotted-string form.
2753
3208
  */
2754
- <const S extends ReadonlyArray<string | number>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, JoinSegments<S>>>, NonNullable<WriteShape<NestedType<Form, JoinSegments<S>>>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value): boolean;
3209
+ <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value): boolean;
2755
3210
  };
2756
3211
  /**
2757
3212
  * Reactive validation status. Re-runs whenever the form (or the
@@ -2784,6 +3239,32 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2784
3239
  * `true` while the promise is in flight.
2785
3240
  */
2786
3241
  validateAsync: (path?: FlatPath<Form>) => Promise<ValidationResponseWithoutValue<Form>>;
3242
+ /**
3243
+ * Imperative one-shot parse. Same pipeline as `validateAsync` —
3244
+ * runs refinements, applies `.transform()`s, composes blank-required
3245
+ * errors — but RETAINS the parsed data instead of stripping it.
3246
+ *
3247
+ * Storage holds the "honest input view" — values you wrote, with
3248
+ * preprocess normalization applied but `.transform()` deferred. For
3249
+ * schemas where the input type differs from the output type (e.g.,
3250
+ * `z.string().transform(v => v.length > 10)`), `form.values.X` is
3251
+ * the input shape and `(await form.process()).data?.X` is the
3252
+ * output shape.
3253
+ *
3254
+ * ```ts
3255
+ * const result = await form.process()
3256
+ * if (result.success) {
3257
+ * // result.data matches z.output<typeof schema>
3258
+ * } else {
3259
+ * // result.errors is the validation failure list
3260
+ * }
3261
+ * ```
3262
+ *
3263
+ * Pass a path to parse a subtree only. Async because refinements may
3264
+ * be async. `meta.validating` flips `true` while the promise is in
3265
+ * flight (shared with validateAsync).
3266
+ */
3267
+ process: (path?: FlatPath<Form>) => Promise<ValidationResponse<GetValueFormType>>;
2787
3268
  /**
2788
3269
  * Bind a path to a native input via `v-register`. Returns a
2789
3270
  * `RegisterValue` carrying the live ref and event handlers the
@@ -2813,8 +3294,8 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2813
3294
  * for storage activity to actually happen.
2814
3295
  */
2815
3296
  register: {
2816
- <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<Form>, Path>>;
2817
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form, keyof Form>] ? unknown : never), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<Form>, JoinSegments<S>>>;
3297
+ <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
3298
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form, keyof Form>] ? unknown : never), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>;
2818
3299
  };
2819
3300
  /**
2820
3301
  * The form's identifier — either the explicit `key` passed to
@@ -2869,8 +3350,8 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2869
3350
  * scripts; `toRef` is for ref-shaped interop only.
2870
3351
  */
2871
3352
  toRef: {
2872
- <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
2873
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, JoinSegments<S>>>>;
3353
+ <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, Path>>>;
3354
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>>;
2874
3355
  };
2875
3356
  /**
2876
3357
  * Replace every field error for this form with the provided list.
@@ -2945,12 +3426,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2945
3426
  clearFormErrors: () => void;
2946
3427
  /**
2947
3428
  * Form-level reactive flags, counters, and aggregates (`dirty`,
2948
- * `valid`, `submitting`, `submitCount`, `canUndo`,
2949
- * `historySize`, and the flat `errors` array). See `FormMeta` for
2950
- * the full shape. Read leaves directly with no `.value`.
3429
+ * `valid`, `submitting`, `submitCount`, and the flat `errors`
3430
+ * array). See `FormMeta` for the full shape. Read leaves directly
3431
+ * with no `.value`.
2951
3432
  *
2952
3433
  * For per-field state (touched, focused, blurred, errors at one
2953
- * path), use `form.fields.<path>` instead.
3434
+ * path), use `form.fields.<path>` instead. Undo/redo state lives at
3435
+ * `form.history` (see `FormHistoryNamespace`).
2954
3436
  */
2955
3437
  meta: FormMeta<Form>;
2956
3438
  /**
@@ -2983,6 +3465,51 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2983
3465
  * from the persisted draft too.
2984
3466
  */
2985
3467
  resetField: (path: FlatPath<Form>) => void;
3468
+ /**
3469
+ * Wipe a field (or the whole form) to the "appropriate nullish
3470
+ * value" for its declared type — the underlying type's empty/falsy
3471
+ * concrete, with any `.default(x)` wrapper INTENTIONALLY skipped.
3472
+ * Orthogonal to `reset` / `resetField` by design.
3473
+ *
3474
+ * ```ts
3475
+ * const schema = z.object({
3476
+ * notify: z.boolean().default(true),
3477
+ * count: z.number().default(5),
3478
+ * })
3479
+ * const form = useForm({ schema })
3480
+ *
3481
+ * form.reset() // notify → true, count → 5 (defaults)
3482
+ * form.clear() // notify → false, count → 0 (falsy-for-type)
3483
+ * form.clear('notify') // → false (NOT the declared default true)
3484
+ * ```
3485
+ *
3486
+ * Per-wrapper semantics:
3487
+ *
3488
+ * - `.default(x)` / `.prefault(x)` / `.catch(x)` → inner-schema
3489
+ * empty (default is INTENTIONALLY skipped).
3490
+ * - `.optional()` → `undefined` (the wrapper's "absent" marker).
3491
+ * - `.nullable()` → `null` (the wrapper's "explicit empty").
3492
+ * - Object → recursive (every property gets its own empty).
3493
+ * - Array / Set / Record → empty.
3494
+ *
3495
+ * Returns `true` when the write was accepted, `false` when the
3496
+ * adapter couldn't resolve an empty value at the path (e.g. the
3497
+ * path doesn't exist in the schema). The form state is unchanged
3498
+ * on a `false` return.
3499
+ *
3500
+ * Sugar over `setValue(path, schema.getEmptyValueAtPath(path))` —
3501
+ * no separate bookkeeping. Variant memory, history, persistence,
3502
+ * and listeners all see this as a regular write at the path.
3503
+ *
3504
+ * `clear()` (no arg) targets the whole form. `clear('')` targets
3505
+ * the empty-string path slot SPECIFICALLY — the two are NOT
3506
+ * interchangeable, matching `touch()` / `touch('')` from #184.
3507
+ */
3508
+ clear: {
3509
+ (): boolean;
3510
+ <Path extends FlatPath<Form> | ''>(path: Path): boolean;
3511
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form> | ''] ? unknown : never)): boolean;
3512
+ };
2986
3513
  /**
2987
3514
  * Write the current value at `path` to storage immediately. Useful
2988
3515
  * for explicit "Save draft" buttons, `beforeunload` handlers, or
@@ -3011,17 +3538,12 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3011
3538
  */
3012
3539
  clearPersistedDraft: (path?: FlatPath<Form>) => Promise<void>;
3013
3540
  /**
3014
- * Revert the form to the previous snapshot. Returns `true` when a
3015
- * snapshot was restored, `false` when there's nothing to undo.
3016
- * No-op (returns `false`) when `useForm({ history })` wasn't configured.
3017
- */
3018
- undo: () => boolean;
3019
- /**
3020
- * Replay a previously-undone snapshot. Returns `true` on success,
3021
- * `false` when the redo stack is empty. The redo stack clears as
3022
- * soon as a new mutation lands.
3541
+ * Consolidated undo/redo namespace `form.history.{undo, redo,
3542
+ * clear, canUndo, canRedo, size}`. Always present; inert when
3543
+ * `useForm({ history })` wasn't configured. See `FormHistoryNamespace`
3544
+ * for field-by-field semantics.
3023
3545
  */
3024
- redo: () => boolean;
3546
+ history: FormHistoryNamespace;
3025
3547
  /**
3026
3548
  * Focus the first errored field's first visible element. Returns
3027
3549
  * `true` when an element was focused, `false` when no candidate
@@ -3115,4 +3637,4 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3115
3637
  };
3116
3638
 
3117
3639
  export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
3118
- export type { AttaformDefaults as A, NestedType as B, CoercionRegistry as C, DeepPartial as D, OnInvalidSubmitPolicy as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, OnSubmit as P, Path as Q, RegisterValue as R, SlimPrimitiveKind as S, PathKey as T, UseFormConfiguration as U, ValidationError as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, CoercionEntry as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterOptions as a3, RegisterSelectModifier as a4, RegisterTextModifier as a5, RegisterTransform as a6, Segment as a7, SetValueCallback as a8, SetValuePayload as a9, SettledValidationStatus as aa, ShouldShowErrorsConfig as ab, SlimRuntimeOf as ac, SubmitHandler as ad, Unset as ae, ValidateOn as af, ValidateOnConfig as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ShouldShowErrors as f, ApiErrorEnvelope as g, ApiErrorDetails as h, ApiErrorEntry as i, ArrayItem as j, ArrayPath as k, CoercionResult as l, CustomDirectiveRegisterAssignerFn as m, DefaultValuesResponse as n, FieldMetaPayload as o, FieldState as p, FieldStateMap as q, FieldStateMapEntry as r, FlatPath as s, FormErrorRecord as t, FormErrorsSurface as u, FormMeta as v, FormStorage as w, FormStorageKind as x, HistoryConfig as y, IsUnion as z };
3640
+ export type { AttaformDefaults as A, NestedType as B, CoercionRegistry as C, DeepPartial as D, OnInvalidSubmitPolicy as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, OnSubmit as P, Path as Q, RegisterValue as R, SlimPrimitiveKind as S, PathKey as T, UseFormConfiguration as U, ValidationError as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, CoercionEntry as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterOptions as a3, RegisterSelectModifier as a4, RegisterTextModifier as a5, RegisterTransform as a6, Segment as a7, SetValueCallback as a8, SetValuePayload as a9, SettledValidationStatus as aa, ShouldShowErrorsConfig as ab, SlimRuntimeOf as ac, SubmitHandler as ad, Unset as ae, ValidateOn as af, ValidateOnConfig as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, SchemaFactoryOptions as ar, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ShouldShowErrors as f, ApiErrorEnvelope as g, ApiErrorDetails as h, ApiErrorEntry as i, ArrayItem as j, ArrayPath as k, CoercionResult as l, CustomDirectiveRegisterAssignerFn as m, DefaultValuesResponse as n, FieldMetaPayload as o, FieldState as p, FieldStateMap as q, FieldStateMapEntry as r, FlatPath as s, FormErrorRecord as t, FormErrorsSurface as u, FormMeta as v, FormStorage as w, FormStorageKind as x, HistoryConfig as y, IsUnion as z };