attaform 0.16.3 → 0.17.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.
Files changed (86) 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 +27 -7
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +80 -8
  15. package/dist/index.d.mts +80 -8
  16. package/dist/index.d.ts +80 -8
  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.KrNw10aW.cjs → attaform.0Wg7UEeX.cjs} +60 -20
  27. package/dist/shared/attaform.0Wg7UEeX.cjs.map +1 -0
  28. package/dist/shared/attaform.AOgGyRoI.d.cts +65 -0
  29. package/dist/shared/{attaform.lFNwBcA3.d.ts → attaform.B0zue7zt.d.ts} +1 -1
  30. package/dist/shared/{attaform.c_NzdRyc.cjs → attaform.BBM2muQ9.cjs} +7 -3
  31. package/dist/shared/attaform.BBM2muQ9.cjs.map +1 -0
  32. package/dist/shared/{attaform.C9Ph2SMx.cjs → attaform.BFumZXY2.cjs} +1514 -391
  33. package/dist/shared/attaform.BFumZXY2.cjs.map +1 -0
  34. package/dist/shared/attaform.BQ-iGGWd.d.mts +65 -0
  35. package/dist/shared/{attaform.DILbdvfo.mjs → attaform.BT55rDNN.mjs} +1514 -393
  36. package/dist/shared/attaform.BT55rDNN.mjs.map +1 -0
  37. package/dist/shared/{attaform._EqYNPYF.d.mts → attaform.BYbsV2Wv.d.cts} +738 -138
  38. package/dist/shared/{attaform._EqYNPYF.d.ts → attaform.BYbsV2Wv.d.mts} +738 -138
  39. package/dist/shared/{attaform._EqYNPYF.d.cts → attaform.BYbsV2Wv.d.ts} +738 -138
  40. package/dist/shared/{attaform.DGuGGNg9.cjs → attaform.C6_zOf8x.cjs} +232 -113
  41. package/dist/shared/attaform.C6_zOf8x.cjs.map +1 -0
  42. package/dist/shared/{attaform.CJttVxRj.cjs → attaform.C8LVFVVe.cjs} +2 -2
  43. package/dist/shared/{attaform.CJttVxRj.cjs.map → attaform.C8LVFVVe.cjs.map} +1 -1
  44. package/dist/shared/{attaform.BfMxsfmE.mjs → attaform.CIEQgJnM.mjs} +143 -78
  45. package/dist/shared/attaform.CIEQgJnM.mjs.map +1 -0
  46. package/dist/shared/attaform.CX9v2M8k.d.ts +65 -0
  47. package/dist/shared/{attaform.XYOMTvuO.mjs → attaform.Cj0pCNVn.mjs} +232 -113
  48. package/dist/shared/attaform.Cj0pCNVn.mjs.map +1 -0
  49. package/dist/shared/{attaform.DLnKT7wk.d.cts → attaform.ClfCi1i2.d.mts} +1 -1
  50. package/dist/shared/{attaform.CFA6y0KF.mjs → attaform.D6Q5ZP8L.mjs} +60 -20
  51. package/dist/shared/attaform.D6Q5ZP8L.mjs.map +1 -0
  52. package/dist/shared/{attaform.Bls_kFR6.d.mts → attaform.D7lomopc.d.cts} +1 -1
  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.jrxE_xZw.mjs → attaform.h1sq3BFu.mjs} +6 -4
  58. package/dist/shared/attaform.h1sq3BFu.mjs.map +1 -0
  59. package/dist/zod-v3.cjs +3 -3
  60. package/dist/zod-v3.d.cts +5 -5
  61. package/dist/zod-v3.d.mts +5 -5
  62. package/dist/zod-v3.d.ts +5 -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 +6 -5
  72. package/dist/zod.d.mts +6 -5
  73. package/dist/zod.d.ts +6 -5
  74. package/dist/zod.mjs +5 -5
  75. package/dist/zod.mjs.map +1 -1
  76. package/package.json +3 -8
  77. package/dist/shared/attaform.BfMxsfmE.mjs.map +0 -1
  78. package/dist/shared/attaform.C9Ph2SMx.cjs.map +0 -1
  79. package/dist/shared/attaform.CFA6y0KF.mjs.map +0 -1
  80. package/dist/shared/attaform.DGuGGNg9.cjs.map +0 -1
  81. package/dist/shared/attaform.DILbdvfo.mjs.map +0 -1
  82. package/dist/shared/attaform.KrNw10aW.cjs.map +0 -1
  83. package/dist/shared/attaform.XYOMTvuO.mjs.map +0 -1
  84. package/dist/shared/attaform.c_NzdRyc.cjs.map +0 -1
  85. package/dist/shared/attaform.jrxE_xZw.mjs.map +0 -1
  86. package/dist/shared/attaform.rIRYSUI1.cjs.map +0 -1
