attaform 0.0.1 → 0.14.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/dist/chunks/devtools.cjs +179 -0
  4. package/dist/chunks/devtools.cjs.map +1 -0
  5. package/dist/chunks/devtools.mjs +177 -0
  6. package/dist/chunks/devtools.mjs.map +1 -0
  7. package/dist/chunks/indexeddb.cjs +119 -0
  8. package/dist/chunks/indexeddb.cjs.map +1 -0
  9. package/dist/chunks/indexeddb.mjs +117 -0
  10. package/dist/chunks/indexeddb.mjs.map +1 -0
  11. package/dist/chunks/local-storage.cjs +58 -0
  12. package/dist/chunks/local-storage.cjs.map +1 -0
  13. package/dist/chunks/local-storage.mjs +56 -0
  14. package/dist/chunks/local-storage.mjs.map +1 -0
  15. package/dist/chunks/session-storage.cjs +58 -0
  16. package/dist/chunks/session-storage.cjs.map +1 -0
  17. package/dist/chunks/session-storage.mjs +56 -0
  18. package/dist/chunks/session-storage.mjs.map +1 -0
  19. package/dist/index.cjs +173 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +493 -0
  22. package/dist/index.d.mts +493 -0
  23. package/dist/index.d.ts +493 -0
  24. package/dist/index.mjs +141 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/nuxt.cjs +97 -0
  27. package/dist/nuxt.cjs.map +1 -0
  28. package/dist/nuxt.d.cts +38 -0
  29. package/dist/nuxt.d.mts +38 -0
  30. package/dist/nuxt.d.ts +38 -0
  31. package/dist/nuxt.mjs +94 -0
  32. package/dist/nuxt.mjs.map +1 -0
  33. package/dist/runtime/plugins/attaform.cjs +32 -0
  34. package/dist/runtime/plugins/attaform.cjs.map +1 -0
  35. package/dist/runtime/plugins/attaform.d.cts +5 -0
  36. package/dist/runtime/plugins/attaform.d.mts +5 -0
  37. package/dist/runtime/plugins/attaform.d.ts +5 -0
  38. package/dist/runtime/plugins/attaform.mjs +30 -0
  39. package/dist/runtime/plugins/attaform.mjs.map +1 -0
  40. package/dist/shared/attaform.B5GWYl76.cjs +386 -0
  41. package/dist/shared/attaform.B5GWYl76.cjs.map +1 -0
  42. package/dist/shared/attaform.BRTxpA3q.mjs +3283 -0
  43. package/dist/shared/attaform.BRTxpA3q.mjs.map +1 -0
  44. package/dist/shared/attaform.BYc9kugA.d.ts +124 -0
  45. package/dist/shared/attaform.Bubm_slq.cjs +622 -0
  46. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -0
  47. package/dist/shared/attaform.BwaYWtMs.d.cts +126 -0
  48. package/dist/shared/attaform.BwaYWtMs.d.mts +126 -0
  49. package/dist/shared/attaform.BwaYWtMs.d.ts +126 -0
  50. package/dist/shared/attaform.CNJO3mME.cjs +3295 -0
  51. package/dist/shared/attaform.CNJO3mME.cjs.map +1 -0
  52. package/dist/shared/attaform.CRgix6_n.cjs +796 -0
  53. package/dist/shared/attaform.CRgix6_n.cjs.map +1 -0
  54. package/dist/shared/attaform.CXZgUECn.d.cts +124 -0
  55. package/dist/shared/attaform.CXpzmj38.mjs +617 -0
  56. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -0
  57. package/dist/shared/attaform.Cc93zNzD.mjs +83 -0
  58. package/dist/shared/attaform.Cc93zNzD.mjs.map +1 -0
  59. package/dist/shared/attaform.DDXrY-1Q.d.cts +2568 -0
  60. package/dist/shared/attaform.DDXrY-1Q.d.mts +2568 -0
  61. package/dist/shared/attaform.DDXrY-1Q.d.ts +2568 -0
  62. package/dist/shared/attaform.DOKOyb3Y.d.mts +124 -0
  63. package/dist/shared/attaform.DlgKK10S.mjs +789 -0
  64. package/dist/shared/attaform.DlgKK10S.mjs.map +1 -0
  65. package/dist/shared/attaform.al_rpt7_.mjs +361 -0
  66. package/dist/shared/attaform.al_rpt7_.mjs.map +1 -0
  67. package/dist/shared/attaform.xKWYHMdq.cjs +89 -0
  68. package/dist/shared/attaform.xKWYHMdq.cjs.map +1 -0
  69. package/dist/transforms.cjs +11 -0
  70. package/dist/transforms.cjs.map +1 -0
  71. package/dist/transforms.d.cts +49 -0
  72. package/dist/transforms.d.mts +49 -0
  73. package/dist/transforms.d.ts +49 -0
  74. package/dist/transforms.mjs +2 -0
  75. package/dist/transforms.mjs.map +1 -0
  76. package/dist/vite.cjs +39 -0
  77. package/dist/vite.cjs.map +1 -0
  78. package/dist/vite.d.cts +53 -0
  79. package/dist/vite.d.mts +53 -0
  80. package/dist/vite.d.ts +53 -0
  81. package/dist/vite.mjs +37 -0
  82. package/dist/vite.mjs.map +1 -0
  83. package/dist/zod-v3.cjs +1511 -0
  84. package/dist/zod-v3.cjs.map +1 -0
  85. package/dist/zod-v3.d.cts +164 -0
  86. package/dist/zod-v3.d.mts +164 -0
  87. package/dist/zod-v3.d.ts +164 -0
  88. package/dist/zod-v3.mjs +1504 -0
  89. package/dist/zod-v3.mjs.map +1 -0
  90. package/dist/zod.cjs +1548 -0
  91. package/dist/zod.cjs.map +1 -0
  92. package/dist/zod.d.cts +67 -0
  93. package/dist/zod.d.mts +67 -0
  94. package/dist/zod.d.ts +67 -0
  95. package/dist/zod.mjs +1541 -0
  96. package/dist/zod.mjs.map +1 -0
  97. package/package.json +182 -6
