attaform 0.22.0 → 0.24.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 (114) hide show
  1. package/README.md +8 -11
  2. package/dist/chunks/dev-key-collision-warnings.cjs +0 -33
  3. package/dist/chunks/dev-key-collision-warnings.cjs.map +1 -1
  4. package/dist/chunks/dev-key-collision-warnings.mjs +1 -33
  5. package/dist/chunks/dev-key-collision-warnings.mjs.map +1 -1
  6. package/dist/chunks/devtools.cjs +3 -5
  7. package/dist/chunks/devtools.cjs.map +1 -1
  8. package/dist/chunks/devtools.mjs +3 -5
  9. package/dist/chunks/devtools.mjs.map +1 -1
  10. package/dist/chunks/fingerprint2.cjs +1 -1
  11. package/dist/chunks/fingerprint2.mjs +1 -1
  12. package/dist/index.cjs +3 -151
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +7 -168
  15. package/dist/index.d.mts +7 -168
  16. package/dist/index.d.ts +7 -168
  17. package/dist/index.mjs +4 -150
  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/components/AttaformDevtoolsPanel.vue +5 -13
  23. package/dist/runtime/plugins/attaform.cjs +2 -2
  24. package/dist/runtime/plugins/attaform.mjs +2 -2
  25. package/dist/shared/{attaform.DgCfLqay.mjs → attaform.BJ_W7q3U.mjs} +8 -6
  26. package/dist/shared/attaform.BJ_W7q3U.mjs.map +1 -0
  27. package/dist/shared/{attaform.aekT7mMx.d.cts → attaform.BNmkKz0q.d.mts} +38 -6
  28. package/dist/shared/{attaform.Q3eAD2wD.cjs → attaform.BV_HyaMO.cjs} +6 -4
  29. package/dist/shared/attaform.BV_HyaMO.cjs.map +1 -0
  30. package/dist/shared/{attaform.AyujQoHp.cjs → attaform.BnUXV01g.cjs} +4 -4
  31. package/dist/shared/attaform.BnUXV01g.cjs.map +1 -0
  32. package/dist/shared/{attaform.DNuiFCXG.mjs → attaform.C-dAB90u.mjs} +4 -4
  33. package/dist/shared/attaform.C-dAB90u.mjs.map +1 -0
  34. package/dist/shared/{attaform.CjMcwV7W.cjs → attaform.C42wL7EJ.cjs} +272 -776
  35. package/dist/shared/attaform.C42wL7EJ.cjs.map +1 -0
  36. package/dist/shared/{attaform.D4XYaasQ.d.ts → attaform.C6eE50re.d.ts} +27 -87
  37. package/dist/shared/{attaform.6xE0Lcfd.mjs → attaform.CAWKNCzc.mjs} +2 -2
  38. package/dist/shared/{attaform.6xE0Lcfd.mjs.map → attaform.CAWKNCzc.mjs.map} +1 -1
  39. package/dist/shared/{attaform.DkA5J8NW.d.cts → attaform.CO0e7YVY.d.cts} +1 -46
  40. package/dist/shared/{attaform.DkA5J8NW.d.ts → attaform.CO0e7YVY.d.mts} +1 -46
  41. package/dist/shared/{attaform.DkA5J8NW.d.mts → attaform.CO0e7YVY.d.ts} +1 -46
  42. package/dist/shared/{attaform.CsB-iKbU.mjs → attaform.CuBdtfbe.mjs} +274 -767
  43. package/dist/shared/attaform.CuBdtfbe.mjs.map +1 -0
  44. package/dist/shared/{attaform.FN0vaQAg.d.mts → attaform.CwFZGv5-.d.ts} +38 -6
  45. package/dist/shared/{attaform.BGwNZ9GV.d.cts → attaform.DdUYEhkV.d.cts} +27 -87
  46. package/dist/shared/attaform.DdjDqTah.d.cts +56 -0
  47. package/dist/shared/attaform.DdjDqTah.d.mts +56 -0
  48. package/dist/shared/attaform.DdjDqTah.d.ts +56 -0
  49. package/dist/shared/{attaform.BKFwekY2.mjs → attaform.Df-s8j1X.mjs} +3 -289
  50. package/dist/shared/attaform.Df-s8j1X.mjs.map +1 -0
  51. package/dist/shared/{attaform.CCCeEPwa.d.mts → attaform.DiWNbKWa.d.mts} +27 -87
  52. package/dist/shared/{attaform.01iKS_lz.cjs → attaform.DwkU0oY9.cjs} +2 -298
  53. package/dist/shared/attaform.DwkU0oY9.cjs.map +1 -0
  54. package/dist/shared/{attaform.DvUH4a3o.d.ts → attaform.GJbSmwLB.d.cts} +224 -824
  55. package/dist/shared/{attaform.DvUH4a3o.d.cts → attaform.GJbSmwLB.d.mts} +224 -824
  56. package/dist/shared/{attaform.DvUH4a3o.d.mts → attaform.GJbSmwLB.d.ts} +224 -824
  57. package/dist/shared/{attaform.DUMWQefY.d.ts → attaform.K-3glmiT.d.cts} +38 -6
  58. package/dist/shared/{attaform.C-RtnCJM.cjs → attaform.Z1qTwOYE.cjs} +8 -6
  59. package/dist/shared/attaform.Z1qTwOYE.cjs.map +1 -0
  60. package/dist/shared/{attaform.CRzpFCjV.cjs → attaform.nycEksJn.cjs} +2 -2
  61. package/dist/shared/{attaform.CRzpFCjV.cjs.map → attaform.nycEksJn.cjs.map} +1 -1
  62. package/dist/shared/{attaform.DCjgGir_.mjs → attaform.o95Kjd3U.mjs} +6 -4
  63. package/dist/shared/attaform.o95Kjd3U.mjs.map +1 -0
  64. package/dist/zod-v3.cjs +2 -2
  65. package/dist/zod-v3.d.cts +12 -11
  66. package/dist/zod-v3.d.mts +12 -11
  67. package/dist/zod-v3.d.ts +12 -11
  68. package/dist/zod-v3.mjs +2 -2
  69. package/dist/zod-v4.cjs +2 -2
  70. package/dist/zod-v4.d.cts +7 -6
  71. package/dist/zod-v4.d.mts +7 -6
  72. package/dist/zod-v4.d.ts +7 -6
  73. package/dist/zod-v4.mjs +2 -2
  74. package/dist/zod.cjs +5 -5
  75. package/dist/zod.cjs.map +1 -1
  76. package/dist/zod.d.cts +21 -52
  77. package/dist/zod.d.mts +21 -52
  78. package/dist/zod.d.ts +21 -52
  79. package/dist/zod.mjs +5 -5
  80. package/dist/zod.mjs.map +1 -1
  81. package/package.json +1 -1
  82. package/dist/chunks/indexeddb.cjs +0 -119
  83. package/dist/chunks/indexeddb.cjs.map +0 -1
  84. package/dist/chunks/indexeddb.mjs +0 -117
  85. package/dist/chunks/indexeddb.mjs.map +0 -1
  86. package/dist/chunks/local-storage.cjs +0 -58
  87. package/dist/chunks/local-storage.cjs.map +0 -1
  88. package/dist/chunks/local-storage.mjs +0 -56
  89. package/dist/chunks/local-storage.mjs.map +0 -1
  90. package/dist/chunks/multi-tab-sync.cjs +0 -367
  91. package/dist/chunks/multi-tab-sync.cjs.map +0 -1
  92. package/dist/chunks/multi-tab-sync.mjs +0 -364
  93. package/dist/chunks/multi-tab-sync.mjs.map +0 -1
  94. package/dist/chunks/session-storage.cjs +0 -58
  95. package/dist/chunks/session-storage.cjs.map +0 -1
  96. package/dist/chunks/session-storage.mjs +0 -56
  97. package/dist/chunks/session-storage.mjs.map +0 -1
  98. package/dist/chunks/wire-persistence.cjs +0 -396
  99. package/dist/chunks/wire-persistence.cjs.map +0 -1
  100. package/dist/chunks/wire-persistence.mjs +0 -394
  101. package/dist/chunks/wire-persistence.mjs.map +0 -1
  102. package/dist/shared/attaform.01iKS_lz.cjs.map +0 -1
  103. package/dist/shared/attaform.AyujQoHp.cjs.map +0 -1
  104. package/dist/shared/attaform.BKFwekY2.mjs.map +0 -1
  105. package/dist/shared/attaform.C-RtnCJM.cjs.map +0 -1
  106. package/dist/shared/attaform.CjMcwV7W.cjs.map +0 -1
  107. package/dist/shared/attaform.CsB-iKbU.mjs.map +0 -1
  108. package/dist/shared/attaform.DCjgGir_.mjs.map +0 -1
  109. package/dist/shared/attaform.DNuiFCXG.mjs.map +0 -1
  110. package/dist/shared/attaform.DgCfLqay.mjs.map +0 -1
  111. package/dist/shared/attaform.Q3eAD2wD.cjs.map +0 -1
  112. package/dist/shared/attaform.nf83TIR5.d.cts +0 -35
  113. package/dist/shared/attaform.nf83TIR5.d.mts +0 -35
  114. package/dist/shared/attaform.nf83TIR5.d.ts +0 -35
@@ -1,4 +1,4 @@
1
- import { ObjectDirective, Ref, MaybeRefOrGetter, ComputedRef } from 'vue';
1
+ import { ObjectDirective, Ref, ComputedRef } from 'vue';
2
2
 
3
3
  /**
4
4
  * Schema-attached field metadata — the shared types used by both Zod
@@ -101,10 +101,11 @@ type Path = readonly Segment[];
101
101
  * parseDottedPath('') // [''] (the empty-string key)
102
102
  * ```
103
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.
104
+ * The empty-string input `''` is the **literal empty-key path** `['']`,
105
+ * an ordinary (if rare) field address, not the root. Use the array
106
+ * form `[]` for the root. Form-level errors (root `.refine()`,
107
+ * `setErrors` with no path) live at the root path `[]` and are read via
108
+ * `errors([])`, never `errors('')`.
108
109
  *
109
110
  * Throws `InvalidPathError` for paths with empty INTERNAL segments
110
111
  * (`'a..b'`, leading or trailing dots). For keys containing literal
@@ -132,8 +133,17 @@ declare function canonicalizePath(input: string | Path): {
132
133
  key: PathKey;
133
134
  };
134
135
  /**
135
- * The root path — an empty segment tuple. Pass to APIs that accept
136
- * a `Path` to address the form value as a whole.
136
+ * The root path — an empty segment tuple. Pass to APIs that accept a
137
+ * `Path` to address the form value as a whole, and the home for
138
+ * form-level (global) errors: root `.refine()` messages, hydration
139
+ * failures, and `setErrors` entries with no path all live at `[]`. Aggregate
140
+ * reads (`errors()`, `meta.errors`) surface them alongside field
141
+ * errors; `errors([])` returns the global bucket alone.
142
+ *
143
+ * The empty SEGMENT tuple `[]` is structurally unconstructible as a
144
+ * field path, so it can never collide with a schema key, unlike the
145
+ * empty STRING key `''` (path `['']`, key `'[""]'`), which is an
146
+ * ordinary field address.
137
147
  */