@@ -73,82 +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('') // [] (root)
102
- * ```
103
- *
104
- * Throws `InvalidPathError` for paths with empty segments
105
- * (`'a..b'`, leading or trailing dots). For keys containing literal
106
- * dots, pass an array form (`['user.name']`) instead.
107
- */
108
- declare function parseDottedPath(path: string): Segment[];
109
- /**
110
- * Canonicalise a path into structured segments plus a stable string
111
- * key. Accepts either dotted-string or array form; integer-looking
112
- * segments normalise to numbers.
113
- *
114
- * ```ts
115
- * canonicalizePath('items.0.name')
116
- * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
117
- *
118
- * canonicalizePath(['items', 0, 'name'])
119
- * // → same result
120
- * ```
121
- *
122
- * The returned `key` is suitable as a `Map`/`Set` key — equal paths
123
- * produce equal keys regardless of input form.
124
- */
125
- declare function canonicalizePath(input: string | Path): {
126
- segments: readonly Segment[];
127
- key: PathKey;
128
- };
129
- /**
130
- * The root path — an empty segment tuple. Pass to APIs that accept
131
- * a `Path` to address the form value as a whole.
132
- */
133
- declare const ROOT_PATH: Path;
134
- /** Stable string key for the root path. */
135
- declare const ROOT_PATH_KEY: PathKey;
136
- /**
137
- * `true` when `path` starts with every segment of `prefix` (in order).
138
- * The empty `prefix` matches every path — ROOT prefix is universal.
139
- *
140
- * Walks segments rather than `PathKey` strings because the data this
141
- * helper operates on (e.g. `meta.errors[].path`) carries segment
142
- * arrays directly.
143
- *
144
- * ```ts
145
- * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
146
- * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
147
- * isPathPrefix([], ['anything']) // true (root prefix)
148
- * ```
149
- */
150
- declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
151
-
152
76
  /** Internal brand for the `Unset` type. Never exposed at runtime. */
153
77
  declare const _unsetBrand: unique symbol;
154
78
  /**
@@ -453,6 +377,98 @@ type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symb
453
377
  [K in keyof T]: DefaultValuesShape<T[K]>;
454
378
  } : T;
455
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
+
456
472
  /**
457
473
  * Identifier for a form. A `FormKey` is the string passed via
458
474
  * `useForm({ key })`, used to look up a form by name from a distant
@@ -465,9 +481,10 @@ type FormKey = string;
465
481
  /**
466
482
  * One validation failure. `path` points at the offending field as a
467
483
  * structured array — `['user', 'address', 0, 'line1']` for a nested
468
- * field, `[]` for a form-level error. `formKey` identifies which
469
- * form produced the error so a single error list can be routed to
470
- * multiple forms.
484
+ * field, `['']` (the empty-string path) for a form-level error
485
+ * (root `.refine()` messages, `setFormErrors()` entries, server-
486
+ * emitted form banners). `formKey` identifies which form produced
487
+ * the error so a single error list can be routed to multiple forms.
471
488
  *
472
489
  * Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
473
490
  * `onError` callback, and by `parseApiErrors` for server responses.
@@ -475,7 +492,12 @@ type FormKey = string;
475
492
  type ValidationError = {
476
493
  /** Human-readable message describing the failure. */
477
494
  message: string;
478
- /** Structured path of the offending field. Empty array means a form-level error. */
495
+ /**
496
+ * Structured path of the offending field. The empty-string path
497
+ * `['']` is the form-level bucket — the dedicated home for errors
498
+ * that don't belong to any specific field, distinct from the
499
+ * whole-form subtree address `[]`.
500
+ */
479
501
  path: (string | number)[];
480
502
  /** Identifies which form produced this error. */
481
503
  formKey: FormKey;
@@ -648,6 +670,38 @@ type AbstractSchema<Form, GetValueFormType> = {
648
670
  * callers treat that as "don't fill" and fall back to existing data.
649
671
  */
650
672
  getDefaultAtPath(path: Path): unknown;
673
+ /**
674
+ * Give the schema a chance to normalize the consumer's write value
675
+ * before it lands in storage / hits the slim-primitive gate. Each
676
+ * schema library exposes this concept differently — Zod calls it
677
+ * `z.preprocess(fn, inner)`, Yup calls it `.transform()`, Valibot
678
+ * spells it `pipe(transform(fn), inner)` — but the shape is the
679
+ * same: "this input shape gets coerced into that storage shape at
680
+ * the boundary."
681
+ *
682
+ * Runs SYNCHRONOUSLY at the write boundary so storage holds the
683
+ * post-normalization shape. Without this, a schema like `notify:
684
+ * z.preprocess(v => v == null ? defaultVar : v, innerDU)` would
685
+ * let the consumer write `null` and lock storage into `null` —
686
+ * because the gate sees the raw input (which the preprocess wrapper
687
+ * accepts as `unknown`) and storage holds a shape no variant
688
+ * matches.
689
+ *
690
+ * Adapters MUST:
691
+ * - Return `value` unchanged when no normalization is declared at
692
+ * the path.
693
+ * - Return `value` unchanged when the user's normalization fn
694
+ * returns a `Promise` (async coercion can't run at write time —
695
+ * validation handles it during parse).
696
+ * - Let user-thrown errors propagate (the user wrote the fn; we
697
+ * just tag the path in the wrapper error for diagnostics).
698
+ *
699
+ * Normalization runs when `path` equals the wrapper's exact
700
+ * location. Writes deeper than the wrapper bypass it (a wrapper
701
+ * over the whole subtree can't be invoked from a partial leaf
702
+ * write).
703
+ */
704
+ normalizeWriteValueAtPath(value: unknown, path: Path): unknown;
651
705
  /**
652
706
  * Distinguish a tuple (fixed-length, position-typed) from an
653
707
  * unbounded array at `path`. The runtime calls this on every
@@ -708,7 +762,7 @@ type AbstractSchema<Form, GetValueFormType> = {
708
762
  * returned as a `success: false` response with a populated
709
763
  * `errors` array.
710
764
  */
711
- validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<Form>>;
765
+ validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<GetValueFormType>>;
712
766
  /**
713
767
  * Sync sister to `getSchemasAtPath` / `validateAtPath`. Returns the
714
768
  * set of primitive `typeof`-style kinds the path's leaf schema
@@ -920,6 +974,14 @@ type UnionDiscriminatorContext = {
920
974
  * skips the reshape and falls back to a plain write.
921
975
  */
922
976
  getVariantDefault(value: unknown): unknown;
977
+ /**
978
+ * Returns `true` iff `value` is a literal recognised by one of the
979
+ * discriminator's variants. Used by reshape to decide whether to
980
+ * seek a variant default or emit a stub state. NOT used at the
981
+ * runtime write gate — consumer-side value validity is a
982
+ * validation-time concern.
983
+ */
984
+ isVariantSelected(value: unknown): boolean;
923
985
  };
924
986
  /**
925
987
  * The set of primitive "kinds" the slim-primitive write contract
@@ -1119,19 +1181,136 @@ type WriteMeta = {
1119
1181
  * an infinite loop. Don't set from consumer code.
1120
1182
  */
1121
1183
  readonly skipDiscriminatorReshape?: boolean;
1184
+ /**
1185
+ * Hint about an array structural mutation, set by `field-arrays.ts`
1186
+ * helpers so `setValueAtPath` can surgically clear variant memory
1187
+ * for indices the operation invalidated. Without this hint, a raw
1188
+ * whole-array `setValue(arrayPath, [...])` clears all memory under
1189
+ * the array (the runtime can't tell which indices stayed put).
1190
+ * Internal — don't set from consumer code.
1191
+ */
1192
+ readonly arrayOp?: {
1193
+ readonly kind: 'shift-from';
1194
+ readonly index: number;
1195
+ } | {
1196
+ readonly kind: 'shift-range';
1197
+ readonly fromIndex: number;
1198
+ readonly toIndex: number;
1199
+ } | {
1200
+ readonly kind: 'swap';
1201
+ readonly a: number;
1202
+ readonly b: number;
1203
+ } | {
1204
+ readonly kind: 'replace-at';
1205
+ readonly index: number;
1206
+ };
1207
+ /**
1208
+ * Per-instance config overrides threaded through writes so each
1209
+ * `useForm({ key })` callsite honors its own `validateOn` /
1210
+ * `debounceMs` / `rememberVariants` even when sharing a FormStore
1211
+ * with sibling calls (e.g., a modal and main form rendering the
1212
+ * same logical form). Internal — set by `buildFormApi` from
1213
+ * the per-instance options bag; the store reads each field with
1214
+ * fallback to its construction-time defaults.
1215
+ */
1216
+ readonly instance?: {
1217
+ readonly validateOn?: ValidateOn;
1218
+ readonly debounceMs?: number;
1219
+ readonly rememberVariants?: boolean;
1220
+ };
1221
+ /**
1222
+ * When `true`, marks this `applyFormReplacement` call as the
1223
+ * persistence hydration step. Modules that snapshot the form state
1224
+ * (notably the history module) treat hydration as the baseline:
1225
+ * stacks reset to a single seed of the post-hydration value, so a
1226
+ * subsequent `undo()` can't recover the transient pre-hydration
1227
+ * default. Internal — set by `wirePersistence`. Don't set from
1228
+ * consumer code.
1229
+ */
1230
+ readonly hydration?: boolean;
1231
+ /**
1232
+ * When `true`, this write originated from a sibling tab's
1233
+ * BroadcastChannel broadcast (the multi-tab sync module's inbound
1234
+ * apply). Listeners that initiate side effects check this flag to
1235
+ * avoid amplification loops and spurious side effects:
1236
+ *
1237
+ * - The multi-tab sync OUTBOUND broadcaster skips so a remote-driven
1238
+ * write doesn't echo back across the channel.
1239
+ * - The history module updates its diff anchor but does NOT push a
1240
+ * delta — remote writes aren't part of the local user's undo
1241
+ * timeline.
1242
+ * - The persistence writer skips so the receiving tab doesn't
1243
+ * double-persist a value the originating tab already wrote.
1244
+ *
1245
+ * Internal — set by `createMultiTabSyncModule`. Don't set from
1246
+ * consumer code.
1247
+ */
1248
+ readonly crossTab?: boolean;
1122
1249
  };
1123
1250
  /**
1124
1251
  * Undo/redo configuration passed via `useForm({ history })`.
1125
1252
  *
1126
- * - `true` — enable with the default snapshot cap (`max: 50`).
1127
- * - `{ max }` — enable and tune the bounded snapshot stack size.
1253
+ * - `true` — enable with the default position cap (`max: 128`).
1254
+ * - `{ max }` — enable and tune the bounded history size.
1128
1255
  *
1129
- * When enabled, every mutation pushes a snapshot; `undo()` /
1130
- * `redo()` walk the stacks. `reset()` clears history.
1256
+ * When enabled, every mutation records a forward delta; `form.history.undo()`
1257
+ * / `form.history.redo()` walk the chain. `reset()` is itself a mutation —
1258
+ * the pre-reset state stays one undo away. Persistence hydration is the
1259
+ * floor: after hydrate applies, the chain reseeds with the hydrated value
1260
+ * and `undo()` cannot reach the transient pre-hydration default.
1131
1261
  */
1132
1262
  type HistoryConfig = true | {
1133
1263
  max?: number;
1134
1264
  };
1265
+ /**
1266
+ * Consolidated undo/redo namespace at `form.history`. All history-related
1267
+ * surface lives here — methods and reactive flags both — so consumers
1268
+ * have one canonical address to read from.
1269
+ *
1270
+ * Always present on `useForm()` return whether or not `history` was
1271
+ * configured. When history isn't enabled, methods are no-ops returning
1272
+ * `false` (or `void`), `canUndo` / `canRedo` read `false`, and `size`
1273
+ * reads `0`. Consumer templates don't need conditional logic.
1274
+ *
1275
+ * Reactivity: built as `readonly(reactive({...}))`, so `canUndo` / `canRedo`
1276
+ * / `size` auto-unwrap on access (plain `boolean` / `number`, not refs).
1277
+ * Method fields (`undo`, `redo`, `clear`) pass through as plain functions.
1278
+ */
1279
+ type FormHistoryNamespace = {
1280
+ /**
1281
+ * Step back one position in the history chain. Returns `true` when a
1282
+ * step was taken, `false` when already at the oldest reachable
1283
+ * position (or when history isn't configured).
1284
+ */
1285
+ readonly undo: () => boolean;
1286
+ /**
1287
+ * Replay the next step forward in the chain. Returns `true` on
1288
+ * success, `false` when there's nothing queued (or history isn't
1289
+ * configured). The forward branch is dropped as soon as a new
1290
+ * mutation lands.
1291
+ */
1292
+ readonly redo: () => boolean;
1293
+ /**
1294
+ * Wipe the undo and redo branches; reseed the chain with the current
1295
+ * form state as the new baseline. The form value, errors, and
1296
+ * blankPaths all stay where they are — only the past/future history
1297
+ * resets. After `clear()`: `canUndo === false`, `canRedo === false`,
1298
+ * `size === 1`. No-op when history isn't configured.
1299
+ */
1300
+ readonly clear: () => void;
1301
+ /** `true` when there is at least one undo step available. */
1302
+ readonly canUndo: boolean;
1303
+ /** `true` when `undo()` has been called and a `redo()` would replay. */
1304
+ readonly canRedo: boolean;
1305
+ /**
1306
+ * Total reachable positions in the history chain (the current
1307
+ * position plus everything reachable via `undo()` / `redo()`).
1308
+ * Useful for debug overlays; UI driving undo/redo buttons should
1309
+ * gate on `canUndo` / `canRedo` instead. Reads `0` when history
1310
+ * isn't configured.
1311
+ */
1312
+ readonly size: number;
1313
+ };
1135
1314
  /**
1136
1315
  * Full options bag for `useForm({ persist })`. Use this when you need
1137
1316
  * to override defaults beyond picking the backend.
@@ -1229,10 +1408,15 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1229
1408
  * abstract entry point accepts any object implementing
1230
1409
  * `AbstractSchema`.
1231
1410
  *
1232
- * For schemas that depend on the form's identity, pass a factory
1233
- * `(key) => schema` instead — the library calls it once per form.
1411
+ * For schemas that depend on the form's identity or per-form
1412
+ * options, pass a factory `(key, options) => schema` instead — the
1413
+ * library calls it once per form, after `mergeWithDefaults` has
1414
+ * resolved the options bag (`maxRecursionDepth`, etc.). Most
1415
+ * adapters ignore the options argument; the typed Zod entry points
1416
+ * use it to thread the resolved recursion cap into the adapter
1417
+ * closure.
1234
1418
  */
1235
- schema: Schema | ((key: FormKey) => Schema);
1419
+ schema: Schema | ((key: FormKey, options: SchemaFactoryOptions) => Schema);
1236
1420
  /**
1237
1421
  * Optional identifier for this form. Omit for one-off forms; the
1238
1422
  * library allocates a unique key automatically (SSR-safe, stable
@@ -1342,13 +1526,14 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1342
1526
  */
1343
1527
  persist?: PersistConfig;
1344
1528
  /**
1345
- * Opt-in undo/redo. Off by default. `true` enables with a 50-snapshot
1529
+ * Opt-in undo/redo. Off by default. `true` enables with a 128-position
1346
1530
  * cap; `{ max: N }` tunes the cap.
1347
1531
  *
1348
- * Every mutation pushes a snapshot. `undo()` pops one; `redo()`
1349
- * replays it. `reset()` clears history. Reactive flags
1350
- * `state.canUndo` / `state.canRedo` / `state.historySize` reflect
1351
- * the current stack.
1532
+ * Every mutation records a forward delta. `form.history.undo()` walks
1533
+ * one step back; `form.history.redo()` walks one step forward.
1534
+ * `reset()` is itself a mutation, so the pre-reset state stays one
1535
+ * undo away. The consolidated `form.history` namespace also exposes
1536
+ * `clear()`, `canUndo`, `canRedo`, and `size`.
1352
1537
  */
1353
1538
  history?: HistoryConfig;
1354
1539
  /**
@@ -1394,6 +1579,82 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1394
1579
  * coerced.
1395
1580
  */
1396
1581
  coerce?: boolean | CoercionRegistry;
1582
+ /**
1583
+ * Per-form override of the `shouldShowErrors` heuristic that drives
1584
+ * `field.showErrors` and `form.meta.showErrors`. Falls back to
1585
+ * `AttaformDefaults.shouldShowErrors`, then to the library default
1586
+ * (`defaultShouldShowErrors`). See `AttaformDefaults.shouldShowErrors`
1587
+ * for the resolution rules and predicate signature.
1588
+ *
1589
+ * Boolean shorthand: `true` → always show *when errors exist*;
1590
+ * `false` → never show.
1591
+ */
1592
+ shouldShowErrors?: ShouldShowErrorsConfig;
1593
+ /**
1594
+ * Recursion ceiling for schema walks that descend through recursive
1595
+ * schemas (Zod's `z.lazy(...)` today). Default `64`. Per-form value
1596
+ * overrides `AttaformDefaults.maxRecursionDepth`, which overrides
1597
+ * the library default.
1598
+ *
1599
+ * Schemas that don't include a recursive boundary ignore this knob
1600
+ * entirely — it's read only at the descent step through a recursive
1601
+ * wrapper. Set it on the specific form whose schema is recursive
1602
+ * (a comment tree, a category tree, a nested-rule editor):
1603
+ *
1604
+ * ```ts
1605
+ * useForm({ schema: commentTreeSchema, maxRecursionDepth: 128 })
1606
+ * ```
1607
+ *
1608
+ * Past the cap, the slim-primitive type gate falls back to permissive
1609
+ * (write-time type checks skip; full schema validation still runs).
1610
+ * Storage and reads work at any depth; only the per-write type gate
1611
+ * stops short of the cap. Raise the cap if you regularly edit nodes
1612
+ * beyond the default depth.
1613
+ *
1614
+ * See `AttaformDefaults.maxRecursionDepth` for the resolution rules
1615
+ * and the broader description of where the cap is read.
1616
+ */
1617
+ maxRecursionDepth?: number;
1618
+ /**
1619
+ * Override the path-segment name stems treated as sensitive for this
1620
+ * form. Sensitive paths are excluded from persistence writes,
1621
+ * multi-tab sync broadcasts, AND the DevTools redact walk.
1622
+ *
1623
+ * Resolution: per-form value (this field) > global default
1624
+ * (`createAttaform({ defaults: { sensitiveNames } })`) > library
1625
+ * default (`DEFAULT_SENSITIVE_NAMES`).
1626
+ *
1627
+ * Pass an empty array `[]` as the explicit opt-out — "nothing is
1628
+ * sensitive on this form" — for fully-trusted internal tooling.
1629
+ * See `AttaformDefaults.sensitiveNames` for composition examples.
1630
+ */
1631
+ sensitiveNames?: readonly string[];
1632
+ /**
1633
+ * Cross-tab synchronisation via BroadcastChannel. Defaults to `true`
1634
+ * (when the browser supports it and the page is in a secure
1635
+ * context): a keyed `useForm` callsite auto-pairs with same-keyed
1636
+ * siblings in other same-origin tabs and mirrors their mutations
1637
+ * in near real-time.
1638
+ *
1639
+ * **Resolution order (per-register override > per-form > global > library):**
1640
+ *
1641
+ * register(path, { multiTab }) > useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
1642
+ *
1643
+ * **When to set `false`:** forms holding PII / PHI, contexts where
1644
+ * tab isolation is required by policy, or any flow where conflicting
1645
+ * tab edits could corrupt user intent. Sensitive-named paths (via
1646
+ * `sensitiveNames`) are always stripped from outbound broadcasts
1647
+ * regardless of this setting.
1648
+ *
1649
+ * **Secure-context requirement.** Multi-tab sync is silently disabled
1650
+ * outside `window.isSecureContext === true` (HTTPS or localhost). On
1651
+ * plain HTTP a one-shot dev warning fires and the module noops.
1652
+ *
1653
+ * **Anonymous (auto-keyed) forms skip sync entirely** — without a
1654
+ * consumer-supplied `key`, cross-tab identity is undefined and the
1655
+ * channel would be solo by construction.
1656
+ */
1657
+ multiTab?: boolean;
1397
1658
  };
1398
1659
  /**
1399
1660
  * App-level defaults applied to every `useForm` call. Set these once
@@ -1459,6 +1720,130 @@ type AttaformDefaults = {
1459
1720
  * authoritative writes whose strict typing is on the caller.
1460
1721
  */
1461
1722
  coerce?: boolean | CoercionRegistry;
1723
+ /**
1724
+ * Default for `useForm({ shouldShowErrors })`. Centralised heuristic
1725
+ * that drives `field.showErrors` (and `form.meta.showErrors`) — a
1726
+ * boolean that gates whether a path's errors are *ready* to render.
1727
+ *
1728
+ * Resolution order (per-form wins):
1729
+ *
1730
+ * useForm({ shouldShowErrors }) > AttaformDefaults > library default
1731
+ *
1732
+ * The library default reads "show after the first submit attempt OR
1733
+ * after the field has been interacted with AND changed":
1734
+ *
1735
+ * ```ts
1736
+ * (field, formMeta) =>
1737
+ * formMeta.submitCount > 0 || (field.touched === true && field.dirty)
1738
+ * ```
1739
+ *
1740
+ * Compose with the library default via the public
1741
+ * `defaultShouldShowErrors` export. Boolean shorthand is supported:
1742
+ * `true` → always show *when errors exist*; `false` → never show. The
1743
+ * predicate is invoked only when `errors.length > 0`, so authors
1744
+ * don't re-check inside.
1745
+ *
1746
+ * The predicate's args are `Omit`'d of `showErrors` / `firstError`
1747
+ * to prevent recursive predicates — those are derived FROM this
1748
+ * predicate, so reading them inside would be a self-reference.
1749
+ */
1750
+ shouldShowErrors?: ShouldShowErrorsConfig;
1751
+ /**
1752
+ * Default for `useForm({ maxRecursionDepth })`. Recursion ceiling
1753
+ * for schema walks that descend through recursive schemas (Zod's
1754
+ * `z.lazy(...)` today, equivalent constructs in any future adapter).
1755
+ * Library default: `64`.
1756
+ *
1757
+ * Resolution order (per-form wins):
1758
+ *
1759
+ * useForm({ maxRecursionDepth }) > AttaformDefaults > library default (64)
1760
+ *
1761
+ * Read at every step of a schema walk that crosses a recursive
1762
+ * boundary — default-value derivation at construction, slim-primitive
1763
+ * type gates on each write, path-by-path schema resolution. Walks
1764
+ * track their descent depth and switch to a permissive fallback once
1765
+ * `depth > maxRecursionDepth`.
1766
+ *
1767
+ * "Permissive fallback" means storage and reads keep working at any
1768
+ * depth; only the per-write type gate stops checking past the cap.
1769
+ * Full schema validation (`validateAsync`, `handleSubmit`) still runs
1770
+ * against the real schema, so refinement errors at any depth still
1771
+ * surface — the cap only affects the *write-time gate*.
1772
+ *
1773
+ * Forms with no recursive schemas ignore this entirely — the cap is
1774
+ * read only at the descent step through a recursive wrapper. Setting
1775
+ * it app-wide is the right move when you have multiple recursive
1776
+ * forms that should share one ceiling:
1777
+ *
1778
+ * ```ts
1779
+ * createAttaform({
1780
+ * defaults: { maxRecursionDepth: 128 },
1781
+ * })
1782
+ * ```
1783
+ *
1784
+ * Per-form override stays available for the one tree-shaped form
1785
+ * whose depth is unusual:
1786
+ *
1787
+ * ```ts
1788
+ * useForm({ schema: deepCategoryTreeSchema, maxRecursionDepth: 256 })
1789
+ * ```
1790
+ *
1791
+ * Setting this app-wide costs nothing for non-recursive forms — the
1792
+ * walks that read the cap never run for them.
1793
+ *
1794
+ * Pass `Infinity` to disable the cap entirely. Walks will then
1795
+ * descend through recursive boundaries until they terminate
1796
+ * structurally; a schema with no structural terminator will exhaust
1797
+ * the JS call stack. Reserve for schemas whose authors are
1798
+ * confident the recursion is bounded by the actual data shape.
1799
+ */
1800
+ maxRecursionDepth?: number;
1801
+ /**
1802
+ * Override the path-segment name stems treated as sensitive.
1803
+ * Sensitive paths are excluded from persistence writes, multi-tab
1804
+ * sync broadcasts, AND the DevTools redact walk — one configurable
1805
+ * source of truth across every surface.
1806
+ *
1807
+ * Library default is `DEFAULT_SENSITIVE_NAMES` (exported from
1808
+ * `attaform`); compose to extend:
1809
+ *
1810
+ * ```ts
1811
+ * import { DEFAULT_SENSITIVE_NAMES, createAttaform } from 'attaform'
1812
+ *
1813
+ * createAttaform({
1814
+ * defaults: { sensitiveNames: [...DEFAULT_SENSITIVE_NAMES, 'mrn', 'tax_id'] }
1815
+ * })
1816
+ * ```
1817
+ *
1818
+ * Pass an empty array `[]` as the explicit opt-out — "nothing is
1819
+ * sensitive" — for fully-trusted internal tooling. When present at
1820
+ * the per-form level via `useForm({ sensitiveNames })`, the per-form
1821
+ * list REPLACES the global one (consumers compose their own
1822
+ * additive lists via the exported default).
1823
+ */
1824
+ sensitiveNames?: readonly string[];
1825
+ /**
1826
+ * App-wide default for `useForm({ multiTab })`. Default `true` when
1827
+ * the runtime supports `BroadcastChannel` AND `window.isSecureContext`
1828
+ * is true (HTTPS in production, localhost in development) — same gate
1829
+ * browsers apply to other sensitive APIs (clipboard, geolocation,
1830
+ * push, web crypto subtle).
1831
+ *
1832
+ * Set to `false` once at the plugin level for a multi-tenant
1833
+ * deployment that prefers tab-isolation by default; individual forms
1834
+ * can still opt back in via `useForm({ multiTab: true })`.
1835
+ *
1836
+ * **Resolution order (per-form wins):**
1837
+ *
1838
+ * useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
1839
+ *
1840
+ * **Secure-context gate.** Multi-tab sync only activates over HTTPS
1841
+ * or localhost. On plain HTTP, the module silently noops with a
1842
+ * one-shot dev-mode warning — production deployments MUST be served
1843
+ * over HTTPS for sync to function. See the multi-tab-sync recipe's
1844
+ * Security section for the threat model.
1845
+ */
1846
+ multiTab?: boolean;
1462
1847
  };
1463
1848
  /**
1464
1849
  * Callback invoked by `handleSubmit` after the form parses successfully.
@@ -1473,6 +1858,46 @@ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
1473
1858
  * automatic `onInvalidSubmit` UI nudge).
1474
1859
  */
1475
1860
  type OnError = (error: ValidationError[]) => void | Promise<void>;
1861
+ /**
1862
+ * Predicate that drives `field.showErrors` (and `form.meta.showErrors`).
1863
+ * Receives the field's reactive state plus the form's reactive meta;
1864
+ * returns `true` to render the field's errors, `false` to keep them
1865
+ * hidden. The framework gates the call on `errors.length > 0`, so
1866
+ * authors don't re-check error presence inside.
1867
+ *
1868
+ * Both arguments are `Omit`'d of `showErrors` / `firstError` — those
1869
+ * are derived FROM this predicate, so reading them inside would be a
1870
+ * self-reference. The omit is enforced at the type level AND at
1871
+ * runtime: the keys literally are not present on the objects passed
1872
+ * in, so `as` casting in TS or vanilla-JS bypass cannot create a
1873
+ * cycle.
1874
+ *
1875
+ * The library default — `defaultShouldShowErrors` — is publicly
1876
+ * exported so a layered predicate can compose with it:
1877
+ *
1878
+ * ```ts
1879
+ * import { defaultShouldShowErrors } from 'attaform'
1880
+ *
1881
+ * useForm({
1882
+ * schema,
1883
+ * shouldShowErrors: (field, formMeta) =>
1884
+ * field.path[0] === 'urgent' || defaultShouldShowErrors(field, formMeta),
1885
+ * })
1886
+ * ```
1887
+ */
1888
+ type ShouldShowErrors = (field: Omit<FieldState, 'showErrors' | 'firstError'>, formMeta: Omit<FormMeta, 'showErrors' | 'firstError'>) => boolean;
1889
+ /**
1890
+ * Configuration shape for `shouldShowErrors`. A predicate function or
1891
+ * a boolean shorthand:
1892
+ *
1893
+ * - `true` — always show errors (when any exist).
1894
+ * - `false` — never show errors.
1895
+ * - function — custom predicate, see `ShouldShowErrors`.
1896
+ *
1897
+ * Resolved through three tiers (per-form > plugin defaults > library
1898
+ * default).
1899
+ */
1900
+ type ShouldShowErrorsConfig = ShouldShowErrors | boolean;
1476
1901
  /**
1477
1902
  * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
1478
1903
  * it to a `<form>`:
@@ -1675,6 +2100,25 @@ type RegisterOptions = {
1675
2100
  * client-side storage for this user's session.
1676
2101
  */
1677
2102
  acknowledgeSensitive?: boolean;
2103
+ /**
2104
+ * Opt this field OUT of multi-tab sync. The form-level cascade
2105
+ * activates sync by default; passing `multiTab: false` on a single
2106
+ * register call keeps that path tab-local — outbound patches at
2107
+ * the path are stripped, and inbound patches at the path are
2108
+ * rejected (symmetric tab-local behaviour).
2109
+ *
2110
+ * The opt-out is downgrade-only — you cannot pass `multiTab: true`
2111
+ * to bring sync back on a form whose form-level `multiTab` is
2112
+ * `false` (in that case the sync module never instantiated; there's
2113
+ * no broadcaster to opt back into).
2114
+ *
2115
+ * Use for fields that hold transient per-tab UI state inside an
2116
+ * otherwise-synced form (e.g. an editor's cursor position field
2117
+ * mirrored into the form for save-on-blur), or for individual
2118
+ * paths the consumer wants to scope to the originating tab without
2119
+ * disabling sync globally.
2120
+ */
2121
+ multiTab?: boolean;
1678
2122
  /**
1679
2123
  * Sync transformation pipeline applied to user-typed values before
1680
2124
  * they reach form state. Composes left-to-right: each transform
@@ -1998,6 +2442,59 @@ type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
1998
2442
  * array elements as possibly-undefined to reflect runtime reality.
1999
2443
  */
2000
2444
  type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
2445
+ /**
2446
+ * Detect `any` distinctly from `unknown`. The trick: `1 & any` is `any`
2447
+ * and `0 extends any` is `true`; `1 & unknown` is `1` and `0 extends 1`
2448
+ * is `false`. Used to fork `PathSetValuePayload` so `z.any()` paths
2449
+ * resolve to `any` (matching the read-side surface) and `z.unknown()` /
2450
+ * preprocess paths resolve to `unknown` (matching Zod's input typing).
2451
+ */
2452
+ type IsAny<T> = 0 extends 1 & T ? true : false;
2453
+ /**
2454
+ * Resolves `setValue`'s `value` argument type at a single `Path` leaf.
2455
+ *
2456
+ * Three branches, one per Zod input-typing case:
2457
+ *
2458
+ * 1. **`any` leaf (`z.any()`)** — schema input type is `any`; the
2459
+ * whole form API surface (read, register, fields) is `any` at
2460
+ * this path. This branch returns raw `any` so `setValue` stays
2461
+ * consistent with the rest. Callsites that pass an unannotated
2462
+ * `(prev) => ...` may surface `noImplicitAny` under the
2463
+ * consumer's tsconfig — annotate `(prev: any) => ...` to opt
2464
+ * into the looser shape explicitly.
2465
+ *
2466
+ * 2. **`unknown` leaf (`z.unknown()`, `z.preprocess()` input)** —
2467
+ * schema input is unconstrained; consumers narrow before use.
2468
+ * The branch returns `({} | null | undefined) | ((prev: unknown)
2469
+ * => unknown)` instead of a `SetValuePayload<unknown, ...>`-style
2470
+ * union for three reasons:
2471
+ *
2472
+ * a. **Union absorption** — `unknown | X` collapses to `unknown`,
2473
+ * erasing the callback union member. With the callback shape
2474
+ * gone, TS has no contextual type for `prev` and decays it to
2475
+ * implicit `any` under `noImplicitAny`. The triple
2476
+ * `{} | null | undefined` is structurally equivalent to
2477
+ * `unknown` (covers the same value space) but is NOT subject
2478
+ * to absorption — the callback branch survives the union and
2479
+ * `prev` infers cleanly to `unknown`.
2480
+ *
2481
+ * b. **`NonNullable<unknown> = {}`** — applying `NonNullable` to
2482
+ * the read slot for an unknown leaf narrows `prev` to `{}`,
2483
+ * which is looser than `unknown` (allows ad-hoc property
2484
+ * access). This branch keeps the read slot as `unknown`
2485
+ * directly so the consumer is forced to narrow.
2486
+ *
2487
+ * c. **`Unset`-widening doesn't apply** — `DefaultValuesShape`
2488
+ * widens primitive leaves to admit `unset`; for an unknown
2489
+ * leaf there's no primitive to widen. The open-form triple
2490
+ * covers the same value space the runtime accepts (any
2491
+ * value, including `unset` — symbols are `{}`).
2492
+ *
2493
+ * 3. **All other leaves** — flow through unchanged via
2494
+ * `SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>`.
2495
+ */
2496
+ type PathSetValuePayload<Leaf> = IsAny<Leaf> extends true ? any : unknown extends Leaf ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
2497
+ ({} | null | undefined) | ((prev: unknown) => unknown) : SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>;
2001
2498
  /**
2002
2499
  * Per-field reactive shape returned by `form.fields.<leaf-path>` and
2003
2500
  * `form.fields(path)`. Slim, readonly across the board. The unified
@@ -2075,6 +2572,51 @@ type FieldState<Value = unknown> = {
2075
2572
  * green-checkmark / `aria-invalid` UX.
2076
2573
  */
2077
2574
  readonly valid: boolean;
2575
+ /**
2576
+ * Centralised "should I render this field's errors right now?"
2577
+ * gate. Wraps `errors.length > 0 && shouldShowErrors(field, formMeta)`
2578
+ * so templates avoid re-spelling the heuristic at every error site:
2579
+ *
2580
+ * ```vue
2581
+ * <span v-if="form.fields.email.showErrors">
2582
+ * {{ form.fields.email.firstError?.message }}
2583
+ * </span>
2584
+ * ```
2585
+ *
2586
+ * The heuristic itself comes from `useForm({ shouldShowErrors })` →
2587
+ * `createAttaform({ defaults: { shouldShowErrors } })` → library
2588
+ * default (`defaultShouldShowErrors` — show after first submit OR
2589
+ * after touched-and-dirty). Override per form, app-wide, or
2590
+ * compose with `defaultShouldShowErrors` for a layered predicate.
2591
+ *
2592
+ * Falls back to `false` whenever there are no errors — the gate
2593
+ * skips the predicate entirely in that case.
2594
+ *
2595
+ * Available on container paths too: `form.fields.users[0].showErrors`
2596
+ * aggregates over the row's descendants (any descendant with a
2597
+ * qualifying error flips the container on).
2598
+ */
2599
+ readonly showErrors: boolean;
2600
+ /**
2601
+ * The first `ValidationError` at this path in the deterministic
2602
+ * schema-declaration order — equivalent to `errors[0]`, exposed as
2603
+ * a sugar accessor for the common case of "show the highest-priority
2604
+ * error message and ignore the rest":
2605
+ *
2606
+ * ```vue
2607
+ * <span v-if="form.fields.email.showErrors">
2608
+ * {{ form.fields.email.firstError?.message }}
2609
+ * </span>
2610
+ * ```
2611
+ *
2612
+ * `undefined` when no errors exist. Independent of `showErrors` —
2613
+ * the data primitive is always available; the heuristic only
2614
+ * decides when to render it.
2615
+ *
2616
+ * On container paths, the first error in the aggregated subtree
2617
+ * (descendants sorted by `pathOrdinal`).
2618
+ */
2619
+ readonly firstError: ValidationError | undefined;
2078
2620
  readonly path: ReadonlyArray<string | number>;
2079
2621
  readonly blank: boolean;
2080
2622
  /**
@@ -2435,16 +2977,6 @@ type FormMeta<F = unknown> = FieldState<F> & {
2435
2977
  * `try { await onSubmit() }` instead.
2436
2978
  */
2437
2979
  readonly submitError: unknown;
2438
- /** `true` when there is at least one undo step available. Always present (false when history is disabled). */
2439
- readonly canUndo: boolean;
2440
- /** `true` when `undo()` has been called and a `redo()` would replay. Always present (false when history is disabled). */
2441
- readonly canRedo: boolean;
2442
- /**
2443
- * Total snapshots across the undo and redo stacks. Useful for
2444
- * debug overlays; UI driving undo/redo buttons should gate on
2445
- * `canUndo` / `canRedo` instead.
2446
- */
2447
- readonly historySize: number;
2448
2980
  /**
2449
2981
  * Per-`useForm()`-call identity. Stable for the lifetime of one
2450
2982
  * `useForm()` call; new on every fresh mount. Orthogonal to
@@ -2483,6 +3015,22 @@ type FormMeta<F = unknown> = FieldState<F> & {
2483
3015
  * form.handleSubmit(onSubmit) // returns a submit handler
2484
3016
  * form.meta.submitting // form-level reactive flag
2485
3017
  * ```