@@ -0,0 +1,2568 @@
1
+ import { Ref, ObjectDirective, ComputedRef } from 'vue';
2
+
3
+ /**
4
+ * Path primitives for advanced integrations. The form library accepts
5
+ * paths in dotted-string form (`'user.email'`) at every public API.
6
+ * These primitives are exposed for adapter authors who need to
7
+ * canonicalise user-provided paths.
8
+ */
9
+ declare const pathKeyBrand: unique symbol;
10
+ /**
11
+ * Branded string identifier for a canonicalised path. Useful as a
12
+ * `Map` key — two paths that resolve to the same canonical form
13
+ * produce the same `PathKey`. Treat as opaque; don't try to parse.
14
+ */
15
+ type PathKey = string & {
16
+ readonly [pathKeyBrand]: 'PathKey';
17
+ };
18
+ /** A single path segment — a property name or array index. */
19
+ type Segment = string | number;
20
+ /** A structured path as a read-only sequence of segments. */
21
+ type Path = readonly Segment[];
22
+ /**
23
+ * Parse a dotted-string path into structured segments.
24
+ *
25
+ * ```ts
26
+ * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
27
+ * parseDottedPath('items.0.name') // ['items', 0, 'name']
28
+ * parseDottedPath('') // [] (root)
29
+ * ```
30
+ *
31
+ * Throws `InvalidPathError` for paths with empty segments
32
+ * (`'a..b'`, leading or trailing dots). For keys containing literal
33
+ * dots, pass an array form (`['user.name']`) instead.
34
+ */
35
+ declare function parseDottedPath(path: string): Segment[];
36
+ /**
37
+ * Canonicalise a path into structured segments plus a stable string
38
+ * key. Accepts either dotted-string or array form; integer-looking
39
+ * segments normalise to numbers.
40
+ *
41
+ * ```ts
42
+ * canonicalizePath('items.0.name')
43
+ * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
44
+ *
45
+ * canonicalizePath(['items', 0, 'name'])
46
+ * // → same result
47
+ * ```
48
+ *
49
+ * The returned `key` is suitable as a `Map`/`Set` key — equal paths
50
+ * produce equal keys regardless of input form.
51
+ */
52
+ declare function canonicalizePath(input: string | Path): {
53
+ segments: readonly Segment[];
54
+ key: PathKey;
55
+ };
56
+ /**
57
+ * The root path — an empty segment tuple. Pass to APIs that accept
58
+ * a `Path` to address the form value as a whole.
59
+ */
60
+ declare const ROOT_PATH: Path;
61
+ /** Stable string key for the root path. */
62
+ declare const ROOT_PATH_KEY: PathKey;
63
+
64
+ /** Internal brand for the `Unset` type. Never exposed at runtime. */
65
+ declare const _unsetBrand: unique symbol;
66
+ /**
67
+ * Brand-typed sentinel admitted at every primitive leaf of
68
+ * `DefaultValuesShape<T>`, `setValue`, and `reset`. The runtime value
69
+ * is exported as {@link unset} under the same name.
70
+ */
71
+ type Unset = typeof _unsetBrand;
72
+ /**
73
+ * The `unset` sentinel — pass it as a primitive leaf's value to mark
74
+ * the field **displayed-empty** while storage holds the schema's slim
75
+ * default (`0` / `''` / `false` / `0n`).
76
+ *
77
+ * Use it wherever a primitive leaf value is expected:
78
+ *
79
+ * ```ts
80
+ * const form = useForm({
81
+ * schema: z.object({ income: z.number() }),
82
+ * defaultValues: { income: unset }, // UI starts blank, storage holds 0
83
+ * key: 'housing',
84
+ * })
85
+ *
86
+ * form.setValue('income', unset) // re-blank a field after a write
87
+ * form.reset({ income: unset }) // reset to the blank state
88
+ * ```
89
+ *
90
+ * Accepted at any `string` / `number` / `boolean` / `bigint` leaf in
91
+ * `defaultValues`, `setValue(path, unset)`, and `form.reset({ … })`.
92
+ *
93
+ * The path joins the form's `blankPaths` set as long as it stays
94
+ * unset. Required schemas (no `.optional()` / `.nullable()` /
95
+ * `.default(N)`) raise `"No value supplied"` on submit while a leaf
96
+ * is in `blankPaths`; optional / nullable / has-default schemas
97
+ * accept the empty case as their wrapper allows.
98
+ *
99
+ * Storage never holds the symbol — the runtime translates it at the
100
+ * API boundary, so reads through `form.values` always see the slim
101
+ * default. Cross-bundle / SSR-safe: backed by `Symbol.for(...)` so
102
+ * every realm gets the same sentinel.
103
+ *
104
+ * @see {@link isUnset} — type guard that narrows a value back to {@link Unset}.
105
+ * @see `docs/blank.md` — the conceptual model behind blank-aware fields.
106
+ */
107
+ declare const unset: Unset;
108
+ /**
109
+ * Type guard — `true` when `value` is the `unset` sentinel.
110
+ *
111
+ * ```ts
112
+ * if (isUnset(payload.income)) {
113
+ * // payload.income is the sentinel; the field will display empty
114
+ * }
115
+ * ```
116
+ */
117
+ declare function isUnset(value: unknown): value is Unset;
118
+
119
+ /**
120
+ * The minimum shape any form value satisfies — a plain record. Use
121
+ * as a constraint for composables that work generically across forms
122
+ * (e.g. a custom hook that takes any form's `useForm` return).
123
+ */
124
+ type GenericForm = Record<string, unknown>;
125
+ /** Internal helper — `true` when `T` is an object or array. */
126
+ type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
127
+ type PartialFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}` | `${Key}.${PartialFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${PartialFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
128
+ type CompleteFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}.${CompleteFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${CompleteFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
129
+ /**
130
+ * Union of dotted-string paths reachable inside `Form`, e.g. for
131
+ * `{ user: { email: string }, items: string[] }`:
132
+ *
133
+ * `'user' | 'user.email' | 'items' | 'items.0' | 'items.1' | …`
134
+ *
135
+ * Used by every path-addressed API (`setValue(path, value)`,
136
+ * `register(path)`, `toRef(path)`, etc.) so paths autocomplete in
137
+ * the IDE and typos compile-error.
138
+ *
139
+ * Set `ForceFullPath` to `true` to restrict to leaf paths only
140
+ * (no intermediate container paths).
141
+ */
142
+ type FlatPath<Form, Key extends keyof Form = keyof Form, ForceFullPath extends boolean = false> = ForceFullPath extends true ? CompleteFlatPath<Form, Key> : PartialFlatPath<Form, Key>;
143
+ /**
144
+ * Recursive `Partial` — every property at every depth is optional.
145
+ * Used as the parameter type of `defaultValues` and `reset()` so
146
+ * partial overrides at any nesting level are valid.
147
+ */
148
+ type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem> ? DeepPartial<ArrayItem>[] : T extends object ? {
149
+ [Key in keyof T]?: DeepPartial<T[Key]>;
150
+ } : T;
151
+ /**
152
+ * Resolve the type at a dotted-string path inside `RootValue`. Used
153
+ * by the strict (write-side) APIs to derive the type at a path:
154
+ *
155
+ * `NestedType<{ user: { email: string } }, 'user.email'>` → `string`
156
+ *
157
+ * TypeScript caps conditional-type recursion at around 50 levels;
158
+ * paths deeper than that resolve to `never`. Real form schemas
159
+ * never reach this depth.
160
+ */
161
+ type NestedType<RootValue, FlattenedPath extends string, FilterOutNullishTypesDuringRecursion extends boolean = true, _RootValue = FilterOutNullishTypesDuringRecursion extends false ? RootValue : NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedType<_RootValue[NumericKey], Rest, FilterOutNullishTypesDuringRecursion> : never : never : Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] : never : never : FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : never;
162
+ type Primitive = string | number | boolean | symbol | bigint | null | undefined;
163
+ /**
164
+ * Distinguish a tuple from a regular array.
165
+ *
166
+ * `IsTuple<[string, number]>` → `true`
167
+ * `IsTuple<string[]>` → `false`
168
+ *
169
+ * Useful for write-side helpers that need to preserve tuple
170
+ * positions instead of widening to `Array<element>`.
171
+ */
172
+ type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
173
+ /**
174
+ * Path-resolved type for read-side APIs. Like `NestedType`, but once
175
+ * the walk crosses an array index segment the resulting type is
176
+ * tagged `| undefined` (the runtime can return undefined for
177
+ * out-of-bounds reads).
178
+ *
179
+ * Used by `form.values.<path>` reads, `form.toRef(path)`, and
180
+ * `register(path).innerRef` so the compile-time type honours the
181
+ * runtime possibility of a missing array position.
182
+ */
183
+ type NestedReadType<RootValue, FlattenedPath extends string, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, true> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedReadType<_RootValue[NumericKey], Rest, true> : never : never : Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] | undefined : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] | undefined : never : never : FlattenedPath extends keyof _RootValue ? _Tainted extends true ? _RootValue[FlattenedPath] | undefined : _RootValue[FlattenedPath] : never;
184
+ /**
185
+ * Filter FlatPath<Form> down to the subset of paths whose resolved leaf
186
+ * is an array. Used by the typed field-array helpers (append / remove /
187
+ * swap / ...) so those helpers only accept paths that actually address
188
+ * an array — calling `append('email', ...)` on a `{ email: string }`
189
+ * is a compile error.
190
+ *
191
+ * `P extends string` re-triggers distribution over the `FlatPath<Form>`
192
+ * union so the conditional evaluates per member. Without it, the
193
+ * branch would reduce against the union as a whole and collapse to
194
+ * `never` whenever a single member failed the predicate.
195
+ */
196
+ type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? NestedType<Form, P> extends readonly unknown[] ? P : never : never;
197
+ /**
198
+ * Extract the element type of the array addressed by `Path`. Callers
199
+ * constrain `Path extends ArrayPath<Form>` so this is always well-defined.
200
+ */
201
+ type ArrayItem<Form, Path extends ArrayPath<Form>> = NestedType<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
202
+ /**
203
+ * Widens primitive-literal leaves to their primitive supertype to
204
+ * match the runtime "slim-primitive write contract."
205
+ *
206
+ * WriteShape<{ color: 'red' | 'green' }>
207
+ * // → { color: string }
208
+ * WriteShape<{ kind: 'on' }>
209
+ * // → { kind: string }
210
+ * WriteShape<{ count: 42 }>
211
+ * // → { count: number }
212
+ *
213
+ * The runtime gate accepts any value at a path whose primitive type
214
+ * matches the schema's slim primitive set at that path. Refinement-
215
+ * level constraints (enum membership, literal equality, format
216
+ * checks, length / range bounds, regex, custom predicates) are NOT
217
+ * enforced at write time — they surface via field-level validation.
218
+ * The type widening here mirrors that runtime behaviour, so
219
+ * `setValue('color', 'magenta')` and `defaultValues: { color: 'teal' }`
220
+ * are not TS errors despite being out-of-enum at the validation
221
+ * layer.
222
+ *
223
+ * Tuple positions preserve their literal types via the homomorphic
224
+ * mapped form (`{ [K in keyof T]: ... }` over a readonly tuple
225
+ * preserves the position labels), so `[string, number]` stays a
226
+ * 2-tuple of widened primitives instead of collapsing to
227
+ * `Array<string | number>`.
228
+ *
229
+ * Date / RegExp / Map / Set / function instances pass through
230
+ * unchanged — those aren't "primitive literals" and the runtime
231
+ * accepts them as their own slim kinds. Tuple-detection runs before
232
+ * the array-recursion branch so positionally-typed array literals
233
+ * survive intact.
234
+ *
235
+ * Read-side types (handleSubmit's `data` argument,
236
+ * validate*() result payloads) intentionally stay STRICT — those
237
+ * payloads have been parsed by the schema, so the widened shape
238
+ * doesn't apply.
239
+ */
240
+ type WriteShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends bigint ? bigint : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
241
+ -readonly [K in keyof T]: WriteShape<T[K]>;
242
+ } : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
243
+ -readonly [K in keyof T]: WriteShape<T[K]>;
244
+ } : Array<WriteShape<U>> : T extends object ? {
245
+ [K in keyof T]: WriteShape<T[K]>;
246
+ } : T;
247
+ /**
248
+ * Like `WriteShape<T>`, but additionally widens every primitive leaf
249
+ * (`string`, `number`, `boolean`, `bigint`) to admit `Unset` — the
250
+ * brand-typed sentinel consumers pass to indicate "this leaf starts
251
+ * displayed-empty" in `defaultValues`, `setValue`, and `reset`.
252
+ *
253
+ * Non-primitive leaves (`Date`, `RegExp`, `Map`, `Set`, functions)
254
+ * stay strict — `defaultValues: { joinedAt: unset }` against a
255
+ * `Date`-typed leaf is a type error.
256
+ *
257
+ * The recursion mirrors `WriteShape<T>` exactly so `defaultValues`
258
+ * stays compatible at every nested position; the only divergence is
259
+ * the leaf widening. Tuple positions, unbounded arrays, and nested
260
+ * records all flow through unchanged.
261
+ *
262
+ * Example:
263
+ *
264
+ * DefaultValuesShape<{ income: number; name: string; age: 21 }>
265
+ * // → { income: number | Unset; name: string | Unset; age: number | Unset }
266
+ *
267
+ * Used by `UseFormConfiguration.defaultValues`, `setValue`'s value
268
+ * parameter, and `reset`'s parameter (commit 7 widens all three).
269
+ */
270
+ type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string | Unset : T extends number ? number | Unset : T extends boolean ? boolean | Unset : T extends bigint ? bigint | Unset : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
271
+ -readonly [K in keyof T]: DefaultValuesShape<T[K]>;
272
+ } : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
273
+ -readonly [K in keyof T]: DefaultValuesShape<T[K]>;
274
+ } : Array<DefaultValuesShape<U>> : T extends object ? {
275
+ [K in keyof T]: DefaultValuesShape<T[K]>;
276
+ } : T;
277
+
278
+ /**
279
+ * Identifier for a form. A `FormKey` is the string passed via
280
+ * `useForm({ key })`, used to look up a form by name from a distant
281
+ * component, namespace persisted drafts, and label errors and
282
+ * DevTools entries. Anonymous `useForm` calls allocate one
283
+ * automatically; you only need to pick one when the form needs
284
+ * stable identity.
285
+ */
286
+ type FormKey = string;
287
+ /**
288
+ * One validation failure. `path` points at the offending field as a
289
+ * structured array — `['user', 'address', 0, 'line1']` for a nested
290
+ * field, `[]` for a form-level error. `formKey` identifies which
291
+ * form produced the error so a single error list can be routed to
292
+ * multiple forms.
293
+ *
294
+ * Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
295
+ * `onError` callback, and by `parseApiErrors` for server responses.
296
+ */
297
+ type ValidationError = {
298
+ /** Human-readable message describing the failure. */
299
+ message: string;
300
+ /** Structured path of the offending field. Empty array means a form-level error. */
301
+ path: (string | number)[];
302
+ /** Identifies which form produced this error. */
303
+ formKey: FormKey;
304
+ /**
305
+ * Stable machine identifier for the failure, scoped by prefix:
306
+ *
307
+ * - `atta:` — library-internal codes (see `AttaformErrorCode`).
308
+ * - adapter prefix (e.g. `zod:`) — forwarded from the underlying
309
+ * schema library's own issue code, when one exists.
310
+ * - consumer-defined — anything else (e.g. `api:duplicate-email`,
311
+ * `auth:expired-token`). Pick a prefix and stay consistent so
312
+ * error renderers and tests can branch on `code` instead of
313
+ * exact-message string matching.
314
+ */
315
+ code: string;
316
+ };
317
+ /** Settled validation result when the form (or subtree) parsed successfully. */
318
+ type ValidationResponseSuccess<TData> = {
319
+ /** The parsed value at the validated subtree (whole form when `validate()` was called without a path). */
320
+ data: TData;
321
+ errors: undefined;
322
+ success: true;
323
+ formKey: FormKey;
324
+ };
325
+ /** Settled validation result when no data could be produced (e.g. a top-level type mismatch). */
326
+ type ValidationResponseErrorWithoutData = {
327
+ data: undefined;
328
+ /** Non-empty list of failures. */
329
+ errors: ValidationError[];
330
+ success: false;
331
+ formKey: FormKey;
332
+ };
333
+ /** Settled validation result when the parser produced partial data alongside failures. */
334
+ type ValidationResponseErrorWithData<TData> = {
335
+ data: TData;
336
+ errors: ValidationError[];
337
+ success: false;
338
+ formKey: FormKey;
339
+ };
340
+ /**
341
+ * Settled validation result. Discriminate on `success`:
342
+ *
343
+ * ```ts
344
+ * if (result.success) {
345
+ * // result.data is the parsed value, errors is undefined
346
+ * } else {
347
+ * // result.errors is non-empty, data may or may not be set
348
+ * }
349
+ * ```
350
+ */
351
+ type ValidationResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData> | ValidationResponseErrorWithoutData;
352
+ /**
353
+ * Result of resolving the form's default values. Always returns at
354
+ * least the shape derived from the schema; `errors` carry any
355
+ * failures from validating those defaults against the schema.
356
+ */
357
+ type DefaultValuesResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData>;
358
+ /**
359
+ * Trimmed `ValidationResponse` that omits the `data` payload. Used by
360
+ * `validate()` / `validateAsync()` since consumers usually only need
361
+ * the success flag and error list at those entry points.
362
+ */
363
+ type ValidationResponseWithoutValue<Form> = Omit<ValidationResponse<Form>, 'data'>;
364
+ /**
365
+ * Sync-or-async return shape for `AbstractSchema.validateAtPath`. The
366
+ * adapter returns the response inline when the schema and the
367
+ * caller's options permit synchronous validation; otherwise a
368
+ * `Promise<T>`. Callers that don't care simply `await` (works for
369
+ * both); callers that DO care (the reshape pre-pass — flicker
370
+ * prevention) branch on `instanceof Promise`.
371
+ */
372
+ type MaybePromise<T> = T | Promise<T>;
373
+ /**
374
+ * Options accepted by `AbstractSchema.validateAtPath`. Currently a
375
+ * single field; kept as an object for forward-compat with future
376
+ * knobs (e.g. cancellation signals, abort tokens) without breaking
377
+ * the call signature.
378
+ *
379
+ * - `sync`: when `true`, the adapter SHOULD return the response
380
+ * inline if the schema permits synchronous validation. When the
381
+ * schema is structurally async (any verdict that resolves only via
382
+ * a Promise — async refinements, async transforms / pipes — in
383
+ * whichever library the adapter wraps), the adapter falls back to
384
+ * a `Promise<T>` — the flag is a preference, not a guarantee.
385
+ *
386
+ * When omitted or `false`, the adapter is free to use its async
387
+ * path (matches the historical Promise-returning contract; every
388
+ * non-reshape callsite uses this default).
389
+ */
390
+ type ValidateOptions = {
391
+ sync?: boolean;
392
+ };
393
+ type GetDefaultValuesConfig<Form> = {
394
+ useDefaultSchemaValues: boolean;
395
+ /**
396
+ * Whether to keep schema refinements when deriving slim defaults.
397
+ * `true` (default) — preserve refinements; `false` — strip them so
398
+ * placeholder data lands without immediate construction-time
399
+ * errors. Mirrors `useForm({ strict })`.
400
+ */
401
+ strict?: boolean;
402
+ constraints?: DeepPartial<WriteShape<Form>> | undefined;
403
+ };
404
+ /**
405
+ * The contract a schema adapter implements so the form runtime can
406
+ * read defaults, validate, and walk paths against any underlying
407
+ * schema library.
408
+ *
409
+ * Most consumers never touch this type directly — the typed entry
410
+ * points (e.g. `attaform/zod`, `attaform/zod-v3`)
411
+ * wire an adapter automatically. Implement this interface only when
412
+ * adding support for a new schema library (Valibot, ArkType, custom).
413
+ */
414
+ type AbstractSchema<Form, GetValueFormType> = {
415
+ /**
416
+ * Structural fingerprint of the schema. Same shape → same string;
417
+ * different shape → (best-effort) different string.
418
+ *
419
+ * The library uses this to detect schema mismatches at a shared
420
+ * form key: two `useForm({ key: 'x', schema })` calls are allowed
421
+ * to land on the same `FormStore` (the "shared store" semantic),
422
+ * but only when their schemas agree. If the second call's
423
+ * fingerprint differs from the first's, the library emits a
424
+ * dev-mode warning — the first call's schema stays canonical and
425
+ * the second call's schema is silently ignored.
426
+ *
427
+ * Guarantees adapter authors should provide:
428
+ * - **Determinism:** equal shapes at different memory addresses
429
+ * must produce the same fingerprint. Referential equality fails
430
+ * 99% of the time across files, so reference-identity is not a
431
+ * substitute.
432
+ * - **Key-order-insensitivity** for record-like shapes (object,
433
+ * struct) — two shapes with the same keys but different iteration
434
+ * order must match.
435
+ * - **Order-insensitivity for unbounded unions** — `a | b` and
436
+ * `b | a` must match (the set of members is what matters, not
437
+ * their source order).
438
+ *
439
+ * Compromises adapter authors may accept:
440
+ * - Function-valued metadata (refinements, transforms, lazy
441
+ * defaults) is not stably hashable. Represent it as an opaque
442
+ * sentinel; two schemas differing only in refinement logic will
443
+ * look identical. The warning is a footgun catcher, not a
444
+ * soundness guarantee.
445
+ */
446
+ fingerprint(): string;
447
+ getDefaultValues(config: GetDefaultValuesConfig<Form>): DefaultValuesResponse<Form>;
448
+ /**
449
+ * Return the schema-prescribed default value at the given path. The
450
+ * runtime uses this to fill structural gaps so every `setValue` write
451
+ * leaves the form satisfying the slim schema (objects/arrays/primitives
452
+ * without refinement-level constraints).
453
+ *
454
+ * Semantics:
455
+ * - **Object property path:** the property's schema default.
456
+ * - **Array element path:** the element default (paths past the
457
+ * array's current length still resolve — every position resolves
458
+ * to the same element type).
459
+ * - **Tuple position path:** the position-specific default. Out-of-
460
+ * range positions return `undefined`.
461
+ * - **Optional/Default/Nullable/Readonly/Catch/Pipe wrappers:** the
462
+ * inner default.
463
+ * - **Discriminated union:** the first variant's default (matches
464
+ * `validateAtPath`'s first-success semantic).
465
+ * - **Leaf:** the primitive default (`''`, `0`, `false`, etc., or the
466
+ * wrapper's `.default(x)` value when present).
467
+ * - **Path doesn't exist in schema:** `undefined`.
468
+ *
469
+ * Adapters may return `undefined` when the path can't be resolved;
470
+ * callers treat that as "don't fill" and fall back to existing data.
471
+ */
472
+ getDefaultAtPath(path: Path): unknown;
473
+ /**
474
+ * Distinguish a tuple (fixed-length, position-typed) from an
475
+ * unbounded array at `path`. The runtime calls this on every
476
+ * `mergeStructural` / `setAtPathWithSchemaFill` write that descends
477
+ * into an array branch — caching the answer at the schema level
478
+ * replaces the per-write 1M-index probe + sequential probe loop
479
+ * (up to 1024 schema lookups) the runtime previously used.
480
+ *
481
+ * Return values:
482
+ * - `number` → tuple of this structural length. The runtime pads
483
+ * the consumer to this length and recurses position-by-position.
484
+ * - `null` → unbounded array. The runtime uses the consumer's
485
+ * length and reuses one element default for every position.
486
+ * - `undefined` → the path doesn't resolve to an array OR the
487
+ * adapter can't determine the shape. The runtime falls back to
488
+ * the legacy probe loop in this case (defensive — every built-in
489
+ * adapter returns `number` or `null`).
490
+ *
491
+ * Wrappers (optional / nullable / default / readonly / catch /
492
+ * pipe / lazy) are peeled transparently before the type check, so
493
+ * `optional(z.tuple([...]))` reports its tuple length.
494
+ */
495
+ arrayShapeAtPath(path: Path): number | null | undefined;
496
+ /**
497
+ * Return every sub-schema that could resolve at the given structured
498
+ * path. Multiple results are only expected for discriminated / union
499
+ * branches where the adapter can't decide a single winner until the
500
+ * data lands. `path` is the canonical `Segment[]` — adapters walk it
501
+ * segment-by-segment so literal-dot keys (`['user.name']`) don't
502
+ * collide with the sibling-pair form (`['user', 'name']`).
503
+ */
504
+ getSchemasAtPath(path: Path): AbstractSchema<unknown, GetValueFormType>[];
505
+ /**
506
+ * Validate a subtree (when `path` is provided) or the whole form (when
507
+ * `path` is `undefined`). `path` is the canonical `Segment[]`, not a
508
+ * dotted string — two schemas with otherwise-colliding dotted forms
509
+ * (`['user.name']` vs `['user', 'name']`) stay distinct at the
510
+ * adapter boundary.
511
+ *
512
+ * Return type is `MaybePromise<ValidationResponse>`:
513
+ * - With `options.sync === true` AND a sync-capable schema, the
514
+ * adapter SHOULD return the response inline (`T`). This lets the
515
+ * runtime batch error writes with a coincident form-value
516
+ * mutation in a single Vue reactive flush — preventing the `{}`
517
+ * flicker observable during DU variant reshape.
518
+ * - With `options.sync === true` AND an async-only schema (any
519
+ * verdict that resolves only via a Promise), the adapter MUST
520
+ * fall back to `Promise<T>`. The flag is a preference, not a
521
+ * guarantee; sync isn't always achievable.
522
+ * - With `options.sync` omitted or `false`, the adapter SHOULD
523
+ * return `Promise<T>` (matches the historical contract — every
524
+ * non-reshape callsite uses this default and immediately
525
+ * `await`s the result).
526
+ *
527
+ * Callers that don't care simply `await` (works for both arms);
528
+ * callers that need to detect sync-vs-async branch on
529
+ * `instanceof Promise`. Adapters MUST NOT throw — errors are
530
+ * returned as a `success: false` response with a populated
531
+ * `errors` array.
532
+ */
533
+ validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<Form>>;
534
+ /**
535
+ * Sync sister to `getSchemasAtPath` / `validateAtPath`. Returns the
536
+ * set of primitive `typeof`-style kinds the path's leaf schema
537
+ * accepts at write time. Wrappers (optional / nullable / default /
538
+ * refinement / transform / pipe / readonly / catch / lazy) are
539
+ * peeled; refinement-level constraints (format checks like email /
540
+ * uuid, min/max length, enum membership, literal equality, regex)
541
+ * are IGNORED — they're a validation-time concern.
542
+ *
543
+ * Used by `setValueAtPath` to gate writes synchronously without
544
+ * round-tripping through async `validateAtPath`. The returned set
545
+ * unions across union branches and intersects across intersection
546
+ * sides.
547
+ *
548
+ * Conventions:
549
+ * - Empty set → no kind admitted. The runtime gate rejects every
550
+ * write to the path. Surfaces for `never`-typed schemas AND for
551
+ * paths that don't resolve in the schema (typo / unknown leaf).
552
+ * - Permissive set (every kind) → "unknown / unconstrained." The
553
+ * gate accepts any value. Surfaces for `any` / `unknown` / `void`
554
+ * and the lazy-peel-failure case where the adapter can't
555
+ * introspect the schema.
556
+ * - For string-valued enums: returns `{'string'}`. For numeric
557
+ * enums: `{'number'}`.
558
+ * - For literal types: returns `{primitiveKindOf(literalValue)}`.
559
+ * - For object / array containers: `{'object'}` / `{'array'}`. The
560
+ * runtime walker recurses into entries / elements at write time.
561
+ * - For nullable / optional wrappers: adds `'null'` / `'undefined'`
562
+ * to the inner's set.
563
+ */
564
+ getSlimPrimitiveTypesAtPath(path: Path): Set<SlimPrimitiveKind>;
565
+ /**
566
+ * Return `true` iff `path` resolves to a **leaf** in the schema — a
567
+ * path whose slim primitive set contains only primitive kinds (no
568
+ * `object`, `array`, `map`, `set`). The runtime proxies (`form.values`,
569
+ * `form.errors`, `form.fields`) query this at every step to decide
570
+ * between **descend into a sub-proxy** (container) and **terminate
571
+ * with a leaf value** (leaf).
572
+ *
573
+ * The leaf-aware branching is what kills the FIELD_STATE_KEYS
574
+ * shadowing problem: reserved leaf-prop names (`dirty`, `errors`,
575
+ * `isValid`, …) inject only at the FieldStateView terminal, not at
576
+ * every depth. A schema field literally named `dirty` at depth ≥ 2
577
+ * stays reachable as a sub-proxy or leaf in its own right.
578
+ *
579
+ * Semantics:
580
+ * - **Object / Array / Map / Set** at any wrapper layer → `false`
581
+ * (container; descend further).
582
+ * - **Primitive** (string/number/boolean/bigint/symbol/null/undefined/
583
+ * date/function) → `true`. `'date'` counts as a leaf (don't drill
584
+ * into `Date`). `'function'` is a leaf for the same reason — opaque
585
+ * value.
586
+ * - **Optional / Nullable / Default / Catch** wrappers transparent —
587
+ * adds `'null'` / `'undefined'` to the inner kind set without
588
+ * changing the leaf classification.
589
+ * - **Discriminated union root** → `false` (variants are objects;
590
+ * the kind set contains `'object'`).
591
+ * - **DU discriminator key** → `true` (the literal type resolves to
592
+ * `{'string'}` / `{'number'}`).
593
+ * - **DU variant-only key** → `true` if it resolves to a primitive
594
+ * in any variant; schema-static (does NOT query live storage to
595
+ * decide which variant is active).
596
+ * - **Empty path (root)** → `false` (root is the form-as-object).
597
+ * - **Path doesn't exist in schema** → `false`. The proxy descends
598
+ * permissively; reads of leaf props at the unknown path return
599
+ * `undefined` from the underlying store. Treating unknown paths
600
+ * as containers preserves the schema's authority and avoids
601
+ * re-introducing shadowing on typos.
602
+ *
603
+ * Adapters MAY cache results per-path — `isLeafAtPath` will be
604
+ * called on every proxy `get` trap hit. The reference implementation
605
+ * memoises a `Map<PathKey, boolean>` keyed by `canonicalizePath(path).key`,
606
+ * lifetime tied to the adapter (one per `useForm()` call).
607
+ */
608
+ isLeafAtPath(path: Path): boolean;
609
+ /**
610
+ * Return `true` if the leaf at `path` is required — i.e. the schema
611
+ * does NOT admit "empty" via `.optional()`, `.nullable()`,
612
+ * `.default(N)`, or `.catch(N)` at the leaf or any wrapper.
613
+ *
614
+ * Used by the submit / validate path to surface a "No value supplied" error
615
+ * when a field is in the form's `blankPaths` set (the user
616
+ * cleared it or never answered) AND the schema treats the field as
617
+ * required. Without this, a strict numeric leaf would silently
618
+ * accept the slim default (`0`) for an unanswered field — the
619
+ * "public-housing" footgun where `$0 income` passes validation.
620
+ *
621
+ * Semantics:
622
+ * - **Optional / Nullable / Default / Catch** at any wrapper layer
623
+ * (root or nested) → `false`. The schema author opted into
624
+ * accepting empty.
625
+ * - **Readonly / Pipe / Lazy** wrappers are transparent — peel and
626
+ * re-check the inner schema.
627
+ * - **Union / Discriminated union** → `false` if ANY branch admits
628
+ * empty (the union accepts what the most permissive branch
629
+ * accepts). This matches the parse-time "first success wins"
630
+ * semantic of `validateAtPath`.
631
+ * - **Intersection** → `true` if EITHER side requires the path
632
+ * (intersection requires both sides to accept; if one rejects
633
+ * empty, the intersection rejects empty).
634
+ * - **Path doesn't exist in schema** → `false` (can't enforce
635
+ * what we don't know about).
636
+ * - **Empty path (root)** → `true` (the root form is always
637
+ * required as an object).
638
+ *
639
+ * Refinement-level constraints (length / format / custom predicates)
640
+ * are NOT consulted here — those run at parse time inside
641
+ * `validateAtPath` and surface as schema errors regardless.
642
+ * `isRequiredAtPath` only answers the "is this leaf at all
643
+ * required?" question; the refinements layer on top.
644
+ */
645
+ isRequiredAtPath(path: Path): boolean;
646
+ /**
647
+ * If the schema at `path` is (or wraps) a discriminated union,
648
+ * return its discriminator key plus a `getVariantDefault(value)`
649
+ * lookup — otherwise `undefined`. Wrappers (optional, default,
650
+ * nullable, readonly, pipe, lazy, catch) are peeled transparently.
651
+ *
652
+ * The runtime uses this for two related reshapes that share the
653
+ * same lookup:
654
+ *
655
+ * 1. **Discriminator-key write** — the runtime calls this with
656
+ * the parent path. If the returned `discriminatorKey` matches
657
+ * the path's last segment, the write changes which variant is
658
+ * active; the parent storage is replaced with the matching
659
+ * variant's slim default so the OLD variant's keys (e.g.
660
+ * `address` after switching to `sms`) don't leak.
661
+ *
662
+ * 2. **Whole-union write** — the runtime calls this with the
663
+ * path itself. If the returned info exists and the consumer's
664
+ * value carries the discriminator key, the merge uses the
665
+ * matching variant's default instead of the first-variant
666
+ * fallback that `getDefaultAtPath` returns for unions.
667
+ *
668
+ * Adapters that don't model discriminated unions can return
669
+ * `undefined` unconditionally; the runtime reshape is a no-op
670
+ * without this hook.
671
+ */
672
+ getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
673
+ /**
674
+ * Return `true` if `validateAtPath` MAY have to run asynchronously
675
+ * to surface every error this schema can produce. The runtime uses
676
+ * this at construction to decide whether to schedule a one-shot
677
+ * full-form async validation: when `false` (or omitted), the
678
+ * construction-time sync seed is the authoritative result and no
679
+ * extra microtask is spent; when `true`, an async pass is queued
680
+ * so any async-only verdicts (refinements / transforms / pipes
681
+ * that resolve only via a Promise) surface without waiting for a
682
+ * user mutation.
683
+ *
684
+ * Optional. The runtime treats a missing implementation as
685
+ * `() => false`, so adapters that don't model async work — or
686
+ * don't yet support detection — can omit it; async-only errors
687
+ * then fall back to firing on first user mutation, matching the
688
+ * pre-detection behavior. Detection is best-effort.
689
+ */
690
+ needsAsyncValidation?(): boolean;
691
+ };
692
+ /**
693
+ * Adapter-returned info for a discriminated union — its discriminator
694
+ * key plus a function that maps a discriminator literal to the slim
695
+ * default of the matching variant. Returned by
696
+ * `AbstractSchema.getUnionDiscriminatorAtPath`.
697
+ */
698
+ type UnionDiscriminatorContext = {
699
+ /**
700
+ * The union's discriminator key — the property name whose literal
701
+ * value selects the variant (e.g. `'channel'` for a union split on
702
+ * `{ channel: 'sms' | 'email' }`).
703
+ */
704
+ readonly discriminatorKey: string;
705
+ /**
706
+ * Slim default for the variant whose discriminator literal equals
707
+ * `value`. Returns `undefined` if no variant matches — the runtime
708
+ * skips the reshape and falls back to a plain write.
709
+ */
710
+ getVariantDefault(value: unknown): unknown;
711
+ };
712
+ /**
713
+ * The set of primitive "kinds" the slim-primitive write contract
714
+ * recognises. Drawn from `typeof` plus a few well-known reference
715
+ * shapes (`Date`, `Array`, `Map`, `Set`, plain `object`, `null`).
716
+ *
717
+ * The runtime gate's `slimKindOf(value)` returns one of these for a
718
+ * value; the adapter's `getSlimPrimitiveTypesAtPath(path)` returns
719
+ * the set of kinds the path's leaf schema accepts. A write is gated
720
+ * by `accepted.has(slimKindOf(value))`.
721
+ */
722
+ type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set';
723
+ /**
724
+ * The "no result yet" status returned by the reactive `validate()` ref
725
+ * while a validation run is in flight.
726
+ *
727
+ * Narrow against `pending` to access the settled fields:
728
+ *
729
+ * ```ts
730
+ * const status = form.validate()
731
+ * watchEffect(() => {
732
+ * if (status.value.pending) return
733
+ * // status.value.success / status.value.errors are now safe to read
734
+ * })
735
+ * ```
736
+ */
737
+ type PendingValidationStatus = {
738
+ readonly pending: true;
739
+ readonly errors: undefined;
740
+ readonly success: false;
741
+ readonly formKey: FormKey;
742
+ };
743
+ /** Settled status of a reactive `validate()` call. Mirrors the latest result. */
744
+ type SettledValidationStatus<Form> = {
745
+ readonly pending: false;
746
+ } & ValidationResponseWithoutValue<Form>;
747
+ /**
748
+ * The value type of the ref returned by `validate()`. Discriminate on
749
+ * `pending` to switch between in-flight and settled states.
750
+ */
751
+ type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
752
+ /**
753
+ * What to do when a submit attempt fails validation. The library can
754
+ * focus and/or scroll the first errored field into view without
755
+ * wiring an `onError` callback yourself. Off by default.
756
+ *
757
+ * - `'none'` (default): no automatic UI nudge.
758
+ * - `'focus-first-error'`: focus the first errored field's first
759
+ * visible element (with `preventScroll: true` so it doesn't fight
760
+ * any `'scroll-to-first-error'` choice you make).
761
+ * - `'scroll-to-first-error'`: scroll that element into view.
762
+ * - `'both'`: scroll first, then focus.
763
+ *
764
+ * If no errored field has a currently mounted, visible element, the
765
+ * policy silently no-ops.
766
+ */
767
+ type OnInvalidSubmitPolicy = 'none' | 'focus-first-error' | 'scroll-to-first-error' | 'both';
768
+ /**
769
+ * When per-field VALIDATION runs. Only validation timing varies per
770
+ * mode; storage commit timing is the directive's concern (the
771
+ * default `<input v-register>` commits per keystroke; `.lazy` defers
772
+ * to blur).
773
+ *
774
+ * - `'change'` (default): every committed write schedules a
775
+ * validation for the affected path. With `debounceMs: 0` (also the
776
+ * default) the run is synchronous in the write handler;
777
+ * positive `debounceMs` coalesces rapid bursts.
778
+ * - `'blur'`: validate immediately when the user tabs away from a
779
+ * registered field. No debounce — `debounceMs` is rejected by the
780
+ * type.
781
+ * - `'submit'`: no live validation. `handleSubmit` and explicit
782
+ * `validate()` / `validateAsync()` calls are the only validation
783
+ * surfaces. `debounceMs` is rejected by the type.
784
+ */
785
+ type ValidateOn = 'change' | 'blur' | 'submit';
786
+ /**
787
+ * Validation timing config — `validateOn` is the trigger, `debounceMs`
788
+ * the wait (after the last committed write) before the next
789
+ * validation run fires. `debounceMs` ONLY governs validation;
790
+ * `setValueWithInternalPath` commits to `form.values` immediately
791
+ * regardless of debounce. (How OFTEN the directive forwards writes
792
+ * to storage is the directive's concern — default `<input
793
+ * v-register>` commits per keystroke; `<input v-register.lazy>`
794
+ * defers to the blur `change` event.)
795
+ *
796
+ * `debounceMs` is only meaningful with `validateOn: 'change'` (the
797
+ * default); `'blur'` and `'submit'` ignore the wait entirely (blur
798
+ * fires validation immediately on focus-out; submit is its own
799
+ * trigger). The discriminated union below makes pairing `debounceMs`
800
+ * with `'blur'` / `'submit'` a TS error instead of a silent runtime
801
+ * drop.
802
+ *
803
+ * Pass `debounceMs: 0` (the default) to disable validation
804
+ * debouncing — every committed write triggers a validation pass with
805
+ * no `setTimeout` indirection. Schema work itself still rides
806
+ * `Promise.resolve().then(validateAtPath)` — async but microtask, so
807
+ * errors land on the next tick. Set `debounceMs` to a positive
808
+ * number to coalesce rapid bursts (useful for slow async adapters or
809
+ * for smoothing inline feedback under heavy typing).
810
+ */
811
+ type ValidateOnConfig = {
812
+ /** Validation trigger. Default `'change'`. */
813
+ validateOn?: 'change';
814
+ /**
815
+ * Milliseconds to wait after the last committed write before
816
+ * running validation. Default `0` (validation runs synchronously
817
+ * after the write; no `setTimeout`). Set to a positive number
818
+ * to coalesce rapid bursts into a single validation pass.
819
+ *
820
+ * Note: this is purely the validation debounce. Storage commits
821
+ * happen at the directive's listener (per-keystroke for
822
+ * `<input v-register>`, per-blur for `<input v-register.lazy>`)
823
+ * — `debounceMs` doesn't change either.
824
+ */
825
+ debounceMs?: number;
826
+ } | {
827
+ /** Validation trigger. */
828
+ validateOn: 'blur' | 'submit';
829
+ /** `debounceMs` is not allowed with `'blur'` or `'submit'`. */
830
+ debounceMs?: never;
831
+ };
832
+ /**
833
+ * Built-in storage backends:
834
+ *
835
+ * - `'local'` — browser `localStorage` (persists across tabs and reloads).
836
+ * - `'session'` — browser `sessionStorage` (cleared when the tab closes).
837
+ * - `'indexeddb'` — IndexedDB via a zero-dependency wrapper (handles
838
+ * structured-cloneable data; suitable for larger drafts).
839
+ *
840
+ * For anything else (encrypted storage, a native bridge, a cookie
841
+ * store) pass a custom `FormStorage` object instead.
842
+ */
843
+ type FormStorageKind = 'local' | 'session' | 'indexeddb';
844
+ /**
845
+ * Custom persistence backend. Implement this when none of the built-in
846
+ * `'local'` / `'session'` / `'indexeddb'` backends fit (e.g. encrypted
847
+ * storage, a cross-window broadcast layer, or a native mobile bridge).
848
+ *
849
+ * All methods are async. Pass values through unchanged — `getItem`
850
+ * should return whatever `setItem` was given, including non-string
851
+ * values. The library handles serialization for the built-in
852
+ * `'local'` / `'session'` backends; custom adapters can store the
853
+ * value directly if their backing store accepts structured data.
854
+ *
855
+ * `listKeys(prefix)` returns every key starting with `prefix`. The
856
+ * library uses it on mount to clean up entries left over from older
857
+ * schema versions (each persisted entry carries a schema fingerprint
858
+ * suffix; mismatched entries are dropped automatically).
859
+ */
860
+ type FormStorage = {
861
+ /** Fetch the value previously stored under `key`. Resolve to `null`/`undefined` for misses. */
862
+ getItem(key: string): Promise<unknown>;
863
+ /** Persist `value` under `key`. */
864
+ setItem(key: string, value: unknown): Promise<void>;
865
+ /** Remove the entry at `key`. No-op if not present. */
866
+ removeItem(key: string): Promise<void>;
867
+ /** Return every key in this backend whose name starts with `prefix`. */
868
+ listKeys(prefix: string): Promise<string[]>;
869
+ };
870
+ /**
871
+ * What to include when persisting:
872
+ *
873
+ * - `'form'` (default) — only the form value. Errors get repopulated
874
+ * by validation on reload anyway.
875
+ * - `'form+errors'` — also persist the current error list. Useful when
876
+ * the error context is expensive to recompute (e.g. cross-field
877
+ * refinements that depend on server data).
878
+ */
879
+ type PersistIncludeMode = 'form' | 'form+errors';
880
+ /**
881
+ * Per-write metadata. Used internally to flag which writes should
882
+ * reach the persistence layer (e.g. only writes from elements opted
883
+ * into persistence via `register(path, { persist: true })`).
884
+ *
885
+ * Custom directive integrations may set `persist: true` to forward
886
+ * a write to the configured storage adapter; otherwise leave unset.
887
+ */
888
+ type WriteMeta = {
889
+ /** When `true`, this write is forwarded to the configured persistence backend. */
890
+ readonly persist?: boolean;
891
+ /**
892
+ * When `true`, the path being written is added to the FormStore's
893
+ * `blankPaths` set — meaning storage holds a real, schema-
894
+ * conformant value (the slim default) but the UI should display the
895
+ * field as empty. The next write to that path WITHOUT this flag
896
+ * implicitly removes the path from the set (the user typed something
897
+ * real). Internal — set by `markBlank()` on the register
898
+ * binding and by the `unset` translation in `setValue` / `reset` /
899
+ * `useAbstractForm` construction. Don't set from consumer code.
900
+ */
901
+ readonly blank?: boolean;
902
+ /**
903
+ * When `true`, the discriminator-aware variant reshape inside
904
+ * `setValueAtPath` is skipped for this write. Internal — set by
905
+ * the reshape itself when re-entering with the new variant default
906
+ * so the literal discriminator inside the default doesn't trigger
907
+ * an infinite loop. Don't set from consumer code.
908
+ */
909
+ readonly skipDiscriminatorReshape?: boolean;
910
+ };
911
+ /**
912
+ * Undo/redo configuration passed via `useForm({ history })`.
913
+ *
914
+ * - `true` — enable with the default snapshot cap (`max: 50`).
915
+ * - `{ max }` — enable and tune the bounded snapshot stack size.
916
+ *
917
+ * When enabled, every mutation pushes a snapshot; `undo()` /
918
+ * `redo()` walk the stacks. `reset()` clears history.
919
+ */
920
+ type HistoryConfig = true | {
921
+ max?: number;
922
+ };
923
+ /**
924
+ * Full options bag for `useForm({ persist })`. Use this when you need
925
+ * to override defaults beyond picking the backend.
926
+ *
927
+ * For backend-only setup, the shorthand forms are equivalent:
928
+ *
929
+ * ```ts
930
+ * useForm({ persist: 'local' })
931
+ * // same as
932
+ * useForm({ persist: { storage: 'local' } })
933
+ * ```
934
+ */
935
+ type PersistConfigOptions = {
936
+ /**
937
+ * Where to persist. Pass `'local'` / `'session'` / `'indexeddb'` to
938
+ * use a built-in backend, or a custom `FormStorage` object for
939
+ * anything else. The built-in backends are loaded on demand, so
940
+ * picking `'local'` doesn't pull in IndexedDB code.
941
+ */
942
+ storage: FormStorageKind | FormStorage;
943
+ /**
944
+ * Storage key namespace. Defaults to `attaform:${formKey}`.
945
+ * Override when you need a custom prefix (e.g. multi-tenant apps
946
+ * where the same form key may exist per-tenant).
947
+ */
948
+ key?: string;
949
+ /**
950
+ * How long to wait after the last mutation before writing. Default
951
+ * `300` ms.
952
+ *
953
+ * Pass `0` to disable debouncing — every form change writes to the
954
+ * storage adapter immediately, no `setTimeout` indirection. Almost
955
+ * never the right choice for production (the storage adapter sees
956
+ * every keystroke), but useful for tests or for diagnosing perceived
957
+ * lag.
958
+ */
959
+ debounceMs?: number;
960
+ /**
961
+ * What to persist. `'form'` (default) is sufficient for most cases —
962
+ * fresh validation on reload repopulates errors. Pick `'form+errors'`
963
+ * when the error state is expensive to recompute (e.g. server-side
964
+ * cross-field validation).
965
+ */
966
+ include?: PersistIncludeMode;
967
+ /**
968
+ * When `true` (default), the persisted entry is wiped after
969
+ * `handleSubmit`'s submit callback resolves successfully. Set to
970
+ * `false` if you need the draft to survive across submissions.
971
+ */
972
+ clearOnSubmitSuccess?: boolean;
973
+ };
974
+ /**
975
+ * Persistence configuration for `useForm({ persist })`. Off by default —
976
+ * with no config, the form does no reads, no writes, and pulls in no
977
+ * storage code.
978
+ *
979
+ * Three input forms; pick the one that reads best at the call site:
980
+ *
981
+ * ```ts
982
+ * // shorthand: built-in backend
983
+ * useForm({ persist: 'local' })
984
+ *
985
+ * // shorthand: custom adapter
986
+ * useForm({ persist: encryptedStorage })
987
+ *
988
+ * // full options bag
989
+ * useForm({ persist: { storage: 'local', debounceMs: 500 } })
990
+ * ```
991
+ *
992
+ * Per-field opt-in: setting `persist` is necessary but not sufficient.
993
+ * Each field that should actually persist also needs
994
+ * `register('foo', { persist: true })` — sensitive fields must opt in
995
+ * explicitly so they don't accidentally land in client-side storage.
996
+ */
997
+ type PersistConfig = FormStorageKind | FormStorage | PersistConfigOptions;
998
+ /**
999
+ * Configuration object passed to `useForm`. All fields except `schema`
1000
+ * are optional.
1001
+ *
1002
+ * ```ts
1003
+ * const form = useForm({
1004
+ * schema: signupSchema,
1005
+ * defaultValues: { email: '' },
1006
+ * validateOn: 'change',
1007
+ * debounceMs: 200,
1008
+ * persist: 'local',
1009
+ * })
1010
+ * ```
1011
+ */
1012
+ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DeepPartial<DefaultValuesShape<Form>>> = {
1013
+ /**
1014
+ * The schema describing the form's shape and validation rules.
1015
+ * Typed entry points like `attaform/zod` accept the
1016
+ * underlying library's schema directly and wrap an adapter; the
1017
+ * abstract entry point accepts any object implementing
1018
+ * `AbstractSchema`.
1019
+ *
1020
+ * For schemas that depend on the form's identity, pass a factory
1021
+ * `(key) => schema` instead — the library calls it once per form.
1022
+ */
1023
+ schema: Schema | ((key: FormKey) => Schema);
1024
+ /**
1025
+ * Optional identifier for this form. Omit for one-off forms; the
1026
+ * library allocates a unique key automatically (SSR-safe, stable
1027
+ * across server→client hydration).
1028
+ *
1029
+ * Pass a string key when the form needs identity:
1030
+ * - to look it up from a distant component via `injectForm(key)`;
1031
+ * - to share state across components (multiple `useForm({ key })`
1032
+ * calls with the same key resolve to the same form);
1033
+ * - to give DevTools and validation errors a recognisable label;
1034
+ * - to namespace persisted drafts.
1035
+ *
1036
+ * Keys starting with `__atta:` are reserved for internal use and
1037
+ * throw `ReservedFormKeyError` if passed.
1038
+ */
1039
+ key?: FormKey;
1040
+ /**
1041
+ * Initial values applied over the schema's defaults. Each field
1042
+ * falls back to the schema default (or the primitive default for
1043
+ * the slot's type) when not provided here.
1044
+ *
1045
+ * Values must satisfy the slim primitive type at each path
1046
+ * (string / number / boolean / Date / etc.) but do NOT have to
1047
+ * satisfy refinement-level constraints (format checks, enum
1048
+ * membership, length / range bounds). Refinement-invalid defaults
1049
+ * pass through and surface as field errors — this lets you
1050
+ * rehydrate stale saved data without losing the user's input.
1051
+ */
1052
+ defaultValues?: DefaultValues;
1053
+ /**
1054
+ * Whether to validate default values at construction. Default
1055
+ * `true`.
1056
+ *
1057
+ * - `true` (default): the schema is run against the derived
1058
+ * defaults immediately; any failures populate `form.errors` from
1059
+ * the first frame. The UI decides when to *show* errors — gate
1060
+ * on `form.fields.<path>.touched`, `form.meta.submitCount`, etc.
1061
+ * - `false`: refinements are stripped during defaults derivation
1062
+ * and construction-time validation is skipped. Useful for
1063
+ * multi-step wizards, field arrays seeded with placeholder
1064
+ * rows, or any form intentionally mounting with incomplete data.
1065
+ *
1066
+ * Runtime validation (per-field on edit, full-form on submit) is
1067
+ * identical regardless of this flag.
1068
+ */
1069
+ strict?: boolean;
1070
+ /**
1071
+ * Automatic UI nudge on submit-validation failure. Fires after
1072
+ * errors are populated and before your `onError` callback runs.
1073
+ * Default `'none'`.
1074
+ *
1075
+ * - `'focus-first-error'`: focus the first errored field's first
1076
+ * visible element (without scrolling).
1077
+ * - `'scroll-to-first-error'`: scroll it into view.
1078
+ * - `'both'`: scroll, then focus.
1079
+ *
1080
+ * If no errored field has a currently-mounted, visible element,
1081
+ * the policy silently no-ops.
1082
+ */
1083
+ onInvalidSubmit?: OnInvalidSubmitPolicy;
1084
+ /**
1085
+ * When per-field VALIDATION runs (the directive's listener controls
1086
+ * how often storage commits — per keystroke by default, per blur
1087
+ * with `.lazy`). Default `'change'`. See `ValidateOn` for mode
1088
+ * semantics.
1089
+ *
1090
+ * The strict public `useForm` signature wraps this type in an
1091
+ * intersection with `ValidateOnConfig`, which enforces that
1092
+ * `debounceMs` is only allowed under `'change'`. Internal callers
1093
+ * (adapters, hydration paths) work with the loose form below.
1094
+ */
1095
+ validateOn?: ValidateOn;
1096
+ /**
1097
+ * Milliseconds to wait after the last committed write before
1098
+ * running validation. Default `0` (validation fires synchronously
1099
+ * after the write; no `setTimeout`). Set to a positive number to
1100
+ * coalesce rapid bursts. Ignored under `validateOn: 'blur'` and
1101
+ * `'submit'`.
1102
+ *
1103
+ * This is purely a VALIDATION debounce — storage commits are the
1104
+ * directive's concern (per keystroke for `<input v-register>`,
1105
+ * per blur for `<input v-register.lazy>`).
1106
+ */
1107
+ debounceMs?: number;
1108
+ /**
1109
+ * Opt-in persistence of the form's draft state. Off by default —
1110
+ * with no config, no reads, no writes, no storage code is loaded.
1111
+ *
1112
+ * Three input forms; pick the one that reads best:
1113
+ *
1114
+ * ```ts
1115
+ * useForm({ persist: 'local' }) // built-in backend
1116
+ * useForm({ persist: encryptedStorage }) // custom backend
1117
+ * useForm({ persist: { storage: 'local', debounceMs: 500 } })
1118
+ * ```
1119
+ *
1120
+ * Per-field opt-in is required: every field that should actually
1121
+ * persist needs `register(path, { persist: true })`. Without any
1122
+ * opt-ins, the form mounts but never writes to storage — and a
1123
+ * dev-mode warning surfaces the misconfiguration. This guard
1124
+ * prevents sensitive fields from accidentally leaking to
1125
+ * client-side storage.
1126
+ *
1127
+ * Switching backends across reloads (e.g. `'local'` → `'session'`)
1128
+ * automatically clears the previous backend's entry so old drafts
1129
+ * don't orphan.
1130
+ */
1131
+ persist?: PersistConfig;
1132
+ /**
1133
+ * Opt-in undo/redo. Off by default. `true` enables with a 50-snapshot
1134
+ * cap; `{ max: N }` tunes the cap.
1135
+ *
1136
+ * Every mutation pushes a snapshot. `undo()` pops one; `redo()`
1137
+ * replays it. `reset()` clears history. Reactive flags
1138
+ * `state.canUndo` / `state.canRedo` / `state.historySize` reflect
1139
+ * the current stack.
1140
+ */
1141
+ history?: HistoryConfig;
1142
+ /**
1143
+ * Whether to remember the typed state of each discriminated-union
1144
+ * variant across switches. Default `true`.
1145
+ *
1146
+ * When `true`, switching `notify.channel` from `email` (with
1147
+ * `address: 'foo@bar.com'`) to `sms` and back lands on
1148
+ * `address: 'foo@bar.com'` again — the runtime snapshots the
1149
+ * outgoing variant's subtree on switch-out and restores the
1150
+ * incoming variant's prior subtree on switch-in. Each
1151
+ * discriminated union at every nesting depth is independently
1152
+ * memorized.
1153
+ *
1154
+ * Set to `false` to drop the outgoing variant's typed state on
1155
+ * every switch (the data is gone). The new variant initializes
1156
+ * from its slim default.
1157
+ *
1158
+ * Memory is in-memory only and does not survive reload. Persisted
1159
+ * state restores values into form storage on hydration, but
1160
+ * variant memory starts empty — the first discriminator switch
1161
+ * after reload loses any persisted typing in the outgoing variant.
1162
+ * Consumers needing cross-session continuity must persist beyond
1163
+ * the variant boundary themselves.
1164
+ *
1165
+ * `reset()` clears variant memory. `resetField(path)` clears any
1166
+ * memory entry whose union path equals or sits under `path`.
1167
+ */
1168
+ rememberVariants?: boolean;
1169
+ /**
1170
+ * Schema-driven coercion of user-typed DOM values at the v-register
1171
+ * directive layer. Per-form override of the plugin-level
1172
+ * `AttaformDefaults.coerce`.
1173
+ *
1174
+ * - `true` / `undefined` — runs the built-in `defaultCoercionRules`.
1175
+ * - `false` — disables coercion; the slim gate rejects mismatches.
1176
+ * - `CoercionRegistry` — a custom array of entries (REPLACES, not
1177
+ * merges, the plugin defaults). Spread `defaultCoercionRules` to
1178
+ * extend.
1179
+ *
1180
+ * Coercion applies ONLY to user-typed DOM values. Programmatic
1181
+ * writes (`form.setValue`, `setValueWithInternalPath`) are NEVER
1182
+ * coerced.
1183
+ */
1184
+ coerce?: boolean | CoercionRegistry;
1185
+ };
1186
+ /**
1187
+ * App-level defaults applied to every `useForm` call. Set these once
1188
+ * per app via `createAttaform({ defaults })` (bare Vue) or
1189
+ * `attaform.defaults` (Nuxt module).
1190
+ *
1191
+ * Resolution order (per-form wins):
1192
+ *
1193
+ * useForm({ ... }) > createAttaform({ defaults }) > library default
1194
+ *
1195
+ * `validateOn` and `debounceMs` resolve per-field — set the debounce
1196
+ * globally while still overriding the trigger per form:
1197
+ *
1198
+ * ```ts
1199
+ * createAttaform({
1200
+ * defaults: { debounceMs: 100 },
1201
+ * })
1202
+ * // later
1203
+ * useForm({ schema, validateOn: 'blur' })
1204
+ * // → { validateOn: 'blur', debounceMs: <ignored under blur> }
1205
+ * ```
1206
+ *
1207
+ * Note: per the discriminated union, `debounceMs` only takes effect
1208
+ * when `validateOn` is `'change'` (or omitted). Setting it as an
1209
+ * app-level default is fine — forms that switch to `'blur'` /
1210
+ * `'submit'` simply ignore the inherited `debounceMs`.
1211
+ *
1212
+ * `schema`, `key`, `defaultValues`, and `persist` are not configurable
1213
+ * here — they belong on the per-form call.
1214
+ */
1215
+ type AttaformDefaults = {
1216
+ /** Default for `useForm({ strict })`. Default `true`. */
1217
+ strict?: boolean;
1218
+ /** Default for `useForm({ onInvalidSubmit })`. */
1219
+ onInvalidSubmit?: OnInvalidSubmitPolicy;
1220
+ /** Default for `useForm({ validateOn })` — when validation runs. */
1221
+ validateOn?: ValidateOn;
1222
+ /**
1223
+ * Default for `useForm({ debounceMs })` — ms to wait after the last
1224
+ * input event before re-running validation. Only meaningful when
1225
+ * `validateOn` resolves to `'change'`. Default `0` (synchronous).
1226
+ */
1227
+ debounceMs?: number;
1228
+ /** Default for `useForm({ history })`. */
1229
+ history?: HistoryConfig;
1230
+ /** Default for `useForm({ rememberVariants })`. */
1231
+ rememberVariants?: boolean;
1232
+ /**
1233
+ * Default for `useForm({ coerce })`. Schema-driven coercion of
1234
+ * user-typed DOM values at the v-register directive layer.
1235
+ *
1236
+ * - `true` (default) — runs the built-in `defaultCoercionRules`
1237
+ * (`string→number`, `string→boolean`).
1238
+ * - `false` — disables coercion globally; the slim-primitive gate
1239
+ * rejects type mismatches with its existing dev-warn instead.
1240
+ * - `CoercionRegistry` — a custom array of `CoercionEntry` records.
1241
+ * Spread `defaultCoercionRules` to extend rather than replace:
1242
+ * `[...defaultCoercionRules, defineCoercion({ ... })]`.
1243
+ *
1244
+ * Coercion applies ONLY to user-typed DOM values flowing through
1245
+ * the directive's assigner. Programmatic writes (`form.setValue`,
1246
+ * `setValueWithInternalPath`) are NEVER coerced — they're
1247
+ * authoritative writes whose strict typing is on the caller.
1248
+ */
1249
+ coerce?: boolean | CoercionRegistry;
1250
+ };
1251
+ /**
1252
+ * Callback invoked by `handleSubmit` after the form parses successfully.
1253
+ * Receives the strictly-typed parsed value — refinements have run, so
1254
+ * enum / literal / format constraints are honoured.
1255
+ */
1256
+ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
1257
+ /**
1258
+ * Callback invoked by `handleSubmit` when validation fails. Receives
1259
+ * the full list of errors. Bind this when you want to react to
1260
+ * submit failures explicitly (alongside or instead of the
1261
+ * automatic `onInvalidSubmit` UI nudge).
1262
+ */
1263
+ type OnError = (error: ValidationError[]) => void | Promise<void>;
1264
+ /**
1265
+ * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
1266
+ * it to a `<form>`:
1267
+ *
1268
+ * ```vue
1269
+ * <form @submit.prevent="onSubmit">…</form>
1270
+ * ```
1271
+ *
1272
+ * It optionally accepts the originating `Event` so it can sit on
1273
+ * `@submit` directly (without `.prevent` if you want to call
1274
+ * `event.preventDefault()` yourself).
1275
+ */
1276
+ type SubmitHandler = (event?: Event) => Promise<void>;
1277
+ /**
1278
+ * Type of `form.handleSubmit`. Pass an `onSubmit` callback for the
1279
+ * happy path and (optionally) an `onError` callback that receives
1280
+ * the validation errors when parsing fails.
1281
+ *
1282
+ * ```ts
1283
+ * const onSubmit = form.handleSubmit(
1284
+ * (data) => api.signup(data),
1285
+ * (errors) => console.log(errors),
1286
+ * )
1287
+ * ```
1288
+ */
1289
+ type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
1290
+ /**
1291
+ * Per-leaf metadata tracked alongside a field's value. Read from
1292
+ * `FieldState.meta` when type-narrowing through that surface.
1293
+ *
1294
+ * - `updatedAt` — ISO timestamp of the most recent write at this path,
1295
+ * or `null` if the field has never been written.
1296
+ * - `rawValue` — the value as it arrived (before any transform);
1297
+ * useful for distinguishing parse-coerced reads from raw user input.
1298
+ * - `isConnected` — whether at least one DOM element bound to this
1299
+ * path is currently mounted. Flips to `false` when every binding
1300
+ * unmounts.
1301
+ * - `formKey` — identifier of the form this metadata belongs to.
1302
+ * - `path` — dotted-string path to this leaf, or `null` when not applicable.
1303
+ */
1304
+ type MetaTrackerValue = {
1305
+ /** ISO timestamp of the most recent write at this path. `null` if never written. */
1306
+ updatedAt: string | null;
1307
+ /** Value as it arrived, before any transforms. */
1308
+ rawValue: unknown;
1309
+ /** `true` while at least one binding to this path is currently mounted. */
1310
+ isConnected: boolean;
1311
+ /** Form this metadata belongs to. */
1312
+ formKey: FormKey;
1313
+ /** Dotted-string path to this leaf. */
1314
+ path: string | null;
1315
+ /**
1316
+ * `true` when this field is **blank** — the runtime has recorded
1317
+ * that storage and the visible display diverge here. Reserved for
1318
+ * the case the schema can't see on its own: storage forces a
1319
+ * value (e.g. `0` for a numeric leaf, `0n` for a bigint leaf)
1320
+ * while the DOM input shows `''`, and the runtime needs a side-
1321
+ * channel to tell "user typed 0" from "user supplied nothing."
1322
+ *
1323
+ * Set automatically for numeric leaves (the directive's input
1324
+ * listener on clear; the construction-time pass when the consumer
1325
+ * didn't supply a value). Set explicitly for any primitive leaf
1326
+ * via `setValue(path, unset)` / `defaultValues: { x: unset }` /
1327
+ * `reset({ x: unset })` — that's the documented opt-in signal for
1328
+ * strings, booleans, and other types that don't otherwise diverge.
1329
+ * Cleared on the first non-`unset` write.
1330
+ *
1331
+ * `errors = f(schema, state)` is reactive end-to-end: any required
1332
+ * path with `blank: true` produces a "No value supplied" entry in
1333
+ * `form.errors` immediately, no `validate()` / `handleSubmit` call
1334
+ * required. Most consumers don't need this flag directly — gate UI
1335
+ * on `errors[path]` and `touched`. Read `blank` itself when you
1336
+ * want pre-error introspection ("the user hasn't decided yet"
1337
+ * indicator, "review unanswered fields" hint).
1338
+ *
1339
+ * See `docs/blank.md` for the full conceptual model.
1340
+ */
1341
+ blank: boolean;
1342
+ };
1343
+ type RegisterFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${RegisterFlatPath<ArrayItem>}` : `${Key}` | `${Key}.${number}` : Value extends GenericForm ? `${Key}.${RegisterFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${RegisterFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${RegisterFlatPath<ArrayItem>}` : `${Key}` | `${Key}.${number}` : never) : never : never;
1344
+ /**
1345
+ * Sync transformation applied to a field's value as user input flows
1346
+ * from DOM through the directive's assigner. Composes left-to-right
1347
+ * via the `transforms: [...]` array on `register()`.
1348
+ *
1349
+ * The shape is intentionally generic-erased (`(unknown) => unknown`)
1350
+ * rather than per-path-typed: a personal library of transforms
1351
+ * (`trim`, `lowercase`, `slugify`, `clamp`, …) should plug into any
1352
+ * `register()` slot regardless of the path's value type. Library
1353
+ * authors write defensive bodies that no-op on type mismatch:
1354
+ *
1355
+ * ```ts
1356
+ * export const trim: RegisterTransform = (v) =>
1357
+ * typeof v === 'string' ? v.trim() : v
1358
+ * ```
1359
+ *
1360
+ * Type-safety at the call site is delegated to attaform's slim-primitive
1361
+ * gate — a transform that produces a value the path's storage
1362
+ * doesn't accept gets rejected at write time with a standard
1363
+ * diagnostic.
1364
+ *
1365
+ * Transforms must be sync. A `Promise` return is treated as a
1366
+ * pipeline failure: the write is aborted and a console.error is
1367
+ * logged. Use async field validation for canonicalize-before-write
1368
+ * patterns; use sync transforms for fire-and-forget side effects
1369
+ * (`void doIt(value); return value`).
1370
+ *
1371
+ * Throws are caught and aborted: attaform wraps each transform call in
1372
+ * try/catch so a buggy or defensive-throw transform doesn't crash
1373
+ * the host app. On throw the pipeline aborts (subsequent transforms
1374
+ * don't run), nothing is written to form state, and the assigner
1375
+ * returns `false`.
1376
+ */
1377
+ type RegisterTransform = (value: unknown) => unknown;
1378
+ /**
1379
+ * Runtime type for a slim primitive kind. Used to narrow the
1380
+ * `transform` parameter and return value on a `CoercionEntry` so
1381
+ * authors writing rules don't have to cast `unknown`.
1382
+ *
1383
+ * Exhaustive over `SlimPrimitiveKind` — adding a new kind to that
1384
+ * union must add a corresponding branch here.
1385
+ */
1386
+ type SlimRuntimeOf<K extends SlimPrimitiveKind> = K extends 'string' ? string : K extends 'number' ? number : K extends 'boolean' ? boolean : K extends 'bigint' ? bigint : K extends 'date' ? Date : K extends 'null' ? null : K extends 'undefined' ? undefined : K extends 'array' ? readonly unknown[] : K extends 'set' ? ReadonlySet<unknown> : K extends 'map' ? ReadonlyMap<unknown, unknown> : K extends 'object' ? Record<string, unknown> : K extends 'symbol' ? symbol : K extends 'function' ? (...args: never[]) => unknown : never;
1387
+ /**
1388
+ * Outcome of a coercion attempt.
1389
+ *
1390
+ * - `coerced: true` — the rule produced `value`, which the directive
1391
+ * forwards to the slim gate (the gate may still reject if the
1392
+ * value doesn't satisfy the path's accept set).
1393
+ * - `coerced: false` — the rule decided it can't coerce this input.
1394
+ * The directive passes the original value through; the slim gate
1395
+ * decides downstream.
1396
+ *
1397
+ * Discriminated rather than `O | undefined` so rules with
1398
+ * `output: 'undefined'` or `output: 'null'` don't conflict with the
1399
+ * "skip" signal.
1400
+ */
1401
+ type CoercionResult<O> = {
1402
+ coerced: true;
1403
+ value: O;
1404
+ } | {
1405
+ coerced: false;
1406
+ };
1407
+ /**
1408
+ * A single coercion rule. `input` and `output` are
1409
+ * `SlimPrimitiveKind` literals; `transform` receives a value already
1410
+ * narrowed to `SlimRuntimeOf<input>` and returns
1411
+ * `CoercionResult<SlimRuntimeOf<output>>`.
1412
+ *
1413
+ * Rules MUST be sync. They SHOULD NOT throw — wrap internal
1414
+ * try/catch when the conversion can fail (e.g. `BigInt(s)` throws
1415
+ * for non-numeric strings). The library wraps each invocation in
1416
+ * try/catch as defense in depth; throws are caught, logged once per
1417
+ * `(input, output)`, and the original value passes through.
1418
+ */
1419
+ type CoercionEntry<I extends SlimPrimitiveKind = SlimPrimitiveKind, O extends SlimPrimitiveKind = SlimPrimitiveKind> = {
1420
+ readonly input: I;
1421
+ readonly output: O;
1422
+ readonly transform: (value: SlimRuntimeOf<I>) => CoercionResult<SlimRuntimeOf<O>>;
1423
+ };
1424
+ /**
1425
+ * A registry is an ordered array of `CoercionEntry` records.
1426
+ * Consumers compose by spreading `defaultCoercionRules` and
1427
+ * appending their own entries. Order is observable only when two
1428
+ * entries share the same `(input, output)` pair — the library emits
1429
+ * a one-shot dev-warn and the LATER entry wins.
1430
+ */
1431
+ type CoercionRegistry = readonly CoercionEntry[];
1432
+ /**
1433
+ * Options for `register(path, options)`. Per-field rather than
1434
+ * per-form so each persisted path is opted in at its own call site —
1435
+ * adding a new field can't accidentally leak into the persistence
1436
+ * pipeline unless the field's `register` call says so explicitly.
1437
+ */
1438
+ type RegisterOptions = {
1439
+ /**
1440
+ * Opt this field into the form's persistence pipeline. The form
1441
+ * also needs `useForm({ persist })` configured for any storage
1442
+ * activity to happen.
1443
+ *
1444
+ * Persistence follows the field's lifecycle: writes flow on
1445
+ * mount, the field is dropped from the persisted draft on unmount.
1446
+ * If multiple inputs bind to the same path, the path keeps
1447
+ * persisting as long as any opted-in input is mounted.
1448
+ *
1449
+ * Throws `SensitivePersistFieldError` when the path looks
1450
+ * sensitive (password / cvv / ssn / token / etc.) unless
1451
+ * `acknowledgeSensitive: true` is also set.
1452
+ */
1453
+ persist?: boolean;
1454
+ /**
1455
+ * Suppress the sensitive-name guard. Required to persist any path
1456
+ * whose name matches the heuristic (password, cvv, ssn, etc.).
1457
+ * Treat this as a code-review checkpoint: setting it should be a
1458
+ * deliberate decision that the path's data is safe to land in
1459
+ * client-side storage for this user's session.
1460
+ */
1461
+ acknowledgeSensitive?: boolean;
1462
+ /**
1463
+ * Sync transformation pipeline applied to user-typed values before
1464
+ * they reach form state. Composes left-to-right: each transform
1465
+ * receives the previous transform's output (or the directive-
1466
+ * extracted DOM value for the first transform).
1467
+ *
1468
+ * Pipeline order:
1469
+ * `DOM event → modifier cast (.lazy/.trim/.number) → transforms[0] → … → transforms[n] → assigner`
1470
+ *
1471
+ * Applies to user input only. Programmatic writes
1472
+ * (`form.setValue(...)`, `rv.setValueWithInternalPath(...)`),
1473
+ * `form.reset()`, hydration, SSR replay, and `markBlank()` all
1474
+ * bypass transforms — those write canonical state, not normalized
1475
+ * user input. If you want the same normalization on a programmatic
1476
+ * write, compose the transforms yourself at the call site:
1477
+ *
1478
+ * ```ts
1479
+ * form.setValue('email', slugify(lowercase(rawValue)))
1480
+ * ```
1481
+ *
1482
+ * Transforms must be sync. Throws and Promise returns abort the
1483
+ * write and log to `console.error` (see `RegisterTransform` for
1484
+ * the failure-mode contract).
1485
+ *
1486
+ * For patterns that need to inspect the `RegisterValue` itself
1487
+ * (rejection-with-side-effect, redirection to other fields, custom
1488
+ * DOM mutation), use `@update:registerValue` on the bound element
1489
+ * instead — see the "Custom assigners" section in the API docs.
1490
+ */
1491
+ transforms?: ReadonlyArray<RegisterTransform>;
1492
+ };
1493
+ /**
1494
+ * The object returned by `form.register(path)`. Pass it to a native
1495
+ * input via `v-register`:
1496
+ *
1497
+ * ```vue
1498
+ * <input v-register="form.register('email')" />
1499
+ * ```
1500
+ *
1501
+ * Or read `innerRef` directly when integrating with custom components.
1502
+ *
1503
+ * The remaining fields support advanced bindings (custom assigners,
1504
+ * SSR optimistic marking, persistence opt-ins). Most consumers only
1505
+ * touch `innerRef`.
1506
+ */
1507
+ type RegisterValue<Value = unknown> = {
1508
+ /**
1509
+ * Live, read-only reactive value at this path. Watch it to drive
1510
+ * UI that depends on the field's current value.
1511
+ */
1512
+ innerRef: Readonly<Ref<Value>>;
1513
+ /**
1514
+ * Attach an HTML element to this binding. Called by `v-register`
1515
+ * automatically; expose it to custom integrations that need to
1516
+ * register an element manually.
1517
+ */
1518
+ registerElement: (el: HTMLElement) => void;
1519
+ /**
1520
+ * Detach an HTML element from this binding. Pair with
1521
+ * `registerElement` for custom integrations.
1522
+ */
1523
+ deregisterElement: (el: HTMLElement) => void;
1524
+ /**
1525
+ * Write the field's value programmatically. Returns `true` when the
1526
+ * write was accepted, `false` when it was rejected (e.g. wrong
1527
+ * primitive type for the path). The optional `meta` lets custom
1528
+ * directives signal whether the write should be persisted.
1529
+ */
1530
+ setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
1531
+ /**
1532
+ * Read-only, string-form view of the field's current value — what
1533
+ * the compile-time `:value` injection reads on every input /
1534
+ * textarea / select bound by `v-register`.
1535
+ *
1536
+ * Returns `''` when the path is in the form's `blankPaths`
1537
+ * set OR storage is `null` / `undefined`; otherwise stringifies
1538
+ * the storage value via `String(...)`. The blank branch
1539
+ * lets the user clear a numeric field without the next Vue render
1540
+ * patching `el.value` back to `'0'` (the slim default).
1541
+ */
1542
+ displayValue: Readonly<Ref<string>>;
1543
+ };
1544
+ /**
1545
+ * Custom assigner installed on an element via the directive's
1546
+ * `[assignKey]` slot OR an `@update:registerValue` listener. Called
1547
+ * by the directive when a DOM event (input / change / etc.) fires
1548
+ * on the bound element.
1549
+ *
1550
+ * The directive passes the extracted value plus the `RegisterValue`
1551
+ * the directive is currently bound to. The second arg lets a
1552
+ * top-level handler write back to form state without having to
1553
+ * capture the RV via closure:
1554
+ *
1555
+ * ```ts
1556
+ * function upperCaseAssigner(value: unknown, rv: RegisterValue): void {
1557
+ * rv.setValueWithInternalPath(String(value ?? '').toUpperCase())
1558
+ * }
1559
+ * ```
1560
+ *
1561
+ * `registerValue` is omitted only for assigners installed directly
1562
+ * via `el[assignKey] = fn` — those callers already have the RV in
1563
+ * scope at install time.
1564
+ *
1565
+ * Return `true` when the write was accepted, `false` when it was
1566
+ * rejected (e.g. the value didn't match the path's expected type).
1567
+ * `undefined` is treated as "succeeded" so simple assigners can
1568
+ * just return `void`.
1569
+ */
1570
+ type CustomDirectiveRegisterAssignerFn = (value: unknown, registerValue?: RegisterValue) => boolean | undefined;
1571
+ /**
1572
+ * Generic shape of a v-register directive variant. Used by the
1573
+ * library's text / checkbox / radio / select directive types and
1574
+ * available for custom integrations that need to drop in their own
1575
+ * variant.
1576
+ *
1577
+ * The value generic admits `undefined` because `useRegister()` may
1578
+ * return `undefined` (a wrapper component rendered without a parent
1579
+ * `registerValue`); binding that value to `v-register` is supported
1580
+ * and installs a no-op assigner at runtime.
1581
+ */
1582
+ type CustomRegisterDirective<T, Modifiers extends string = string> = ObjectDirective<T & {
1583
+ _assigning?: boolean;
1584
+ /**
1585
+ * Snapshot of the last `value.innerRef.value` reference the
1586
+ * directive's DOM-sync (setSelected / setChecked / radio
1587
+ * `el.checked = …`) was applied for. Used by every input
1588
+ * directive's `updated` / `beforeUpdate` to skip the per-render
1589
+ * DOM sync when the model is identity-unchanged — preventing
1590
+ * parent re-renders (a typed character in a sibling, an async-
1591
+ * validation tick, any reactive read) from clobbering an in-
1592
+ * progress user interaction. Identity comparison is sound:
1593
+ * every form write produces a fresh value at the path (scalars
1594
+ * are new primitives; arrays/Sets get fresh references along the
1595
+ * spine via diff-apply), so reference equality on
1596
+ * `innerRef.value` tracks "did the model move" exactly. The
1597
+ * `_assigning` gate stays alongside — it short-circuits the
1598
+ * immediate post-write render where the DOM is already in sync
1599
+ * from the user's input.
1600
+ */
1601
+ _lastAppliedModel?: unknown;
1602
+ [S: symbol]: CustomDirectiveRegisterAssignerFn;
1603
+ }, RegisterValue | undefined, Modifiers, string>;
1604
+ /**
1605
+ * Modifier names supported by `v-register` on `<input type="text">`,
1606
+ * `<input type="number">`, and `<textarea>`. Mirrors Vue's
1607
+ * `v-model` modifier semantics on the same elements; combine freely
1608
+ * (`<input v-register.lazy.trim.number="..." />`).
1609
+ */
1610
+ type RegisterTextModifier =
1611
+ /**
1612
+ * Write on `change` (blur) instead of `input`. The reactive
1613
+ * model only updates after the user tabs/clicks out of the
1614
+ * field. IME composition handlers are skipped under `.lazy` —
1615
+ * composition events do not gate writes.
1616
+ */
1617
+ 'lazy'
1618
+ /**
1619
+ * Strip leading and trailing whitespace on blur. The form holds
1620
+ * the user's raw input (whitespace included) while they're
1621
+ * typing; on `change` (blur / commit) the value is trimmed
1622
+ * once and written back to both the model and the visible DOM.
1623
+ * Combine with `.lazy` to skip the mid-typing writes entirely.
1624
+ */
1625
+ | 'trim'
1626
+ /**
1627
+ * Cast the value via `parseFloat` before writing. Values that
1628
+ * can't be parsed as a number (e.g. `'abc'`) pass through
1629
+ * unchanged — the slim-primitive gate then sees a string
1630
+ * heading to a numeric slot and rejects the write. Auto-applied
1631
+ * for `<input type="number">`; explicit `.number` is redundant
1632
+ * there.
1633
+ */
1634
+ | 'number';
1635
+ /**
1636
+ * v-register directive variant for `<input type="text">`,
1637
+ * `<input type="number">`, and `<textarea>`. Supports the
1638
+ * `.lazy`, `.trim`, and `.number` modifiers — see
1639
+ * `RegisterTextModifier` for per-modifier semantics.
1640
+ */
1641
+ type RegisterTextCustomDirective = CustomRegisterDirective<HTMLInputElement | HTMLTextAreaElement, RegisterTextModifier>;
1642
+ /** v-register directive variant for checkboxes. No modifiers. */
1643
+ type RegisterCheckboxCustomDirective = CustomRegisterDirective<HTMLInputElement>;
1644
+ /** v-register directive variant for radio inputs. No modifiers. */
1645
+ type RegisterRadioCustomDirective = CustomRegisterDirective<HTMLInputElement>;
1646
+ /**
1647
+ * Modifier name supported by `v-register` on `<select>`. Mirrors
1648
+ * Vue's `v-model` `.number` on the same element.
1649
+ */
1650
+ type RegisterSelectModifier =
1651
+ /**
1652
+ * Cast each selected option's `value` via `parseFloat` before
1653
+ * writing. The form state holds numbers, not numeric strings —
1654
+ * useful when option values are written as strings in the
1655
+ * markup but the schema expects numbers.
1656
+ */
1657
+ 'number';
1658
+ /**
1659
+ * v-register directive variant for `<select>`. Supports `.number`
1660
+ * — see `RegisterSelectModifier` for semantics.
1661
+ */
1662
+ type RegisterSelectCustomDirective = CustomRegisterDirective<HTMLSelectElement, RegisterSelectModifier>;
1663
+ /** v-register directive variant for the dynamic input/select/textarea bridge. */
1664
+ type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, RegisterValue | undefined, string>;
1665
+ /**
1666
+ * The `v-register` directive. Binds a form field to a native
1667
+ * input, select, textarea, checkbox, or radio:
1668
+ *
1669
+ * ```vue
1670
+ * <input v-register="form.register('email')" />
1671
+ * <select v-register="form.register('country')">
1672
+ * <option value="us">US</option>
1673
+ * <option value="uk">UK</option>
1674
+ * </select>
1675
+ * ```
1676
+ *
1677
+ * Also works on custom components whose root is NOT a native
1678
+ * input — call `useRegister()` in the child's setup to read the
1679
+ * parent's binding, then re-bind `v-register` onto an inner native
1680
+ * element. (When the wrapper's root IS the input itself, attribute
1681
+ * fallthrough handles it; `useRegister` is unnecessary.)
1682
+ *
1683
+ * ```vue
1684
+ * <!-- Parent -->
1685
+ * <MyField label="Email" v-register="form.register('email')" />
1686
+ *
1687
+ * <!-- MyField.vue (root is <label>, not <input>) -->
1688
+ * <script setup>
1689
+ * import { useRegister } from 'attaform'
1690
+ * defineProps<{ label: string }>()
1691
+ * const register = useRegister()
1692
+ * </script>
1693
+ * <template>
1694
+ * <label>
1695
+ * <span>{{ label }}</span>
1696
+ * <input v-register="register" />
1697
+ * </label>
1698
+ * </template>
1699
+ * ```
1700
+ *
1701
+ * Modifier support varies by element:
1702
+ * - text / number / textarea: `.lazy`, `.trim`, `.number`
1703
+ * - select: `.number`
1704
+ * - checkbox / radio: none
1705
+ *
1706
+ * See `RegisterTextModifier` / `RegisterSelectModifier` for
1707
+ * per-modifier semantics.
1708
+ *
1709
+ * Registered globally by `createAttaform()` (and by the
1710
+ * `attaform/nuxt` module). Most consumers don't import the
1711
+ * directive itself — it's exposed for integrations that install
1712
+ * directives manually.
1713
+ */
1714
+ type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
1715
+ /**
1716
+ * Callback form of `setValue`'s value argument. Receives the previous
1717
+ * value at the path and returns the next value:
1718
+ *
1719
+ * ```ts
1720
+ * form.setValue('count', (prev) => prev + 1)
1721
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
1722
+ * ```
1723
+ *
1724
+ * The library fills any missing structural slots (e.g. nested
1725
+ * objects) against the schema's defaults after the callback returns,
1726
+ * so partial returns are safe.
1727
+ */
1728
+ type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
1729
+ /**
1730
+ * The value argument of `form.setValue`. Either the next value
1731
+ * directly, or a callback that derives it from the previous value.
1732
+ *
1733
+ * Type parameters:
1734
+ * - `Write` — what the direct value form accepts (the storable shape
1735
+ * at the path).
1736
+ * - `Read` — what the callback's `prev` argument exposes (defaults
1737
+ * to `Write`). For whole-form callbacks the read shape tags
1738
+ * array elements as possibly-undefined to reflect runtime reality.
1739
+ */
1740
+ type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
1741
+ type DeepFlatten<T> = T extends object ? {
1742
+ [K in keyof T]: DeepFlatten<T[K]>;
1743
+ } : T;
1744
+ /**
1745
+ * Focus / blur / touched flags for a registered field.
1746
+ *
1747
+ * - `focused` — `true` while the user is interacting with the field;
1748
+ * `false` after blur. `null` until the field has ever been focused.
1749
+ * - `blurred` — `true` after the field has lost focus at least once.
1750
+ * `null` before any blur event.
1751
+ * - `touched` — flips to `true` on the first blur after a focus and
1752
+ * stays `true` thereafter. Useful for "show errors only after the
1753
+ * user has interacted" UX.
1754
+ */
1755
+ type DOMFieldState = {
1756
+ /** `true` while focused; `false` after blur; `null` before first focus. */
1757
+ focused: boolean | null;
1758
+ /** `true` once the field has lost focus at least once; `null` before. */
1759
+ blurred: boolean | null;
1760
+ /** Flips to `true` on the first blur after a focus and stays there. */
1761
+ touched: boolean | null;
1762
+ };
1763
+ /**
1764
+ * Richer per-field type kept for type-level utility code (e.g.
1765
+ * higher-order helpers that pass field state around). Carries
1766
+ * `currentValue` / `originalValue` / `previousValue` (typed `Value`),
1767
+ * the same flag set as `FieldStateLeaf`, plus `meta`
1768
+ * (`MetaTrackerValue`).
1769
+ *
1770
+ * `form.fields.<path>` returns the slim `FieldStateLeaf` shape;
1771
+ * pick `FieldState<Value>` for code that needs `meta` or the historical
1772
+ * `previousValue` slot.
1773
+ */
1774
+ type FieldState<Value = unknown> = DeepFlatten<DOMFieldState & {
1775
+ /** Per-field metadata (timestamps, raw value, connection state). */
1776
+ meta: MetaTrackerValue;
1777
+ /**
1778
+ * Validation errors for this path. Populated automatically by
1779
+ * `handleSubmit` and per-field validation; also writable via
1780
+ * `setFieldErrors` / `addFieldErrors`. Empty when valid — safe
1781
+ * to read without a null check.
1782
+ */
1783
+ errors: ValidationError[];
1784
+ /** The value the field was initialised with. */
1785
+ originalValue: Value;
1786
+ /** The value before the most recent write. */
1787
+ previousValue: Value;
1788
+ /** The current value at this path. */
1789
+ currentValue: Value;
1790
+ /** `true` when `currentValue` matches `originalValue`. */
1791
+ pristine: boolean;
1792
+ /** `true` when `currentValue` differs from `originalValue`. */
1793
+ dirty: boolean;
1794
+ /**
1795
+ * `true` when this field is **blank** — the side-channel for
1796
+ * storage / display divergence (numeric leaves where storage
1797
+ * holds `0` / `0n` but the DOM shows `''`, plus any primitive
1798
+ * leaf the consumer explicitly opted in via `unset`). Surfaces
1799
+ * both as a top-level field here AND via `meta.blank` (the meta
1800
+ * projection mirrors the same value). See `docs/blank.md`.
1801
+ */
1802
+ blank: boolean;
1803
+ }>;
1804
+ /**
1805
+ * Per-field reactive shape returned by `form.fields.<leaf-path>`.
1806
+ * Slim, readonly across the board. Leaf-aware: this shape only
1807
+ * appears at LEAF paths (primitives, dates). At container paths
1808
+ * the proxy descends without injecting these keys, so a schema
1809
+ * field literally named `dirty` at depth 2+ stays reachable as a
1810
+ * descent target — no shadowing.
1811
+ */
1812
+ type FieldStateLeaf<Value = unknown> = {
1813
+ readonly value: Value;
1814
+ readonly original: Value;
1815
+ readonly pristine: boolean;
1816
+ readonly dirty: boolean;
1817
+ readonly focused: boolean | null;
1818
+ readonly blurred: boolean | null;
1819
+ readonly touched: boolean | null;
1820
+ readonly isConnected: boolean;
1821
+ readonly updatedAt: string | null;
1822
+ readonly errors: readonly ValidationError[];
1823
+ readonly path: ReadonlyArray<string | number>;
1824
+ readonly blank: boolean;
1825
+ };
1826
+ /**
1827
+ * Recursive type behind `form.fields`. Leaf-aware branching: at
1828
+ * primitive paths (string, number, boolean, bigint, Date, …) the
1829
+ * proxy returns a `FieldStateLeaf`; at container paths (object,
1830
+ * array, …) the proxy descends without injecting leaf-keys.
1831
+ *
1832
+ * Field-name collisions at depth 2+ resolve unambiguously: a schema
1833
+ * field literally named `dirty` at depth 2 is reachable as a
1834
+ * descent target (`form.fields.address.dirty` returns the
1835
+ * FieldStateView for `address.dirty`). Reading `dirty` AT the
1836
+ * leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's
1837
+ * own dirty boolean — path-segment and leaf-prop occupy different
1838
+ * proxy depths.
1839
+ *
1840
+ * The runtime implementation queries `schema.isLeafAtPath(segments)`
1841
+ * at every step; this type approximates that decision using
1842
+ * "T extends primitive". The two stay in sync for typical schemas;
1843
+ * exotic adapter-defined leaf kinds (custom `Date`-like) may need
1844
+ * a runtime check (the runtime is authoritative).
1845
+ */
1846
+ type FieldStateMapEntry<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? FieldStateLeaf<T> : T extends ReadonlyArray<infer U> ? {
1847
+ readonly [K: number]: FieldStateMapEntry<U>;
1848
+ } : T extends object ? {
1849
+ readonly [K in keyof T]: FieldStateMapEntry<T[K]>;
1850
+ } : FieldStateLeaf<T>;
1851
+ /**
1852
+ * Type of `form.fields` — leaf-aware drillable callable Proxy. At
1853
+ * a leaf path the proxy resolves to a `FieldStateLeaf<Value>`; at
1854
+ * a container path it returns a sub-proxy you can keep drilling.
1855
+ *
1856
+ * Augmented with the callable signatures so dot-access and function-
1857
+ * call coexist on the same identifier:
1858
+ *
1859
+ * ```ts
1860
+ * form.fields.email.value // string (leaf-prop on FieldStateView)
1861
+ * form.fields('email').value // function-call (dynamic / programmatic)
1862
+ * form.fields(['users', 0, 'name']) // path-array form
1863
+ * form.fields() // root proxy
1864
+ * ```
1865
+ *
1866
+ * Single-bracket dotted access (`form.fields['address.city']`) is
1867
+ * intentionally NOT supported — JS object semantics treat the dotted
1868
+ * string as a single key. Use chained dot/bracket or the callable
1869
+ * form.
1870
+ */
1871
+ type FieldStateMap<Form extends GenericForm> = {
1872
+ readonly [K in keyof Form]: FieldStateMapEntry<Form[K]>;
1873
+ } & {
1874
+ (path: string): unknown;
1875
+ (path: ReadonlyArray<string | number>): unknown;
1876
+ (): FieldStateMap<Form>;
1877
+ };
1878
+ /**
1879
+ * Untyped error map keyed by dotted-string path. The same data
1880
+ * exposed by `form.errors`, but as a plain record — useful when
1881
+ * routing API errors that may land on paths the form's TypeScript
1882
+ * type doesn't know about.
1883
+ */
1884
+ type FormErrorRecord = Record<string, ValidationError[]>;
1885
+ /**
1886
+ * Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
1887
+ * leaf path the proxy resolves to `ValidationError[] | undefined`;
1888
+ * at a container path it returns a sub-proxy you can keep drilling.
1889
+ *
1890
+ * Dot/bracket access mirrors the schema shape:
1891
+ *
1892
+ * ```ts
1893
+ * form.errors.email // ValidationError[] | undefined (leaf)
1894
+ * form.errors.user.profile.email // ValidationError[] | undefined (chained leaves)
1895
+ * form.errors.address // sub-proxy (container — descend further)
1896
+ * ```
1897
+ *
1898
+ * Callable form for dynamic / programmatic paths:
1899
+ *
1900
+ * ```ts
1901
+ * form.errors('user.profile.email') // dotted-string
1902
+ * form.errors(['user', 'profile', 'email']) // path-array
1903
+ * form.errors() // root proxy
1904
+ * ```
1905
+ *
1906
+ * Single-bracket dotted access (`form.errors['user.profile.email']`)
1907
+ * is intentionally NOT supported — JS object semantics treat the
1908
+ * dotted string as a single key, which would land on a non-existent
1909
+ * path. Use chained dot/bracket access or the callable form.
1910
+ */
1911
+ /**
1912
+ * Recursive shape of the `form.errors` proxy. Mirrors the schema:
1913
+ * primitive leaves expose `ValidationError[] | undefined` directly;
1914
+ * containers expose a sub-shape you can keep drilling. Arrays expose
1915
+ * numeric-indexed sub-shapes.
1916
+ *
1917
+ * Augmented with the callable signatures so dot-access and function-
1918
+ * call coexist on the same identifier.
1919
+ */
1920
+ type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
1921
+ (path: string): readonly ValidationError[] | undefined;
1922
+ (path: ReadonlyArray<string | number>): readonly ValidationError[] | undefined;
1923
+ (): FormErrorsSurface<Form>;
1924
+ };
1925
+ type ErrorsProxyShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? readonly ValidationError[] | undefined : T extends ReadonlyArray<infer U> ? {
1926
+ readonly [K: number]: ErrorsProxyShape<U>;
1927
+ } : T extends object ? {
1928
+ readonly [K in keyof T]: ErrorsProxyShape<T[K]>;
1929
+ } : readonly ValidationError[] | undefined;
1930
+ /**
1931
+ * Type of `form.values`. Drillable readonly callable proxy. Unlike
1932
+ * `form.errors` and `form.fields`, containers are USEFUL terminals:
1933
+ * `form.values.address` returns the actual `{ city, … }` subtree
1934
+ * (and keeps drilling). Asymmetry justified by density — every
1935
+ * container in `values` carries meaningful data; in errors / fields
1936
+ * containers are derivations.
1937
+ *
1938
+ * ```ts
1939
+ * form.values.email // string (the value)
1940
+ * form.values.address // { city, … } — object (drillable)
1941
+ * form.values.address.city // string (chained descent)
1942
+ * form.values('address.city') // function-call (dynamic / programmatic)
1943
+ * form.values(['address', 'city']) // path-array form
1944
+ * form.values() // the whole form value (root)
1945
+ * ```
1946
+ *
1947
+ * Single-bracket dotted access (`form.values['address.city']`) is
1948
+ * intentionally NOT supported — JS object semantics treat the dotted
1949
+ * string as a single key. Use chained dot/bracket or the callable
1950
+ * form.
1951
+ */
1952
+ type ValuesSurface<F> = Readonly<F> & {
1953
+ (path: string): unknown;
1954
+ (path: ReadonlyArray<string | number>): unknown;
1955
+ (): Readonly<F>;
1956
+ };
1957
+ /**
1958
+ * A single server-side error entry. Carries both the human-readable
1959
+ * `message` and a stable `code` identifier — both fields are required.
1960
+ * The `code` is stamped verbatim onto the produced `ValidationError`,
1961
+ * so consumers can branch on it without string-matching on `message`.
1962
+ *
1963
+ * Pick a prefix for your codes (`api:`, `auth:`, etc.) and stay
1964
+ * consistent so error-rendering UIs can switch on the code.
1965
+ */
1966
+ type ApiErrorEntry = {
1967
+ /** Human-readable failure description. */
1968
+ message: string;
1969
+ /**
1970
+ * Stable machine identifier for the failure (e.g. `'api:duplicate-email'`).
1971
+ * Forwarded verbatim onto the produced `ValidationError`.
1972
+ */
1973
+ code: string;
1974
+ };
1975
+ /**
1976
+ * Shape of a server-side error details record. Keys are dotted field
1977
+ * paths; values are either a single entry, an array of entries, or a
1978
+ * mix of structured and bare-string entries. Each entry is one of:
1979
+ *
1980
+ * - **Structured** — `{ message: string, code: string }`. The `code`
1981
+ * forwards verbatim onto the produced `ValidationError`.
1982
+ * - **Bare string** — a plain string. The Rails / Django REST
1983
+ * Framework / Laravel default JSON shape (`{ field: ["msg"] }`).
1984
+ * Synthesized into `{ message: <string>, code: <defaultCode> }` at
1985
+ * parse time, where `defaultCode` defaults to `'api:unknown'` and
1986
+ * is configurable via `parseApiErrors`'s options bag.
1987
+ *
1988
+ * Multiple entries at the same path produce multiple
1989
+ * `ValidationError`s — useful for a single field that fails multiple
1990
+ * checks (e.g. `password` is too short *and* missing a digit).
1991
+ */
1992
+ type ApiErrorDetails = Record<string, ApiErrorValue>;
1993
+ /**
1994
+ * One entry inside an {@link ApiErrorDetails} value — either the
1995
+ * strict `{ message, code }` object, or a bare string (synthesised
1996
+ * with the parser's `defaultCode`).
1997
+ */
1998
+ type ApiErrorValue = string | ApiErrorEntry | ReadonlyArray<string | ApiErrorEntry>;
1999
+ /**
2000
+ * Outer envelope `parseApiErrors` accepts. Both the wrapped form
2001
+ * (`{ error: { details } }`) and the unwrapped form (`{ details }`)
2002
+ * are recognised; raw detail records (`{ email: { message, code } }`)
2003
+ * are also accepted directly.
2004
+ */
2005
+ type ApiErrorEnvelope = {
2006
+ /** Wrapped error envelope — `parseApiErrors` reads `details` from inside. */
2007
+ error?: {
2008
+ details?: ApiErrorDetails;
2009
+ [k: string]: unknown;
2010
+ };
2011
+ /** Unwrapped error envelope. */
2012
+ details?: ApiErrorDetails;
2013
+ };
2014
+ /**
2015
+ * Reactive form-level flags, counters, and aggregates returned as
2016
+ * `form.meta`. "Meta" because every other surface (`form.values`,
2017
+ * `form.errors`, `form.fields`) is data-shaped — `form.meta` holds
2018
+ * facts derived ABOUT the form.
2019
+ *
2020
+ * Read fields directly with no `.value` — they auto-unwrap inside
2021
+ * the reactive object:
2022
+ *
2023
+ * ```vue
2024
+ * <button :disabled="form.meta.isSubmitting">Save</button>
2025
+ * ```
2026
+ *
2027
+ * Watch a single field via the getter form:
2028
+ *
2029
+ * ```ts
2030
+ * watch(() => form.meta.isSubmitting, (value) => …)
2031
+ * ```
2032
+ *
2033
+ * Per-field state (touched, dirty, errors) lives behind
2034
+ * `form.fields.<path>`; this is the aggregate view across the
2035
+ * whole form.
2036
+ *
2037
+ * Read-only at runtime — assignments throw. Destructuring snapshots
2038
+ * the current values; use `toRefs()` if you need reactive handles
2039
+ * to individual fields.
2040
+ */
2041
+ interface FormMeta {
2042
+ /**
2043
+ * `true` when any field's current value differs from its initial
2044
+ * value. `false` for a pristine form and for one where every change
2045
+ * has been undone. Restore the pristine baseline via `reset()`.
2046
+ *
2047
+ * Note: object/array leaves are compared by reference, so replacing
2048
+ * an array with an equal copy still reads as dirty.
2049
+ */
2050
+ readonly isDirty: boolean;
2051
+ /**
2052
+ * `true` when the form currently has no validation errors. Flips
2053
+ * with every `validate()` / `handleSubmit` outcome.
2054
+ */
2055
+ readonly isValid: boolean;
2056
+ /**
2057
+ * `true` while a `handleSubmit`-produced submit handler is running.
2058
+ * Covers both the validation phase and your async submit callback.
2059
+ * Useful for disabling the submit button.
2060
+ */
2061
+ readonly isSubmitting: boolean;
2062
+ /**
2063
+ * `true` while any validation run is in flight (the reactive
2064
+ * `validate()` re-run, an imperative `validateAsync()`, or the
2065
+ * pre-submit validation inside `handleSubmit`).
2066
+ */
2067
+ readonly isValidating: boolean;
2068
+ /**
2069
+ * How many times the submit handler has been invoked, regardless of
2070
+ * outcome (validation failure, callback success, callback throw).
2071
+ * Useful for "show errors after first submit attempt" UX.
2072
+ */
2073
+ readonly submitCount: number;
2074
+ /**
2075
+ * The error thrown or rejected by the most recent submit callback
2076
+ * (or its `onError` handler). Cleared to `null` at the start of
2077
+ * each new submission attempt; stays `null` on success.
2078
+ *
2079
+ * The submit handler still throws normally — this is the reactive
2080
+ * mirror for templates. Imperative callers can use
2081
+ * `try { await onSubmit() }` instead.
2082
+ */
2083
+ readonly submitError: unknown;
2084
+ /** `true` when there is at least one undo step available. Always present (false when history is disabled). */
2085
+ readonly canUndo: boolean;
2086
+ /** `true` when `undo()` has been called and a `redo()` would replay. Always present (false when history is disabled). */
2087
+ readonly canRedo: boolean;
2088
+ /**
2089
+ * Total snapshots across the undo and redo stacks. Useful for
2090
+ * debug overlays; UI driving undo/redo buttons should gate on
2091
+ * `canUndo` / `canRedo` instead.
2092
+ */
2093
+ readonly historySize: number;
2094
+ /**
2095
+ * Flat aggregate of EVERY validation error in the form — schema-
2096
+ * keyed entries, form-level errors (path: []), unmapped server
2097
+ * errors (paths not in `FlatPath`), and cross-field-refine errors
2098
+ * (paths at containers). Reads as English: "the form's errors."
2099
+ *
2100
+ * Unlike `form.errors.<path>` (per-leaf, active-path-filtered),
2101
+ * `form.meta.errors` is unfiltered — inactive-variant errors stay
2102
+ * in the array. Consumers who want only addressable errors filter
2103
+ * the array themselves (`form.meta.errors.filter(e => …)`).
2104
+ *
2105
+ * Common patterns:
2106
+ *
2107
+ * ```vue
2108
+ * <p v-if="form.meta.errors.length">{{ form.meta.errors.length }} issue(s)</p>
2109
+ * <ul>
2110
+ * <li v-for="err in form.meta.errors" :key="err.path.join('.')">
2111
+ * {{ err.path.join('.') || 'form' }}: {{ err.message }}
2112
+ * </li>
2113
+ * </ul>
2114
+ * ```
2115
+ *
2116
+ * The array re-allocates on any underlying store change (schema /
2117
+ * derived-blank / user); reactivity propagates through the standard
2118
+ * Vue computed graph.
2119
+ */
2120
+ readonly errors: readonly ValidationError[];
2121
+ /**
2122
+ * Per-`useForm()`-call identity. Stable for the lifetime of one
2123
+ * `useForm()` call; new on every fresh mount. Orthogonal to
2124
+ * `form.key`: the key identifies a SHARED FormStore (so two
2125
+ * `useForm({ key: 'signup' })` calls return the same store and the
2126
+ * same key), while `instanceId` identifies THIS specific callsite —
2127
+ * useful when two forms share a key (sidebar + main rendering the
2128
+ * same form) and you need to disambiguate which caller is which.
2129
+ *
2130
+ * Format is opaque (Vue 3.5+ `useId()`-derived). Treat as identity,
2131
+ * not state — don't parse, don't compare ordinally, don't persist.
2132
+ *
2133
+ * Common patterns:
2134
+ *
2135
+ * - **Devtools panels** disambiguating shared-key form mounts.
2136
+ * - **Telemetry / logging hooks** tagging events with which mount
2137
+ * triggered them.
2138
+ * - **E2E test selectors** stamping `data-form-id={form.meta.instanceId}`
2139
+ * onto a wrapper to assert which form was focused.
2140
+ * - **Vue `:key`** for keyed lists of dynamically-rendered forms
2141
+ * (drag-reorder, etc.) — stable identity per useForm() call.
2142
+ */
2143
+ readonly instanceId: string;
2144
+ }
2145
+ /**
2146
+ * The object returned by `useForm`. Holds every reactive ref, write
2147
+ * helper, and lifecycle method bound to one form.
2148
+ *
2149
+ * ```ts
2150
+ * const form = useForm({ schema })
2151
+ * form.register('email') // bind to <input v-register>
2152
+ * form.values.email // current value (proxy, no .value)
2153
+ * form.fields.email.dirty // per-field flags
2154
+ * form.errors.email // ValidationError[] | undefined
2155
+ * form.setValue('email', 'a@b.c')
2156
+ * form.handleSubmit(onSubmit) // returns a submit handler
2157
+ * form.meta.isSubmitting // form-level reactive flag
2158
+ * ```
2159
+ */
2160
+ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form> = {
2161
+ /**
2162
+ * Wraps your submit logic with validation and error routing.
2163
+ *
2164
+ * ```ts
2165
+ * <form @submit.prevent="form.handleSubmit(
2166
+ * (data) => api.signup(data),
2167
+ * (errors) => console.log(errors),
2168
+ * )">
2169
+ * ```
2170
+ *
2171
+ * `data` is the strictly-typed parsed value — refinements have
2172
+ * fired, so every leaf is guaranteed to satisfy its schema-level
2173
+ * format / range / membership constraints.
2174
+ */
2175
+ handleSubmit: HandleSubmit<Form>;
2176
+ /**
2177
+ * Reactive readonly proxy over the form's storage value. Read
2178
+ * identically in script and template — no `.value`, no auto-unwrap
2179
+ * rules. Pinia setup-store pattern.
2180
+ *
2181
+ * ```vue
2182
+ * <script setup>
2183
+ * const form = useForm({ schema, key: 'login' })
2184
+ * </script>
2185
+ *
2186
+ * <template>
2187
+ * <p>{{ form.values.email }}</p>
2188
+ * <p>{{ form.values.address.city }}</p>
2189
+ * </template>
2190
+ * ```
2191
+ *
2192
+ * Writes are blocked at the proxy boundary — go through `setValue`,
2193
+ * the directive, or one of the field-array helpers. The
2194
+ * slim-primitive write gate stays the only path into storage.
2195
+ *
2196
+ * Reads reflect what's storable: enum-typed slots widen to their
2197
+ * primitive supertype (`string`), so refinement-invalid but
2198
+ * structurally-valid values are visible. Use `handleSubmit` /
2199
+ * `validateAsync()` when you need the post-validation strict type.
2200
+ */
2201
+ values: ValuesSurface<WriteShape<GetValueFormType>>;
2202
+ /**
2203
+ * Reactive per-field state proxy. Pinia-style nested object — read
2204
+ * leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
2205
+ * `focused`, `blank`, …) directly off the field's path:
2206
+ *
2207
+ * ```vue
2208
+ * <p v-if="form.fields.email.touched && form.fields.email.errors.length">
2209
+ * {{ form.fields.email.errors[0].message }}
2210
+ * </p>
2211
+ * <p>City dirty? {{ form.fields.address.city.dirty }}</p>
2212
+ * ```
2213
+ *
2214
+ * The same proxy supports descent at every level — `address` reads
2215
+ * the FieldStateLeaf for the address object, and `address.city`
2216
+ * descends into the nested leaf.
2217
+ *
2218
+ * Leaf values follow the slim WriteShape contract: enum-typed leaves
2219
+ * widen to their primitive supertype. The errors array, dirty flag,
2220
+ * focus state, etc. are unaffected.
2221
+ *
2222
+ * Shadowing: at depth 2+, FieldStateLeaf keys (`dirty`, `touched`,
2223
+ * `errors`, `blank`, `focused`, `blurred`, `value`,
2224
+ * `original`, `pristine`, `isConnected`, `updatedAt`, `path`) win
2225
+ * over schema field names. Top-level fields are NOT shadowed.
2226
+ * Document edge case; rename the offending schema field if the
2227
+ * collision matters.
2228
+ */
2229
+ fields: FieldStateMap<WriteShape<GetValueFormType>>;
2230
+ /**
2231
+ * Write to the form programmatically. Two forms:
2232
+ *
2233
+ * - `setValue(value)` — replace the whole form.
2234
+ * - `setValue(path, value)` — write at a specific path.
2235
+ *
2236
+ * Either takes a callback in place of `value` to derive the next
2237
+ * value from the previous one:
2238
+ *
2239
+ * ```ts
2240
+ * form.setValue('count', (prev) => prev + 1)
2241
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
2242
+ * ```
2243
+ *
2244
+ * Returns `true` when the write is accepted. A `false` return
2245
+ * means the value didn't match the slot's expected type
2246
+ * (e.g. writing a number to a string field) — the form state
2247
+ * stays unchanged. Refinement-level mismatches (out-of-enum
2248
+ * values, failing format checks, etc.) DO succeed and surface as
2249
+ * field errors instead.
2250
+ */
2251
+ setValue: {
2252
+ /**
2253
+ * Replace the whole form. Pass a value or a callback receiving
2254
+ * the previous form.
2255
+ *
2256
+ * ```ts
2257
+ * form.setValue({ name: 'Ada', email: 'a@b.c' })
2258
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
2259
+ * ```
2260
+ *
2261
+ * Returns `true` when the write was accepted, `false` when the
2262
+ * value didn't match the expected shape (e.g. wrong primitive
2263
+ * type at a leaf). Refinement-level mismatches (out-of-enum
2264
+ * values, failing format checks, etc.) succeed and surface as
2265
+ * field errors instead.
2266
+ */
2267
+ <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
2268
+ /**
2269
+ * Write at a specific path. Pass a value or a callback receiving
2270
+ * the previous value at that path.
2271
+ *
2272
+ * ```ts
2273
+ * form.setValue('email', 'a@b.c')
2274
+ * form.setValue('count', (prev) => prev + 1)
2275
+ * form.setValue('income', unset) // numeric leaf marked displayed-empty
2276
+ * ```
2277
+ *
2278
+ * Returns `true` when the write was accepted, `false` when the
2279
+ * value didn't match the slot's expected primitive type.
2280
+ * Refinement-level mismatches succeed and surface as field
2281
+ * errors. Pass the `unset` symbol at any primitive leaf to mark
2282
+ * it blank (storage holds the slim default; UI displays
2283
+ * empty; submit raises "No value supplied" for required schemas).
2284
+ */
2285
+ <Path extends FlatPath<Form>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, Path>>, NonNullable<WriteShape<NestedType<Form, Path>>>>>(path: Path, value: Value): boolean;
2286
+ };
2287
+ /**
2288
+ * Reactive validation status. Re-runs whenever the form (or the
2289
+ * subtree at `path`) mutates. The returned ref carries a `pending`
2290
+ * flag — gate on `!status.value.pending` before reading
2291
+ * `success` / `errors`.
2292
+ *
2293
+ * ```ts
2294
+ * const status = form.validate()
2295
+ * watchEffect(() => {
2296
+ * if (status.value.pending) return
2297
+ * if (!status.value.success) console.log(status.value.errors)
2298
+ * })
2299
+ * ```
2300
+ *
2301
+ * Stale in-flight runs are dropped automatically — the ref only
2302
+ * settles to results from the most recent call.
2303
+ */
2304
+ validate: (path?: FlatPath<Form>) => Readonly<Ref<ReactiveValidationStatus<Form>>>;
2305
+ /**
2306
+ * Run validation once and return the result. Unlike `validate()`,
2307
+ * this does not subscribe to form reactivity.
2308
+ *
2309
+ * ```ts
2310
+ * const result = await form.validateAsync()
2311
+ * if (!result.success) showErrors(result.errors)
2312
+ * ```
2313
+ *
2314
+ * Pass a path to validate a subtree. `state.isValidating` flips
2315
+ * `true` while the promise is in flight.
2316
+ */
2317
+ validateAsync: (path?: FlatPath<Form>) => Promise<ValidationResponseWithoutValue<Form>>;
2318
+ /**
2319
+ * Bind a path to a native input via `v-register`. Returns a
2320
+ * `RegisterValue` carrying the live ref and event handlers the
2321
+ * directive needs.
2322
+ *
2323
+ * ```vue
2324
+ * <input v-register="form.register('email')" />
2325
+ * <input
2326
+ * type="password"
2327
+ * v-register="form.register('password', { persist: true, acknowledgeSensitive: true })"
2328
+ * />
2329
+ * ```
2330
+ *
2331
+ * Pass `options.persist` to opt into the form's persistence
2332
+ * pipeline. Persistence requires `useForm({ persist })` configured
2333
+ * for storage activity to actually happen.
2334
+ */
2335
+ register: <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions) => RegisterValue<NestedReadType<WriteShape<Form>, Path>>;
2336
+ /**
2337
+ * The form's identifier — either the explicit `key` passed to
2338
+ * `useForm` or an auto-generated unique id when `key` was omitted.
2339
+ * Use it when feeding API errors through `parseApiErrors`:
2340
+ *
2341
+ * ```ts
2342
+ * const result = parseApiErrors(serverPayload, { formKey: form.key })
2343
+ * if (result.ok) form.setFieldErrors(result.errors)
2344
+ * ```
2345
+ */
2346
+ key: FormKey;
2347
+ /**
2348
+ * Reactive map of field errors, keyed by dotted path. Populated
2349
+ * automatically by `handleSubmit` and per-field validation; cleared
2350
+ * on validation success.
2351
+ *
2352
+ * Read in templates with no `.value`:
2353
+ *
2354
+ * ```vue
2355
+ * <p v-if="form.errors.email">{{ form.errors.email[0].message }}</p>
2356
+ * ```
2357
+ *
2358
+ * Watch from script via the getter form:
2359
+ *
2360
+ * ```ts
2361
+ * watch(() => form.errors.email, (errors) => …)
2362
+ * ```
2363
+ *
2364
+ * Use bracket access for nested dotted keys
2365
+ * (`form.errors['user.profile.email']`) — JS dot notation splits
2366
+ * on literal dots.
2367
+ *
2368
+ * Read-only — populate via `setFieldErrors`, `addFieldErrors`, and
2369
+ * `clearFieldErrors`. Server-side errors flow through
2370
+ * `parseApiErrors` first.
2371
+ */
2372
+ errors: FormErrorsSurface<Form>;
2373
+ /**
2374
+ * Escape hatch for the rare case a consumer needs a `Ref<T>` —
2375
+ * e.g. handing the value to an external composable that expects a
2376
+ * Vue ref, or watching a single path with `watch(formRef, ...)`.
2377
+ *
2378
+ * ```ts
2379
+ * const emailRef = form.toRef('email') // Readonly<Ref<string>>
2380
+ * watch(emailRef, (next) => console.log(next))
2381
+ * ```
2382
+ *
2383
+ * Returns `Readonly<Ref<...>>` — writes go through `setValue`,
2384
+ * `register()`, or the field-array helpers, never via the ref.
2385
+ * Prefer `form.values.email` for direct reads in templates +
2386
+ * scripts; `toRef` is for ref-shaped interop only.
2387
+ */
2388
+ toRef: <Path extends FlatPath<Form>>(path: Path) => Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
2389
+ /**
2390
+ * Replace every field error for this form with the provided list.
2391
+ * Useful after `parseApiErrors` produces a fresh batch from a
2392
+ * server response.
2393
+ *
2394
+ * ```ts
2395
+ * const result = parseApiErrors(payload, { formKey: form.key })
2396
+ * if (result.ok) form.setFieldErrors(result.errors)
2397
+ * ```
2398
+ */
2399
+ setFieldErrors: (errors: ValidationError[]) => void;
2400
+ /**
2401
+ * Append errors to the existing set without clearing prior entries.
2402
+ * Use when reporting an additional issue alongside existing errors
2403
+ * (e.g. a partial server response).
2404
+ */
2405
+ addFieldErrors: (errors: ValidationError[]) => void;
2406
+ /**
2407
+ * Clear errors. Pass a path to clear errors for a single field;
2408
+ * call with no arguments to clear every error on the form.
2409
+ *
2410
+ * ```ts
2411
+ * form.clearFieldErrors('email') // clear one field
2412
+ * form.clearFieldErrors() // clear all
2413
+ * ```
2414
+ */
2415
+ clearFieldErrors: (path?: string | (string | number)[]) => void;
2416
+ /**
2417
+ * Form-level reactive flags, counters, and aggregates (`isDirty`,
2418
+ * `isValid`, `isSubmitting`, `submitCount`, `canUndo`,
2419
+ * `historySize`, and the flat `errors` array). See `FormMeta` for
2420
+ * the full shape. Read leaves directly with no `.value`.
2421
+ *
2422
+ * For per-field state (touched, focused, blurred, errors at one
2423
+ * path), use `form.fields.<path>` instead.
2424
+ */
2425
+ meta: FormMeta;
2426
+ /**
2427
+ * Restore the form to its initial state. Without arguments,
2428
+ * re-applies the schema defaults (and any `defaultValues` passed
2429
+ * to `useForm`). Pass `nextDefaultValues` to seed the reset with
2430
+ * a fresh set of overrides.
2431
+ *
2432
+ * Resets:
2433
+ * - the form value back to defaults;
2434
+ * - the dirty baseline (so the next edit flips `isDirty` correctly);
2435
+ * - field errors;
2436
+ * - touched / focused / blurred per-field flags;
2437
+ * - submission state (`isSubmitting` / `submitCount` / `submitError`);
2438
+ * - the persisted draft, if persistence is configured.
2439
+ *
2440
+ * The next edit on a still-mounted opted-in input will start
2441
+ * persisting again automatically.
2442
+ */
2443
+ reset: (nextDefaultValues?: DeepPartial<DefaultValuesShape<Form>>) => void;
2444
+ /**
2445
+ * Restore a single field (or a sub-tree like `'user'`) to its
2446
+ * initial value. Clears errors and touched flags for the field
2447
+ * and its descendants; leaves siblings and submission state alone.
2448
+ *
2449
+ * No-op when the path doesn't exist on the form (e.g. a typo'd
2450
+ * dynamic key).
2451
+ *
2452
+ * If persistence is configured, the matching subpath is removed
2453
+ * from the persisted draft too.
2454
+ */
2455
+ resetField: (path: FlatPath<Form>) => void;
2456
+ /**
2457
+ * Write the current value at `path` to storage immediately. Useful
2458
+ * for explicit "Save draft" buttons, `beforeunload` handlers, or
2459
+ * multi-step checkpoints where the user shouldn't wait for the
2460
+ * debounce window.
2461
+ *
2462
+ * Bypasses both the per-field opt-in and the debouncer. Existing
2463
+ * paths in the persisted draft are preserved (this is a merge,
2464
+ * not a replace).
2465
+ *
2466
+ * Throws `SensitivePersistFieldError` for sensitive-looking paths
2467
+ * unless you pass `{ acknowledgeSensitive: true }`. No-op when
2468
+ * `useForm({ persist })` wasn't configured.
2469
+ */
2470
+ persist: (path: FlatPath<Form>, options?: {
2471
+ acknowledgeSensitive?: boolean;
2472
+ }) => Promise<void>;
2473
+ /**
2474
+ * Remove data from the persisted draft. Without arguments, wipes
2475
+ * the entire entry. With a path, removes just that subpath.
2476
+ *
2477
+ * Does not change the in-memory form state — pair with `reset()`
2478
+ * / `resetField()` if you need both. Future edits to still-mounted
2479
+ * opted-in fields will re-populate the entry. No-op when
2480
+ * persistence isn't configured.
2481
+ */
2482
+ clearPersistedDraft: (path?: FlatPath<Form>) => Promise<void>;
2483
+ /**
2484
+ * Revert the form to the previous snapshot. Returns `true` when a
2485
+ * snapshot was restored, `false` when there's nothing to undo.
2486
+ * No-op (returns `false`) when `useForm({ history })` wasn't configured.
2487
+ */
2488
+ undo: () => boolean;
2489
+ /**
2490
+ * Replay a previously-undone snapshot. Returns `true` on success,
2491
+ * `false` when the redo stack is empty. The redo stack clears as
2492
+ * soon as a new mutation lands.
2493
+ */
2494
+ redo: () => boolean;
2495
+ /**
2496
+ * Focus the first errored field's first visible element. Returns
2497
+ * `true` when an element was focused, `false` when no candidate
2498
+ * element exists (no errors, or every errored field is unmounted
2499
+ * or hidden).
2500
+ *
2501
+ * Pass `{ preventScroll: true }` if you're scrolling separately
2502
+ * (e.g. via `scrollToFirstError`) and don't want the browser to
2503
+ * fight the explicit scroll.
2504
+ */
2505
+ focusFirstError: (options?: {
2506
+ preventScroll?: boolean;
2507
+ }) => boolean;
2508
+ /**
2509
+ * Scroll the first errored field's first visible element into
2510
+ * view. Returns `true` when the call ran, `false` when no
2511
+ * candidate element exists.
2512
+ *
2513
+ * `options` is forwarded to `Element.scrollIntoView` unchanged.
2514
+ */
2515
+ scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
2516
+ /**
2517
+ * Append `value` to the array at `path`.
2518
+ *
2519
+ * ```ts
2520
+ * form.append('items', { name: 'New' })
2521
+ * ```
2522
+ */
2523
+ append: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
2524
+ /** Prepend `value` to the array at `path`. */
2525
+ prepend: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
2526
+ /**
2527
+ * Insert `value` into the array at `path` at the given `index`.
2528
+ * Behaves like `Array.prototype.splice`: `index` is clamped into
2529
+ * `[0, length]`, and negative indices count from the end.
2530
+ */
2531
+ insert: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
2532
+ /** Remove the element at `index` from the array at `path`. No-op when out of range. */
2533
+ remove: <Path extends ArrayPath<Form>>(path: Path, index: number) => void;
2534
+ /** Swap the elements at indices `a` and `b`. No-op when either is out of range. */
2535
+ swap: <Path extends ArrayPath<Form>>(path: Path, a: number, b: number) => void;
2536
+ /**
2537
+ * Move the element at `from` to `to`. Useful for drag-and-drop
2538
+ * reordering. No-op when either index is out of range.
2539
+ */
2540
+ move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
2541
+ /** Replace the element at `index` with `value`. No-op when out of range. */
2542
+ replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
2543
+ /**
2544
+ * Read-only view of the form's blank path set. Each entry
2545
+ * is a canonical `PathKey` (the `JSON.stringify(segments)` form
2546
+ * `canonicalizePath` produces). The set is reactive — Vue 3.5
2547
+ * tracks `.has()` / `for..of` / size accesses, so consumers can
2548
+ * drive conditional UI off it directly:
2549
+ *
2550
+ * ```ts
2551
+ * watchEffect(() => {
2552
+ * if (form.blankPaths.value.size > 0) {
2553
+ * console.warn('unanswered fields:', [...form.blankPaths.value])
2554
+ * }
2555
+ * })
2556
+ * ```
2557
+ *
2558
+ * For per-path access, use `form.fields.<path>.blank`.
2559
+ * Writes happen through `setValue(path, unset)`,
2560
+ * `markBlank()` on a register binding, and the directive's
2561
+ * input listener on numeric clear. Mutating the snapshot returned
2562
+ * here does nothing — it's `Object.freeze`-d.
2563
+ */
2564
+ blankPaths: ComputedRef<ReadonlySet<string>>;
2565
+ };
2566
+
2567
+ export { ROOT_PATH as L, ROOT_PATH_KEY as Q, canonicalizePath as ac, isUnset as ad, parseDottedPath as ae, unset as af };
2568
+ export type { RegisterTransform as $, AttaformDefaults as A, PendingValidationStatus as B, CoercionRegistry as C, DeepPartial as D, PersistConfig as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, PersistConfigOptions as J, PersistIncludeMode as K, MetaTrackerValue as M, NestedReadType as N, OnError as O, Path as P, RegisterValue as R, SlimPrimitiveKind as S, ReactiveValidationStatus as T, UseFormConfiguration as U, ValidationError as V, RegisterDirective as W, RegisterFlatPath as X, RegisterOptions as Y, RegisterSelectModifier as Z, RegisterTextModifier as _, CoercionEntry as a, Segment as a0, SetValueCallback as a1, SetValuePayload as a2, SettledValidationStatus as a3, SlimRuntimeOf as a4, SubmitHandler as a5, Unset as a6, ValidateOn as a7, ValidateOnConfig as a8, ValidationResponse as a9, ValidationResponseWithoutValue as aa, WriteMeta as ab, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, CoercionResult as i, CustomDirectiveRegisterAssignerFn as j, DefaultValuesResponse as k, FieldState as l, FieldStateLeaf as m, FieldStateMap as n, FieldStateMapEntry as o, FlatPath as p, FormErrorRecord as q, FormErrorsSurface as r, FormMeta as s, FormStorage as t, FormStorageKind as u, HistoryConfig as v, NestedType as w, OnInvalidSubmitPolicy as x, OnSubmit as y, PathKey as z };