138
148
  declare const ROOT_PATH: Path;
139
149
  /** Stable string key for the root path. */
@@ -154,42 +164,6 @@ declare const ROOT_PATH_KEY: PathKey;
154
164
  */
155
165
  declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
156
166
 
157
- /**
158
- * Per-FormStore registry tracking which DOM elements have opted into
159
- * persistence for which paths. Lives on the FormStore so that two SFCs
160
- * sharing a key share the registry — opt-ins are per-element, not
161
- * per-component.
162
- *
163
- * The directive's input handler computes `meta.persist` for each write
164
- * by calling `hasOptIn(elementId, path)` — only THIS element's writes
165
- * persist if THIS element opted in. Other call sites that aren't tied
166
- * to a single element (history undo/redo, field-array helpers, devtools
167
- * edits) use `hasAnyOptInForPath(path)` — persist if any element has
168
- * opted into that path.
169
- *
170
- * Internal data structure: `Map<PathKey, Set<elementId>>`. Small forms
171
- * have ~10-50 paths; iteration is cheap. All operations are O(1) given
172
- * (id, path).
173
- */
174
- type PersistOptInRegistry = {
175
- /** Add an opt-in entry; idempotent. */
176
- add(elementId: string, path: PathKey): void;
177
- /** Remove a single (element, path) entry. */
178
- remove(elementId: string, path: PathKey): void;
179
- /** Remove every opt-in for `elementId`. Called from directive's beforeUnmount. */
180
- removeAllFor(elementId: string): void;
181
- /** Check whether THIS element has opted into THIS path. */
182
- hasOptIn(elementId: string, path: PathKey): boolean;
183
- /** Check whether ANY element has opted into this path. */
184
- hasAnyOptInForPath(path: PathKey): boolean;
185
- /** Iterate every path that currently has at least one opt-in. */
186
- optedInPaths(): IterableIterator<PathKey>;
187
- /** True iff no element has opted into any path. */
188
- isEmpty(): boolean;
189
- /** Drop every entry. Called from FormStore.dispose. */
190
- clear(): void;
191
- };
192
-
193
167
  /** Internal brand for the `Unset` type. Never exposed at runtime. */
194
168
  declare const _unsetBrand: unique symbol;
195
169
  /**
@@ -284,8 +258,17 @@ type FlatPathBuilder<Form, Mode extends 'partial' | 'register', Key extends keyo
284
258
  * Inlining at consumer call sites compounds into TS2589 territory
285
259
  * when multiple complex forms share a scope. Consumers should reach
286
260
  * for `FlatPath` instead; this alias is not part of the stable surface.
261
+ *
262
+ * The `Form extends unknown` wrapper distributes over a top-level
263
+ * union so each member contributes its OWN `keyof` — a discriminated-
264
+ * union root (`z.discriminatedUnion`) thus exposes every variant's
265
+ * keys as addressable paths, not just the discriminator that naked
266
+ * `keyof (A | B)` would intersect to. For a single object / array /
267
+ * record `Form` it is a one-member no-op and stays byte-identical to
268
+ * the prior walk. Interior unions already distribute inside
269
+ * `FlatPathBuilder` via its `infer Value` step.
287
270
  */
288
- type PartialFlatPath<Form, Key extends keyof Form = keyof Form> = FlatPathBuilder<Form, 'partial', Key>;
271
+ type PartialFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'partial'> : never;
289
272
  /**
290
273
  * Union of dotted-string paths reachable inside `Form`, e.g. for
291
274
  * `{ user: { email: string }, items: string[] }`:
@@ -296,7 +279,7 @@ type PartialFlatPath<Form, Key extends keyof Form = keyof Form> = FlatPathBuilde
296
279
  * `register(path)`, `toRef(path)`, etc.) so paths autocomplete in
297
280
  * the IDE and typos compile-error.
298
281
  */
299
- type FlatPath<Form, Key extends keyof Form = keyof Form> = PartialFlatPath<Form, Key>;
282
+ type FlatPath<Form> = PartialFlatPath<Form>;
300
283
  /**
301
284
  * Convert a tuple of path segments to its dotted-string equivalent.
302
285
  *
@@ -665,8 +648,8 @@ type DefaultValuesInput<T> = T extends string ? string | Unset : T extends numbe
665
648
  /**
666
649
  * Identifier for a form. A `FormKey` is the string passed via
667
650
  * `useForm({ key })`, used to look up a form by name from a distant
668
- * component, namespace persisted drafts, and label errors and
669
- * DevTools entries. Anonymous `useForm` calls allocate one
651
+ * component and to label errors and DevTools entries. Anonymous
652
+ * `useForm` calls allocate one
670
653
  * automatically; you only need to pick one when the form needs
671
654
  * stable identity.
672
655
  */
@@ -681,25 +664,45 @@ interface SchemaFactoryOptions {
681
664
  /** Resolved recursion ceiling (per-form > app-default > library default). */
682
665
  maxRecursionDepth: number;
683
666
  }
667
+ /**
668
+ * A JSON-serialisable value: the recursive shape of anything that
669
+ * survives a `JSON.stringify` / `JSON.parse` round-trip unchanged.
670
+ * The type of `ValidationError.data`.
671
+ *
672
+ * The object arm is a named interface rather than an inline index
673
+ * signature: TypeScript expands an anonymous recursive alias eagerly
674
+ * and tips into a depth-limit error (TS2589) when `Json` is checked
675
+ * inside a large structural type (e.g. the form store). A named
676
+ * reference defers that expansion.
677
+ */
678
+ type Json = string | number | boolean | null | JsonArray | JsonObject;
679
+ interface JsonArray extends Array<Json> {
680
+ }
681
+ interface JsonObject {
682
+ [key: string]: Json;
683
+ }
684
684
  /**
685
685
  * One validation failure. `path` points at the offending field as a
686
686
  * structured array — `['user', 'address', 0, 'line1']` for a nested
687
- * field, `['']` (the empty-string path) for a form-level error
688
- * (root `.refine()` messages, `setFormErrors()` entries, server-
689
- * emitted form banners). `formKey` identifies which form produced
690
- * the error so a single error list can be routed to multiple forms.
687
+ * field, `[]` (the root path) for a form-level error (root `.refine()`
688
+ * messages, `setErrors` entries with no path, hydration failures,
689
+ * server-emitted form banners). `formKey` identifies which form
690
+ * produced the error so a single error list can be routed to multiple
691
+ * forms. The optional `data` slot carries an arbitrary server payload.
691
692
  *
692
693
  * Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
693
- * `onError` callback, and by `parseApiErrors` for server responses.
694
+ * `onError` callback, and accepted (leniently, as `ErrorInput`) by
695
+ * `form.setErrors`.
694
696
  */