3018
+ *
3019
+ * Two generic slots split the input view from the output view:
3020
+ *
3021
+ * - `Form` — the **input / storage shape** (`z.input<Schema>`). Used
3022
+ * by `setValue`, `defaultValues`, `values`, `fields`, `register`,
3023
+ * `toRef`, and every path-addressed API. Storage holds values as
3024
+ * the consumer wrote them; preprocess normalization runs at the
3025
+ * write boundary, but `.transform()`s are deferred to parse-time.
3026
+ *
3027
+ * - `GetValueFormType` — the **output / parsed shape**
3028
+ * (`z.output<Schema>`). Used by `handleSubmit`'s `onSubmit`
3029
+ * callback and by `form.process()`'s success payload. This is the
3030
+ * shape after refinements have fired and transforms have run.
3031
+ *
3032
+ * For schemas without transforms the two are identical, and the
3033
+ * default `GetValueFormType = Form` keeps the surface ergonomic.
2486
3034
  */
2487
3035
  type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form> = {
2488
3036
  /**
@@ -2496,10 +3044,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2496
3044
  * ```
2497
3045
  *
2498
3046
  * `data` is the strictly-typed parsed value — refinements have
2499
- * fired, so every leaf is guaranteed to satisfy its schema-level
2500
- * format / range / membership constraints.
3047
+ * fired and `.transform()`s have run, so the payload matches
3048
+ * `z.output<Schema>` (the post-parse output shape). For schemas
3049
+ * where the input type differs from the output type (e.g.
3050
+ * `z.string().transform(v => v.length > 10)`), `data` is the
3051
+ * output shape while `form.values` stays the input shape.
2501
3052
  */
2502
- handleSubmit: HandleSubmit<Form>;
3053
+ handleSubmit: HandleSubmit<GetValueFormType>;
2503
3054
  /**
2504
3055
  * Reactive readonly proxy over the form's storage value. Read
2505
3056
  * identically in script and template — no `.value`, no auto-unwrap
@@ -2522,10 +3073,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2522
3073
  *
2523
3074
  * Reads reflect what's storable: enum-typed slots widen to their
2524
3075
  * primitive supertype (`string`), so refinement-invalid but
2525
- * structurally-valid values are visible. Use `handleSubmit` /
2526
- * `validateAsync()` when you need the post-validation strict type.
3076
+ * structurally-valid values are visible. Storage holds the
3077
+ * `z.input<Schema>` shape — `.transform()`s have NOT run, so for
3078
+ * a schema like `z.string().transform(v => v.length > 10)` the
3079
+ * value reads as `string`, not `boolean`. Use `handleSubmit` or
3080
+ * `form.process()` when you need the post-transform output shape.
2527
3081
  */
2528
- values: ValuesSurface<WriteShape<GetValueFormType>>;
3082
+ values: ValuesSurface<WriteShape<Form>>;
2529
3083
  /**
2530
3084
  * Reactive per-field state proxy. Pinia-style nested object — read
2531
3085
  * leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
@@ -2543,8 +3097,9 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2543
3097
  * descends into the nested leaf.
2544
3098
  *
2545
3099
  * Leaf values follow the slim WriteShape contract: enum-typed leaves
2546
- * widen to their primitive supertype. The errors array, dirty flag,
2547
- * focus state, etc. are unaffected.
3100
+ * widen to their primitive supertype, and the leaf value reflects
3101
+ * the `z.input<Schema>` shape (transforms deferred until parse).
3102
+ * The errors array, dirty flag, focus state, etc. are unaffected.
2548
3103
  *
2549
3104
  * Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
2550
3105
  * `errors`, `blank`, `focused`, `blurred`, `value`,
@@ -2553,7 +3108,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2553
3108
  * Document edge case; rename the offending schema field if the
2554
3109
  * collision matters.
2555
3110
  */
2556
- fields: FieldStateMap<WriteShape<GetValueFormType>>;
3111
+ fields: FieldStateMap<WriteShape<Form>>;
2557
3112
  /**
2558
3113
  * Write to the form programmatically. Two forms:
2559
3114
  *
@@ -2609,14 +3164,14 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2609
3164
  * it blank (storage holds the slim default; UI displays
2610
3165
  * empty; submit raises "No value supplied" for required schemas).
2611
3166
  */
2612
- <Path extends FlatPath<Form>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, Path>>, NonNullable<WriteShape<NestedType<Form, Path>>>>>(path: Path, value: Value): boolean;
3167
+ <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
2613
3168
  /**
2614
3169
  * Tuple-segment form. Equivalent to the dotted-string overload —
2615
3170
  * useful when paths are built from variables or arrays:
2616
3171
  * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
2617
3172
  * type is exact, matching the dotted-string form.
2618
3173
  */
2619
- <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;
3174
+ <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;
2620
3175
  };
2621
3176
  /**
2622
3177
  * Reactive validation status. Re-runs whenever the form (or the
@@ -2649,6 +3204,32 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2649
3204
  * `true` while the promise is in flight.
2650
3205
  */
2651
3206
  validateAsync: (path?: FlatPath<Form>) => Promise<ValidationResponseWithoutValue<Form>>;
3207
+ /**
3208
+ * Imperative one-shot parse. Same pipeline as `validateAsync` —
3209
+ * runs refinements, applies `.transform()`s, composes blank-required
3210
+ * errors — but RETAINS the parsed data instead of stripping it.
3211
+ *
3212
+ * Storage holds the "honest input view" — values you wrote, with
3213
+ * preprocess normalization applied but `.transform()` deferred. For
3214
+ * schemas where the input type differs from the output type (e.g.,
3215
+ * `z.string().transform(v => v.length > 10)`), `form.values.X` is
3216
+ * the input shape and `(await form.process()).data?.X` is the
3217
+ * output shape.
3218
+ *
3219
+ * ```ts
3220
+ * const result = await form.process()
3221
+ * if (result.success) {
3222
+ * // result.data matches z.output<typeof schema>
3223
+ * } else {
3224
+ * // result.errors is the validation failure list
3225
+ * }
3226
+ * ```
3227
+ *
3228
+ * Pass a path to parse a subtree only. Async because refinements may
3229
+ * be async. `meta.validating` flips `true` while the promise is in
3230
+ * flight (shared with validateAsync).
3231
+ */
3232
+ process: (path?: FlatPath<Form>) => Promise<ValidationResponse<GetValueFormType>>;
2652
3233
  /**
2653
3234
  * Bind a path to a native input via `v-register`. Returns a
2654
3235
  * `RegisterValue` carrying the live ref and event handlers the
@@ -2734,8 +3315,8 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2734
3315
  * scripts; `toRef` is for ref-shaped interop only.
2735
3316
  */