695
697
  type ValidationError = {
696
698
  /** Human-readable message describing the failure. */
697
699
  message: string;
698
700
  /**
699
- * Structured path of the offending field. The empty-string path
700
- * `['']` is the form-level bucket — the dedicated home for errors
701
- * that don't belong to any specific field, distinct from the
702
- * whole-form subtree address `[]`.
701
+ * Structured path of the offending field. The root path `[]` is the
702
+ * form-level bucket — the dedicated home for errors that don't belong
703
+ * to any specific field (root `.refine()`, `setErrors`, hydration
704
+ * failures). The empty-STRING path `['']` is unrelated: an ordinary
705
+ * literal empty-key field.
703
706
  */
704
707
  path: (string | number)[];
705
708
  /** Identifies which form produced this error. */
@@ -716,6 +719,38 @@ type ValidationError = {
716
719
  * exact-message string matching.
717
720
  */
718
721
  code: string;
722
+ /**
723
+ * Optional structured payload attached to the error. Attaform never
724
+ * sets or reads this; it is a passthrough slot for whatever a server
725
+ * sends alongside the message (a captcha challenge, a lockout
726
+ * `unlocks_at` timestamp, an MFA step-up descriptor) so the UI can
727
+ * act on it. Survives serialise / hydrate and undo / redo unchanged.
728
+ */
729
+ data?: Json | null;
730
+ };
731
+ /**
732
+ * The lenient input shape `form.setErrors` accepts: a real `Error`, or
733
+ * a partial `ValidationError` where every field is optional.
734
+ *
735
+ * - `message`: omitted or empty coerces to `"Unknown error"`.
736
+ * - `path`: defaults to `[]` (a global, form-level error). Ignored by
737
+ * the path-scoped `setErrors(path, …)` form, which stamps its own.
738
+ * - `code`: defaults to `atta:user-error`.
739
+ * - `data`: forwarded verbatim onto the produced `ValidationError`.
740
+ * - `formKey`: accepted but ignored — the form always stamps its own.
741
+ *
742
+ * Because every field is optional and `formKey` is accepted-and-ignored,
743
+ * `ValidationError` is a subtype of `ErrorInput`: a `ValidationError[]`
744
+ * you read back, or a server response that already emits the shape, pipes
745
+ * straight into `form.setErrors` with no adapter and no excess-property
746
+ * friction.
747
+ */
748
+ type ErrorInput = Error | {
749
+ message?: string;
750
+ path?: (string | number)[];
751
+ code?: string;
752
+ data?: Json | null;
753
+ formKey?: FormKey;
719
754
  };
720
755
  /** Settled validation result when the form (or subtree) parsed successfully. */
721
756
  type ValidationResponseSuccess<TData> = {
@@ -830,9 +865,9 @@ type AbstractSchema<Form, GetValueFormType> = {
830
865
  *
831
866
  * Resolves a `Promise` so adapters can defer the structural walk (and
832
867
  * its `canonicalStringify` helper) onto a dynamic import. The framework
833
- * only ever needs the fingerprint on opt-in async paths (the multi-tab
834
- * channel name, the persistence storage key) plus a dev-only mismatch
835
- * warning, so none of those bytes belong on the eager `useForm` path.
868
+ * only ever needs the fingerprint for the dev-only shared-key schema
869
+ * mismatch warning, so none of those bytes belong on the eager
870
+ * `useForm` path.
836
871
  *
837
872
  * The library uses this to detect schema mismatches at a shared
838
873
  * form key: two `useForm({ key: 'x', schema })` calls are allowed
@@ -1394,64 +1429,11 @@ type ValidateOnConfig = {
1394
1429
  debounceMs?: never;
1395
1430
  };
1396
1431
  /**
1397
- * Built-in storage backends:
1398
- *
1399
- * - `'local'` browser `localStorage` (persists across tabs and reloads).
1400
- * - `'session'` — browser `sessionStorage` (cleared when the tab closes).
1401
- * - `'indexeddb'` — IndexedDB via a zero-dependency wrapper (handles
1402
- * structured-cloneable data; suitable for larger drafts).
1403
- *
1404
- * For anything else (encrypted storage, a native bridge, a cookie
1405
- * store) pass a custom `FormStorage` object instead.
1406
- */
1407
- type FormStorageKind = 'local' | 'session' | 'indexeddb';
1408
- /**
1409
- * Custom persistence backend. Implement this when none of the built-in
1410
- * `'local'` / `'session'` / `'indexeddb'` backends fit (e.g. encrypted
1411
- * storage, a cross-window broadcast layer, or a native mobile bridge).
1412
- *
1413
- * All methods are async. Pass values through unchanged — `getItem`
1414
- * should return whatever `setItem` was given, including non-string
1415
- * values. The library handles serialization for the built-in
1416
- * `'local'` / `'session'` backends; custom adapters can store the
1417
- * value directly if their backing store accepts structured data.
1418
- *
1419
- * `listKeys(prefix)` returns every key starting with `prefix`. The
1420
- * library uses it on mount to clean up entries left over from older
1421
- * schema versions (each persisted entry carries a schema fingerprint
1422
- * suffix; mismatched entries are dropped automatically).
1423
- */
1424
- type FormStorage = {
1425
- /** Fetch the value previously stored under `key`. Resolve to `null`/`undefined` for misses. */
1426
- getItem(key: string): Promise<unknown>;
1427
- /** Persist `value` under `key`. */
1428
- setItem(key: string, value: unknown): Promise<void>;
1429
- /** Remove the entry at `key`. No-op if not present. */
1430
- removeItem(key: string): Promise<void>;
1431
- /** Return every key in this backend whose name starts with `prefix`. */
1432
- listKeys(prefix: string): Promise<string[]>;
1433
- };
1434
- /**
1435
- * What to include when persisting:
1436
- *
1437
- * - `'form'` (default) — only the form value. Errors get repopulated
1438
- * by validation on reload anyway.
1439
- * - `'form+errors'` — also persist the current error list. Useful when
1440
- * the error context is expensive to recompute (e.g. cross-field
1441
- * refinements that depend on server data).
1442
- */
1443
- type PersistIncludeMode = 'form' | 'form+errors';
1444
- /**
1445
- * Per-write metadata. Used internally to flag which writes should
1446
- * reach the persistence layer (e.g. only writes from elements opted
1447
- * into persistence via `register(path, { persist: true })`).
1448
- *
1449
- * Custom directive integrations may set `persist: true` to forward
1450
- * a write to the configured storage adapter; otherwise leave unset.
1432
+ * Per-write metadata. Used internally to tag writes so listeners and
1433
+ * the write funnel can treat a write specially (a blank mark, an array
1434
+ * structural op, a hydration replay, a per-instance config override).
1451
1435
  */
1452
1436
  type WriteMeta = {
1453
- /** When `true`, this write is forwarded to the configured persistence backend. */
1454
- readonly persist?: boolean;
1455
1437
  /**
1456
1438
  * When `true`, the path being written is added to the FormStore's
1457
1439
  * `blankPaths` set — meaning storage holds a real, schema-
@@ -1513,159 +1495,16 @@ type WriteMeta = {
1513
1495
  readonly rememberVariants?: boolean;
1514
1496
  };
1515
1497
  /**
1516
- * When `true`, marks this `applyFormReplacement` call as the
1517
- * persistence hydration step. Modules that snapshot the form state
1518
- * (notably the history module) treat hydration as the baseline:
1519
- * stacks reset to a single seed of the post-hydration value, so a
1520
- * subsequent `undo()` can't recover the transient pre-hydration
1521
- * default. Internal — set by `wirePersistence`. Don't set from
1522
- * consumer code.
1498
+ * When `true`, marks this `applyFormReplacement` call as a hydration
1499
+ * step (the async-`defaultValues` / `activate()` / `rehydrate()`
1500
+ * path). Modules that snapshot the form state (notably the history
1501
+ * module) treat hydration as the baseline: stacks reset to a single
1502
+ * seed of the post-hydration value, so a subsequent `undo()` can't
1503
+ * recover the transient pre-hydration default. Internal — set by the
1504
+ * activate path in `create-form-store.ts`. Don't set from consumer
1505
+ * code.
1523
1506
  */
1524
1507
  readonly hydration?: boolean;
1525
- /**
1526
- * When `true`, this write originated from a sibling tab's
1527
- * BroadcastChannel broadcast (the multi-tab sync module's inbound
1528
- * apply). Listeners that initiate side effects check this flag to
1529
- * avoid amplification loops and spurious side effects:
1530
- *
1531
- * - The multi-tab sync OUTBOUND broadcaster skips so a remote-driven
1532
- * write doesn't echo back across the channel.
1533
- * - The history module updates its diff anchor but does NOT push a
1534
- * delta — remote writes aren't part of the local user's undo
1535
- * timeline.
1536
- * - The persistence writer skips so the receiving tab doesn't
1537
- * double-persist a value the originating tab already wrote.
1538
- *
1539
- * Internal — set by `createMultiTabSyncModule`. Don't set from
1540
- * consumer code.
1541
- */
1542
- readonly crossTab?: boolean;
1543
- /**
1544
- * When `true`, this write lands normally (storage, validation,
1545
- * persistence, history) but does NOT notify `form.onChange` handlers.
1546
- * The side-channel reacts to user edits, not programmatic rebaselines:
1547
- * `reset()` tags its replacement with this flag, and the public
1548
- * `setValue(path, value, { silent: true })` option forwards it so a
1549
- * consumer hydrating the form (loading a saved record into the fields)
1550
- * can land values without echoing each one back through an autosave
1551
- * loop. The store's internal taggers and that single consumer-facing
1552
- * option are the only writers.
1553
- */
1554
- readonly silent?: boolean;
1555
- };
1556
- /** Options for a `setValue` write. */
1557
- type SetValueOptions = {
1558
- /**
1559
- * When `true`, the write lands normally (storage, validation, persistence,
1560
- * history) but does NOT notify `form.onChange` handlers. Use it to hydrate
1561
- * the form (load a saved record into the fields) without echoing every
1562
- * field back through an autosave loop.
1563
- */
1564
- readonly silent?: boolean;
1565
- };
1566
- /**
1567
- * A source address for `form.onChange` — the subtree(s) a handler reacts to.
1568
- *
1569
- * - a dotted path string (`'user.email'`) — one leaf or subtree;
1570
- * - a list of path strings (`['shipping', 'billing']`) — react to any of
1571
- * several paths; the handler fires once per matched path, `ctx.path`
1572
- * distinguishing which;
1573
- * - a getter or ref / computed resolving to either — re-read on each write,
1574
- * so the aim can follow a moving target (the active list row). Re-aiming
1575
- * is never itself a trigger; only a real write dispatches.
1576
- *
1577
- * Omit the source entirely (`form.onChange(handler)`) to react to the whole
1578
- * form. An empty list (`form.onChange([], handler)`) lists zero paths, so it
1579
- * never fires — that is a deliberate no-op, NOT a shorthand for the root.
1580
- */
1581
- type OnChangeSource = MaybeRefOrGetter<string | readonly string[]>;
1582
- /**
1583
- * Context handed to an `onChange` handler alongside the changed value.
1584
- *
1585
- * `onChange` is a pure side-channel: it reacts to value changes and runs
1586
- * side effects, but never touches the form's own lifecycle. Nothing a
1587
- * handler does here marks the form dirty, pending, or validating — autosave
1588
- * status lives in the consumer's own state, validation in `.refine` and
1589
- * `field.show*`.
1590
- */
1591
- type OnChangeContext<FormApi = unknown> = {
1592
- /**
1593
- * The source path this fire is for, in dotted form (`'user.email'`).
1594
- * The empty string `''` is the whole form (a root handler). For a
1595
- * multi-path source, the handler fires once per matched path and `path`
1596
- * names which one.
1597
- */
1598
- readonly path: string;
1599
- /**
1600
- * The value at the source path BEFORE this change, seeded at registration.
1601
- * Accurate for leaf sources. For a container or the whole form, an
1602
- * in-place leaf edit preserves the container's reference, so `previous`
1603
- * can be reference-equal to the current value (Vue's deep-watch gotcha) —
1604
- * snapshot inside the handler if a true container diff is needed.
1605
- */
1606
- readonly previous: unknown;
1607
- /**
1608
- * Aborted when a newer write to the same source supersedes this run. A
1609
- * debounced or awaiting handler should bail on `signal.aborted` (or pass
1610
- * `signal` straight to `fetch`) so superseded work cancels itself.
1611
- */
1612
- readonly signal: AbortSignal;
1613
- /** Retry counter — `0` on the first run, incremented by `onError`'s `retry()`. */
1614
- readonly attempt: number;
1615
- /**
1616
- * The form handle, so a portable `useForm({ onChange })` handler can reach
1617
- * back into the form (e.g. `ctx.form.validateAsync(ctx.path)` to gate an
1618
- * autosave on validity). Typed precisely by the `form.onChange` overload.
1619
- */
1620
- readonly form: FormApi;
1621
- /**
1622
- * The leaf path(s), in dotted form, that actually changed in this
1623
- * dispatch. For a leaf source this is just `[path]`; for a container or
1624
- * the whole form it is every changed descendant.
1625
- */
1626
- readonly changed: readonly string[];
1627
- };
1628
- /**
1629
- * Context handed to an `onChange` handler's `onError` callback when the
1630
- * handler throws or rejects.
1631
- */
1632
- type OnChangeErrorContext<FormApi = unknown> = {
1633
- /** The source path this run was for, in dotted form (`''` for the whole form). */
1634
- readonly path: string;
1635
- /** The value passed to the handler that failed. */
1636
- readonly value: unknown;
1637
- /** The attempt number that failed (`0` on the first run). */
1638
- readonly attempt: number;
1639
- /**
1640
- * Re-run the handler with the same value and `attempt + 1`. A no-op once a
1641
- * newer write has superseded this run — stale work is never resurrected.
1642
- * Backoff and a retry cap are the consumer's to impose.
1643
- */
1644
- readonly retry: () => void;
1645
- /** The form handle. Typed precisely by the `form.onChange` overload. */
1646
- readonly form: FormApi;
1647
- };
1648
- /** A reaction to form value changes. Its return is ignored; throws route to `onError`. */
1649
- type OnChangeHandler<Value = unknown, FormApi = unknown> = (value: Value, ctx: OnChangeContext<FormApi>) => void | Promise<void>;
1650
- /** Handles a throw / rejection from an `onChange` handler. Must not throw. */
1651
- type OnChangeErrorHandler<FormApi = unknown> = (error: unknown, ctx: OnChangeErrorContext<FormApi>) => void;
1652
- /** Options for `form.onChange`. */
1653
- type OnChangeOptions<FormApi = unknown> = {
1654
- /**
1655
- * Called when the handler throws or its promise rejects. Without it, a
1656
- * failure is swallowed (logged in dev). `onChange` never throws into the
1657
- * write that triggered it.
1658
- */
1659
- readonly onError?: OnChangeErrorHandler<FormApi>;
1660
- };
1661
- /**
1662
- * The `useForm({ onChange })` option — a whole-form handler registered at
1663
- * construction, bound to the form's lifetime. Either a bare handler or a
1664
- * `{ handler, onError }` pair.
1665
- */
1666
- type OnChangeConfig<Value = unknown, FormApi = unknown> = OnChangeHandler<Value, FormApi> | {
1667
- readonly handler: OnChangeHandler<Value, FormApi>;
1668
- readonly onError?: OnChangeErrorHandler<FormApi>;
1669
1508
  };
1670
1509
  /**
1671
1510
  * Undo/redo configuration passed via `useForm({ history })`.
@@ -1731,81 +1570,6 @@ type FormHistoryNamespace = {
1731
1570
  */
1732
1571
  readonly size: number;
1733
1572
  };
1734
- /**
1735
- * Full options bag for `useForm({ persist })`. Use this when you need
1736
- * to override defaults beyond picking the backend.
1737
- *
1738
- * For backend-only setup, the shorthand forms are equivalent:
1739
- *
1740
- * ```ts
1741
- * useForm({ persist: 'local' })
1742
- * // same as
1743
- * useForm({ persist: { storage: 'local' } })
1744
- * ```
1745
- */
1746
- type PersistConfigOptions = {
1747
- /**
1748
- * Where to persist. Pass `'local'` / `'session'` / `'indexeddb'` to
1749
- * use a built-in backend, or a custom `FormStorage` object for
1750
- * anything else. The built-in backends are loaded on demand, so
1751
- * picking `'local'` doesn't pull in IndexedDB code.
1752
- */
1753
- storage: FormStorageKind | FormStorage;
1754
- /**
1755
- * Storage key namespace. Defaults to `attaform:${formKey}`.
1756
- * Override when you need a custom prefix (e.g. multi-tenant apps
1757
- * where the same form key may exist per-tenant).
1758
- */
1759
- key?: string;
1760
- /**
1761
- * How long to wait after the last mutation before writing. Default
1762
- * `300` ms.
1763
- *
1764
- * Pass `0` to disable debouncing — every form change writes to the
1765
- * storage adapter immediately, no `setTimeout` indirection. Almost
1766
- * never the right choice for production (the storage adapter sees
1767
- * every keystroke), but useful for tests or for diagnosing perceived
1768
- * lag.
1769
- */
1770
- debounceMs?: number;
1771
- /**
1772
- * What to persist. `'form'` (default) is sufficient for most cases —
1773
- * fresh validation on reload repopulates errors. Pick `'form+errors'`
1774
- * when the error state is expensive to recompute (e.g. server-side
1775
- * cross-field validation).
1776
- */
1777
- include?: PersistIncludeMode;
1778
- /**
1779
- * When `true` (default), the persisted entry is wiped after
1780
- * `handleSubmit`'s submit callback resolves successfully. Set to
1781
- * `false` if you need the draft to survive across submissions.
1782
- */
1783
- clearOnSubmitSuccess?: boolean;
1784
- };
1785
- /**
1786
- * Persistence configuration for `useForm({ persist })`. Off by default —
1787
- * with no config, the form does no reads, no writes, and pulls in no
1788
- * storage code.
1789
- *
1790
- * Three input forms; pick the one that reads best at the call site:
1791
- *
1792
- * ```ts
1793
- * // shorthand: built-in backend
1794
- * useForm({ persist: 'local' })
1795
- *
1796
- * // shorthand: custom adapter
1797
- * useForm({ persist: encryptedStorage })
1798
- *
1799
- * // full options bag
1800
- * useForm({ persist: { storage: 'local', debounceMs: 500 } })
1801
- * ```
1802
- *
1803
- * Per-field opt-in: setting `persist` is necessary but not sufficient.
1804
- * Each field that should actually persist also needs
1805
- * `register('foo', { persist: true })` — sensitive fields must opt in
1806
- * explicitly so they don't accidentally land in client-side storage.
1807
- */
1808
- type PersistConfig = FormStorageKind | FormStorage | PersistConfigOptions;
1809
1573
  /**
1810
1574
  * Configuration object passed to `useForm`. All fields except `schema`
1811
1575
  * are optional.
@@ -1816,7 +1580,6 @@ type PersistConfig = FormStorageKind | FormStorage | PersistConfigOptions;
1816
1580
  * defaultValues: { email: '' },
1817
1581
  * validateOn: 'change',
1818
1582
  * debounceMs: 200,
1819
- * persist: 'local',
1820
1583
  * })
1821
1584
  * ```
1822
1585
  */
@@ -1846,8 +1609,7 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1846
1609
  * - to look it up from a distant component via `injectForm(key)`;
1847
1610
  * - to share state across components (multiple `useForm({ key })`
1848
1611
  * calls with the same key resolve to the same form);
1849
- * - to give DevTools and validation errors a recognisable label;
1850
- * - to namespace persisted drafts.
1612
+ * - to give DevTools and validation errors a recognisable label.
1851
1613
  *
1852
1614
  * Keys starting with `__atta:` are reserved for internal use and
1853
1615
  * throw `ReservedFormKeyError` if passed.
@@ -1951,41 +1713,6 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1951
1713
  * per blur for `<input v-register.lazy>`).
1952
1714
  */
1953
1715
  debounceMs?: number;
1954
- /**
1955
- * A whole-form `onChange` handler, registered at construction and bound to
1956
- * the form's lifetime. The same side-channel as `form.onChange(handler)`,
1957
- * but declared in the options bag so it travels with the form (handy for a
1958
- * `useAutosave`-style composable). Pass a handler, or `{ handler, onError }`.
1959
- *
1960
- * For path-scoped reactions, call `form.onChange('path', handler)` on the
1961
- * returned form instead. `onChange` never touches the form's own
1962
- * lifecycle — keep validation in `.refine` and `field.show*`.
1963
- */
1964
- onChange?: OnChangeConfig<Form, UseFormReturnType<Form>>;
1965
- /**
1966
- * Opt-in persistence of the form's draft state. Off by default —
1967
- * with no config, no reads, no writes, no storage code is loaded.
1968
- *
1969
- * Three input forms; pick the one that reads best:
1970
- *
1971
- * ```ts
1972
- * useForm({ persist: 'local' }) // built-in backend
1973
- * useForm({ persist: encryptedStorage }) // custom backend
1974
- * useForm({ persist: { storage: 'local', debounceMs: 500 } })
1975
- * ```
1976
- *
1977
- * Per-field opt-in is required: every field that should actually
1978
- * persist needs `register(path, { persist: true })`. Without any
1979
- * opt-ins, the form mounts but never writes to storage — and a
1980
- * dev-mode warning surfaces the misconfiguration. This guard
1981
- * prevents sensitive fields from accidentally leaking to
1982
- * client-side storage.
1983
- *
1984
- * Switching backends across reloads (e.g. `'local'` → `'session'`)
1985
- * automatically clears the previous backend's entry so old drafts
1986
- * don't orphan.
1987
- */
1988
- persist?: PersistConfig;
1989
1716
  /**
1990
1717
  * Opt-in undo/redo. Off by default. `true` enables with a 128-position
1991
1718
  * cap; `{ max: N }` tunes the cap.
@@ -2013,12 +1740,8 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
2013
1740
  * every switch (the data is gone). The new variant initializes
2014
1741
  * from its slim default.
2015
1742
  *
2016
- * Memory is in-memory only and does not survive reload. Persisted
2017
- * state restores values into form storage on hydration, but
2018
- * variant memory starts empty — the first discriminator switch
2019
- * after reload loses any persisted typing in the outgoing variant.
2020
- * Consumers needing cross-session continuity must persist beyond
2021
- * the variant boundary themselves.
1743
+ * Memory is in-memory only and does not survive a fresh mount: a
1744
+ * page reload starts every discriminator's variant memory empty.
2022
1745
  *
2023
1746
  * `reset()` clears variant memory. `resetField(path)` clears any
2024
1747
  * memory entry whose union path equals or sits under `path`.
@@ -2074,54 +1797,6 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
2074
1797
  * and the broader description of where the cap is read.
2075
1798
  */
2076
1799
  maxRecursionDepth?: number;
2077
- /**
2078
- * Override the path-segment name stems treated as sensitive for this
2079
- * form. Sensitive paths are excluded from persistence writes and
2080
- * multi-tab sync broadcasts. (DevTools renders raw values by design;
2081
- * it does not redact.)
2082
- *
2083
- * Resolution: per-form value (this field) > global default
2084
- * (`createAttaform({ defaults: { sensitiveNames } })`) > library
2085
- * default (`DEFAULT_SENSITIVE_NAMES`).
2086
- *
2087
- * Pass an empty array `[]` as the explicit opt-out — "nothing is
2088
- * sensitive on this form" — for fully-trusted internal tooling.
2089
- * See `AttaformDefaults.sensitiveNames` for composition examples.
2090
- */
2091
- sensitiveNames?: readonly string[];
2092
- /**
2093
- * Cross-tab synchronisation via BroadcastChannel. **Defaults to
2094
- * `false` (opt-in).** Setting `true` on a keyed `useForm` callsite
2095
- * auto-pairs the form with same-keyed siblings in other same-origin
2096
- * tabs and mirrors their mutations in near real-time.
2097
- *
2098
- * **Resolution order (per-register override > per-form > global > library):**
2099
- *
2100
- * register(path, { multiTab }) > useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`false`)
2101
- *
2102
- * **Why opt-in.** Same-keyed forms broadcasting by default leaks
2103
- * surprise: a user editing in one tab sees their values appear in a
2104
- * sibling tab they forgot was open, including PII / PHI for forms
2105
- * that don't explicitly use `sensitiveNames`. Mirrors the `persist`
2106
- * default (also opt-in): both opt-in surfaces compose into one
2107
- * consistent "richer state needs explicit consent" rule.
2108
- *
2109
- * **What's stripped even when opt-in.** `File` and `Blob` values
2110
- * never traverse the channel regardless of this flag (security +
2111
- * `structuredClone` cost). Sensitive-named paths (via
2112
- * `sensitiveNames`) are stripped both directions. See the
2113
- * multi-tab sync page for the full security model.
2114
- *
2115
- * **Secure-context requirement.** Even with `multiTab: true`, sync
2116
- * is silently disabled outside `window.isSecureContext === true`
2117
- * (HTTPS or localhost). On plain HTTP a one-shot dev warning fires
2118
- * and the module noops.
2119
- *
2120
- * **Anonymous (auto-keyed) forms skip sync entirely** — without a
2121
- * consumer-supplied `key`, cross-tab identity is undefined and the
2122
- * channel would be solo by construction.
2123
- */
2124
- multiTab?: boolean;
2125
1800
  /**
2126
1801
  * Whether `v-register` automatically manages aria attributes
2127
1802
  * (`aria-invalid`, `aria-busy`, `aria-required`, `aria-describedby`)
@@ -2178,8 +1853,8 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
2178
1853
  * app-level default is fine — forms that switch to `'blur'` /
2179
1854
  * `'submit'` simply ignore the inherited `debounceMs`.
2180
1855
  *
2181
- * `schema`, `key`, `defaultValues`, and `persist` are not configurable
2182
- * here — they belong on the per-form call.
1856
+ * `schema`, `key`, and `defaultValues` are not configurable here —
1857
+ * they belong on the per-form call.
2183
1858
  */
2184
1859
  type AttaformDefaults = {
2185
1860
  /** Default for `useForm({ strict })`. Default `true`. */
@@ -2304,56 +1979,6 @@ type AttaformDefaults = {
2304
1979
  * confident the recursion is bounded by the actual data shape.
2305
1980
  */
2306
1981
  maxRecursionDepth?: number;
2307
- /**
2308
- * Override the path-segment name stems treated as sensitive.
2309
- * Sensitive paths are excluded from persistence writes and multi-tab
2310
- * sync broadcasts — one configurable source of truth across those
2311
- * surfaces. (DevTools renders raw values by design; it does not
2312
- * redact.)
2313
- *
2314
- * Library default is `DEFAULT_SENSITIVE_NAMES` (exported from
2315
- * `attaform`); compose to extend:
2316
- *
2317
- * ```ts
2318
- * import { DEFAULT_SENSITIVE_NAMES, createAttaform } from 'attaform'
2319
- *
2320
- * createAttaform({
2321
- * defaults: { sensitiveNames: [...DEFAULT_SENSITIVE_NAMES, 'mrn', 'tax_id'] }
2322
- * })
2323
- * ```
2324
- *
2325
- * Pass an empty array `[]` as the explicit opt-out — "nothing is
2326
- * sensitive" — for fully-trusted internal tooling. When present at
2327
- * the per-form level via `useForm({ sensitiveNames })`, the per-form
2328
- * list REPLACES the global one (consumers compose their own
2329
- * additive lists via the exported default).
2330
- */
2331
- sensitiveNames?: readonly string[];
2332
- /**
2333
- * App-wide default for `useForm({ multiTab })`. Library default is
2334
- * `false` (opt-in) — same posture as `persist`. Set to `true` once
2335
- * at the plugin level to enable cross-tab sync for every form in
2336
- * the app by default; individual forms can still opt out via
2337
- * `useForm({ multiTab: false })`.
2338
- *
2339
- * **Resolution order (per-form wins):**
2340
- *
2341
- * useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`false`)
2342
- *
2343
- * **Why opt-in.** Auto-broadcasting same-keyed forms surprises users
2344
- * (a value typed in one tab appearing in another they forgot was
2345
- * open) and leaks state for forms that don't explicitly use
2346
- * `sensitiveNames`. Paired with `persist` (also opt-in), the two
2347
- * "richer state" surfaces follow one consistent rule: explicit
2348
- * consent.
2349
- *
2350
- * **Secure-context gate.** Even with `multiTab: true`, sync only
2351
- * activates over HTTPS or localhost. On plain HTTP, the module
2352
- * silently noops with a one-shot dev-mode warning — production
2353
- * deployments MUST be served over HTTPS for sync to function. See
2354
- * the multi-tab-sync recipe's Security section for the threat model.
2355
- */
2356
- multiTab?: boolean;
2357
1982
  /**
2358
1983
  * App-wide default for `useForm({ autoAria })`. Library default is
2359
1984
  * `true`: `v-register` keeps `aria-invalid` / `aria-busy` /
@@ -2570,7 +2195,7 @@ type MetaTrackerValue = {
2570
2195
  */
2571
2196
  blank: boolean;
2572
2197
  };
2573
- type RegisterFlatPath<Form, Key extends keyof Form = keyof Form> = FlatPathBuilder<Form, 'register', Key>;
2198
+ type RegisterFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'register'> : never;
2574
2199
  /**
2575
2200
  * A transformation applied to a field's value as user input flows
2576
2201
  * from DOM through the directive's assigner. Composes left-to-right
@@ -2674,55 +2299,10 @@ type CoercionEntry<I extends SlimPrimitiveKind = SlimPrimitiveKind, O extends Sl
2674
2299
  */
2675
2300
  type CoercionRegistry = readonly CoercionEntry[];
2676
2301
  /**
2677
- * Options for `register(path, options)`. Per-field rather than
2678
- * per-form so each persisted path is opted in at its own call site
2679
- * adding a new field can't accidentally leak into the persistence
2680
- * pipeline unless the field's `register` call says so explicitly.
2302
+ * Options for `register(path, options)`. Per-field configuration
2303
+ * applied at the binding's own call site.
2681
2304
  */
2682
2305
  type RegisterOptions = {
2683
- /**
2684
- * Opt this field into the form's persistence pipeline. The form
2685
- * also needs `useForm({ persist })` configured for any storage
2686
- * activity to happen.
2687
- *
2688
- * Persistence follows the field's lifecycle: writes flow on
2689
- * mount, the field is dropped from the persisted draft on unmount.
2690
- * If multiple inputs bind to the same path, the path keeps
2691
- * persisting as long as any opted-in input is mounted.
2692
- *
2693
- * When the path looks sensitive (password / cvv / ssn / token /
2694
- * etc.) the opt-in is skipped with a one-shot dev warning unless
2695
- * `acknowledgeSensitive: true` is also set — the field simply isn't
2696
- * persisted (the secure default). It never throws.
2697
- */
2698
- persist?: boolean;
2699
- /**
2700
- * Suppress the sensitive-name guard. Required to persist any path
2701
- * whose name matches the heuristic (password, cvv, ssn, etc.).
2702
- * Treat this as a code-review checkpoint: setting it should be a
2703
- * deliberate decision that the path's data is safe to land in
2704
- * client-side storage for this user's session.
2705
- */
2706
- acknowledgeSensitive?: boolean;
2707
- /**
2708
- * Opt this field OUT of multi-tab sync. The form-level cascade
2709
- * activates sync by default; passing `multiTab: false` on a single
2710
- * register call keeps that path tab-local — outbound patches at
2711
- * the path are stripped, and inbound patches at the path are
2712
- * rejected (symmetric tab-local behaviour).
2713
- *
2714
- * The opt-out is downgrade-only — you cannot pass `multiTab: true`
2715
- * to bring sync back on a form whose form-level `multiTab` is
2716
- * `false` (in that case the sync module never instantiated; there's
2717
- * no broadcaster to opt back into).
2718
- *
2719
- * Use for fields that hold transient per-tab UI state inside an
2720
- * otherwise-synced form (e.g. an editor's cursor position field
2721
- * mirrored into the form for save-on-blur), or for individual
2722
- * paths the consumer wants to scope to the originating tab without
2723
- * disabling sync globally.
2724
- */
2725
- multiTab?: boolean;
2726
2306
  /**
2727
2307
  * Sync transformation pipeline applied to user-typed values before
2728
2308
  * they reach form state. Composes left-to-right: each transform
@@ -2783,7 +2363,7 @@ type RegisterOptions = {
2783
2363
  * Or read `innerRef` directly when integrating with custom components.
2784
2364
  *
2785
2365
  * The returned value is a `shallowReadonly` reactive proxy: top-level
2786
- * reads (`rv.path`, `rv.formKey`, `rv.persist`, …) track in reactive
2366
+ * reads (`rv.path`, `rv.formKey`, `rv.segments`, …) track in reactive
2787
2367
  * scopes, mutations are blocked, and inner refs (`innerRef`,
2788
2368
  * `displayValue`) keep their `Ref` shape.
2789
2369
  *
@@ -2803,16 +2383,14 @@ type RegisterValue<Value = unknown> = Readonly<{
2803
2383
  * automatically; expose it to custom integrations that need to
2804
2384
  * register an element manually.
2805
2385
  *
2806
- * Recording the element also enables `setValueWithInternalPath` to
2807
- * auto-attach per-element persist meta on writes that don't carry
2808
- * their own.
2386
+ * Recording the element drives the form's element map (used for
2387
+ * `field.meta.connected`, `focusFirstError`, and `scrollToFirstError`).
2809
2388
  */
2810
2389
  registerElement: (el: HTMLElement) => void;
2811
2390
  /**
2812
2391
  * Detach an HTML element from this binding. Pair with
2813
- * `registerElement` for custom integrations. Clears the recorded
2814
- * element so subsequent writes without explicit `meta` fall back to
2815
- * "no auto-persist".
2392
+ * `registerElement` for custom integrations. Drops the element from
2393
+ * the form's element map.
2816
2394
  */
2817
2395
  deregisterElement: (el: HTMLElement) => void;
2818
2396
  /**
@@ -2820,16 +2398,10 @@ type RegisterValue<Value = unknown> = Readonly<{
2820
2398
  * write was accepted, `false` when it was rejected (e.g. wrong
2821
2399
  * primitive type for the path).
2822
2400
  *
2823
- * When `meta` is undefined and the binding has a registered element,
2824
- * the rv consults the per-element opt-in (set by
2825
- * `register(path, { persist: true })` against that element) and
2826
- * auto-attaches `{ persist: true }` when opted in. Custom directives
2827
- * and consumer assigners can omit `meta` to participate in the same
2828
- * persistence channel the default assigner uses.
2829
- *
2830
- * Pass an explicit `meta` to override the auto-derivation, e.g.
2831
- * `{ persist: false }` to skip persistence for a transient write
2832
- * even when the element is opted in.
2401
+ * The write path for custom directives and consumer assigners: it
2402
+ * routes through the same funnel (and per-instance meta) as the
2403
+ * directive's default assigner. Caller-supplied `meta` passes through
2404
+ * unchanged.
2833
2405
  */
2834
2406
  setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
2835
2407
  /**
@@ -2878,55 +2450,6 @@ type RegisterValue<Value = unknown> = Readonly<{
2878
2450
  * `key`.
2879
2451
  */
2880
2452
  formInstanceId: string;
2881
- /**
2882
- * Whether this binding opted into persistence via `register(path, { persist: true })`.
2883
- * @internal
2884
- */
2885
- persist: boolean;
2886
- /**
2887
- * Whether this binding acknowledged a sensitive-name override.
2888
- * @internal
2889
- */
2890
- acknowledgeSensitive: boolean;
2891
- /**
2892
- * Per-element persistence opt-in registry. Used by directive integrations.
2893
- * @internal
2894
- */
2895
- persistOptIns: PersistOptInRegistry;
2896
- /**
2897
- * Resolved sensitive-path predicate honoring this form's
2898
- * `sensitiveNames` cascade. The directive calls this through
2899
- * `allowSensitivePersist` when a `register('path', { persist: true })`
2900
- * binding mounts so a per-form custom list (e.g. extending with
2901
- * `'mrn'`) gates persistence enrolment correctly.
2902
- * @internal
2903
- */
2904
- isSensitivePath: (path: Path | PathKey | string) => boolean;
2905
- /**
2906
- * Whether this binding declared `register('path', { multiTab: false })`.
2907
- * Drives the directive's mount/unmount lifecycle: when `false`, the
2908
- * directive's `created` hook bumps `state.noSyncPaths` for this
2909
- * path, and `beforeUnmount` decrements. When `true` (the default),
2910
- * the binding rides the form-level cascade.
2911
- * @internal
2912
- */
2913
- multiTab: boolean;
2914
- /**
2915
- * Pre-bound mount hook for `multiTab: false` bindings — calls
2916
- * `state.incrementNoSyncOptOut(path)` with this binding's path.
2917
- * `undefined` when `multiTab !== false`. The directive invokes
2918
- * during the mount lifecycle.
2919
- * @internal
2920
- */
2921
- markNoSync?: () => void;
2922
- /**
2923
- * Pre-bound unmount hook for `multiTab: false` bindings — calls
2924
- * `state.decrementNoSyncOptOut(path)`. Paired with `markNoSync`;
2925
- * the directive invokes on `beforeUnmount` (and on `beforeUpdate`
2926
- * when the binding transitions out of opt-out).
2927
- * @internal
2928
- */
2929
- unmarkNoSync?: () => void;
2930
2453
  /**
2931
2454
  * Sync transform pipeline applied by the directive's assigner to
2932
2455
  * user-typed values before they reach form state. See
@@ -2978,8 +2501,7 @@ type RegisterValue<Value = unknown> = Readonly<{
2978
2501
  * Add this field's path to the form's `blankPaths` set,
2979
2502
  * writing the slim default to storage. Returns the `setValueAtPath`
2980
2503
  * boolean (`true` accepted, `false` rejected by the slim-primitive
2981
- * gate). Inherits the binding's `persist` meta so the mark rides
2982
- * the same persistence channel as user-typed writes.
2504
+ * gate).
2983
2505
  *
2984
2506
  * Called by the directive's input listener on numeric clear (commit
2985
2507
  * 5) and by the imperative `setValue(path, unset)` translation
@@ -3855,13 +3377,17 @@ type LeafWalker<T, Kind extends keyof LeafSchemeFor<unknown>, StripOptional exte
3855
3377
  type DiscriminatedLeaf<T, K extends PropertyKey, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean> = [T] extends [Record<K, unknown>] ? LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> : LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> | undefined;
3856
3378
  /**
3857
3379
  * Intersection augmenting every container in the `form.errors` walker
3858
- * with a `''` sentinel slot the per-container home for cross-field
3859
- * refine errors, server-side container marks, and (at root) form-level
3860
- * errors. Gated on `Kind extends 'errors'` so `form.values` and
3861
- * `form.fields` surfaces stay untouched. Carve-out for schemas that
3380
+ * with a `''` slot. At a depth >= 1 container it's the container-self
3381
+ * sentinel the home for cross-field refine errors and server-side
3382
+ * container marks; at root the `''` property addresses the literal
3383
+ * empty-key field. Gated on `Kind extends 'errors'` so `form.values`
3384
+ * and `form.fields` surfaces stay untouched. Carve-out for schemas that
3862
3385
  * legitimately declare a `''` field: the declared field type wins; at
3863
3386
  * runtime the two collide harmlessly (errors at the literal leaf and
3864
3387
  * any container-self errors share the slot via array concat).
3388
+ *
3389
+ * Global (root) errors are NOT this slot: they live at the root `[]`
3390
+ * and are read via the `errors([])` call-form, never `errors['']`.
3865
3391
  */
3866
3392
  type ContainerSelfErrorsSlot<T, Kind> = Kind extends 'errors' ? '' extends keyof T ? unknown : {
3867
3393
  readonly ['']: readonly ValidationError[];
@@ -3991,9 +3517,11 @@ type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
3991
3517
  <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): readonly ValidationError[];
3992
3518
  (segments: ReadonlyArray<string | number>): readonly ValidationError[];
3993
3519
  /**
3994
- * No-arg call returns the form-level error aggregate — same as
3995
- * `form.errors([])` and `form.meta.errors`. Always a readonly array;
3996
- * empty when the form has no errors.
3520
+ * No-arg call returns the whole-form error aggregate — same as
3521
+ * `form.meta.errors`: every field error plus the global bucket.
3522
+ * Distinct from `form.errors([])`, which returns ONLY the global
3523
+ * (root) bucket. Always a readonly array; empty when the form has no
3524
+ * errors.
3997
3525
  */
3998
3526
  (): readonly ValidationError[];
3999
3527
  };
@@ -4049,63 +3577,6 @@ type ValuesSurface<F> = Readonly<LiftedValueShape<F>> & {
4049
3577
  (path: ReadonlyArray<string | number>): unknown;
4050
3578
  (): Readonly<F>;
4051
3579
  };
4052
- /**
4053
- * A single server-side error entry. Carries both the human-readable
4054
- * `message` and a stable `code` identifier — both fields are required.
4055
- * The `code` is stamped verbatim onto the produced `ValidationError`,
4056
- * so consumers can branch on it without string-matching on `message`.
4057
- *
4058
- * Pick a prefix for your codes (`api:`, `auth:`, etc.) and stay
4059
- * consistent so error-rendering UIs can switch on the code.
4060
- */
4061
- type ApiErrorEntry = {
4062
- /** Human-readable failure description. */
4063
- message: string;
4064
- /**
4065
- * Stable machine identifier for the failure (e.g. `'api:duplicate-email'`).
4066
- * Forwarded verbatim onto the produced `ValidationError`.
4067
- */
4068
- code: string;
4069
- };
4070
- /**
4071
- * Shape of a server-side error details record. Keys are dotted field
4072
- * paths; values are either a single entry, an array of entries, or a
4073
- * mix of structured and bare-string entries. Each entry is one of:
4074
- *
4075
- * - **Structured** — `{ message: string, code: string }`. The `code`
4076
- * forwards verbatim onto the produced `ValidationError`.
4077
- * - **Bare string** — a plain string. The Rails / Django REST
4078
- * Framework / Laravel default JSON shape (`{ field: ["msg"] }`).
4079
- * Synthesized into `{ message: <string>, code: <defaultCode> }` at
4080
- * parse time, where `defaultCode` defaults to `'api:unknown'` and
4081
- * is configurable via `parseApiErrors`'s options bag.
4082
- *
4083
- * Multiple entries at the same path produce multiple
4084
- * `ValidationError`s — useful for a single field that fails multiple
4085
- * checks (e.g. `password` is too short *and* missing a digit).
4086
- */
4087
- type ApiErrorDetails = Record<string, ApiErrorValue>;
4088
- /**
4089
- * One entry inside an {@link ApiErrorDetails} value — either the
4090
- * strict `{ message, code }` object, or a bare string (synthesised
4091
- * with the parser's `defaultCode`).
4092
- */
4093
- type ApiErrorValue = string | ApiErrorEntry | ReadonlyArray<string | ApiErrorEntry>;
4094
- /**
4095
- * Outer envelope `parseApiErrors` accepts. Both the wrapped form
4096
- * (`{ error: { details } }`) and the unwrapped form (`{ details }`)
4097
- * are recognised; raw detail records (`{ email: { message, code } }`)
4098
- * are also accepted directly.
4099
- */
4100
- type ApiErrorEnvelope = {
4101
- /** Wrapped error envelope — `parseApiErrors` reads `details` from inside. */
4102
- error?: {
4103
- details?: ApiErrorDetails;
4104
- [k: string]: unknown;
4105
- };
4106
- /** Unwrapped error envelope. */
4107
- details?: ApiErrorDetails;
4108
- };
4109
3580
  /**
4110
3581
  * Reactive form-level flags, counters, and aggregates returned as
4111
3582
  * `form.meta`. "Meta" because every other surface (`form.values`,
@@ -4309,6 +3780,19 @@ interface BlankPathsView {
4309
3780
  /** Iterates the blank-marked paths as segment arrays. */
4310
3781
  [Symbol.iterator](): IterableIterator<Path>;
4311
3782
  }
3783
+ /**
3784
+ * The no-arg `form.record()` call form, present only when the form root
3785
+ * is itself an open record (`z.record(K, V)`). `string extends keyof
3786
+ * Form` is the open-keyset probe: true for `Record<string, V>`, false
3787
+ * for a fixed `z.object` shape. On a fixed object this resolves to
3788
+ * `unknown`, which contributes nothing to the intersection in `record`
3789
+ * below, so the no-arg call form simply does not exist there (and
3790
+ * `form.record()` stays a compile error, as it should when the root has
3791
+ * a closed key set). On a record root it mirrors the path-addressed
3792
+ * `record(path)` overload: one `FieldState` per entry, keyed by the
3793
+ * record's own runtime keys.
3794
+ */
3795
+ type RootRecordView<Form> = string extends keyof Form ? Form extends Record<string, infer RootValue> ? () => Readonly<Record<string, FieldState<RootValue>>> : unknown : unknown;
4312
3796
  type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form, K extends FormKey = FormKey> = {
4313
3797
  /**
4314
3798
  * Wraps your submit logic with validation and error routing.
@@ -4422,11 +3906,8 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4422
3906
  * type at a leaf). Refinement-level mismatches (out-of-enum
4423
3907
  * values, failing format checks, etc.) succeed and surface as
4424
3908
  * field errors instead.
4425
- *
4426
- * Pass `{ silent: true }` to land the write without notifying
4427
- * `form.onChange` handlers (e.g. hydrating a saved record).
4428
3909
  */
4429
- <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value, options?: SetValueOptions): boolean;
3910
+ <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
4430
3911
  /**
4431
3912
  * Write at a specific path. Pass a value or a callback receiving
4432
3913
  * the previous value at that path.
@@ -4444,49 +3925,14 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4444
3925
  * it blank (storage holds the slim default; UI displays
4445
3926
  * empty; submit raises "No value supplied" for required schemas).
4446
3927
  */
4447
- <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value, options?: SetValueOptions): boolean;
3928
+ <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
4448
3929
  /**
4449
3930
  * Tuple-segment form. Equivalent to the dotted-string overload —
4450
3931
  * useful when paths are built from variables or arrays:
4451
3932
  * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
4452
3933
  * type is exact, matching the dotted-string form.
4453
3934
  */
4454
- <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value, options?: SetValueOptions): boolean;
4455
- };
4456
- /**
4457
- * Subscribe to form value changes — the side-channel autosave is built on.
4458
- * Three call forms:
4459
- *
4460
- * - `form.onChange(handler, options?)` — react to the whole form.
4461
- * - `form.onChange('user.email', handler, options?)` — react to one path;
4462
- * `value` is that path's value.
4463
- * - `form.onChange(source, handler, options?)` — react to a list of paths,
4464
- * or a getter / ref resolving to a path or list (re-read on each write,
4465
- * so the aim can follow a moving target like the active list row).
4466
- *
4467
- * The handler runs AFTER the value lands. Its return is ignored, and a
4468
- * throw or rejection routes to `options.onError`, never into the write that
4469
- * triggered it. Returns an idempotent `stop()`; called inside a component's
4470
- * setup it also stops automatically on unmount.
4471
- *
4472
- * `onChange` is a pure side-channel: nothing it does marks the form dirty,
4473
- * pending, or validating. Keep validation feedback in `.refine` and
4474
- * `field.show*`, and track autosave status in your own state.
4475
- *
4476
- * ```ts
4477
- * form.onChange('user.email', async (email, ctx) => {
4478
- * const verdict = await ctx.form.validateAsync(ctx.path)
4479
- * if (verdict.success) await api.save({ email }, { signal: ctx.signal })
4480
- * }, { onError: (error, ctx) => ctx.retry() })
4481
- * ```
4482
- */
4483
- onChange: {
4484
- /** React to the whole form. `value` is the current form. */
4485
- (handler: OnChangeHandler<ReadForm, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
4486
- /** React to one path. `value` is that path's value. */
4487
- <P extends FlatPath<Form>>(source: P, handler: OnChangeHandler<NestedType<Form, P>, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
4488
- /** React to a list of paths, or a getter / ref / computed. `value` is unknown. */
4489
- (source: OnChangeSource, handler: OnChangeHandler<unknown, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
3935
+ <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;
4490
3936
  };
4491
3937
  /**
4492
3938
  * Reactive validation status. Re-runs whenever the form (or the
@@ -4577,10 +4023,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4577
4023
  *
4578
4024
  * ```vue
4579
4025
  * <input v-register="form.register('email')" />
4580
- * <input
4581
- * type="password"
4582
- * v-register="form.register('password', { persist: true, acknowledgeSensitive: true })"
4583
- * />
4026
+ * <input v-register="form.register('username', { transforms: [trim] })" />
4584
4027
  * ```
4585
4028
  *
4586
4029
  * Also accepts a segment-array form for callers building paths
@@ -4594,23 +4037,18 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4594
4037
  * </fieldset>
4595
4038
  * ```
4596
4039
  *
4597
- * Pass `options.persist` to opt into the form's persistence
4598
- * pipeline. Persistence requires `useForm({ persist })` configured
4599
- * for storage activity to actually happen.
4040
+ * Pass `options.transforms` to run a sync normalisation pipeline over
4041
+ * user-typed values before they reach form state.
4600
4042
  */
4601
4043
  register: {
4602
- <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
4603
- <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>>>;
4044
+ <Path extends RegisterFlatPath<Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
4045
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form>] ? unknown : never), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>;
4604
4046
  };
4605
4047
  /**
4606
4048
  * The form's identifier — either the explicit `key` passed to
4607
4049
  * `useForm` or an auto-generated unique id when `key` was omitted.
4608
- * Use it when feeding API errors through `parseApiErrors`:
4609
- *
4610
- * ```ts
4611
- * const result = parseApiErrors(serverPayload, { formKey: form.key })
4612
- * if (result.ok) form.setFieldErrors(result.errors)
4613
- * ```
4050
+ * Every `ValidationError` this form produces carries it as `formKey`,
4051
+ * so a shared error list can be routed back to the right form.
4614
4052
  *
4615
4053
  * Typed as the literal `K` when an explicit `key` was passed; falls
4616
4054
  * back to `FormKey` when omitted (auto-generated id).
@@ -4722,9 +4160,9 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4722
4160
  * (`form.errors['user.profile.email']`) — JS dot notation splits
4723
4161
  * on literal dots.
4724
4162
  *
4725
- * Read-only — populate via `setFieldErrors`, `addFieldErrors`, and
4726
- * `clearFieldErrors`. Server-side errors flow through
4727
- * `parseApiErrors` first.
4163
+ * Read-only — populate via `setErrors` / `clearErrors`. A server
4164
+ * response that already emits `ValidationError[]` pipes straight into
4165
+ * `setErrors` with no adapter.
4728
4166
  */
4729
4167
  errors: FormErrorsSurface<Form>;
4730
4168
  /**
@@ -4747,76 +4185,61 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4747
4185
  <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>>;
4748
4186
  };
4749
4187
  /**
4750
- * Replace every field error for this form with the provided list.
4751
- * Useful after `parseApiErrors` produces a fresh batch from a
4752
- * server response.
4188
+ * Set the form's manual error layer. One surface for server-side
4189
+ * errors, optimistic-UI errors, and form-level banners: a field error
4190
+ * and a global (form-level) error are the same thing at different
4191
+ * paths, so there is no separate field/form split.
4753
4192
  *
4754
- * ```ts
4755
- * const result = parseApiErrors(payload, { formKey: form.key })
4756
- * if (result.ok) form.setFieldErrors(result.errors)
4757
- * ```
4758
- */
4759
- setFieldErrors: (errors: ValidationError[]) => void;
4760
- /**
4761
- * Append errors to the existing set without clearing prior entries.
4762
- * Use when reporting an additional issue alongside existing errors
4763
- * (e.g. a partial server response).
4764
- */
4765
- addFieldErrors: (errors: ValidationError[]) => void;
4766
- /**
4767
- * Clear errors. Pass a path to clear errors for a single field;
4768
- * call with no arguments to clear every error on the form.
4193
+ * Input is lenient ({@link ErrorInput}): a real `Error`, a partial
4194
+ * `{ message?, path?, code?, data? }`, or an array of either. The form
4195
+ * stamps its own `formKey`, defaults a missing `code` to
4196
+ * `atta:user-error`, and coerces a missing or empty `message` to
4197
+ * `"Unknown error"` instead of throwing. A server that already emits
4198
+ * `ValidationError[]` satisfies the input shape directly, so
4199
+ * `form.setErrors(response.errors)` needs no adapter.
4769
4200
  *
4770
- * ```ts
4771
- * form.clearFieldErrors('email') // clear one field
4772
- * form.clearFieldErrors() // clear all
4773
- * ```
4774
- */
4775
- clearFieldErrors: (path?: string | (string | number)[]) => void;
4776
- /**
4777
- * Replace the form-level errors — the entries at the empty path
4778
- * (`path: []`) — without disturbing any field-level errors. Pass an
4779
- * empty array to clear them all.
4201
+ * Three call forms, mirroring `setValue`:
4780
4202
  *
4781
4203
  * ```ts
4782
- * form.setFormErrors([{ message: 'Capacity exceeded' }])
4783
- * form.setFormErrors([
4784
- * { message: 'Capacity exceeded', code: 'capacity:exceeded' },
4785
- * { message: 'Pickup window full' },
4786
- * ])
4787
- * form.setFormErrors([]) // clear
4204
+ * // Whole-layer replace. An entry with no `path` is a global,
4205
+ * // form-level error (path `[]`); add a `path` to target a field.
4206
+ * form.setErrors([{ path: ['email'], message: 'Already taken' }])
4207
+ * form.setErrors({ message: 'Capacity exceeded' }) // global banner
4208
+ * form.setErrors(new Error('Network unreachable')) // message coerced
4209
+ *
4210
+ * // Functional update. `prev` is the current manual layer, flat.
4211
+ * form.setErrors((prev) => [...prev, { message: 'And one more' }])
4212
+ *
4213
+ * // Path-scoped. The path is stamped onto every entry, and only that
4214
+ * // path's bucket is replaced. `prev` is that path's manual errors.
4215
+ * form.setErrors('email', [{ message: 'Already taken' }])
4216
+ * form.setErrors(['profile', 'handle'], { message: 'Reserved' })
4217
+ * form.setErrors('email', (prev) => prev.slice(0, 1))
4788
4218
  * ```
4789
4219
  *
4790
- * Only `message` is required. `code` defaults to `'atta:form-error'`.
4791
- * Any caller-provided `path` or `formKey` is ignored `path` is
4792
- * always forced to `[]` (this API is form-level-only by definition)
4793
- * and `formKey` is filled in from the form instance. The lenient
4794
- * input shape lets you pipe `parseApiErrors` output (or any
4795
- * `ValidationError[]`) straight in:
4220
+ * Replaces only the manual layer; schema/validation errors live in a
4221
+ * separate store and merge on read. Pass `[]` to clear the manual
4222
+ * layer (or use `clearErrors`, which also clears the schema layer).
4223
+ */
4224
+ setErrors: {
4225
+ (update: (prev: ValidationError[]) => ErrorInput | ErrorInput[]): void;
4226
+ (errors: ErrorInput | ErrorInput[]): void;
4227
+ (path: string | (string | number)[], errors: ErrorInput | ErrorInput[] | ((prev: ValidationError[]) => ErrorInput | ErrorInput[])): void;
4228
+ };
4229
+ /**
4230
+ * Clear errors at one path, or everywhere. Clears BOTH the manual
4231
+ * layer (set through `setErrors`) and the schema/validation layer at
4232
+ * the target. With always-on validation the schema half re-populates
4233
+ * on the next mutation if the value is still invalid, so the cleared
4234
+ * state is short-lived for a field that is still wrong.
4796
4235
  *
4797
4236
  * ```ts
4798
- * const result = parseApiErrors(payload, { formKey: form.key })
4799
- * if (result.ok) form.setFormErrors(result.errors)
4237
+ * form.clearErrors('email') // one field
4238
+ * form.clearErrors([]) // the global, form-level bucket
4239
+ * form.clearErrors() // every error on the form
4800
4240
  * ```
4801
- *
4802
- * Form-level errors land at the empty-string path bucket
4803
- * (`path: ['']`). They surface in `form.meta.errors` (alongside
4804
- * field errors), in `form.errors()` / `form.errors([])` (whole-form
4805
- * subtree aggregates), and — uniquely — in `form.errors('')`,
4806
- * which returns ONLY the form-level bucket. They're excluded from
4807
- * the path-keyed `form.errors` drill proxy because no nested-object
4808
- * key represents the empty-string path. Read them via
4809
- * `meta.errors.filter(e => e.path.length === 1 && e.path[0] === '')`
4810
- * if you need a programmatic split.
4811
4241
  */
4812
- setFormErrors: (errors: ReadonlyArray<Partial<ValidationError> & {
4813
- message: string;
4814
- }>) => void;
4815
- /**
4816
- * Clear every form-level error. Equivalent to `setFormErrors([])`;
4817
- * field errors are untouched.
4818
- */
4819
- clearFormErrors: () => void;
4242
+ clearErrors: (path?: string | (string | number)[]) => void;
4820
4243
  /**
4821
4244
  * Form-level reactive flags, counters, and aggregates (`dirty`,
4822
4245
  * `valid`, `submitting`, `submissionAttempts`, and the flat `errors`
@@ -4840,11 +4263,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4840
4263
  * - field errors;
4841
4264
  * - touched / focused / blurred per-field flags;
4842
4265
  * - submission state (`submitting` / `submissionAttempts` /
4843
- * `submitted` / `submitError`);
4844
- * - the persisted draft, if persistence is configured.
4845
- *
4846
- * The next edit on a still-mounted opted-in input will start
4847
- * persisting again automatically.
4266
+ * `submitted` / `submitError`).
4848
4267
  */
4849
4268
  reset: (nextDefaultValues?: DefaultValuesInput<Form>) => void;
4850
4269
  /**
@@ -4854,9 +4273,6 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4854
4273
  *
4855
4274
  * No-op when the path doesn't exist on the form (e.g. a typo'd
4856
4275
  * dynamic key).
4857
- *
4858
- * If persistence is configured, the matching subpath is removed
4859
- * from the persisted draft too.
4860
4276
  */
4861
4277
  resetField: (path: FlatPath<Form>) => void;
4862
4278
  /**
@@ -4892,8 +4308,8 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4892
4308
  * on a `false` return.
4893
4309
  *
4894
4310
  * Sugar over `setValue(path, schema.getEmptyValueAtPath(path))` —
4895
- * no separate bookkeeping. Variant memory, history, persistence,
4896
- * and listeners all see this as a regular write at the path.
4311
+ * no separate bookkeeping. Variant memory, history, and listeners
4312
+ * all see this as a regular write at the path.
4897
4313
  *
4898
4314
  * `clear()` (no arg) targets the whole form. `clear('')` targets
4899
4315
  * the empty-string path slot SPECIFICALLY — the two are NOT
@@ -4904,33 +4320,6 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4904
4320
  <Path extends FlatPath<Form> | ''>(path: Path): boolean;
4905
4321
  <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form> | ''] ? unknown : never)): boolean;
4906
4322
  };
4907
- /**
4908
- * Write the current value at `path` to storage immediately. Useful
4909
- * for explicit "Save draft" buttons, `beforeunload` handlers, or
4910
- * multi-step checkpoints where the user shouldn't wait for the
4911
- * debounce window.
4912
- *
4913
- * Bypasses both the per-field opt-in and the debouncer. Existing
4914
- * paths in the persisted draft are preserved (this is a merge,
4915
- * not a replace).
4916
- *
4917
- * For sensitive-looking paths, warns once and no-ops unless you pass
4918
- * `{ acknowledgeSensitive: true }` — it never throws. Also a no-op
4919
- * when `useForm({ persist })` wasn't configured.
4920
- */
4921
- persist: (path: FlatPath<Form>, options?: {
4922
- acknowledgeSensitive?: boolean;
4923
- }) => Promise<void>;
4924
- /**
4925
- * Remove data from the persisted draft. Without arguments, wipes
4926
- * the entire entry. With a path, removes just that subpath.
4927
- *
4928
- * Does not change the in-memory form state — pair with `reset()`
4929
- * / `resetField()` if you need both. Future edits to still-mounted
4930
- * opted-in fields will re-populate the entry. No-op when
4931
- * persistence isn't configured.
4932
- */
4933
- clearPersistedDraft: (path?: FlatPath<Form>) => Promise<void>;
4934
4323
  /**
4935
4324
  * Consolidated undo/redo namespace — `form.history.{undo, redo,
4936
4325
  * clear, canUndo, canRedo, size}`. Always present; inert when
@@ -5061,8 +4450,19 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
5061
4450
  * when the key leaves. `form.fields(path)` remains the single
5062
4451
  * aggregated container for the whole record; `record` is the
5063
4452
  * per-entry view.
4453
+ *
4454
+ * When the form root is itself a record (`useForm({ schema:
4455
+ * z.record(K, V) })` — a dictionary form), call `form.record()` with
4456
+ * no argument for the root entry view:
4457
+ *
4458
+ * ```vue
4459
+ * <div v-for="(member, id) in form.record()" :key="id">
4460
+ * <input v-register="form.register(id)" />
4461
+ * <p v-if="member.showErrors">{{ member.firstError?.message }}</p>
4462
+ * </div>
4463
+ * ```
5064
4464
  */
5065
- record: <Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>;
4465
+ record: RootRecordView<Form> & (<Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>);
5066
4466
  /**
5067
4467
  * Read-only view of the form's blank path set. Reactive — Vue 3.5
5068
4468
  * tracks `.has()` / `for..of` / size accesses, so consumers can drive
@@ -5090,5 +4490,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
5090
4490
  blankPaths: ComputedRef<BlankPathsView>;
5091
4491
  };
5092
4492
 
5093
- export { canonicalizePath as aA, isPathPrefix as aB, isUnset as aC, parseDottedPath as aD, unset as aE, ROOT_PATH as ab, ROOT_PATH_KEY as ac };
5094
- export type { OnChangeSource as $, AttaformDefaults as A, FormStorage as B, CoercionEntry as C, DefaultValuesInput as D, ErrorsProxyShape as E, FormKey as F, GenericForm as G, FormStorageKind as H, HandleSubmit as I, HistoryConfig as J, IsTuple as K, IsUnion as L, JoinSegments as M, KeyofUnion as N, LiftedValueShape as O, MetaTrackerValue as P, NestedReadType as Q, RegisterModelDynamicCustomDirective as R, NestedType as S, OnChangeConfig as T, UseFormConfiguration as U, ValidationError as V, OnChangeContext as W, OnChangeErrorContext as X, OnChangeErrorHandler as Y, OnChangeHandler as Z, OnChangeOptions as _, AbstractSchema as a, OnError as a0, OnInvalidSubmitPolicy as a1, OnSubmit as a2, PartialFlatPath as a3, Path as a4, PathKey as a5, PendingValidationStatus as a6, PersistConfig as a7, PersistConfigOptions as a8, PersistIncludeMode as a9, SchemaFactoryOptions as aF, TransformAbortHolder as aG, PersistOptInRegistry as aH, Primitive as aa, ReactiveValidationStatus as ad, RegisterDirective as ae, RegisterFlatPath as af, RegisterOptions as ag, RegisterSelectModifier as ah, RegisterTextModifier as ai, RegisterTransform as aj, Segment as ak, SetValueCallback as al, SetValueOptions as am, SetValuePayload as an, SettledValidationStatus as ao, SlimPrimitiveKind as ap, SlimRuntimeOf as aq, SubmitHandler as ar, Unset as as, ValidateOn as at, ValidateOnConfig as au, ValidationResponse as av, ValidationResponseWithoutValue as aw, ValueOfUnion as ax, WriteMeta as ay, WriteShape as az, UseFormReturnType as b, RegisterValue as c, GetDisplayState as d, ApiErrorEnvelope as e, ApiErrorDetails as f, ApiErrorEntry as g, ArrayItem as h, ArrayPath as i, CoercionRegistry as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DeepPartial as m, DefaultValuesResponse as n, DefaultValuesShape as o, DisplayCtx as p, DisplayMachine as q, DisplayState as r, FieldMetaPayload as s, FieldState as t, FieldStateMap as u, FieldStateMapEntry as v, FlatPath as w, FormErrorRecord as x, FormErrorsSurface as y, FormMeta as z };
4493
+ export { ROOT_PATH as Z, ROOT_PATH_KEY as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
4494
+ export type { ReactiveValidationStatus as $, AttaformDefaults as A, Json as B, CoercionEntry as C, DefaultValuesInput as D, ErrorInput 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, NestedType as O, OnError as P, OnInvalidSubmitPolicy as Q, RegisterModelDynamicCustomDirective as R, OnSubmit as S, PartialFlatPath as T, UseFormConfiguration as U, Path as V, PathKey as W, PendingValidationStatus as X, Primitive as Y, AbstractSchema 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, SlimPrimitiveKind as aa, SlimRuntimeOf as ab, SubmitHandler as ac, Unset as ad, ValidateOn as ae, ValidateOnConfig as af, ValidationError as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, SchemaFactoryOptions as ar, TransformAbortHolder as as, UseFormReturnType as b, RegisterValue as c, GetDisplayState as d, ArrayItem as e, ArrayPath as f, CoercionRegistry as g, CoercionResult as h, CustomDirectiveRegisterAssignerFn as i, DeepPartial as j, DefaultValuesResponse as k, DefaultValuesShape as l, DisplayCtx as m, DisplayMachine as n, DisplayState as o, ErrorsProxyShape as p, FieldMetaPayload as q, FieldState as r, FieldStateMap as s, FieldStateMapEntry as t, FlatPath as u, FormErrorRecord as v, FormErrorsSurface as w, FormMeta as x, HistoryConfig as y, IsUnion as z };