2736
3317
  toRef: {
2737
- <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
2738
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, JoinSegments<S>>>>;
3318
+ <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<Form>, Path>>>;
3319
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<Form>, JoinSegments<S>>>>;
2739
3320
  };
2740
3321
  /**
2741
3322
  * Replace every field error for this form with the provided list.
@@ -2790,12 +3371,15 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2790
3371
  * if (result.ok) form.setFormErrors(result.errors)
2791
3372
  * ```
2792
3373
  *
2793
- * Form-level errors surface in `form.meta.errors` (alongside field
2794
- * errors) but are intentionally excluded from the path-keyed
2795
- * `form.errors` proxy (no key represents `[]` in a nested object) —
2796
- * read them via `meta.errors.filter(e => e.path.length === 0)` or
2797
- * `form.errors([])` (the call-form aggregates everywhere, including
2798
- * form-level errors at `path: []`).
3374
+ * Form-level errors land at the empty-string path bucket
3375
+ * (`path: ['']`). They surface in `form.meta.errors` (alongside
3376
+ * field errors), in `form.errors()` / `form.errors([])` (whole-form
3377
+ * subtree aggregates), and — uniquely — in `form.errors('')`,
3378
+ * which returns ONLY the form-level bucket. They're excluded from
3379
+ * the path-keyed `form.errors` drill proxy because no nested-object
3380
+ * key represents the empty-string path. Read them via
3381
+ * `meta.errors.filter(e => e.path.length === 1 && e.path[0] === '')`
3382
+ * if you need a programmatic split.
2799
3383
  */
2800
3384
  setFormErrors: (errors: ReadonlyArray<Partial<ValidationError> & {
2801
3385
  message: string;
@@ -2807,12 +3391,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2807
3391
  clearFormErrors: () => void;
2808
3392
  /**
2809
3393
  * Form-level reactive flags, counters, and aggregates (`dirty`,
2810
- * `valid`, `submitting`, `submitCount`, `canUndo`,
2811
- * `historySize`, and the flat `errors` array). See `FormMeta` for
2812
- * the full shape. Read leaves directly with no `.value`.
3394
+ * `valid`, `submitting`, `submitCount`, and the flat `errors`
3395
+ * array). See `FormMeta` for the full shape. Read leaves directly
3396
+ * with no `.value`.
2813
3397
  *
2814
3398
  * For per-field state (touched, focused, blurred, errors at one
2815
- * path), use `form.fields.<path>` instead.
3399
+ * path), use `form.fields.<path>` instead. Undo/redo state lives at
3400
+ * `form.history` (see `FormHistoryNamespace`).
2816
3401
  */
2817
3402
  meta: FormMeta<Form>;
2818
3403
  /**
@@ -2873,17 +3458,12 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2873
3458
  */
2874
3459
  clearPersistedDraft: (path?: FlatPath<Form>) => Promise<void>;
2875
3460
  /**
2876
- * Revert the form to the previous snapshot. Returns `true` when a
2877
- * snapshot was restored, `false` when there's nothing to undo.
2878
- * No-op (returns `false`) when `useForm({ history })` wasn't configured.
2879
- */
2880
- undo: () => boolean;
2881
- /**
2882
- * Replay a previously-undone snapshot. Returns `true` on success,
2883
- * `false` when the redo stack is empty. The redo stack clears as
2884
- * soon as a new mutation lands.
3461
+ * Consolidated undo/redo namespace `form.history.{undo, redo,
3462
+ * clear, canUndo, canRedo, size}`. Always present; inert when
3463
+ * `useForm({ history })` wasn't configured. See `FormHistoryNamespace`
3464
+ * for field-by-field semantics.
2885
3465
  */
2886
- redo: () => boolean;
3466
+ history: FormHistoryNamespace;
2887
3467
  /**
2888
3468
  * Focus the first errored field's first visible element. Returns
2889
3469
  * `true` when an element was focused, `false` when no candidate
@@ -2905,6 +3485,26 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2905
3485
  * `options` is forwarded to `Element.scrollIntoView` unchanged.
2906
3486
  */
2907
3487
  scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
3488
+ /**
3489
+ * Programmatically mark fields as touched — the sticky flag the
3490
+ * standard "show errors after interaction" pattern reads. Closes
3491
+ * the gap when fields are populated without a DOM gesture (post-
3492
+ * import, paste, autofill, server-seeded values you want to
3493
+ * validate immediately).
3494
+ *
3495
+ * ```ts
3496
+ * form.touch('email') // one leaf
3497
+ * form.touch('profile') // every leaf under profile
3498
+ * form.touch(['profile', 'name']) // segment-array form
3499
+ * form.touch() // every leaf in the form
3500
+ * ```
3501
+ *
3502
+ * Pure flag write — does not mutate value, focused, blurred, or
3503
+ * trigger validation. Idempotent: re-calling on an already-touched
3504
+ * field is a no-op. Touched is sticky-true; pair with
3505
+ * `form.reset()` / `form.resetField()` to clear.
3506
+ */
3507
+ touch: (path?: FlatPath<Form> | (string | number)[]) => void;
2908
3508
  /**
2909
3509
  * Append `value` to the array at `path`.
2910
3510
  *
@@ -2956,5 +3556,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2956
3556
  blankPaths: ComputedRef<ReadonlySet<string>>;
2957
3557
  };
2958
3558
 
2959
- export { ROOT_PATH as Z, ROOT_PATH_KEY as _, canonicalizePath as ak, isPathPrefix as al, isUnset as am, parseDottedPath as an, unset as ao };
2960
- export type { ReactiveValidationStatus as $, AttaformDefaults as A, OnInvalidSubmitPolicy as B, CoercionRegistry as C, DeepPartial as D, OnSubmit 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, Path as P, PathKey as Q, RegisterValue as R, SlimPrimitiveKind as S, PendingValidationStatus as T, UseFormConfiguration as U, ValidationError as V, PersistConfig as W, PersistConfigOptions as X, PersistIncludeMode as Y, CoercionEntry as a, RegisterDirective as a0, RegisterFlatPath as a1, RegisterOptions as a2, RegisterSelectModifier as a3, RegisterTextModifier as a4, RegisterTransform as a5, Segment as a6, SetValueCallback as a7, SetValuePayload as a8, SettledValidationStatus as a9, SlimRuntimeOf as aa, SubmitHandler as ab, Unset as ac, ValidateOn as ad, ValidateOnConfig as ae, ValidationResponse as af, ValidationResponseWithoutValue as ag, ValueOfUnion as ah, WriteMeta as ai, WriteShape as aj, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, ArrayItem as i, ArrayPath as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DefaultValuesResponse as m, FieldMetaPayload as n, FieldState as o, FieldStateMap as p, FieldStateMapEntry as q, FlatPath as r, FormErrorRecord as s, FormErrorsSurface as t, FormMeta as u, FormStorage as v, FormStorageKind as w, HistoryConfig as x, IsUnion as y, NestedType as z };
3559
+ export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
3560
+ 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 };