attaform 0.30.0 → 0.31.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 (71) hide show
  1. package/dist/abstract.d.mts +15 -21
  2. package/dist/abstract.mjs +3 -3
  3. package/dist/dev/abstract.mjs +4 -4
  4. package/dist/dev/chunks/devtools.mjs +4 -4
  5. package/dist/dev/directive.mjs +1 -1
  6. package/dist/dev/history.mjs +2 -2
  7. package/dist/dev/index.mjs +5 -5
  8. package/dist/dev/runtime/plugins/attaform.mjs +1 -1
  9. package/dist/dev/shared/{attaform-dev.DUZckyV8.mjs → attaform-dev.B1q-x9R9.mjs} +4 -4
  10. package/dist/dev/shared/{attaform-dev.BcYT6tFh.mjs → attaform-dev.CZz4GYZ6.mjs} +21 -9
  11. package/dist/dev/shared/{attaform-dev.BisFstxh.mjs → attaform-dev.Caibt7sb.mjs} +16 -15
  12. package/dist/dev/shared/{attaform-dev.DTrHonpc.mjs → attaform-dev.CtyqGia7.mjs} +176 -260
  13. package/dist/dev/shared/{attaform-dev.BK6Ge2qR.mjs → attaform-dev.DJ3nsWw6.mjs} +86 -122
  14. package/dist/dev/shared/{attaform-dev.BcNtyOmf.mjs → attaform-dev.DlaT4xGd.mjs} +4 -4
  15. package/dist/dev/shared/{attaform-dev.D6CLvaQT.mjs → attaform-dev.k5X47GMD.mjs} +520 -395
  16. package/dist/dev/shared/{attaform-dev.CQ2rJiAX.mjs → attaform-dev.y9x9Vp9F.mjs} +324 -234
  17. package/dist/dev/shared/{attaform-dev.BQc5QV3Y.mjs → attaform-dev.zlvDr0WB.mjs} +2 -2
  18. package/dist/dev/zod-v3.mjs +6 -6
  19. package/dist/dev/zod-v4.mjs +6 -6
  20. package/dist/dev/zod.mjs +5 -5
  21. package/dist/directive.d.mts +4 -4
  22. package/dist/directive.mjs +1 -1
  23. package/dist/esbuild.d.mts +11 -0
  24. package/dist/esbuild.mjs +1 -1
  25. package/dist/history.d.mts +3 -32
  26. package/dist/history.mjs +2 -2
  27. package/dist/index.d.mts +67 -70
  28. package/dist/index.mjs +4 -4
  29. package/dist/nuxt.d.mts +4 -3
  30. package/dist/nuxt.mjs +3 -6
  31. package/dist/rollup.d.mts +11 -0
  32. package/dist/rollup.mjs +1 -1
  33. package/dist/rspack.d.mts +19 -11
  34. package/dist/rspack.mjs +1 -1
  35. package/dist/runtime/components/AttaformDevtoolsPanel.vue +15 -15
  36. package/dist/runtime/components/DevtoolsValueTree.vue +2 -2
  37. package/dist/runtime/plugins/attaform.mjs +1 -1
  38. package/dist/shared/{attaform.BRDDQeHV.mjs → attaform.8mfKZCwH.mjs} +2 -2
  39. package/dist/shared/{attaform.DRUT703x.d.mts → attaform.B-Tb62qV.d.mts} +12 -12
  40. package/dist/shared/{attaform.v0fQtWyA.mjs → attaform.B1rH9ht6.mjs} +1 -1
  41. package/dist/shared/{attaform.DhnPb6nK.mjs → attaform.BAf3UNn4.mjs} +1 -1
  42. package/dist/shared/{attaform.BXneBxEj.mjs → attaform.BWUn8Kjy.mjs} +85 -121
  43. package/dist/shared/{attaform.WiGdIqgx.mjs → attaform.C4shmwyJ.mjs} +2 -2
  44. package/dist/shared/{attaform.Rc9u4Vfw.d.mts → attaform.CRVe8s82.d.mts} +83 -95
  45. package/dist/shared/attaform.D9RqPgGu.d.mts +42 -0
  46. package/dist/shared/attaform.DAo0QBmS.d.mts +2173 -0
  47. package/dist/shared/{attaform.BK0cgu2K.mjs → attaform.DBCbxlyk.mjs} +62 -4
  48. package/dist/shared/{attaform.CUfrMK7t.mjs → attaform.DL2nl_QR.mjs} +15 -14
  49. package/dist/shared/{attaform.DNJOWjQw.mjs → attaform.DNPJPOO7.mjs} +299 -218
  50. package/dist/shared/attaform.DZnrDrNb.d.mts +4657 -0
  51. package/dist/shared/{attaform.B3resMQ1.mjs → attaform.DxKQsqFZ.mjs} +496 -371
  52. package/dist/shared/{attaform.ByWP6kSx.mjs → attaform.L011tHgJ.mjs} +3 -3
  53. package/dist/shared/{attaform.CJ8DpRGj.mjs → attaform.LUPC-2Qg.mjs} +175 -257
  54. package/dist/transforms.d.mts +153 -26
  55. package/dist/transforms.mjs +1 -1
  56. package/dist/vite.d.mts +29 -52
  57. package/dist/vite.mjs +3 -3
  58. package/dist/webpack.d.mts +20 -12
  59. package/dist/webpack.mjs +1 -1
  60. package/dist/zod-v3.d.mts +90 -96
  61. package/dist/zod-v3.mjs +5 -5
  62. package/dist/zod-v4.d.mts +120 -118
  63. package/dist/zod-v4.mjs +5 -5
  64. package/dist/zod.d.mts +5 -5
  65. package/dist/zod.mjs +4 -4
  66. package/package.json +7 -6
  67. package/skills/attaform/SKILL.md +1 -1
  68. package/skills/attaform/references/custom-components.md +4 -0
  69. package/dist/shared/attaform.Clz0ArIi.d.mts +0 -44
  70. package/dist/shared/attaform.UGyOK83T.d.mts +0 -2366
  71. package/dist/shared/attaform.bKLzMU0o.d.mts +0 -4761
@@ -1,4761 +0,0 @@
1
- import { ObjectDirective, Ref, ComputedRef, MaybeRefOrGetter } from 'vue';
2
-
3
- /**
4
- * Schema-attached field metadata — the shared types used by both Zod
5
- * adapters and the unified `attaform/zod` entry so a consumer's data
6
- * flow reads the same shape regardless of which path runs at lookup.
7
- *
8
- * Storage lives in the cross-adapter `field-meta-store` core: a pair
9
- * of WeakMaps (single-payload for last-write-wins reads, list-of-
10
- * payloads for shared-schema disambiguation). Every entry's
11
- * `fieldMeta` re-exports the same registry-shaped object, so
12
- * `withMeta`/`fieldMeta.add` writes from one entry surface at lookup
13
- * through any other.
14
- *
15
- * `withMeta(schema, payload)` clones the schema before registering,
16
- * so each call gets fresh identity (the WeakMap keys on reference).
17
- * The cloning strategy depends on the major: Zod 4 schemas use the
18
- * native `.clone()`, Zod 3 schemas reconstruct via constructor +
19
- * `_def`. The unified entry's `withMeta` runtime-branches on which
20
- * one is in play.
21
- *
22
- * Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
23
- * which returns a fully-resolved `ResolvedFieldMeta` (label /
24
- * description / placeholder / meta) so the per-leaf and per-container
25
- * `FieldState` producers in core never see the version split.
26
- */
27
- /**
28
- * The metadata a consumer attaches to a schema node — short label
29
- * (presentational), longer description (helper text), placeholder
30
- * (input affordance). Declared as `interface` (not `type`) so
31
- * downstream apps can extend the shape via TypeScript declaration
32
- * merging when they want to register richer payloads (tooltips,
33
- * icons, badge counts, etc.):
34
- *
35
- * declare module 'attaform/zod' {
36
- * interface FieldMetaPayload {
37
- * tooltip?: string
38
- * }
39
- * }
40
- *
41
- * After augmentation, `withMeta(schema, { tooltip: '…' })` is typed
42
- * and `state.meta.tooltip` reads back as `string | undefined`.
43
- *
44
- * Every key is optional. Empty payloads (no keys registered) are
45
- * indistinguishable from "not registered at all" — both surface as
46
- * fallbacks (humanize for label, undefined for the rest).
47
- */
48
- interface FieldMetaPayload {
49
- label?: string;
50
- description?: string;
51
- placeholder?: string;
52
- }
53
- /**
54
- * The fully-resolved metadata returned by
55
- * `AbstractSchema.getFieldMetaAtPath(path)`. Adapters apply the
56
- * precedence rules:
57
- *
58
- * - `label`: registry payload → `humanize(lastSegment)`
59
- * - `description`: registry payload → schema's `.describe()` value → `undefined`
60
- * - `placeholder`: registry payload → `undefined`
61
- * - `meta`: full registered payload, frozen — empty object if nothing registered
62
- *
63
- * `label` is always a non-empty string at leaves (humanize fallback
64
- * guarantees this for any non-numeric segment). For containers it
65
- * may collapse to the empty string when the path is empty (root) or
66
- * the segment is a numeric index — callers display "" or substitute
67
- * a context-appropriate fallback.
68
- */
69
- type ResolvedFieldMeta = {
70
- readonly label: string;
71
- readonly description: string | undefined;
72
- readonly placeholder: string | undefined;
73
- readonly meta: Readonly<FieldMetaPayload>;
74
- };
75
-
76
- /**
77
- * Path primitives for advanced integrations. The form library accepts
78
- * paths in dotted-string form (`'user.email'`) at every public API.
79
- * These primitives are exposed for adapter authors who need to
80
- * canonicalise user-provided paths.
81
- */
82
- declare const pathKeyBrand: unique symbol;
83
- /**
84
- * Branded string identifier for a canonicalised path. Useful as a
85
- * `Map` key — two paths that resolve to the same canonical form
86
- * produce the same `PathKey`. Treat as opaque; don't try to parse.
87
- */
88
- type PathKey = string & {
89
- readonly [pathKeyBrand]: 'PathKey';
90
- };
91
- /** A single path segment — a property name or array index. */
92
- type Segment = string | number;
93
- /** A structured path as a read-only sequence of segments. */
94
- type Path = readonly Segment[];
95
- /**
96
- * Parse a dotted-string path into structured segments.
97
- *
98
- * ```ts
99
- * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
100
- * parseDottedPath('items.0.name') // ['items', 0, 'name']
101
- * parseDottedPath('') // [''] (the empty-string key)
102
- * ```
103
- *
104
- * The empty-string input `''` is the **literal empty-key path** `['']`,
105
- * an ordinary (if rare) field address, not the root. Use the array
106
- * form `[]` for the root. Form-level errors (root `.refine()`,
107
- * `setErrors` with no path) live at the root path `[]` and are read via
108
- * `errors([])`, never `errors('')`.
109
- *
110
- * Throws `InvalidPathError` for paths with empty INTERNAL segments
111
- * (`'a..b'`, leading or trailing dots). For keys containing literal
112
- * dots, pass an array form (`['user.name']`) instead.
113
- */
114
- declare function parseDottedPath(path: string): Segment[];
115
- /**
116
- * Canonicalise a path into structured segments plus a stable string
117
- * key. Accepts either dotted-string or array form; integer-looking
118
- * segments normalise to numbers.
119
- *
120
- * ```ts
121
- * canonicalizePath('items.0.name')
122
- * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
123
- *
124
- * canonicalizePath(['items', 0, 'name'])
125
- * // → same result
126
- * ```
127
- *
128
- * The returned `key` is suitable as a `Map`/`Set` key — equal paths
129
- * produce equal keys regardless of input form.
130
- */
131
- declare function canonicalizePath(input: string | Path): {
132
- segments: readonly Segment[];
133
- key: PathKey;
134
- };
135
- /**
136
- * The root path — an empty segment tuple. Pass to APIs that accept a
137
- * `Path` to address the form value as a whole, and the home for
138
- * form-level (global) errors: root `.refine()` messages, hydration
139
- * failures, and `setErrors` entries with no path all live at `[]`. Aggregate
140
- * reads (`errors()`, `errors([])`, `meta.errors`) surface them alongside
141
- * field errors; `meta.ownErrors` returns the root bucket alone.
142
- *
143
- * The empty SEGMENT tuple `[]` is structurally unconstructible as a
144
- * field path, so it can never collide with a schema key, unlike the
145
- * empty STRING key `''` (path `['']`, key `'[""]'`), which is an
146
- * ordinary field address.
147
- */
148
- declare const ROOT_PATH: Path;
149
- /** Stable string key for the root path. */
150
- declare const ROOT_PATH_KEY: PathKey;
151
- /**
152
- * `true` when `path` starts with every segment of `prefix` (in order).
153
- * The empty `prefix` matches every path — ROOT prefix is universal.
154
- *
155
- * Walks segments rather than `PathKey` strings because the data this
156
- * helper operates on (e.g. `meta.errors[].path`) carries segment
157
- * arrays directly.
158
- *
159
- * ```ts
160
- * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
161
- * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
162
- * isPathPrefix([], ['anything']) // true (root prefix)
163
- * ```
164
- */
165
- declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
166
-
167
- /**
168
- * Per-path field status. Replaced wholesale (not mutated in place) on
169
- * every change. Five semantic groups:
170
- *
171
- * - `connected` — is a DOM element registered for this path?
172
- * - `focused` / `blurred` — DOM-state flags. `null` while no element
173
- * is connected (no DOM means the concepts don't apply); plain
174
- * booleans once connected, with the invariant `blurred === !focused`
175
- * enforced by `markFocused`.
176
- * - `touched` — focus/blur history, not DOM state. Always a plain
177
- * boolean: `false` at registration, sticky `true` after first blur,
178
- * cleared only by `form.reset()` / `form.resetField(path)`. Persists
179
- * across disconnects so v-if'd-away fields don't lose their touched
180
- * state on rehide (wizard "show review of touched fields" patterns
181
- * rely on this).
182
- * - `interacted` — value-mutation history, not DOM state. Plain
183
- * boolean: `false` at registration, sticky `true` once the user
184
- * issues a value edit through the directive's input listeners
185
- * (never on hydration, default seeding, or programmatic setValue);
186
- * cleared with `touched` by `form.reset()` / `form.resetField(path)`.
187
- * - `blurredAfterInteraction` — the first blur that follows a value
188
- * edit (the field has been edited and then left). Plain boolean,
189
- * sticky `true`. A tab-through blur with no prior edit does NOT set
190
- * it (`interacted` is still false at that blur). Composes
191
- * `interacted` with the departure; drives the default display gate.
192
- */
193
- type FieldRecord = {
194
- readonly path: Path;
195
- readonly updatedAt: string | null;
196
- readonly connected: boolean;
197
- readonly focused: boolean | null;
198
- readonly blurred: boolean | null;
199
- readonly touched: boolean;
200
- readonly interacted: boolean;
201
- readonly blurredAfterInteraction: boolean;
202
- };
203
- /** Per-path DOM element tracking. Client-only. */
204
- type ElementRecord = {
205
- /**
206
- * Original Path captured at first registration. Stored alongside the
207
- * elements Set so the DOM-order sort cache can recover the structured
208
- * Path without round-tripping through `JSON.parse(pathKey)`.
209
- */
210
- readonly path: Path;
211
- readonly elements: Set<HTMLElement>;
212
- };
213
- /**
214
- * Per-path record stored in `originals`. Pairing `segments` with the tracked
215
- * value means `dirty` and `resetField`'s container loop don't have to
216
- * `JSON.parse(pathKey)` on every iteration — the canonical Path is already
217
- * sitting next to the value it belongs to. PathKey still keys the Map (the
218
- * stable string is the only collision-free identifier), but downstream
219
- * iteration reads `segments` directly.
220
- */
221
- type OriginalsRecord = {
222
- readonly segments: Path;
223
- readonly value: unknown;
224
- };
225
-
226
- /** Internal brand for the `Unset` type. Never exposed at runtime. */
227
- declare const _unsetBrand: unique symbol;
228
- /**
229
- * Brand-typed sentinel admitted at every primitive leaf of
230
- * `DefaultValuesShape<T>`, `setValue`, and `reset`. The runtime value
231
- * is exported as {@link unset} under the same name.
232
- */
233
- type Unset = typeof _unsetBrand;
234
- /**
235
- * The `unset` sentinel — pass it as a primitive leaf's value to mark
236
- * the field **displayed-empty** while storage holds the schema's slim
237
- * default (`0` / `''` / `false` / `0n`).
238
- *
239
- * Use it wherever a primitive leaf value is expected:
240
- *
241
- * ```ts
242
- * const form = useForm({
243
- * schema: z.object({ income: z.number() }),
244
- * defaultValues: { income: unset }, // UI starts blank, storage holds 0
245
- * key: 'housing',
246
- * })
247
- *
248
- * form.setValue('income', unset) // re-blank a field after a write
249
- * form.reset({ income: unset }) // reset to the blank state
250
- * ```
251
- *
252
- * Accepted at any `string` / `number` / `boolean` / `bigint` leaf in
253
- * `defaultValues`, `setValue(path, unset)`, and `form.reset({ … })`.
254
- *
255
- * The path joins the form's `blankPaths` set as long as it stays
256
- * unset. Required schemas (no `.optional()` / `.nullable()` /
257
- * `.default(N)`) raise `"No value supplied"` on submit while a leaf
258
- * is in `blankPaths`; optional / nullable / has-default schemas
259
- * accept the empty case as their wrapper allows.
260
- *
261
- * Storage never holds the symbol — the runtime translates it at the
262
- * API boundary, so reads through `form.values` always see the slim
263
- * default. Cross-bundle / SSR-safe: backed by `Symbol.for(...)` so
264
- * every realm gets the same sentinel.
265
- *
266
- * @see {@link isUnset} — type guard that narrows a value back to {@link Unset}.
267
- * @see `docs/validation/blank.md` — the conceptual model behind blank-aware fields.
268
- */
269
- declare const unset: Unset;
270
- /**
271
- * Type guard — `true` when `value` is the `unset` sentinel.
272
- *
273
- * ```ts
274
- * if (isUnset(payload.income)) {
275
- * // payload.income is the sentinel; the field will display empty
276
- * }
277
- * ```
278
- */
279
- declare function isUnset(value: unknown): value is Unset;
280
-
281
- /**
282
- * The minimum shape any form value satisfies — a plain record. Use
283
- * as a constraint for composables that work generically across forms
284
- * (e.g. a custom hook that takes any form's `useForm` return).
285
- */
286
- type GenericForm = Record<string, unknown>;
287
- /** Internal helper — `true` when `T` is an object or array. */
288
- type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
289
- /**
290
- * Shared recursion body backing `PartialFlatPath` and `RegisterFlatPath`.
291
- * One walk over `Form`; `Mode` controls whether intermediate container
292
- * paths are emitted alongside their reachable leaves.
293
- *
294
- * - `'partial'`: emit every container path (object containers, nested-
295
- * object array roots, array-of-object element containers). Used by
296
- * `setValue` / `form.values.<path>` / every read-side API that needs
297
- * to address a container.
298
- * - `'register'`: skip container paths. `v-register` binds onto a
299
- * leaf-backing native element (`<input>`, `<select>`, `<textarea>`),
300
- * so container paths aren't registrable. Primitive arrays still
301
- * admit the array-root path under both modes (multi-select and
302
- * grouped-checkbox bindings register onto the array itself).
303
- *
304
- * Both walkers were independent recursions before; threading `Mode`
305
- * through one body keeps the surface guarded by a single source of
306
- * truth, and `PartialFlatPath` / `RegisterFlatPath` stay byte-
307
- * identical to the prior hand-written walks (see
308
- * `test/types/flat-path-walker.test.ts`).
309
- */
310
- type FlatPathBuilder<Form, Mode extends 'partial' | 'register', 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 ? Mode extends 'partial' ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}` | `${Key}.${number}` : Value extends GenericForm ? Mode extends 'partial' ? `${Key}` | `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${FlatPathBuilder<Form[Key], Mode>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? Mode extends 'partial' ? `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}` : never) : never : never;
311
- /**
312
- * Implementation detail backing `FlatPath` in its default
313
- * (partial-path) mode. Exported so `rollup-plugin-dts` preserves it
314
- * as a named alias in the bundled `.d.ts` rather than inlining the
315
- * full template-literal recursion body at every reference site
316
- * (`FlatPath`, `RegisterFlatPath`, every path-addressed API method).
317
- * Inlining at consumer call sites compounds into TS2589 territory
318
- * when multiple complex forms share a scope. Consumers should reach
319
- * for `FlatPath` instead; this alias is not part of the stable surface.
320
- *
321
- * The `Form extends unknown` wrapper distributes over a top-level
322
- * union so each member contributes its OWN `keyof` — a discriminated-
323
- * union root (`z.discriminatedUnion`) thus exposes every variant's
324
- * keys as addressable paths, not just the discriminator that naked
325
- * `keyof (A | B)` would intersect to. For a single object / array /
326
- * record `Form` it is a one-member no-op and stays byte-identical to
327
- * the prior walk. Interior unions already distribute inside
328
- * `FlatPathBuilder` via its `infer Value` step.
329
- */
330
- type PartialFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'partial'> : never;
331
- /**
332
- * Union of dotted-string paths reachable inside `Form`, e.g. for
333
- * `{ user: { email: string }, items: string[] }`:
334
- *
335
- * `'user' | 'user.email' | 'items' | 'items.0' | 'items.1' | …`
336
- *
337
- * Used by every path-addressed API (`setValue(path, value)`,
338
- * `register(path)`, `toRef(path)`, etc.) so paths autocomplete in
339
- * the IDE and typos compile-error.
340
- */
341
- type FlatPath<Form> = PartialFlatPath<Form>;
342
- /**
343
- * Convert a tuple of path segments to its dotted-string equivalent.
344
- *
345
- * `JoinSegments<['cargo', 'items', 0, 'sku']>` → `'cargo.items.0.sku'`
346
- *
347
- * Recursion depth is bounded by the tuple length (typically 3–4),
348
- * not by form depth — the cost does not scale with `FlatPath<Form>`.
349
- * Template literal types distribute over union members, so segments
350
- * containing unions like `'pickup' | 'delivery'` propagate through
351
- * to the joined path's union: `JoinSegments<['pickup' | 'delivery', 'line1']>`
352
- * → `'pickup.line1' | 'delivery.line1'`. This is what makes
353
- * tuple-form path APIs work cleanly inside `v-for` over a prefix
354
- * variable: the joined result is checked against `FlatPath<Form>` /
355
- * `RegisterFlatPath<Form>` (which already exist), so we don't
356
- * enumerate a separate tuple-path union.
357
- */
358
- type JoinSegments<S extends ReadonlyArray<string | number>, Acc extends string = ''> = S extends readonly [
359
- infer Head extends string | number,
360
- ...infer Rest extends ReadonlyArray<string | number>
361
- ] ? Acc extends '' ? JoinSegments<Rest, `${Head}`> : JoinSegments<Rest, `${Acc}.${Head}`> : Acc;
362
- /**
363
- * The advice half of a segment-array rejection, used when `S` never
364
- * inferred a tuple at all.
365
- *
366
- * A caller who passes a plain `string` reaches this branch, because
367
- * inference falls back to the constraint and `JoinSegments` of a
368
- * non-tuple array is `''`. Naming a path would be a lie there, so the
369
- * message names the remedies instead. This is the case behind #568: a
370
- * row component received its path prefix as an untyped `string` prop,
371
- * every concatenation widened to `string`, and the only signal was
372
- * `segments: never`.
373
- */
374
- type PlainStringAdvice = 'attaform: a plain string cannot be checked against the schema. Pass a literal path, type the dynamic prefix, or use the segment-array form.';
375
- /**
376
- * Why a segment array was rejected, as a type the compiler prints.
377
- *
378
- * Every segment-array overload brands its parameter
379
- * `S & (<joined path is valid> ? unknown : <this>)`. The brand used to
380
- * be `never`, and `S & never` is `never`, so TypeScript reported the
381
- * parameter as `never` and said nothing about what was wrong. A string
382
- * literal intersects without collapsing, so the sentence survives into
383
- * the diagnostic and the argument keeps its inferred tuple type:
384
- *
385
- * ```
386
- * Argument of type '["boxes", 3, "nope"]' is not assignable to parameter
387
- * of type 'readonly ["boxes", 3, "nope"] &
388
- * "attaform: 'boxes.3.nope' is not a path in this form's schema"'.
389
- * ```
390
- *
391
- * Nothing can satisfy the brand, so a wrong path is still a compile
392
- * error; only the message changes.
393
- */
394
- type SegmentPathRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a path in this form's schema`;
395
- /**
396
- * `SegmentPathRejection` for `register`, whose accepted set is
397
- * `RegisterFlatPath` rather than `FlatPath`.
398
- *
399
- * The distinction is worth its own sentence: a container path is in the
400
- * schema and still not registrable, so "not a path in this form's
401
- * schema" would be false where it is most likely to be read.
402
- */
403
- type SegmentRegisterRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a registrable path. v-register binds a leaf input, so container paths are excluded.`;
404
- /**
405
- * `true` when `T` is a union (multiple members), `false` when it's a
406
- * single type. Used to gate non-homomorphic mapped-type forms so
407
- * single-object types retain their homomorphic `[K in keyof T]`
408
- * lookup (preserving literal keys instead of widening to an index
409
- * signature).
410
- */
411
- type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
412
- /**
413
- * Union of all keys across all members of `T`. For a single object
414
- * type this equals `keyof T`; for a discriminated union `A | B`, it
415
- * produces `keyof A | keyof B` (whereas naked `keyof (A | B)` would
416
- * intersect to common keys only).
417
- *
418
- * Paired with `ValueOfUnion` to merge variant key sets in chained
419
- * metadata proxies (`form.fields`, `form.errors`) so per-variant
420
- * leaves are addressable through one chained-access shape, regardless
421
- * of which discriminant is currently active.
422
- */
423
- type KeyofUnion<T> = T extends unknown ? keyof T : never;
424
- /**
425
- * Value at key `K` across union members of `T`. Members containing
426
- * `K` contribute `T[K]`; members lacking `K` contribute `undefined`.
427
- *
428
- * The resulting union mirrors the runtime semantics of metadata
429
- * proxies: chained access works at every union member, with the leaf
430
- * carrying `T | undefined` to reflect that the key is absent in some
431
- * variants and the runtime returns a stable stub there.
432
- */
433
- type ValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : undefined : never;
434
- /**
435
- * Value at key `K` across union members of `T`, dropping members that
436
- * LACK `K` entirely (they contribute `never`, not `undefined`). The
437
- * counterpart to `ValueOfUnion`: where that injects a SYNTHETIC
438
- * `undefined` for absent-variant keys (so chained reads stay safe),
439
- * this yields only the PRESENT value type.
440
- *
441
- * `form.fields` uses it at discriminated-union keys so a variant-only
442
- * field types as node-optional `FieldState<X> | undefined` (the node is
443
- * absent when its variant isn't active) rather than value-optional
444
- * `FieldState<X | undefined>` (which would falsely promise a readable
445
- * node). A genuine `undefined` from an OPTIONAL declaration survives —
446
- * only the synthetic absent-variant `undefined` is stripped.
447
- */
448
- type PresentValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : never : never;
449
- /**
450
- * Apply the discriminated-union "lift" to a value shape (i.e., a
451
- * shape carrying actual values, not metadata leaves like
452
- * `FieldState`). Single-object types map homomorphically;
453
- * discriminated unions of objects merge keys via
454
- * `KeyofUnion` / `ValueOfUnion` so per-variant fields are reachable
455
- * through one chained-access shape.
456
- *
457
- * Used by `ValuesSurface` to make `form.values.cargo.permitNumber`
458
- * (oversized-only) typecheck regardless of the active variant —
459
- * matching the runtime, where plain JS object access on a missing
460
- * variant key returns `undefined` rather than throwing.
461
- *
462
- * Distinct from `FieldStateMapEntry`: that variant carries
463
- * `FieldState<T>` at the leaf; this one carries the leaf VALUE
464
- * directly. They share the same union-merging logic but differ in
465
- * what the recursion bottoms out at.
466
- *
467
- * Date / Map / Set / RegExp / function leaves stay opaque (not
468
- * recursed into) — value reads of those types should preserve the
469
- * platform shape unchanged.
470
- */
471
- type LiftedValueShape<T> = [T] extends [
472
- string | number | boolean | bigint | symbol | null | undefined
473
- ] ? T : [T] extends [
474
- Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown)
475
- ] ? T : [T] extends [ReadonlyArray<unknown>] ? T : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
476
- [K in KeyofUnion<T>]: LiftedValueShape<ValueOfUnion<T, K>>;
477
- } : {
478
- [K in keyof T]: LiftedValueShape<T[K]>;
479
- } : T;
480
- /**
481
- * Recursive `Partial` — every property at every depth is optional.
482
- * Used as the parameter type of `defaultValues` and `reset()` so
483
- * partial overrides at any nesting level are valid.
484
- */
485
- type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem> ? DeepPartial<ArrayItem>[] : T extends object ? {
486
- [Key in keyof T]?: DeepPartial<T[Key]>;
487
- } : T;
488
- /**
489
- * Shared descent body backing `NestedType` and `NestedReadType`. The
490
- * recursion is identical — segment-by-segment, distributing over
491
- * union members via `KeyofUnion` / `ValueOfUnion`. The two walkers
492
- * diverge only at the leaf:
493
- *
494
- * - `TaintArrayCrossings extends false` (`NestedType` mode): leaves
495
- * are returned untouched. Used by strict write-side APIs that need
496
- * the exact resolved type (`setValue`'s value parameter,
497
- * `form.fields.<path>`'s state map).
498
- * - `TaintArrayCrossings extends true` (`NestedReadType` mode): leaves
499
- * are widened with `| undefined` whenever any segment in the walk
500
- * was numeric (array index). Reflects the runtime possibility of an
501
- * out-of-bounds read.
502
- *
503
- * `_Tainted` propagates the array-crossing under taint mode; under
504
- * strict mode it stays `false` through every recursion arm. Both
505
- * arms strip nullishness at the root (`_RootValue = NonNullable<…>`)
506
- * — the prior `NestedType.FilterOutNullishTypesDuringRecursion` flag
507
- * was vestigial, never overridden from the outside.
508
- *
509
- * Not part of the stable consumer surface — reach for `NestedType` or
510
- * `NestedReadType` directly.
511
- */
512
- type NestedTypeBuilder<RootValue, FlattenedPath extends string, TaintArrayCrossings extends boolean, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : Key extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, NumericKey>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : never : never : Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, NumericKey> | undefined : ValueOfUnion<_RootValue, NumericKey> : never : never : FlattenedPath extends KeyofUnion<_RootValue> ? _Tainted extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : never;
513
- /**
514
- * Resolve the type at a dotted-string path inside `RootValue`. Used
515
- * by the strict (write-side) APIs to derive the type at a path:
516
- *
517
- * `NestedType<{ user: { email: string } }, 'user.email'>` → `string`
518
- *
519
- * On discriminated-union descents (e.g. `cargo` is `A | B | C`), uses
520
- * `KeyofUnion` / `ValueOfUnion` so per-variant keys resolve to
521
- * `T | undefined` instead of `never`. This keeps NestedType in lockstep
522
- * with `FlatPath`: any path FlatPath says is reachable resolves to a
523
- * useful value type (vs. silently collapsing to `never` because
524
- * `keyof (A|B|C)` would be the intersection of all variants' keys).
525
- *
526
- * Composed over `NestedTypeBuilder` with array-crossing tainting OFF,
527
- * so leaves return their exact resolved type. The companion
528
- * `NestedReadType` shares the same recursion body but enables
529
- * tainting for read-side APIs.
530
- *
531
- * TypeScript caps conditional-type recursion at around 50 levels;
532
- * paths deeper than that resolve to `never`. Real form schemas
533
- * never reach this depth.
534
- */
535
- type NestedType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, false>;
536
- /**
537
- * Implementation-detail primitive-leaf marker used by `DeepPartial`
538
- * and sibling structural walkers. Exported so the bundled `.d.ts`
539
- * references one alias instead of re-emitting the union at every
540
- * recursion branch of every walker that depends on it. Not part of
541
- * the stable consumer-facing surface — reach for `DeepPartial`
542
- * instead.
543
- */
544
- type Primitive = string | number | boolean | symbol | bigint | null | undefined;
545
- /**
546
- * Distinguish a tuple from a regular array.
547
- *
548
- * `IsTuple<[string, number]>` → `true`
549
- * `IsTuple<string[]>` → `false`
550
- *
551
- * Useful for write-side helpers that need to preserve tuple
552
- * positions instead of widening to `Array<element>`.
553
- */
554
- type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
555
- /**
556
- * Path-resolved type for read-side APIs. Like `NestedType`, but once
557
- * the walk crosses an array index segment the resulting type is
558
- * tagged `| undefined` (the runtime can return undefined for
559
- * out-of-bounds reads). Discriminated-union descents follow the same
560
- * `KeyofUnion`/`ValueOfUnion` rule as `NestedType` — per-variant
561
- * keys resolve to `T | undefined`, agreeing with `FlatPath`.
562
- *
563
- * Used by `form.values.<path>` reads, `form.toRef(path)`, and
564
- * `register(path).innerRef` so the compile-time type honours the
565
- * runtime possibility of a missing array position.
566
- */
567
- type NestedReadType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, true>;
568
- /**
569
- * The array a path actually addresses, nullish stripped, or `never`
570
- * when the path does not address an array. Shared by `ArrayPath`
571
- * (membership) and `ArrayItem` (element type) so the filter and the
572
- * extractor cannot disagree about what counts as one.
573
- *
574
- * The strip is the load-bearing part. `Form` is the schema's INPUT
575
- * shape, so `z.array(row).default([])` resolves to `row[] | undefined`,
576
- * and `NestedType` tags a discriminated union's per-variant keys the
577
- * same way on purpose: that tagging is what keeps `NestedType` in
578
- * lockstep with `FlatPath`, so every path `FlatPath` offers resolves to
579
- * a useful value type. A bare `extends readonly unknown[]` predicate
580
- * read the tag as "not an array", which is how every optional,
581
- * defaulted, nullable, and DU-variant array fell out of the field-array
582
- * helpers while the runtime went on accepting all of them (#541).
583
- *
584
- * `extends infer Leaf` binds the stripped leaf once so the guards below
585
- * do not re-instantiate `NestedType`, and the tuple wraps suppress
586
- * distribution so a union leaf is judged whole: `A[] | B[]` is an
587
- * array, `A[] | string` is not.
588
- *
589
- * Exported so `rollup-plugin-dts` keeps it as a named alias in the
590
- * bundled `.d.ts` instead of inlining the body at every reference.
591
- * Consumers reach for `ArrayPath` / `ArrayItem`; this alias is not part
592
- * of the stable surface.
593
- */
594
- type ArrayLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? Leaf : never : never;
595
- /**
596
- * Filter FlatPath<Form> down to the subset of paths whose resolved leaf
597
- * is an array. Used by the typed field-array helpers (append / remove /
598
- * swap / ...) so those helpers only accept paths that actually address
599
- * an array — calling `append('email', ...)` on a `{ email: string }`
600
- * is a compile error. Optionality is not a disqualifier; see
601
- * `ArrayLeafOf` for what "is an array" means here.
602
- *
603
- * `P extends string` re-triggers distribution over the `FlatPath<Form>`
604
- * union so the conditional evaluates per member. Without it, the
605
- * branch would reduce against the union as a whole and collapse to
606
- * `never` whenever a single member failed the predicate.
607
- */
608
- type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [ArrayLeafOf<Form, P>] extends [never] ? never : P : never;
609
- /**
610
- * Extract the element type of the array addressed by `Path`. Callers
611
- * constrain `Path extends ArrayPath<Form>` so this is always well-defined.
612
- * Rides on `ArrayLeafOf`, the same alias `ArrayPath` admits the path
613
- * with, so an accepted path never hands its helper a `never` value slot.
614
- */
615
- type ArrayItem<Form, Path extends ArrayPath<Form>> = ArrayLeafOf<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
616
- /**
617
- * The record a path actually addresses, nullish stripped, or `never`
618
- * when the path does not address one. Shared by `RecordPath`
619
- * (membership) and `RecordValue` (value type).
620
- *
621
- * `string extends keyof Leaf` is the index-signature probe: it holds for
622
- * `Record<string, V>` (`keyof` is `string`) and fails for a fixed object
623
- * (`keyof` is the literal key union). The leading array guard keeps
624
- * arrays (which also satisfy the object check) out of the record set.
625
- *
626
- * Nullish is stripped first for the reason `ArrayLeafOf` gives, with an
627
- * extra bite here: `keyof (Record<string, V> | undefined)` is `never`,
628
- * so on a `.default({})` or `.optional()` record the probe could not
629
- * fire at all and `form.record(path)` rejected the path (#541).
630
- *
631
- * Exported for the same `.d.ts` reason as `ArrayLeafOf`, and equally
632
- * not part of the stable surface.
633
- */
634
- type RecordLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? never : [Leaf] extends [Record<string, unknown>] ? string extends keyof Leaf ? Leaf : never : never : never;
635
- /**
636
- * Companion to `ArrayPath`: filter `FlatPath<Form>` down to the subset
637
- * of paths whose resolved leaf is a record (an object with an open
638
- * string-keyed index signature, e.g. `z.record(z.string(), V)`). A
639
- * fixed-shape object (`z.object({ ... })`) is excluded — its keys are
640
- * statically known, so it has no `string` index signature.
641
- */
642
- type RecordPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [RecordLeafOf<Form, P>] extends [never] ? never : P : never;
643
- /**
644
- * Value type of the record addressed by `Path` — the `V` in a
645
- * `Record<string, V>`. Callers constrain `Path extends RecordPath<Form>`,
646
- * so the leaf is always an open string-keyed record and this is
647
- * well-defined. Rides on `RecordLeafOf`, the same alias `RecordPath`
648
- * admits the path with, so the two cannot disagree.
649
- */
650
- type RecordValue<Form, Path extends RecordPath<Form>> = RecordLeafOf<Form, Path> extends Record<string, infer Value> ? Value : never;
651
- /**
652
- * Widens primitive-literal leaves to their primitive supertype to
653
- * match the runtime "slim-primitive write contract."
654
- *
655
- * WriteShape<{ color: 'red' | 'green' }>
656
- * // → { color: string }
657
- * WriteShape<{ kind: 'on' }>
658
- * // → { kind: string }
659
- * WriteShape<{ count: 42 }>
660
- * // → { count: number }
661
- *
662
- * The runtime gate accepts any value at a path whose primitive type
663
- * matches the schema's slim primitive set at that path. Refinement-
664
- * level constraints (enum membership, literal equality, format
665
- * checks, length / range bounds, regex, custom predicates) are NOT
666
- * enforced at write time — they surface via field-level validation.
667
- * The type widening here mirrors that runtime behaviour, so
668
- * `setValue('color', 'magenta')` and `defaultValues: { color: 'teal' }`
669
- * are not TS errors despite being out-of-enum at the validation
670
- * layer.
671
- *
672
- * Tuple positions preserve their literal types via the homomorphic
673
- * mapped form (`{ [K in keyof T]: ... }` over a readonly tuple
674
- * preserves the position labels), so `[string, number]` stays a
675
- * 2-tuple of widened primitives instead of collapsing to
676
- * `Array<string | number>`.
677
- *
678
- * Date / RegExp / Map / Set / function instances pass through
679
- * unchanged — those aren't "primitive literals" and the runtime
680
- * accepts them as their own slim kinds. Tuple-detection runs before
681
- * the array-recursion branch so positionally-typed array literals
682
- * survive intact.
683
- *
684
- * `WriteShape<T>` stays STRICT — no `| Unset` widening. It's used for
685
- * the callback's prev-value argument in `setValue(path, prev => ...)`
686
- * (which always receives a real value, never the sentinel), for
687
- * read-side types where consumers expect a structural form shape, and
688
- * by internal types like `FieldStateMap<T>`. The consumer-facing
689
- * write-value type is `DefaultValuesShape<T>` (composed via
690
- * `SetValuePayload`), which adds `| Unset` at every recursable
691
- * position.
692
- */
693
- 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[]] ? {
694
- -readonly [K in keyof T]: WriteShape<T[K]>;
695
- } : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
696
- -readonly [K in keyof T]: WriteShape<T[K]>;
697
- } : Array<WriteShape<U>> : T extends object ? {
698
- [K in keyof T]: WriteShape<T[K]>;
699
- } : T;
700
- /**
701
- * Walk `T` and add `| Unset` at every primitive leaf (except symbol /
702
- * null / undefined), every opaque leaf (`Date`, `RegExp`, `Map`,
703
- * `Set`, functions), and every container position (object, tuple,
704
- * array). The recursion topology mirrors `WriteShape<T>` exactly —
705
- * `DefaultValuesShape<T>` is then a 1-line composition.
706
- *
707
- * Symbol / null / undefined leaves pass through untouched so the
708
- * runtime sentinel doesn't pollute leaf semantics it has no business
709
- * carrying. Container positions widen so a single `unset` at any
710
- * level recursively marks every descendant primitive blank.
711
- *
712
- * Not part of the stable consumer-facing surface — reach for
713
- * `DefaultValuesShape` instead.
714
- */
715
- type AugmentWithUnset<T> = T extends string | number | boolean | bigint ? T | Unset : T extends symbol | null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
716
- -readonly [K in keyof T]: AugmentWithUnset<T[K]>;
717
- } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
718
- -readonly [K in keyof T]: AugmentWithUnset<T[K]>;
719
- } | Unset : Array<AugmentWithUnset<U>> | Unset : T extends object ? {
720
- [K in keyof T]: AugmentWithUnset<T[K]>;
721
- } | Unset : T;
722
- /**
723
- * Like `WriteShape<T>`, but additionally widens every primitive leaf
724
- * (`string`, `number`, `boolean`, `bigint`) to admit `Unset` — the
725
- * brand-typed sentinel consumers pass to indicate "this leaf starts
726
- * displayed-empty" in `defaultValues`, `setValue`, and `reset`.
727
- *
728
- * `Unset` is admitted at every position — primitive leaves, opaque
729
- * leaves (`Date`, `RegExp`, `Map`, `Set`, functions), and containers
730
- * (objects, arrays, tuples, records, DUs, optional / nullable
731
- * wrappers). A container `unset` recursively marks every primitive
732
- * descendant blank, so `defaultValues: { profile: unset }` and
733
- * `setValue('cargo', unset)` typecheck cleanly.
734
- *
735
- * Composed as `AugmentWithUnset<WriteShape<T>>`: stage 1 widens
736
- * primitive literals, stage 2 adds `| Unset` everywhere — the prior
737
- * inline walker hand-synced this in a single body. The composition
738
- * stays compatible at every nested position; tuple positions,
739
- * unbounded arrays, and nested records all flow through unchanged.
740
- *
741
- * Example:
742
- *
743
- * DefaultValuesShape<{ income: number; name: string; age: 21 }>
744
- * // → { income: number | Unset; name: string | Unset; age: number | Unset } | Unset
745
- *
746
- * Used by `UseFormConfiguration.defaultValues`, `setValue`'s value
747
- * parameter, and `reset`'s parameter.
748
- */
749
- type DefaultValuesShape<T> = AugmentWithUnset<WriteShape<T>>;
750
- /**
751
- * Single-walker fusion of `DeepPartial` and `DefaultValuesShape` — the
752
- * type accepted at `defaultValues`, `reset()`'s parameter, and every
753
- * partial-shape consumer. Every level is optional and every position
754
- * — primitive leaf, opaque leaf, or container — admits `| Unset`, in
755
- * one tree walk where the prior `DeepPartial<DefaultValuesShape<F>>`
756
- * composition walked twice.
757
- *
758
- * Both passes had identical topology (object → mapped, tuple →
759
- * positional, array → recurse, primitive → terminal) — the doubled
760
- * recursion exhausted the depth budget at consumer call sites that
761
- * wire multiple complex forms into one scope. Collapsing them buys
762
- * back the headroom plus a side fix: opaque leaves (`Date`, `Map`,
763
- * `Set`, `RegExp`, functions) now stay intact when their containing
764
- * property is optional, rather than getting structurally destructured
765
- * by `DeepPartial`'s pass.
766
- *
767
- * Tuple positions, array elements, and discriminated-union variants
768
- * all flow through unchanged from the prior semantics. Container
769
- * positions widen with `| Unset` so `defaultValues: { profile: unset }`
770
- * and `reset({ cargo: unset })` typecheck — the runtime recursively
771
- * marks every primitive descendant blank.
772
- *
773
- * ```ts
774
- * type T = DefaultValuesInput<{
775
- * email: string
776
- * joinedAt: Date
777
- * profile: { name: string; age: number }
778
- * }>
779
- * // → {
780
- * // email?: string | Unset
781
- * // joinedAt?: Date | Unset
782
- * // profile?: { name?: string | Unset; age?: number | Unset } | Unset
783
- * // } | Unset
784
- * ```
785
- */
786
- type DefaultValuesInput<T> = 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 extends null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
787
- -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
788
- } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
789
- -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
790
- } | Unset : Array<DefaultValuesInput<U>> | Unset : T extends object ? {
791
- [K in keyof T]?: DefaultValuesInput<T[K]>;
792
- } | Unset : T;
793
- /**
794
- * The type accepted at a `useForm` overload's `defaultValues` slot.
795
- *
796
- * `Form` is the schema's input projection; `SchemaInput` is the schema's
797
- * own input type (`z.input<Schema>`). A `defaultValues` value is one of:
798
- * - `DefaultValuesInput<Form>` — the partial, `Unset`-widened in-progress
799
- * shape. `defaultValues` deliberately accepts a form mid-completion: a
800
- * leaf may be blank or hold a not-yet-valid value (a `z.email()` field
801
- * accepts any `string`, not only valid emails). Sharp, fully-validated
802
- * types are produced only at `handleSubmit`.
803
- * - `SchemaInput` — the schema's full input shape.
804
- * - a sync or async factory returning either of the above.
805
- *
806
- * `SchemaInput` is REDUNDANT at a concrete call site: a schema's input is
807
- * always assignable to its own `DefaultValuesInput`, so this arm adds
808
- * nothing a concrete caller could not already pass and does not weaken the
809
- * per-field checking (`number` is still rejected where `string` is
810
- * expected). It earns its place for the generic form-wrapper case (#422):
811
- * when the schema is a free type parameter `S`, a forwarded `z.input<S>`
812
- * is REFLEXIVELY assignable to the `SchemaInput` arm — a relation TS can
813
- * decide even under a generic — whereas `DefaultValuesInput<Form>` alone
814
- * stays a deferred conditional cascade that is not provably assignable,
815
- * tripping TS2589 / TS2769. No runtime effect; the slot only governs what
816
- * the type checker accepts.
817
- *
818
- * `DefaultValuesInput<Form>` is computed ONCE — passed as the `DVI` type
819
- * argument to `AcceptableDefaultsOf` and referenced across the value and
820
- * factory arms — rather than inlined in each arm, which would
821
- * re-instantiate the (deep) cascade three times per overload and, across
822
- * the unified entry's two overloads at a concrete call site, tip the
823
- * bundled `.d.ts` into TS2589. The arms stay a DIRECT union (not a
824
- * conditional over the cascade) so the `SchemaInput` arm remains
825
- * reflexively matchable under a generic.
826
- */
827
- type AcceptableDefaults<Form, SchemaInput> = AcceptableDefaultsOf<DefaultValuesInput<Form>, SchemaInput>;
828
- type AcceptableDefaultsOf<DVI, SchemaInput> = DVI | SchemaInput | (() => DVI | SchemaInput) | (() => Promise<DVI | SchemaInput>);
829
-
830
- /**
831
- * Identifier for a form. A `FormKey` is the string passed via
832
- * `useForm({ key })`, used to look up a form by name from a distant
833
- * component and to label errors and DevTools entries. Anonymous
834
- * `useForm` calls allocate one
835
- * automatically; you only need to pick one when the form needs
836
- * stable identity.
837
- */
838
- type FormKey = string;
839
- /**
840
- * Per-form options threaded from `useForm` into the adapter factory.
841
- * Today carries `maxRecursionDepth` so adapter walks can cap their
842
- * descent through recursive schemas; future per-form runtime knobs
843
- * land here too.
844
- */
845
- interface SchemaFactoryOptions {
846
- /** Recursion ceiling for walks through recursive schemas. */
847
- maxRecursionDepth: number;
848
- }
849
- /**
850
- * A JSON-serialisable value: the recursive shape of anything that
851
- * survives a `JSON.stringify` / `JSON.parse` round-trip unchanged.
852
- * The type of `ValidationError.data`.
853
- *
854
- * The object arm is a named interface rather than an inline index
855
- * signature: TypeScript expands an anonymous recursive alias eagerly
856
- * and tips into a depth-limit error (TS2589) when `Json` is checked
857
- * inside a large structural type (e.g. the form store). A named
858
- * reference defers that expansion.
859
- */
860
- type Json = string | number | boolean | null | JsonArray | JsonObject;
861
- interface JsonArray extends Array<Json> {
862
- }
863
- interface JsonObject {
864
- [key: string]: Json;
865
- }
866
- /**
867
- * One validation failure. `path` points at the offending field as a
868
- * structured array — `['user', 'address', 0, 'line1']` for a nested
869
- * field, `[]` (the root path) for a form-level error (root `.refine()`
870
- * messages, `setErrors` entries with no path, hydration failures,
871
- * server-emitted form banners). Which form produced an error is
872
- * envelope-level identity: the `ValidationResponse` / submit result
873
- * that carries the list stamps `formKey` once, and aggregators that
874
- * merge lists across forms (the wizard) stamp their own envelope. The
875
- * optional `data` slot carries an arbitrary server payload.
876
- *
877
- * Returned by `validate()` / `parse()` / `handleSubmit`'s
878
- * `onError` callback, and accepted (leniently, as `ErrorInput`) by
879
- * `form.setErrors`.
880
- */
881
- type ValidationError = {
882
- /** Human-readable message describing the failure. */
883
- message: string;
884
- /**
885
- * Structured path of the offending field. The root path `[]` is the
886
- * form-level bucket — the dedicated home for errors that don't belong
887
- * to any specific field (root `.refine()`, `setErrors`, hydration
888
- * failures). The empty-STRING path `['']` is unrelated: an ordinary
889
- * literal empty-key field.
890
- */
891
- path: (string | number)[];
892
- /**
893
- * Stable machine identifier for the failure, scoped by prefix:
894
- *
895
- * - `atta:` — library-internal codes (see `AttaformErrorCode`).
896
- * - adapter prefix (e.g. `zod:`) — forwarded from the underlying
897
- * schema library's own issue code, when one exists.
898
- * - consumer-defined — anything else (e.g. `api:duplicate-email`,
899
- * `auth:expired-token`). Pick a prefix and stay consistent so
900
- * error renderers and tests can branch on `code` instead of
901
- * exact-message string matching.
902
- */
903
- code: string;
904
- /**
905
- * Optional structured payload attached to the error. Attaform never
906
- * sets or reads this; it is a passthrough slot for whatever a server
907
- * sends alongside the message (a captcha challenge, a lockout
908
- * `unlocks_at` timestamp, an MFA step-up descriptor) so the UI can
909
- * act on it. Survives serialise / hydrate and undo / redo unchanged.
910
- */
911
- data?: Json | null;
912
- };
913
- /**
914
- * One path's slot in the form's tagged error store. The two sources
915
- * that can put an error at a path stay segregated inside the cell:
916
- * `schema` holds the validation pipeline's verdicts, `user` holds
917
- * `setErrors` entries (and their history / SSR replays). Reads merge
918
- * schema -> derived-blank -> user; each writer replaces exactly its
919
- * own side. Cells are immutable — every write replaces the whole cell
920
- * (or deletes the key when both sides empty), so Vue's per-key Map
921
- * tracking fires for any side's change. An empty side is the shared
922
- * frozen `NO_ERRORS` array, never a fresh allocation.
923
- *
924
- * @internal
925
- */
926
- type ErrorCell = {
927
- readonly schema: readonly ValidationError[];
928
- readonly user: readonly ValidationError[];
929
- };
930
- /**
931
- * The lenient input shape `form.setErrors` accepts: a real `Error`, or
932
- * a partial `ValidationError` where every field is optional.
933
- *
934
- * - `message`: omitted or empty coerces to `"Unknown error"`.
935
- * - `path`: defaults to `[]` (a global, form-level error). Ignored by
936
- * the path-scoped `setErrors(path, …)` form, which stamps its own.
937
- * - `code`: defaults to `atta:user-error`.
938
- * - `data`: forwarded verbatim onto the produced `ValidationError`.
939
- *
940
- * Because every field is optional, `ValidationError` is a subtype of
941
- * `ErrorInput`: a `ValidationError[]` you read back, or a server
942
- * response that already emits the shape, pipes straight into
943
- * `form.setErrors` with no adapter.
944
- */
945
- type ErrorInput = Error | {
946
- message?: string;
947
- path?: (string | number)[];
948
- code?: string;
949
- data?: Json | null;
950
- };
951
- /**
952
- * A parse verdict as an `AbstractSchema` produces it.
953
- *
954
- * A schema knows nothing about which form asked: it is a pure function
955
- * of the schema it wraps, which is what lets one `AbstractSchema` serve
956
- * every form built on the same schema. The owning store stamps its own
957
- * `formKey` on the way out, producing the `ValidationResponse*` arms
958
- * below. Adapter authors implement THESE.
959
- */
960
- type SchemaParseSuccess<TData> = {
961
- /** The parsed value at the validated subtree (whole form when `validate()` was called without a path). */
962
- data: TData;
963
- errors: undefined;
964
- success: true;
965
- };
966
- /** Schema-level verdict when no data could be produced (e.g. a top-level type mismatch). */
967
- type SchemaParseErrorWithoutData = {
968
- data: undefined;
969
- /** Non-empty list of failures. */
970
- errors: ValidationError[];
971
- success: false;
972
- };
973
- /** Schema-level verdict when the parser produced partial data alongside failures. */
974
- type SchemaParseErrorWithData<TData> = {
975
- data: TData;
976
- errors: ValidationError[];
977
- success: false;
978
- };
979
- /** Settled schema-level verdict. Discriminate on `success`. */
980
- type SchemaParseResult<TData> = SchemaParseSuccess<TData> | SchemaParseErrorWithData<TData> | SchemaParseErrorWithoutData;
981
- /** Schema-level `getDefaultValues` verdict: defaults always come back. */
982
- type SchemaDefaultsResult<TData> = SchemaParseSuccess<TData> | SchemaParseErrorWithData<TData>;
983
- /** Settled validation result when the form (or subtree) parsed successfully. */
984
- type ValidationResponseSuccess<TData> = SchemaParseSuccess<TData> & {
985
- /** The form this verdict belongs to. Stamped by the store, not the schema. */
986
- formKey: FormKey;
987
- };
988
- /** Settled validation result when no data could be produced (e.g. a top-level type mismatch). */
989
- type ValidationResponseErrorWithoutData = SchemaParseErrorWithoutData & {
990
- formKey: FormKey;
991
- };
992
- /** Settled validation result when the parser produced partial data alongside failures. */
993
- type ValidationResponseErrorWithData<TData> = SchemaParseErrorWithData<TData> & {
994
- formKey: FormKey;
995
- };
996
- /**
997
- * Settled validation result. Discriminate on `success`:
998
- *
999
- * ```ts
1000
- * if (result.success) {
1001
- * // result.data is the parsed value, errors is undefined
1002
- * } else {
1003
- * // result.errors is non-empty, data may or may not be set
1004
- * }
1005
- * ```
1006
- */
1007
- type ValidationResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData> | ValidationResponseErrorWithoutData;
1008
- /**
1009
- * Trimmed `ValidationResponse` that omits the `data` payload. Used by
1010
- * the reactive `validate()` status, whose consumers only need the
1011
- * success flag and error list.
1012
- */
1013
- type ValidationResponseWithoutValue<Form> = Omit<ValidationResponse<Form>, 'data'>;
1014
- /**
1015
- * Options bag for `form.parse`. `commit: true` turns the pure read
1016
- * into an authoritative run: the verdict is committed to the error
1017
- * store at the parsed scope and in-flight per-field validation runs
1018
- * are cancelled first. Default `false`.
1019
- */
1020
- type ParseOptions = {
1021
- readonly commit?: boolean;
1022
- };
1023
- /**
1024
- * Sync-or-async return shape for `AbstractSchema.validateAtPath`. The
1025
- * adapter returns the response inline when the schema and the
1026
- * caller's options permit synchronous validation; otherwise a
1027
- * `Promise<T>`. Callers that don't care simply `await` (works for
1028
- * both); callers that DO care (the reshape pre-pass — flicker
1029
- * prevention) branch on `instanceof Promise`.
1030
- */
1031
- type MaybePromise<T> = T | Promise<T>;
1032
- /**
1033
- * Options accepted by `AbstractSchema.validateAtPath`. Currently a
1034
- * single field; kept as an object for forward-compat with future
1035
- * knobs (e.g. cancellation signals, abort tokens) without breaking
1036
- * the call signature.
1037
- *
1038
- * - `sync`: when `true`, the adapter SHOULD return the response
1039
- * inline if the schema permits synchronous validation. When the
1040
- * schema is structurally async (any verdict that resolves only via
1041
- * a Promise — async refinements, async transforms / pipes — in
1042
- * whichever library the adapter wraps), the adapter falls back to
1043
- * a `Promise<T>` — the flag is a preference, not a guarantee.
1044
- *
1045
- * When omitted or `false`, the adapter is free to use its async
1046
- * path (matches the historical Promise-returning contract; every
1047
- * non-reshape callsite uses this default).
1048
- */
1049
- type ValidateOptions = {
1050
- sync?: boolean;
1051
- };
1052
- /**
1053
- * Configuration passed to `AbstractSchema.getDefaultValues`. Adapters
1054
- * receive `useDefaultSchemaValues` (honor `.default(x)` wrappers vs.
1055
- * empty/falsy fallbacks) and an optional `constraints` overlay merged
1056
- * into the derived defaults so the runtime can stamp user-supplied
1057
- * defaults at construction. Exported so adapter authors can
1058
- * co-implement the service contract.
1059
- */
1060
- type GetDefaultValuesConfig<Form> = {
1061
- useDefaultSchemaValues: boolean;
1062
- constraints?: DeepPartial<WriteShape<Form>> | undefined;
1063
- };
1064
- /**
1065
- * The contract a schema adapter implements so the form runtime can
1066
- * read defaults, validate, and walk paths against any underlying
1067
- * schema library.
1068
- *
1069
- * Most consumers never touch this type directly — the typed entry
1070
- * points (e.g. `attaform/zod`, `attaform/zod-v3`)
1071
- * wire an adapter automatically. Implement this interface only when
1072
- * adding support for a new schema library (Valibot, ArkType, custom).
1073
- */
1074
- type AbstractSchema<Form, GetValueFormType> = {
1075
- getDefaultValues(config: GetDefaultValuesConfig<Form>): SchemaDefaultsResult<Form>;
1076
- /**
1077
- * Return the schema-prescribed default value at the given path. The
1078
- * runtime uses this to fill structural gaps so every `setValue` write
1079
- * leaves the form satisfying the slim schema (objects/arrays/primitives
1080
- * without refinement-level constraints).
1081
- *
1082
- * Semantics:
1083
- * - **Object property path:** the property's schema default.
1084
- * - **Array element path:** the element default (paths past the
1085
- * array's current length still resolve — every position resolves
1086
- * to the same element type).
1087
- * - **Tuple position path:** the position-specific default. Out-of-
1088
- * range positions return `undefined`.
1089
- * - **Optional/Default/Nullable/Readonly/Catch/Pipe wrappers:** the
1090
- * inner default.
1091
- * - **Discriminated union:** the first variant's default (matches
1092
- * `validateAtPath`'s first-success semantic).
1093
- * - **Leaf:** the primitive default (`''`, `0`, `false`, etc., or the
1094
- * wrapper's `.default(x)` value when present).
1095
- * - **Path doesn't exist in schema:** `undefined`.
1096
- *
1097
- * Adapters may return `undefined` when the path can't be resolved;
1098
- * callers treat that as "don't fill" and fall back to existing data.
1099
- */
1100
- getDefaultAtPath(path: Path): unknown;
1101
- /**
1102
- * Return the schema's "appropriate nullish value" at the given path
1103
- * — the underlying type's empty/falsy concrete, with `.default(x)`
1104
- * wrappers explicitly NOT honoured. Powers `form.clear(path)`:
1105
- * `clear` differs from `reset` precisely in that it ignores
1106
- * declared defaults and produces `false` / `0` / `''` / `[]` / a
1107
- * recursively-empty object instead.
1108
- *
1109
- * Semantics (mirrors `getDefaultAtPath`'s sub-path resolution,
1110
- * differs at leaves):
1111
- * - **Primitive leaf:** the primitive's falsy concrete
1112
- * (`''` / `0` / `false` / `0n` / `new Date(0)`, etc.).
1113
- * - **Array / Set / Record:** empty.
1114
- * - **Optional<T>:** `undefined` (the wrapper's "absent" marker).
1115
- * - **Nullable<T>:** `null` (the wrapper's "explicit empty").
1116
- * - **Default<T> / Prefault<T> / Catch<T>:** inner-schema empty
1117
- * — the declared default value is INTENTIONALLY skipped.
1118
- * - **Readonly<T> / preprocess(fn, T):** inner-schema empty.
1119
- * - **Object:** recursive — every property gets its own empty.
1120
- * - **Discriminated union:** first variant's recursive empty
1121
- * (parallels `getDefaultAtPath`'s first-success precedent).
1122
- * - **Path doesn't exist in schema:** `undefined`.
1123
- *
1124
- * Adapters may return `undefined` when the path can't be resolved;
1125
- * callers treat that as "don't write" and leave existing storage
1126
- * unchanged.
1127
- */
1128
- getEmptyValueAtPath(path: Path): unknown;
1129
- /**
1130
- * Reports whether `path` resolves to (or descends through) a
1131
- * schema-side normalizer that runs at parse, not at the write
1132
- * boundary. In Zod v4 that's `z.preprocess(fn, inner)` and
1133
- * `z.coerce.X()` (both desugar to `ZodPipe<ZodTransform, inner>`);
1134
- * in Zod v3 it's `ZodEffects` with `_def.effect.type === 'preprocess'`.
1135
- *
1136
- * Consulted by the slim-primitive write gate. When true at a path,
1137
- * the gate accepts the consumer's raw value verbatim and stops
1138
- * walking children — storage holds the user's input, and the
1139
- * normalizer fires during `safeParse` (handleSubmit / validate /
1140
- * parse), not at `setValue` time.
1141
- *
1142
- * Path-prefix semantic: returns true if ANY ancestor of `path`
1143
- * resolves to such a wrapper, so descendants under a preprocess-
1144
- * wrapped container also short-circuit the gate. Adapters cache
1145
- * the result by canonicalized path key.
1146
- */
1147
- isPreprocessOrCoerceLeaf(path: Path): boolean;
1148
- /**
1149
- * Reports whether `path` resolves to an opaque leaf: a schema that
1150
- * declares a value without describing its shape. In Zod v4 that's
1151
- * `z.any()`, `z.unknown()`, and `z.custom(...)` (the kind both
1152
- * `z.instanceof(X)` and `z.custom<T>()` compile to); in Zod v3 it's
1153
- * `z.any()` / `z.unknown()`, with `z.instanceof(X)` peeling through
1154
- * `ZodEffects` to `ZodAny`.
1155
- *
1156
- * Consulted by the slim-primitive write gate. An opaque leaf admits
1157
- * every kind, INCLUDING containers, and it declares no sub-paths.
1158
- * Without this the gate accepts an array at the leaf and then walks
1159
- * into it, checking `files.0` against a schema that never declared
1160
- * that path; the empty accept set there reads as "not in your
1161
- * schema" and no-ops the whole write (#542).
1162
- *
1163
- * Exact-path semantic, unlike `isPreprocessOrCoerceLeaf`: it answers
1164
- * for the node at `path` only. Descendants stay unwritable on their
1165
- * own, because the schema genuinely does not declare them and
1166
- * fabricating them would put phantom nodes on `form.fields`. Writing
1167
- * the whole value at the opaque leaf is the supported move.
1168
- */
1169
- isOpaqueLeafAtPath(path: Path): boolean;
1170
- /**
1171
- * Distinguish a tuple (fixed-length, position-typed) from an
1172
- * unbounded array at `path`. The runtime calls this on every
1173
- * `mergeStructural` / `setAtPathWithSchemaFill` write that descends
1174
- * into an array branch, and the answer is definitive — the runtime
1175
- * never second-guesses it.
1176
- *
1177
- * Return values:
1178
- * - `number` → tuple of this structural length. The runtime pads
1179
- * the consumer to this length and recurses position-by-position.
1180
- * - `null` → not a tuple. Covers the unbounded array (the runtime
1181
- * uses the consumer's length and reuses one element default for
1182
- * every position) and every path that doesn't resolve to an
1183
- * array at all — there `getDefaultAtPath` yields no element
1184
- * default either, so the consumer's array passes through
1185
- * unchanged.
1186
- *
1187
- * Wrappers (optional / nullable / default / readonly / catch /
1188
- * pipe / lazy) are peeled transparently before the type check, so
1189
- * `optional(z.tuple([...]))` reports its tuple length.
1190
- */
1191
- arrayShapeAtPath(path: Path): number | null;
1192
- /**
1193
- * Whether the schema at `path` is a FIXED object: a closed set of
1194
- * declared keys (`z.object`), as opposed to an open or union container
1195
- * (array / record / map / set / union / discriminated union) whose
1196
- * element schema matches any segment.
1197
- *
1198
- * The surface proxies (`form.fields` / `form.errors`) use this to
1199
- * resolve a collision: a fixed object's declared keys are known ahead
1200
- * of any data, so a key the schema owns descends to a real terminal
1201
- * even when the live value hasn't been populated yet (a declared-but-
1202
- * absent `optional` field stays registrable). An open container can't
1203
- * make that promise — its element schema accepts ANY segment, so the
1204
- * proxy must fall back to the keys the data currently holds and read
1205
- * a genuinely-absent key (out-of-bounds index, missing record key,
1206
- * inactive variant key) as `undefined` rather than a phantom node.
1207
- *
1208
- * The empty path (the root form) is always a fixed object. Wrappers
1209
- * (optional / nullable / default / readonly / catch / pipe / lazy)
1210
- * are peeled before the kind check, so `z.object({...}).optional()`
1211
- * still reports `true`. A path the schema doesn't declare reports
1212
- * `false`.
1213
- */
1214
- isFixedObjectAtPath(path: Path): boolean;
1215
- /**
1216
- * How the container at `path` spells the keys of its own entries:
1217
- * `'number'` for an array or a tuple, `'string'` for an object or a
1218
- * record, and for a `z.map` the kind its declared key type accepts.
1219
- * `undefined` for anything else, a leaf and an undeclared path
1220
- * included.
1221
- *
1222
- * The write walkers consult it at a `Map`, where a new entry has to
1223
- * be filed under a key of the declared type and the path segment
1224
- * alone cannot say which: an integer-looking segment canonicalises
1225
- * to a number, so `scores.42` against a `z.map(z.string(), V)` would
1226
- * otherwise take the number `42` as its key and fail the map's own
1227
- * parse. An entry the map already holds needs no ruling — its
1228
- * existing key wins.
1229
- *
1230
- * A map whose key type is neither string-ish nor number-ish, or
1231
- * which admits both, reports `undefined`: no segment can spell such
1232
- * a key, so the map stays a whole value with no addressable entries.
1233
- *
1234
- * Adapters MAY cache per path; the runtime calls this on the write
1235
- * path.
1236
- */
1237
- entryKeyKindAtPath(path: Path): 'string' | 'number' | undefined;
1238
- /**
1239
- * Return every sub-schema that could resolve at the given structured
1240
- * path. Multiple results are only expected for discriminated / union
1241
- * branches where the adapter can't decide a single winner until the
1242
- * data lands. `path` is the canonical `Segment[]` — adapters walk it
1243
- * segment-by-segment so literal-dot keys (`['user.name']`) don't
1244
- * collide with the sibling-pair form (`['user', 'name']`).
1245
- */
1246
- getSchemasAtPath(path: Path): AbstractSchema<unknown, GetValueFormType>[];
1247
- /**
1248
- * Validate a subtree (when `path` is provided) or the whole form (when
1249
- * `path` is `undefined`). `path` is the canonical `Segment[]`, not a
1250
- * dotted string — two schemas with otherwise-colliding dotted forms
1251
- * (`['user.name']` vs `['user', 'name']`) stay distinct at the
1252
- * adapter boundary.
1253
- *
1254
- * Return type is `MaybePromise<ValidationResponse>`:
1255
- * - With `options.sync === true` AND a sync-capable schema, the
1256
- * adapter SHOULD return the response inline (`T`). This lets the
1257
- * runtime batch error writes with a coincident form-value
1258
- * mutation in a single Vue reactive flush — preventing the `{}`
1259
- * flicker observable during DU variant reshape.
1260
- * - With `options.sync === true` AND an async-only schema (any
1261
- * verdict that resolves only via a Promise), the adapter MUST
1262
- * fall back to `Promise<T>`. The flag is a preference, not a
1263
- * guarantee; sync isn't always achievable.
1264
- * - With `options.sync` omitted or `false`, the adapter SHOULD
1265
- * return `Promise<T>` (matches the historical contract — every
1266
- * non-reshape callsite uses this default and immediately
1267
- * `await`s the result).
1268
- *
1269
- * Callers that don't care simply `await` (works for both arms);
1270
- * callers that need to detect sync-vs-async branch on
1271
- * `instanceof Promise`. Adapters MUST NOT throw — errors are
1272
- * returned as a `success: false` response with a populated
1273
- * `errors` array.
1274
- */
1275
- validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<SchemaParseResult<GetValueFormType>>;
1276
- /**
1277
- * Sync sister to `getSchemasAtPath` / `validateAtPath`. Returns the
1278
- * set of primitive `typeof`-style kinds the path's leaf schema
1279
- * accepts at write time. Wrappers (optional / nullable / default /
1280
- * refinement / transform / pipe / readonly / catch / lazy) are
1281
- * peeled; refinement-level constraints (format checks like email /
1282
- * uuid, min/max length, enum membership, literal equality, regex)
1283
- * are IGNORED — they're a validation-time concern.
1284
- *
1285
- * Used by `setValueAtPath` to gate writes synchronously without
1286
- * round-tripping through async `validateAtPath`. The returned set
1287
- * unions across union branches and intersects across intersection
1288
- * sides.
1289
- *
1290
- * Conventions:
1291
- * - Empty set → no kind admitted. The runtime gate rejects every
1292
- * write to the path. Surfaces for `never`-typed schemas AND for
1293
- * paths that don't resolve in the schema (typo / unknown leaf).
1294
- * - Permissive set (every kind) → "unknown / unconstrained." The
1295
- * gate accepts any value. Surfaces for `any` / `unknown` / `void`
1296
- * and the lazy-peel-failure case where the adapter can't
1297
- * introspect the schema.
1298
- * - For string-valued enums: returns `{'string'}`. For numeric
1299
- * enums: `{'number'}`.
1300
- * - For literal types: returns `{primitiveKindOf(literalValue)}`.
1301
- * - For object / array containers: `{'object'}` / `{'array'}`. The
1302
- * runtime walker recurses into entries / elements at write time.
1303
- * - For nullable / optional wrappers: adds `'null'` / `'undefined'`
1304
- * to the inner's set.
1305
- */
1306
- getSlimPrimitiveTypesAtPath(path: Path): ReadonlySet<SlimPrimitiveKind>;
1307
- /**
1308
- * Return `true` iff `path` resolves to a **leaf** in the schema: a
1309
- * path the schema declares no sub-paths under. That is every path
1310
- * whose slim primitive set contains only primitive kinds (no
1311
- * `object`, `array`, `map`, `set`), plus the opaque leaves, which
1312
- * admit every kind and are classified by kind instead (below). The
1313
- * runtime proxies (`form.values`,
1314
- * `form.errors`, `form.fields`) query this at every step to decide
1315
- * between **descend into a sub-proxy** (container) and **terminate
1316
- * with a leaf value** (leaf).
1317
- *
1318
- * The leaf-aware branching is what kills the FIELD_STATE_KEYS
1319
- * shadowing problem: reserved leaf-prop names (`dirty`, `errors`,
1320
- * `valid`, …) inject only at the FieldState terminal, not at
1321
- * every depth. A schema field literally named `dirty` at depth ≥ 2
1322
- * stays reachable as a sub-proxy or leaf in its own right.
1323
- *
1324
- * Semantics:
1325
- * - **Object / Array / Map / Set** at any wrapper layer → `false`
1326
- * (container; descend further).
1327
- * - **Primitive** (string/number/boolean/bigint/symbol/null/undefined/
1328
- * date/function) → `true`. `'date'` counts as a leaf (don't drill
1329
- * into `Date`). `'function'` is a leaf for the same reason — opaque
1330
- * value.
1331
- * - **Opaque leaf** (`z.any()`, `z.unknown()`, `z.custom()`, and the
1332
- * `z.instanceof(X)` that compiles to it) → `true`, and it answers
1333
- * ahead of the kind test. An opaque leaf admits every kind
1334
- * INCLUDING the container ones, so reading it off the slim
1335
- * primitive set alone classifies it as a container and puts
1336
- * phantom nodes under it on `form.fields` — see
1337
- * `isOpaqueLeafAtPath`. What leafness asks is whether the schema
1338
- * declares sub-paths here, and an opaque leaf declares none.
1339
- * - **Optional / Nullable / Default / Catch** wrappers transparent —
1340
- * adds `'null'` / `'undefined'` to the inner kind set without
1341
- * changing the leaf classification.
1342
- * - **Discriminated union root** → `false` (variants are objects;
1343
- * the kind set contains `'object'`).
1344
- * - **DU discriminator key** → `true` (the literal type resolves to
1345
- * `{'string'}` / `{'number'}`).
1346
- * - **DU variant-only key** → `true` if it resolves to a primitive
1347
- * in any variant; schema-static (does NOT query live storage to
1348
- * decide which variant is active).
1349
- * - **Empty path (root)** → `false` (root is the form-as-object).
1350
- * - **Path doesn't exist in schema** → `false`. The proxy descends
1351
- * permissively; reads of leaf props at the unknown path return
1352
- * `undefined` from the underlying store. Treating unknown paths
1353
- * as containers preserves the schema's authority and avoids
1354
- * re-introducing shadowing on typos.
1355
- *
1356
- * Adapters MAY cache results per-path — `isLeafAtPath` will be
1357
- * called on every proxy `get` trap hit. The reference implementation
1358
- * memoises a `Map<PathKey, boolean>` keyed by `canonicalizePath(path).key`,
1359
- * lifetime tied to the adapter (one per `useForm()` call).
1360
- */
1361
- isLeafAtPath(path: Path): boolean;
1362
- /**
1363
- * Return `true` if the leaf at `path` is required — i.e. the schema
1364
- * does NOT admit "empty" via `.optional()`, `.nullable()`,
1365
- * `.default(N)`, or `.catch(N)` at the leaf or any wrapper.
1366
- *
1367
- * Used by the submit / validate path to surface a "No value supplied" error
1368
- * when a field is in the form's `blankPaths` set (the user
1369
- * cleared it or never answered) AND the schema treats the field as
1370
- * required. Without this, a strict numeric leaf would silently
1371
- * accept the slim default (`0`) for an unanswered field — the
1372
- * "public-housing" footgun where `$0 income` passes validation.
1373
- *
1374
- * Semantics:
1375
- * - **Optional / Nullable / Default / Catch** at any wrapper layer
1376
- * (root or nested) → `false`. The schema author opted into
1377
- * accepting empty.
1378
- * - **Readonly / Pipe / Lazy** wrappers are transparent — peel and
1379
- * re-check the inner schema.
1380
- * - **Union / Discriminated union** → `false` if ANY branch admits
1381
- * empty (the union accepts what the most permissive branch
1382
- * accepts). This matches the parse-time "first success wins"
1383
- * semantic of `validateAtPath`.
1384
- * - **Intersection** → `true` if EITHER side requires the path
1385
- * (intersection requires both sides to accept; if one rejects
1386
- * empty, the intersection rejects empty).
1387
- * - **Path doesn't exist in schema** → `false` (can't enforce
1388
- * what we don't know about).
1389
- * - **Empty path (root)** → `true` (the root form is always
1390
- * required as an object).
1391
- *
1392
- * Refinement-level constraints (length / format / custom predicates)
1393
- * are NOT consulted here — those run at parse time inside
1394
- * `validateAtPath` and surface as schema errors regardless.
1395
- * `isRequiredAtPath` only answers the "is this leaf at all
1396
- * required?" question; the refinements layer on top.
1397
- */
1398
- isRequiredAtPath(path: Path): boolean;
1399
- /**
1400
- * If the schema at `path` is (or wraps) a discriminated union,
1401
- * return its discriminator key plus a `getVariantDefault(value)`
1402
- * lookup — otherwise `undefined`. Wrappers (optional, default,
1403
- * nullable, readonly, pipe, lazy, catch) are peeled transparently.
1404
- *
1405
- * The runtime uses this for two related reshapes that share the
1406
- * same lookup:
1407
- *
1408
- * 1. **Discriminator-key write** — the runtime calls this with
1409
- * the parent path. If the returned `discriminatorKey` matches
1410
- * the path's last segment, the write changes which variant is
1411
- * active; the parent storage is replaced with the matching
1412
- * variant's slim default so the OLD variant's keys (e.g.
1413
- * `address` after switching to `sms`) don't leak.
1414
- *
1415
- * 2. **Whole-union write** — the runtime calls this with the
1416
- * path itself. If the returned info exists and the consumer's
1417
- * value carries the discriminator key, the merge uses the
1418
- * matching variant's default instead of the first-variant
1419
- * fallback that `getDefaultAtPath` returns for unions.
1420
- *
1421
- * Adapters that don't model discriminated unions can return
1422
- * `undefined` unconditionally; the runtime reshape is a no-op
1423
- * without this hook.
1424
- */
1425
- getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
1426
- /**
1427
- * Return the resolved field metadata for the schema node at `path`
1428
- * — label, description, placeholder, plus the full registered
1429
- * payload as `meta` for consumer-augmented keys. Reads through the
1430
- * shared cross-adapter field-meta store and applies these one-way
1431
- * fallbacks:
1432
- *
1433
- * - `label`: registry payload → `humanize(lastSegment)`
1434
- * - `description`: registry payload → `schema.description`
1435
- * (`.describe()` value) → `undefined`
1436
- * - `placeholder`: registry payload → `undefined`
1437
- * - `meta`: registry payload (frozen) — empty object when
1438
- * nothing was registered
1439
- *
1440
- * `path` is the canonical `Segment[]`. The empty path resolves to
1441
- * the root schema's metadata. Multiple candidates (DU branches)
1442
- * resolve against the first candidate to match the existing
1443
- * first-success precedent in `getDefaultAtPath` /
1444
- * `validateAtPath` — schema authors register on the union root
1445
- * for shared metadata, on individual branches for variant-
1446
- * specific metadata.
1447
- *
1448
- * Optional. The runtime treats a missing implementation as a
1449
- * stub that returns `EMPTY_RESOLVED_FIELD_META` — so adapters
1450
- * that don't model field metadata yet can omit it; consumers
1451
- * see humanized fallbacks for `label`, undefined elsewhere.
1452
- */
1453
- getFieldMetaAtPath?(path: Path): ResolvedFieldMeta;
1454
- /**
1455
- * Return `true` if `validateAtPath` MAY have to run asynchronously
1456
- * to surface every error this schema can produce. The runtime uses
1457
- * this at construction to decide whether to schedule a one-shot
1458
- * full-form async validation: when `false` (or omitted), the
1459
- * construction-time sync seed is the authoritative result and no
1460
- * extra microtask is spent; when `true`, an async pass is queued
1461
- * so any async-only verdicts (refinements / transforms / pipes
1462
- * that resolve only via a Promise) surface without waiting for a
1463
- * user mutation.
1464
- *
1465
- * Optional. The runtime treats a missing implementation as
1466
- * `() => false`, so adapters that don't model async work — or
1467
- * don't yet support detection — can omit it; async-only errors
1468
- * then fall back to firing on first user mutation, matching the
1469
- * pre-detection behavior. Detection is best-effort.
1470
- *
1471
- * For per-path queries, compose with `getSchemasAtPath(path)`:
1472
- * each candidate sub-schema exposes its own
1473
- * `needsAsyncValidation`, so a caller asking "does the cargo
1474
- * subtree contain async work?" can union the per-candidate
1475
- * answers without a separate top-level overload.
1476
- */
1477
- needsAsyncValidation?(): boolean;
1478
- /**
1479
- * Return `true` iff the schema carries a refine / check / transform
1480
- * at any NON-LEAF position — a container node (object / array /
1481
- * tuple / union / intersection / record / map / set) or the root
1482
- * itself. False means every check this schema runs is leaf-local,
1483
- * so a per-keystroke `validateAtPath(form, leafPath)` catches the
1484
- * same verdicts as a whole-form pass — no ancestor refine reads
1485
- * the form's wider state.
1486
- *
1487
- * The runtime uses this at the per-keystroke schedule to scope
1488
- * field-level validation to the changed subtree when it can,
1489
- * falling back to a whole-form pass when an ancestor refine
1490
- * (cross-field equality, sum constraints, etc.) could be moved
1491
- * by a leaf write. Optional. The runtime treats a missing
1492
- * implementation as `() => true` — conservative whole-form,
1493
- * preserving correctness for adapters that don't yet model
1494
- * container-refine detection.
1495
- *
1496
- * Detection is best-effort: false negatives (returning `true`
1497
- * when no container refine exists) only lose a perf win and
1498
- * still validate correctly; false positives (returning `false`
1499
- * when a container refine exists) would let an ancestor verdict
1500
- * go stale and are the real risk — implementations should bias
1501
- * toward returning `true` when in doubt.
1502
- */
1503
- hasContainerOrRootRefine?(): boolean;
1504
- /**
1505
- * `true` when the schema tree contains at least one discriminated
1506
- * union at any depth (inside arrays, tuples, records, and lazy
1507
- * schemas included). The store computes its per-form DU capability
1508
- * flag from this once at construction: `false` lets every write skip
1509
- * the cross-variant ancestor guard and the variant-reshape dispatch,
1510
- * and DU stub correction never runs. Optional: an adapter that omits
1511
- * it is treated as containing unions, keeping the conservative
1512
- * per-write probes on. Never return `false` for a schema that DOES
1513
- * hold a discriminated union — that disables variant reshape and
1514
- * stub correction for the form.
1515
- */
1516
- hasDiscriminatedUnions?(): boolean;
1517
- };
1518
- /**
1519
- * Adapter-returned info for a discriminated union — its discriminator
1520
- * key plus a function that maps a discriminator literal to the slim
1521
- * default of the matching variant. Returned by
1522
- * `AbstractSchema.getUnionDiscriminatorAtPath`.
1523
- */
1524
- type UnionDiscriminatorContext = {
1525
- /**
1526
- * The union's discriminator key — the property name whose literal
1527
- * value selects the variant (e.g. `'channel'` for a union split on
1528
- * `{ channel: 'sms' | 'email' }`).
1529
- */
1530
- readonly discriminatorKey: string;
1531
- /**
1532
- * Slim default for the variant whose discriminator literal equals
1533
- * `value`. Returns `undefined` if no variant matches — the runtime
1534
- * skips the reshape and falls back to a plain write.
1535
- */
1536
- getVariantDefault(value: unknown): unknown;
1537
- /**
1538
- * Returns `true` iff `value` is a literal recognised by one of the
1539
- * discriminator's variants. Used by reshape to decide whether to
1540
- * seek a variant default or emit a stub state. NOT used at the
1541
- * runtime write gate — consumer-side value validity is a
1542
- * validation-time concern.
1543
- */
1544
- isVariantSelected(value: unknown): boolean;
1545
- };
1546
- /**
1547
- * The set of primitive "kinds" the slim-primitive write contract
1548
- * recognises. Drawn from `typeof` plus a few well-known reference
1549
- * shapes (`Date`, `Array`, `Map`, `Set`, plain `object`, `null`).
1550
- *
1551
- * The runtime gate's `slimKindOf(value)` returns one of these for a
1552
- * value; the adapter's `getSlimPrimitiveTypesAtPath(path)` returns
1553
- * the set of kinds the path's leaf schema accepts. A write is gated
1554
- * by `accepted.has(slimKindOf(value))`.
1555
- */
1556
- type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set' | 'file';
1557
- /**
1558
- * The "no result yet" status returned by the reactive `validate()` ref
1559
- * while a validation run is in flight.
1560
- *
1561
- * Narrow against `pending` to access the settled fields:
1562
- *
1563
- * ```ts
1564
- * const status = form.validate()
1565
- * watchEffect(() => {
1566
- * if (status.value.pending) return
1567
- * // status.value.success / status.value.errors are now safe to read
1568
- * })
1569
- * ```
1570
- */
1571
- type PendingValidationStatus = {
1572
- readonly pending: true;
1573
- readonly errors: undefined;
1574
- readonly success: false;
1575
- readonly formKey: FormKey;
1576
- };
1577
- /** Settled status of a reactive `validate()` call. Mirrors the latest result. */
1578
- type SettledValidationStatus<Form> = {
1579
- readonly pending: false;
1580
- } & ValidationResponseWithoutValue<Form>;
1581
- /**
1582
- * The value type of the ref returned by `validate()`. Discriminate on
1583
- * `pending` to switch between in-flight and settled states.
1584
- */
1585
- type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
1586
- /**
1587
- * When per-field VALIDATION runs. Only validation timing varies per
1588
- * mode; storage commit timing is the directive's concern (the
1589
- * default `<input v-register>` commits per keystroke; `.lazy` defers
1590
- * to blur).
1591
- *
1592
- * - `'change'` (default): every committed write schedules a
1593
- * validation for the affected path. With `debounceMs: 0` (also the
1594
- * default) the run is synchronous in the write handler;
1595
- * positive `debounceMs` coalesces rapid bursts.
1596
- * - `'blur'`: validate immediately when the user tabs away from a
1597
- * registered field. No debounce — `debounceMs` is rejected by the
1598
- * type.
1599
- * - `'submit'`: no live validation. `handleSubmit` and explicit
1600
- * `validate()` / `parse()` calls are the only validation
1601
- * surfaces. `debounceMs` is rejected by the type.
1602
- */
1603
- type ValidateOn = 'change' | 'blur' | 'submit';
1604
- /**
1605
- * Validation timing config — `validateOn` is the trigger, `debounceMs`
1606
- * the wait (after the last committed write) before the next
1607
- * validation run fires. `debounceMs` ONLY governs validation;
1608
- * `setValueWithInternalPath` commits to `form.values` immediately
1609
- * regardless of debounce. (How OFTEN the directive forwards writes
1610
- * to storage is the directive's concern — default `<input
1611
- * v-register>` commits per keystroke; `<input v-register.lazy>`
1612
- * defers to the blur `change` event.)
1613
- *
1614
- * `debounceMs` is only meaningful with `validateOn: 'change'` (the
1615
- * default); `'blur'` and `'submit'` ignore the wait entirely (blur
1616
- * fires validation immediately on focus-out; submit is its own
1617
- * trigger). The discriminated union below makes pairing `debounceMs`
1618
- * with `'blur'` / `'submit'` a TS error instead of a silent runtime
1619
- * drop.
1620
- *
1621
- * Pass `debounceMs: 0` (the default) to disable validation
1622
- * debouncing — every committed write triggers a validation pass with
1623
- * no `setTimeout` indirection. Schema work itself still rides
1624
- * `Promise.resolve().then(validateAtPath)` — async but microtask, so
1625
- * errors land on the next tick. Set `debounceMs` to a positive
1626
- * number to coalesce rapid bursts (useful for slow async adapters or
1627
- * for smoothing inline feedback under heavy typing).
1628
- */
1629
- type ValidateOnConfig = {
1630
- /** Validation trigger. Default `'change'`. */
1631
- validateOn?: 'change';
1632
- /**
1633
- * Milliseconds to wait after the last committed write before
1634
- * running validation. Default `0` (validation runs synchronously
1635
- * after the write; no `setTimeout`). Set to a positive number
1636
- * to coalesce rapid bursts into a single validation pass.
1637
- *
1638
- * Note: this is purely the validation debounce. Storage commits
1639
- * happen at the directive's listener (per-keystroke for
1640
- * `<input v-register>`, per-blur for `<input v-register.lazy>`)
1641
- * — `debounceMs` doesn't change either.
1642
- */
1643
- debounceMs?: number;
1644
- } | {
1645
- /** Validation trigger. */
1646
- validateOn: 'blur' | 'submit';
1647
- /** `debounceMs` is not allowed with `'blur'` or `'submit'`. */
1648
- debounceMs?: never;
1649
- };
1650
- /**
1651
- * Per-write metadata. Used internally to tag writes so listeners and
1652
- * the write funnel can treat a write specially (a blank mark, an array
1653
- * structural op, a hydration replay, a per-instance config override).
1654
- */
1655
- type WriteMeta = {
1656
- /**
1657
- * When `true`, the path being written is added to the FormStore's
1658
- * `blankPaths` set — meaning storage holds a real, schema-
1659
- * conformant value (the slim default) but the UI should display the
1660
- * field as empty. The next write to that path WITHOUT this flag
1661
- * implicitly removes the path from the set (the user typed something
1662
- * real). Internal — set by `markBlank()` on the register
1663
- * binding and by the `unset` translation in `setValue` / `reset` /
1664
- * `useAbstractForm` construction. Don't set from consumer code.
1665
- */
1666
- readonly blank?: boolean;
1667
- /**
1668
- * When `true`, the discriminator-aware variant reshape inside
1669
- * `setValueAtPath` is skipped for this write. Internal — set by
1670
- * the reshape itself when re-entering with the new variant default
1671
- * so the literal discriminator inside the default doesn't trigger
1672
- * an infinite loop. Don't set from consumer code.
1673
- */
1674
- readonly skipDiscriminatorReshape?: boolean;
1675
- /**
1676
- * Records an array structural mutation precisely enough to replay the
1677
- * exact index permutation it produced, set by the typed array helpers
1678
- * in `array-engine.ts`. `setValueAtPath` uses it to surgically clear variant memory
1679
- * for the indices the operation invalidated. Without this hint, a raw
1680
- * whole-array `setValue(arrayPath, [...])` clears all memory under the
1681
- * array (the runtime can't tell which indices stayed put). Internal —
1682
- * don't set from consumer code.
1683
- */
1684
- readonly arrayOp?: {
1685
- readonly kind: 'insert';
1686
- readonly index: number;
1687
- } | {
1688
- readonly kind: 'remove';
1689
- readonly index: number;
1690
- } | {
1691
- readonly kind: 'move';
1692
- readonly from: number;
1693
- readonly to: number;
1694
- } | {
1695
- readonly kind: 'swap';
1696
- readonly a: number;
1697
- readonly b: number;
1698
- } | {
1699
- readonly kind: 'replace-at';
1700
- readonly index: number;
1701
- };
1702
- /**
1703
- * Per-instance config overrides threaded through writes so each
1704
- * `useForm({ key })` callsite honors its own `validateOn` /
1705
- * `debounceMs` / `rememberVariants` even when sharing a FormStore
1706
- * with sibling calls (e.g., a modal and main form rendering the
1707
- * same logical form). Internal — set by `buildFormApi` from
1708
- * the per-instance options bag; the store reads each field with
1709
- * fallback to its construction-time defaults.
1710
- */
1711
- readonly instance?: {
1712
- readonly validateOn?: ValidateOn;
1713
- readonly debounceMs?: number;
1714
- readonly rememberVariants?: boolean;
1715
- };
1716
- /**
1717
- * When `true`, marks this `applyFormReplacement` call as a hydration
1718
- * step (the async-`defaultValues` / `activate()` / `rehydrate()`
1719
- * path). Modules that snapshot the form state (notably the history
1720
- * module) treat hydration as the baseline: stacks reset to a single
1721
- * seed of the post-hydration value, so a subsequent `undo()` can't
1722
- * recover the transient pre-hydration default. Internal — set by the
1723
- * activate path in `create-form-store.ts`. Don't set from consumer
1724
- * code.
1725
- */
1726
- readonly hydration?: boolean;
1727
- };
1728
- /**
1729
- * The store slice the history runtime binds to. Structural on purpose:
1730
- * `historyPlugin()` ships from the separate `attaform/history` entry, so
1731
- * the runtime receives the store through this seam instead of importing
1732
- * the store module (which would pull the history internals onto every
1733
- * form's eager path). A FormStore satisfies it as-is.
1734
- *
1735
- * @internal
1736
- */
1737
- type HistoryKernel = {
1738
- readonly form: Ref<unknown>;
1739
- readonly blankPaths: Set<PathKey>;
1740
- readonly errorCells: Map<PathKey, ErrorCell>;
1741
- onFormChange(listener: (next: unknown, meta?: WriteMeta) => void): () => void;
1742
- applyFormReplacement(next: unknown): void;
1743
- restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
1744
- };
1745
- /**
1746
- * The live undo/redo runtime a {@link HistoryPlugin} attaches to one
1747
- * form. Cached on the FormStore so every `useForm` / `injectForm`
1748
- * consumer of the same key shares one chain; `buildFormApi` adapts it
1749
- * into the public `form.history` namespace.
1750
- *
1751
- * @internal
1752
- */
1753
- type HistoryModule = {
1754
- undo(): boolean;
1755
- redo(): boolean;
1756
- clear(): void;
1757
- canUndo: Readonly<ComputedRef<boolean>>;
1758
- canRedo: Readonly<ComputedRef<boolean>>;
1759
- historySize: Readonly<ComputedRef<number>>;
1760
- dispose(): void;
1761
- };
1762
- /**
1763
- * Opt-in undo/redo, created by `historyPlugin()` from `attaform/history`
1764
- * and passed via `useForm({ history })`:
1765
- *
1766
- * ```ts
1767
- * import { historyPlugin } from 'attaform/history'
1768
- *
1769
- * const form = useForm({ schema, history: historyPlugin({ max: 200 }) })
1770
- * ```
1771
- *
1772
- * When enabled, every mutation records a position; `form.history.undo()`
1773
- * / `form.history.redo()` walk the chain. `reset()` is itself a mutation —
1774
- * the pre-reset state stays one undo away. Persistence hydration is the
1775
- * floor: after hydrate applies, the chain reseeds with the hydrated value
1776
- * and `undo()` cannot reach the transient pre-hydration default.
1777
- *
1778
- * One plugin instance is a reusable configuration, not per-form state:
1779
- * passing the same instance to several forms gives each form its own
1780
- * independent chain.
1781
- */
1782
- type HistoryPlugin = {
1783
- /**
1784
- * Bind a fresh history runtime to one form's store. Called by
1785
- * `useForm` when the store is first created.
1786
- *
1787
- * @internal
1788
- */
1789
- readonly attach: (kernel: HistoryKernel) => HistoryModule;
1790
- };
1791
- /**
1792
- * Consolidated undo/redo namespace at `form.history`. All history-related
1793
- * surface lives here — methods and reactive flags both — so consumers
1794
- * have one canonical address to read from.
1795
- *
1796
- * Always present on `useForm()` return whether or not `history` was
1797
- * configured. When history isn't enabled, methods are no-ops returning
1798
- * `false` (or `void`), `canUndo` / `canRedo` read `false`, and `size`
1799
- * reads `0`. Consumer templates don't need conditional logic.
1800
- *
1801
- * Reactivity: built as `readonly(reactive({...}))`, so `canUndo` / `canRedo`
1802
- * / `size` auto-unwrap on access (plain `boolean` / `number`, not refs).
1803
- * Method fields (`undo`, `redo`, `clear`) pass through as plain functions.
1804
- */
1805
- type FormHistoryNamespace = {
1806
- /**
1807
- * Step back one position in the history chain. Returns `true` when a
1808
- * step was taken, `false` when already at the oldest reachable
1809
- * position (or when history isn't configured).
1810
- */
1811
- readonly undo: () => boolean;
1812
- /**
1813
- * Replay the next step forward in the chain. Returns `true` on
1814
- * success, `false` when there's nothing queued (or history isn't
1815
- * configured). The forward branch is dropped as soon as a new
1816
- * mutation lands.
1817
- */
1818
- readonly redo: () => boolean;
1819
- /**
1820
- * Wipe the undo and redo branches; reseed the chain with the current
1821
- * form state as the new baseline. The form value, errors, and
1822
- * blankPaths all stay where they are — only the past/future history
1823
- * resets. After `clear()`: `canUndo === false`, `canRedo === false`,
1824
- * `size === 1`. No-op when history isn't configured.
1825
- */
1826
- readonly clear: () => void;
1827
- /** `true` when there is at least one undo step available. */
1828
- readonly canUndo: boolean;
1829
- /** `true` when `undo()` has been called and a `redo()` would replay. */
1830
- readonly canRedo: boolean;
1831
- /**
1832
- * Total reachable positions in the history chain (the current
1833
- * position plus everything reachable via `undo()` / `redo()`).
1834
- * Useful for debug overlays; UI driving undo/redo buttons should
1835
- * gate on `canUndo` / `canRedo` instead. Reads `0` when history
1836
- * isn't configured.
1837
- */
1838
- readonly size: number;
1839
- };
1840
- /**
1841
- * Configuration object passed to `useForm`. All fields except `schema`
1842
- * are optional.
1843
- *
1844
- * ```ts
1845
- * const form = useForm({
1846
- * schema: signupSchema,
1847
- * defaultValues: { email: '' },
1848
- * validateOn: 'change',
1849
- * debounceMs: 200,
1850
- * })
1851
- * ```
1852
- */
1853
- type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DefaultValuesInput<Form>, K extends FormKey = FormKey> = {
1854
- /**
1855
- * The schema describing the form's shape and validation rules.
1856
- * Typed entry points like `attaform/zod` accept the
1857
- * underlying library's schema directly and wrap an adapter; the
1858
- * abstract entry point accepts any object implementing
1859
- * `AbstractSchema`.
1860
- *
1861
- * For schemas that depend on the form's identity or per-form
1862
- * options, pass a factory `(key, options) => schema` instead — the
1863
- * library calls it once per form. Most adapters ignore the options
1864
- * argument; the typed Zod entry points use it to thread the
1865
- * recursion cap into the adapter closure.
1866
- */
1867
- schema: Schema | ((key: FormKey, options: SchemaFactoryOptions) => Schema);
1868
- /**
1869
- * Optional identifier for this form. Omit for one-off forms; the
1870
- * library allocates a unique key automatically (SSR-safe, stable
1871
- * across server→client hydration).
1872
- *
1873
- * Pass a string key when the form needs identity:
1874
- * - to look it up from a distant component via `injectForm(key)`;
1875
- * - to share state across components (multiple `useForm({ key })`
1876
- * calls with the same key resolve to the same form);
1877
- * - to give DevTools and validation errors a recognisable label.
1878
- *
1879
- * Keys starting with `__atta:` are reserved for internal use and
1880
- * throw `ReservedFormKeyError` if passed.
1881
- *
1882
- * When passed as a string literal, the literal is preserved on
1883
- * `form.key` so `useWizard` and other consumers can discriminate
1884
- * against the union of known keys at compile time.
1885
- */
1886
- key?: K;
1887
- /**
1888
- * Initial values applied over the schema's defaults. Each field
1889
- * falls back to the schema default (or the primitive default for
1890
- * the slot's type) when not provided here.
1891
- *
1892
- * Values must satisfy the slim primitive type at each path
1893
- * (string / number / boolean / Date / etc.) but do NOT have to
1894
- * satisfy refinement-level constraints (format checks, enum
1895
- * membership, length / range bounds). Refinement-invalid defaults
1896
- * pass through and surface as field errors — this lets you
1897
- * rehydrate stale saved data without losing the user's input.
1898
- *
1899
- * Accepts a plain value, a sync function, or an async function:
1900
- *
1901
- * ```ts
1902
- * // Plain value — applies at construction.
1903
- * defaultValues: { email: '' }
1904
- *
1905
- * // Sync function — invoked on a microtask after construction.
1906
- * defaultValues: () => buildDraft()
1907
- *
1908
- * // Async function — form starts with the schema's slim defaults
1909
- * // and `form.hydrating` flips true while the promise is
1910
- * // in flight; on resolve the values apply and `hydrating` flips
1911
- * // false. Under SSR the factory fires via `onServerPrefetch` so
1912
- * // the resolved payload bakes into hydration transfer state and
1913
- * // the client never re-fetches.
1914
- * defaultValues: async () => api.fetchDraft(userId)
1915
- * ```
1916
- *
1917
- * Errors thrown by a function-form factory surface on
1918
- * `form.hydrateError`; the form stays usable with slim defaults.
1919
- * Call `form.rehydrate()` to re-fire the factory.
1920
- */
1921
- defaultValues?: DefaultValues | (() => DefaultValues) | (() => Promise<DefaultValues>);
1922
- /**
1923
- * Move keyboard / screen-reader focus to the first errored field when
1924
- * a submit attempt fails validation. Fires after errors are populated
1925
- * and before your `onError` callback runs.
1926
- *
1927
- * Default `true`: focusing the broken field on submit is an
1928
- * accessibility baseline, and modern browsers scroll the focused
1929
- * element into view as part of the move. Pass `false` when you drive
1930
- * the nudge yourself from an `onError` callback with
1931
- * `form.focusFirstError()` or `form.scrollToFirstError()`, both of
1932
- * which stay available either way.
1933
- *
1934
- * The focus requests `focusVisible: true`, so the browser paints a
1935
- * focus ring even though the move is programmatic. Where a UA doesn't
1936
- * yet support that hint, non-text controls (radio, checkbox, custom
1937
- * widgets) may end up focused without a visible ring after a
1938
- * pointer-driven submit; if you target those browsers, pair this with
1939
- * your own indicator (e.g. a `:focus-within` ring on the option
1940
- * wrapper).
1941
- *
1942
- * If no errored field has a currently-mounted, visible element, the
1943
- * focus silently no-ops.
1944
- */
1945
- focusOnInvalidSubmit?: boolean;
1946
- /**
1947
- * Freeze the form's data. When this resolves truthy every value write
1948
- * no-ops at the store's write chokepoint (programmatic `setValue`, the
1949
- * `v-register` directive, and host-model events alike), native
1950
- * registered inputs render the HTML `disabled` attribute, component
1951
- * hosts receive a `disabled` flag on their register value, and every
1952
- * field's `displayState` drops to `'idle'` so error, pending, and
1953
- * success signals stand down. The first blocked write logs a one-time
1954
- * dev warning; writes never throw.
1955
- *
1956
- * `reset()` and `defaultValues` hydration still apply while frozen, so
1957
- * a disabled form can be populated or cleared programmatically. This is
1958
- * a data freeze only: a disabled form stays navigable, which is what a
1959
- * read-only review page wants.
1960
- *
1961
- * Accepts a boolean, a ref, a computed, or a getter, read live so the
1962
- * freeze tracks a reactive source. `undefined` resolves to `false`.
1963
- *
1964
- * A shared FormStore resolves `disabled` from its first `useForm({ key })`
1965
- * call; a later caller passing a different value is ignored.
1966
- */
1967
- disabled?: MaybeRefOrGetter<boolean | undefined>;
1968
- /**
1969
- * When per-field VALIDATION runs (the directive's listener controls
1970
- * how often storage commits — per keystroke by default, per blur
1971
- * with `.lazy`). Default `'change'`. See `ValidateOn` for mode
1972
- * semantics.
1973
- *
1974
- * The strict public `useForm` signature wraps this type in an
1975
- * intersection with `ValidateOnConfig`, which enforces that
1976
- * `debounceMs` is only allowed under `'change'`. Internal callers
1977
- * (adapters, hydration paths) work with the loose form below.
1978
- */
1979
- validateOn?: ValidateOn;
1980
- /**
1981
- * Milliseconds to wait after the last committed write before
1982
- * running validation. Default `0` (validation fires synchronously
1983
- * after the write; no `setTimeout`). Set to a positive number to
1984
- * coalesce rapid bursts. Ignored under `validateOn: 'blur'` and
1985
- * `'submit'`.
1986
- *
1987
- * This is purely a VALIDATION debounce — storage commits are the
1988
- * directive's concern (per keystroke for `<input v-register>`,
1989
- * per blur for `<input v-register.lazy>`).
1990
- */
1991
- debounceMs?: number;
1992
- /**
1993
- * Opt-in undo/redo. Off by default. Pass `historyPlugin()` from
1994
- * `attaform/history` — a 128-position cap by default,
1995
- * `historyPlugin({ max: N })` to tune it.
1996
- *
1997
- * Every mutation records a position. `form.history.undo()` walks
1998
- * one step back; `form.history.redo()` walks one step forward.
1999
- * `reset()` is itself a mutation, so the pre-reset state stays one
2000
- * undo away. The consolidated `form.history` namespace also exposes
2001
- * `clear()`, `canUndo`, `canRedo`, and `size`.
2002
- */
2003
- history?: HistoryPlugin;
2004
- /**
2005
- * Whether to remember the typed state of each discriminated-union
2006
- * variant across switches. Default `true`.
2007
- *
2008
- * When `true`, switching `notify.channel` from `email` (with
2009
- * `address: 'foo@bar.com'`) to `sms` and back lands on
2010
- * `address: 'foo@bar.com'` again — the runtime snapshots the
2011
- * outgoing variant's subtree on switch-out and restores the
2012
- * incoming variant's prior subtree on switch-in. Each
2013
- * discriminated union at every nesting depth is independently
2014
- * memorized.
2015
- *
2016
- * Set to `false` to drop the outgoing variant's typed state on
2017
- * every switch (the data is gone). The new variant initializes
2018
- * from its slim default.
2019
- *
2020
- * Memory is in-memory only and does not survive a fresh mount: a
2021
- * page reload starts every discriminator's variant memory empty.
2022
- *
2023
- * `reset()` clears variant memory. `resetField(path)` clears any
2024
- * memory entry whose union path equals or sits under `path`.
2025
- */
2026
- rememberVariants?: boolean;
2027
- /**
2028
- * Schema-driven coercion of user-typed DOM values at the v-register
2029
- * directive layer.
2030
- *
2031
- * Two rules ship: string→number and string→boolean, each firing
2032
- * only when the schema declares that single type at the path.
2033
- * Defaults to on; `false` disables coercion form-wide and the slim
2034
- * gate rejects mismatches instead.
2035
- *
2036
- * Coercion applies ONLY to user-typed DOM values. Programmatic
2037
- * writes (`form.setValue`, `setValueWithInternalPath`) are NEVER
2038
- * coerced.
2039
- */
2040
- coerce?: boolean;
2041
- /**
2042
- * @internal
2043
- * SSR prefetch mark — set by the `attaform/vite` compile-time
2044
- * transform on `useForm` calls whose surrounding SFC template (or a
2045
- * computed feeding it) reads the form's reactive state. The flag
2046
- * enqueues the form on the registry's SSR prefetch queue so an
2047
- * async `defaultValues` factory runs inside `onServerPrefetch` and
2048
- * the resolved payload bakes into the hydration transfer state.
2049
- *
2050
- * Consumers do not write this directly — `form.activate()` is the
2051
- * documented escape hatch when the transform's static analysis
2052
- * can't see a reference (cross-module sharing, dynamic property
2053
- * access, headless contexts).
2054
- */
2055
- __ssrAccessed?: boolean;
2056
- };
2057
- /**
2058
- * Callback invoked by `handleSubmit` after the form parses successfully.
2059
- * Receives the strictly-typed parsed value — refinements have run, so
2060
- * enum / literal / format constraints are honoured.
2061
- */
2062
- type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
2063
- /**
2064
- * Callback invoked by `handleSubmit` on a failed submit. Fires when
2065
- * client validation fails AND when the submit callback leaves errors in
2066
- * the user-error layer (the `setErrors(...); return` server-rejection
2067
- * pattern). Receives the full list of errors. Bind this when you want to
2068
- * react to submit failures explicitly (alongside or instead of the
2069
- * automatic `focusOnInvalidSubmit` nudge).
2070
- */
2071
- type OnError = (error: ValidationError[]) => void | Promise<void>;
2072
- /**
2073
- * The display-state verdict at a path: the single signal a UI needs to
2074
- * decide what (if anything) to surface about validation right now.
2075
- * Rolled up at containers and at the form root (`form.meta.displayState`).
2076
- *
2077
- * - `'idle'` — nothing to surface. Either pre-interaction (the timing
2078
- * gate hasn't opened) or gate-open with no verdict worth showing.
2079
- * - `'pending'` — a validation run is in flight at this path; the prior
2080
- * verdict is stale. Drive a spinner / "Checking…" affordance.
2081
- * - `'error'` — a blocking error the timing gate has cleared for display.
2082
- * - `'success'` — validation passed and the gate has cleared a positive
2083
- * confirmation (the green-check pattern).
2084
- *
2085
- * The four `show*` booleans on `FieldState` are sugar over this enum
2086
- * (`showErrors === (displayState === 'error')`, and so on), so they can
2087
- * never contradict it.
2088
- */
2089
- type DisplayState = 'idle' | 'pending' | 'error' | 'success';
2090
- /**
2091
- * Keys on `FieldState` layered on FROM the display-state predicate
2092
- * (plus `firstError` / `firstOwnError`, computed alongside them).
2093
- * `Omit`'d from the predicate's arguments so a predicate cannot read
2094
- * its own output and form a cycle — enforced at the type level AND at
2095
- * runtime: the base objects passed in literally lack these keys, so an
2096
- * `as` cast in TS or a vanilla-JS caller still can't reach them.
2097
- * `FieldStateBase` / `FormMetaBase` (field-state-api.ts) omit the same
2098
- * set in lockstep.
2099
- */
2100
- type FieldStateDerivedKey = 'displayState' | 'showErrors' | 'showPending' | 'showSuccess' | 'showIdle' | 'firstError' | 'firstOwnError';
2101
- /**
2102
- * One step of the display state machine: the verdict the field should
2103
- * render right now (`display`, projected to `displayState` and the
2104
- * `show*` booleans) plus two optional timing cells the engine reads.
2105
- *
2106
- * - `reviewAt` — an absolute `Date.now()` millisecond stamp telling the
2107
- * engine "re-evaluate this field no later than here." The engine keeps
2108
- * a single timer per form aimed at the nearest `reviewAt` across all
2109
- * active fields; when it fires, the dependent field computeds re-run
2110
- * and call the reducer again. A machine with no `reviewAt` and a
2111
- * non-pending `display` is terminal — the engine evicts it.
2112
- * - `pendingShownAt` — the stamp at which `'pending'` was first shown,
2113
- * the memory the min-visible hold needs so a spinner that just appeared
2114
- * is not yanked away the instant validation resolves. Opaque to the
2115
- * engine; a custom reducer may carry its own extra memory fields too.
2116
- */
2117
- type DisplayMachine = {
2118
- readonly display: DisplayState;
2119
- readonly reviewAt?: number;
2120
- readonly pendingShownAt?: number;
2121
- };
2122
- /**
2123
- * Inputs to the display reducer. `field` and `formMeta` are the
2124
- * reactive snapshots it resolves against (minus the derived
2125
- * `displayState` / `show*` / `firstError` keys — see
2126
- * `FieldStateDerivedKey` — so the reducer can never read its own output
2127
- * and form a cycle), joined by:
2128
- *
2129
- * - `validatingSince` — `Date.now()` at which the field's current
2130
- * validation streak opened, or `null` when nothing is in flight. This,
2131
- * not `field.validating`, is the timing anchor: the elapsed wait is
2132
- * `now - validatingSince`. Pinned to the start of the streak, so
2133
- * overlapping sub-runs do not reset it.
2134
- * - `transformingSince` — the same anchor for an in-flight async
2135
- * `register` transform, or `null` when none is running. Folds into the
2136
- * one in-flight clock the reducer already runs for validation, so a
2137
- * deferred transform rides the anti-flash spinner timing identically.
2138
- * `null` for a sync-only chain, which never defers.
2139
- * - `now` — the engine's clock, injected so the reducer stays pure and
2140
- * deterministic (and frozen to `0` under SSR, where there is no clock).
2141
- */
2142
- type DisplayCtx = {
2143
- readonly field: Omit<FieldState, FieldStateDerivedKey>;
2144
- readonly formMeta: Omit<FormMeta, FieldStateDerivedKey>;
2145
- readonly validatingSince: number | null;
2146
- readonly transformingSince: number | null;
2147
- readonly now: number;
2148
- };
2149
- /**
2150
- * Pure transition reducer that resolves a path's `displayState`. Given
2151
- * the field's previous `DisplayMachine` and the current `DisplayCtx`, it
2152
- * returns the next machine; the engine owns the clock and the timers, the
2153
- * reducer owns the timing policy. Runs on every field-state read (and
2154
- * again whenever a `reviewAt` deadline fires), so the whole app's
2155
- * validation-display behavior flows from this one function
2156
- * (`core/display-state.ts`).
2157
- * @internal
2158
- */
2159
- type GetDisplayState = (prev: DisplayMachine, ctx: DisplayCtx) => DisplayMachine;
2160
- /**
2161
- * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
2162
- * it to a `<form>`:
2163
- *
2164
- * ```vue
2165
- * <form @submit="onSubmit">…</form>
2166
- * ```
2167
- *
2168
- * It accepts the originating `Event` and calls `event.preventDefault()`
2169
- * itself, so bind it with `@submit`, not `@submit.prevent` (the
2170
- * modifier would only prevent the default a second time). Called
2171
- * imperatively with no event, the `preventDefault` step is skipped.
2172
- */
2173
- type SubmitHandler = (event?: Event) => Promise<void>;
2174
- /**
2175
- * Type of `form.handleSubmit`. Pass an `onSubmit` callback for the
2176
- * happy path and (optionally) an `onError` callback that receives
2177
- * the validation errors when parsing fails.
2178
- *
2179
- * ```ts
2180
- * const onSubmit = form.handleSubmit(
2181
- * (data) => api.signup(data),
2182
- * (errors) => console.log(errors),
2183
- * )
2184
- * ```
2185
- */
2186
- type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
2187
- /**
2188
- * Per-leaf internal tracker record. Distinct from `FieldState.meta`
2189
- * (which surfaces as `Readonly<FieldMetaPayload>` — the registry-
2190
- * attached label / description / placeholder payload). Surfaced for
2191
- * custom-adapter authors threading metadata through their own
2192
- * pipelines; most consumers don't reach for it directly — the
2193
- * matching fields appear with friendlier shape on `FieldState`.
2194
- *
2195
- * - `updatedAt` — ISO timestamp of the most recent write at this path,
2196
- * or `null` if the field has never been written.
2197
- * - `rawValue` — the value as it arrived (before any transform);
2198
- * useful for distinguishing parse-coerced reads from raw user input.
2199
- * - `connected` — whether at least one DOM element bound to this
2200
- * path is currently mounted. Flips to `false` when every binding
2201
- * unmounts.
2202
- * - `formKey` — identifier of the form this metadata belongs to.
2203
- * - `path` — dotted-string path to this leaf, or `null` when not applicable.
2204
- */
2205
- type MetaTrackerValue = {
2206
- /** ISO timestamp of the most recent write at this path. `null` if never written. */
2207
- updatedAt: string | null;
2208
- /** Value as it arrived, before any transforms. */
2209
- rawValue: unknown;
2210
- /** `true` while at least one binding to this path is currently mounted. */
2211
- connected: boolean;
2212
- /** Form this metadata belongs to. */
2213
- formKey: FormKey;
2214
- /** Dotted-string path to this leaf. */
2215
- path: string | null;
2216
- /**
2217
- * `true` when this field is **blank** — the runtime has recorded
2218
- * that storage and the visible display diverge here. Reserved for
2219
- * the case the schema can't see on its own: storage forces a
2220
- * value (e.g. `0` for a numeric leaf, `0n` for a bigint leaf)
2221
- * while the DOM input shows `''`, and the runtime needs a side-
2222
- * channel to tell "user typed 0" from "user supplied nothing."
2223
- *
2224
- * Set automatically for numeric leaves (the directive's input
2225
- * listener on clear; the construction-time pass when the consumer
2226
- * didn't supply a value). Set explicitly for any primitive leaf
2227
- * via `setValue(path, unset)` / `defaultValues: { x: unset }` /
2228
- * `reset({ x: unset })` — that's the documented opt-in signal for
2229
- * strings, booleans, and other types that don't otherwise diverge.
2230
- * Cleared on the first non-`unset` write.
2231
- *
2232
- * `errors = f(schema, state)` is reactive end-to-end: any required
2233
- * path with `blank: true` produces a "No value supplied" entry in
2234
- * `form.errors` immediately, no `validate()` / `handleSubmit` call
2235
- * required. Most consumers don't need this flag directly — gate UI
2236
- * on `errors[path]` and `touched`. Read `blank` itself when you
2237
- * want pre-error introspection ("the user hasn't decided yet"
2238
- * indicator, "review unanswered fields" hint).
2239
- *
2240
- * See `docs/validation/blank.md` for the full conceptual model.
2241
- */
2242
- blank: boolean;
2243
- };
2244
- type RegisterFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'register'> : never;
2245
- /**
2246
- * A transformation applied to a field's value as user input flows
2247
- * from DOM through the directive's assigner. Composes left-to-right
2248
- * via the `transforms: [...]` array on `register()`.
2249
- *
2250
- * The shape is intentionally generic-erased (`(unknown) => unknown`)
2251
- * rather than per-path-typed: a personal library of transforms
2252
- * (`trim`, `lowercase`, `slugify`, `clamp`, …) should plug into any
2253
- * `register()` slot regardless of the path's value type. Library
2254
- * authors write defensive bodies that no-op on type mismatch:
2255
- *
2256
- * ```ts
2257
- * export const trim: RegisterTransform = (v) =>
2258
- * typeof v === 'string' ? v.trim() : v
2259
- * ```
2260
- *
2261
- * Type-safety at the call site is delegated to attaform's slim-primitive
2262
- * gate — a transform that produces a value the path's storage
2263
- * doesn't accept gets rejected at write time with a standard
2264
- * diagnostic.
2265
- *
2266
- * Transforms may be sync or async. The chain stays fully synchronous —
2267
- * the value reaches form state in the same tick — until a transform
2268
- * returns a thenable; from there the write defers, the field reads
2269
- * `busy` / `transforming` while the chain settles, and the resolved
2270
- * value commits to canonical state once it lands. Rapid edits discard
2271
- * all but the latest (latest-request-wins), and a rejection surfaces on
2272
- * `field.transformError` rather than throwing or logging:
2273
- *
2274
- * ```ts
2275
- * export const normalize: RegisterTransform = async (v, ctx) => {
2276
- * const res = await fetch(`/normalize?q=${v}`, { signal: ctx?.signal })
2277
- * return res.text()
2278
- * }
2279
- * ```
2280
- *
2281
- * `ctx.signal` is an `AbortSignal` aborted when the run is superseded by
2282
- * a newer edit, or torn down by `reset()` / unmount — thread it into
2283
- * cancellable I/O so a stale request is dropped. A sync chain never
2284
- * touches it and allocates no controller.
2285
- *
2286
- * A synchronous throw is caught and aborts the pipeline: subsequent
2287
- * transforms don't run, nothing is written to form state, and the
2288
- * assigner returns `false` — so a buggy or defensive-throw transform
2289
- * never crashes the host app. (An async rejection is the
2290
- * `transformError` channel above, not a throw into the host app.)
2291
- */
2292
- type RegisterTransform = (value: unknown, ctx?: TransformContext) => unknown;
2293
- /**
2294
- * Options for `register(path, options)`. Per-field configuration
2295
- * applied at the binding's own call site.
2296
- */
2297
- type RegisterOptions = {
2298
- /**
2299
- * Sync transformation pipeline applied to user-typed values before
2300
- * they reach form state. Composes left-to-right: each transform
2301
- * receives the previous transform's output (or the directive-
2302
- * extracted DOM value for the first transform).
2303
- *
2304
- * Pipeline order:
2305
- * `DOM event → modifier cast (.lazy/.trim/.number) → transforms[0] → … → transforms[n] → assigner`
2306
- *
2307
- * Applies to user input only. Programmatic writes
2308
- * (`form.setValue(...)`, `rv.setValueWithInternalPath(...)`),
2309
- * `form.reset()`, hydration, SSR replay, and `markBlank()` all
2310
- * bypass transforms — those write canonical state, not normalized
2311
- * user input. If you want the same normalization on a programmatic
2312
- * write, compose the transforms yourself at the call site:
2313
- *
2314
- * ```ts
2315
- * form.setValue('email', slugify(lowercase(rawValue)))
2316
- * ```
2317
- *
2318
- * Transforms may be sync or async: the chain stays synchronous until
2319
- * one returns a thenable, then the write defers and commits the
2320
- * resolved value (the field reads `busy` meanwhile). A sync throw
2321
- * aborts the write; an async rejection lands on `field.transformError`
2322
- * (see `RegisterTransform` for the full contract).
2323
- *
2324
- * For patterns that need to inspect the `RegisterValue` itself
2325
- * (rejection-with-side-effect, redirection to other fields, custom
2326
- * DOM mutation), use `@update:registerValue` on the bound element
2327
- * instead — see the "Custom assigners" section in the API docs.
2328
- */
2329
- transforms?: ReadonlyArray<RegisterTransform>;
2330
- };
2331
- /**
2332
- * The narrow kernel surface the DOM binding drives. Structurally a
2333
- * subset of the form store: the field-record connect/disconnect
2334
- * transitions, focus marking (which owns blur-validation), the record
2335
- * and merged-error reads the focus walk needs, and the async-transform
2336
- * abort for a fully-detached path.
2337
- * @internal
2338
- */
2339
- type DomBindingKernel = {
2340
- readonly noteDomConnected: (path: Path) => void;
2341
- readonly noteDomDisconnected: (path: Path) => void;
2342
- readonly markFocused: (path: Path, focused: boolean, meta?: {
2343
- readonly instance?: WriteMeta['instance'];
2344
- }) => void;
2345
- readonly getFieldRecord: (path: Path) => FieldRecord | undefined;
2346
- readonly getErrorsForPath: (path: Path) => ValidationError[];
2347
- readonly cancelTransformsUnder: (prefix: Path) => void;
2348
- };
2349
- /**
2350
- * The form store's DOM slice: the element registry backing
2351
- * `field.element` / `field.elements`, no-latch host focus anchors, and
2352
- * first-error focus resolution. Implemented in the directive cluster's
2353
- * lazy graph and armed into the store's `domBinding` slot on first use
2354
- * (see `RegisterValue.ensureDomBinding`); a `null` slot means nothing
2355
- * in the app ever registered an element.
2356
- * @internal
2357
- */
2358
- type AttaformDomBinding = {
2359
- readonly elements: Map<PathKey, ElementRecord>;
2360
- readonly attach: (segments: Path, element: HTMLElement, formInstanceId: string, instanceMeta: WriteMeta['instance'] | undefined) => void;
2361
- readonly detach: (segments: Path, element: HTMLElement) => void;
2362
- readonly markHostConnected: (segments: Path, connected: boolean, hostEl: HTMLElement, formInstanceId: string) => void;
2363
- readonly getFirstErrorElement: (formInstanceId: string) => {
2364
- path: Path;
2365
- element: HTMLElement;
2366
- } | null;
2367
- };
2368
- /**
2369
- * Factory the directive / `useRegister` inject through
2370
- * `RegisterValue.ensureDomBinding` to arm a store's `domBinding` slot.
2371
- * @internal
2372
- */
2373
- type DomBindingFactory = (kernel: DomBindingKernel) => AttaformDomBinding;
2374
- /**
2375
- * The object returned by `form.register(path)`. Pass it to a native
2376
- * input via `v-register`:
2377
- *
2378
- * ```vue
2379
- * <input v-register="form.register('email')" />
2380
- * ```
2381
- *
2382
- * Or read `innerRef` directly when integrating with custom components.
2383
- *
2384
- * The returned value is a `shallowReadonly` reactive proxy: top-level
2385
- * reads (`rv.path`, `rv.formKey`, `rv.segments`, …) track in reactive
2386
- * scopes, mutations are blocked, and inner refs (`innerRef`,
2387
- * `displayValue`) keep their `Ref` shape.
2388
- *
2389
- * `path`, `formKey`, and `formInstanceId` are the wrapper-component
2390
- * primitives — a generic component using `useRegister()` can derive
2391
- * field state and form identity from them without re-threading props
2392
- * from the parent.
2393
- */
2394
- type RegisterValue<Value = unknown> = Readonly<{
2395
- /**
2396
- * Live, read-only reactive value at this path. Watch it to drive
2397
- * UI that depends on the field's current value.
2398
- */
2399
- innerRef: Readonly<Ref<Value>>;
2400
- /**
2401
- * Attach an HTML element to this binding. Called by `v-register`
2402
- * automatically; expose it to custom integrations that need to
2403
- * register an element manually.
2404
- *
2405
- * Recording the element drives the form's element map (used for
2406
- * `field.meta.connected`, `focusFirstError`, and `scrollToFirstError`).
2407
- */
2408
- registerElement: (el: HTMLElement) => void;
2409
- /**
2410
- * Detach an HTML element from this binding. Pair with
2411
- * `registerElement` for custom integrations. Drops the element from
2412
- * the form's element map.
2413
- */
2414
- deregisterElement: (el: HTMLElement) => void;
2415
- /**
2416
- * Write the field's value programmatically. Returns `true` when the
2417
- * write was accepted, `false` when it was rejected (e.g. wrong
2418
- * primitive type for the path).
2419
- *
2420
- * The write path for custom directives and consumer assigners: it
2421
- * routes through the same funnel (and per-instance meta) as the
2422
- * directive's default assigner. Caller-supplied `meta` passes through
2423
- * unchanged.
2424
- */
2425
- setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
2426
- /**
2427
- * Commit a value emitted by a third-party component bound through
2428
- * `v-register`'s compile-time v-model desugar. Writes the value (the
2429
- * component's typed model output, authoritative -- no coercion) AND marks
2430
- * the field interacted, since a v-model host has no DOM input listener to
2431
- * flip the sticky `interacted` bit. The injected `onUpdate:modelValue`
2432
- * handler is the only caller. Returns `true` when the write was accepted.
2433
- * @internal
2434
- */
2435
- setValueFromHost: (value: unknown) => boolean;
2436
- /**
2437
- * Mark this field as DOM-connected during SSR so a server-rendered
2438
- * template that reads `form.fields.<path>.connected` doesn't
2439
- * flicker on hydration. The `v-register` directive calls this for
2440
- * you; no-op on the client.
2441
- * @internal
2442
- */
2443
- markConnectedOptimistically: () => void;
2444
- /**
2445
- * Mark this field DOM-connected (`true`) or disconnected (`false`) for a
2446
- * `v-register` component host that binds value through the v-model desugar
2447
- * but exposes no single inner control to register -- a composite widget,
2448
- * or none found. The directive calls it on mount / unmount; distinct from
2449
- * the SSR-only `markConnectedOptimistically`.
2450
- *
2451
- * `hostEl` is the host root element, recorded on connect as the field's
2452
- * focus-first-error anchor: a no-latch host registers no control, so it
2453
- * would otherwise be invisible to `focusFirstError` / `scrollToFirstError`
2454
- * (which walk the registered-element set). On an invalid submit the error
2455
- * walk resolves the host root to its first focusable descendant. The
2456
- * directive passes the same element on both edges.
2457
- * @internal
2458
- */
2459
- markHostConnected: (connected: boolean, hostEl: HTMLElement) => void;
2460
- /**
2461
- * Mark this field focused (`true`) or blurred (`false`) for a `v-register`
2462
- * component host with no single latched control -- a composite widget whose
2463
- * focus moves between inner segments, or a control-less one. The directive
2464
- * tracks focusin / focusout on the widget root and forwards here, so focus
2465
- * state and blur-validation still arm without an element-level focus
2466
- * listener. Carries this binding's instance meta (its `validateOn`).
2467
- * @internal
2468
- */
2469
- markFocused: (focused: boolean) => void;
2470
- /**
2471
- * `true` when an element already registered for this binding's path is
2472
- * contained within (or equal to) `hostElement`. The directive's
2473
- * component-host branch reads it to tell a `useRegister` wrapper (whose
2474
- * inner control self-registered) from a third-party component (which
2475
- * registered nothing), so the latch only runs for the latter.
2476
- * @internal
2477
- */
2478
- hasRegisteredDescendant: (hostElement: HTMLElement) => boolean;
2479
- /**
2480
- * Arm this binding's form store with the DOM-binding implementation
2481
- * (element registry, focus listeners, first-error focus resolution).
2482
- * The implementation lives in the directive cluster's lazy graph, so
2483
- * the directive and `useRegister` inject its factory here before any
2484
- * element call; once armed, the store's `domBinding` slot serves every
2485
- * later registration. Optional so hand-rolled RegisterValues, which
2486
- * own their element handling, don't have to declare it.
2487
- * @internal
2488
- */
2489
- ensureDomBinding?: (factory: DomBindingFactory) => void;
2490
- /**
2491
- * Canonical, JSON-encoded path key for this binding (e.g.
2492
- * `'["items",0,"name"]'`). Useful for stable Map / Set keys, log
2493
- * messages, and equality checks against another `RegisterValue`'s
2494
- * path. Treat as opaque — for `form.fields(...)` / `form.values(...)`
2495
- * lookups inside wrapper components, use `segments` instead.
2496
- */
2497
- path: PathKey;
2498
- /**
2499
- * Structured path segments for this binding (e.g.
2500
- * `['items', 0, 'name']`). The consumer-friendly form for
2501
- * `form.fields(...)` / `form.values(...)` lookups in generic
2502
- * wrapper components:
2503
- *
2504
- * ```ts
2505
- * const rv = useRegister()
2506
- * const form = injectForm()
2507
- * const field = computed(() => form.fields(rv.value?.segments ?? []))
2508
- * ```
2509
- *
2510
- * Frozen at runtime so wrapper components can read it without
2511
- * defensive copying.
2512
- */
2513
- segments: Path;
2514
- /**
2515
- * The form's user-supplied (or auto-allocated) `key`, mirroring
2516
- * `form.key` on the public form API. Useful in wrapper components
2517
- * that target a specific form by key without prop-drilling.
2518
- */
2519
- formKey: string;
2520
- /**
2521
- * Per-mount runtime identifier for the form instance. Stable across
2522
- * the form's lifetime. Used by the directive to scope element
2523
- * registrations to a single mount and exposed here for wrapper
2524
- * components that need to disambiguate sibling forms with the same
2525
- * `key`.
2526
- */
2527
- formInstanceId: string;
2528
- /**
2529
- * Sync transform pipeline applied by the directive's assigner to
2530
- * user-typed values before they reach form state. See
2531
- * `RegisterOptions.transforms` for the public contract; this is
2532
- * the readonly internal handle the directive iterates. Optional
2533
- * so hand-rolled `RegisterValue` mocks (test fixtures, custom
2534
- * integrations) don't have to declare an empty array — the
2535
- * directive falls back to a no-op pipeline.
2536
- * @internal
2537
- */
2538
- transforms?: ReadonlyArray<RegisterTransform>;
2539
- /**
2540
- * Schema-driven coercion closure baked at register-time. Captures
2541
- * the path's slim accept set and the resolved coercion index so
2542
- * the per-event hot path is a single function call. Identity
2543
- * function when coercion is disabled or the path admits no
2544
- * coercion target. Optional so hand-rolled `RegisterValue` mocks
2545
- * (test fixtures, custom integrations) don't have to declare it —
2546
- * the directive falls back to identity.
2547
- * @internal
2548
- */
2549
- coerce?: (value: unknown) => unknown;
2550
- /**
2551
- * Element-level coercion closure for container paths
2552
- * (`z.array(...)` / `z.set(...)`). Coerces a scalar DOM-side
2553
- * value (an option's `value` attribute, a checkbox's value)
2554
- * against the container's element type. `undefined` when the
2555
- * path isn't a container — scalar paths use `coerce` exclusively.
2556
- *
2557
- * Used by the directive's read-side comparisons in setChecked
2558
- * (array/Set branches) and setSelected (multi-select) to keep
2559
- * parity with the change handler's WRITE-side path-level coerce.
2560
- * @internal
2561
- */
2562
- coerceElement?: (value: unknown) => unknown;
2563
- /**
2564
- * Read-only, string-form view of the field's current value — what
2565
- * the compile-time `:value` injection reads on every input /
2566
- * textarea / select bound by `v-register`.
2567
- *
2568
- * Returns `''` when the path is in the form's `blankPaths`
2569
- * set OR storage is `null` / `undefined`; otherwise stringifies
2570
- * the storage value via `String(...)`. The blank branch
2571
- * lets the user clear a numeric field without the next Vue render
2572
- * patching `el.value` back to `'0'` (the slim default).
2573
- */
2574
- displayValue: Readonly<Ref<string>>;
2575
- /**
2576
- * Blank-aware model presentation for a `v-register` component host's
2577
- * `:modelValue`. Returns `undefined` when the path is in the form's
2578
- * `blankPaths` set (the typed-model analog of `displayValue`'s `''`),
2579
- * otherwise the raw typed storage. Lets a cleared numeric field read
2580
- * empty in a v-model-bound component the way it does in a native
2581
- * input. Read only by the compile-time component-bridge transform's
2582
- * `:modelValue` injection.
2583
- * @internal
2584
- */
2585
- hostModelValue: Readonly<Ref<Value | undefined>>;
2586
- /**
2587
- * Live `true` when this binding's form is frozen via
2588
- * `useForm({ disabled })`. The compile-time transforms bind it to the
2589
- * host's `:disabled`, so a native `<input v-register>` renders the
2590
- * HTML `disabled` attribute (SSR and client) and a component host
2591
- * receives a `disabled` prop. Read it directly from a custom
2592
- * `useRegister` integration to drive your own disabled affordance.
2593
- */
2594
- disabled: Readonly<Ref<boolean>>;
2595
- /**
2596
- * Add this field's path to the form's `blankPaths` set,
2597
- * writing the slim default to storage. Returns the `setValueAtPath`
2598
- * boolean (`true` accepted, `false` rejected by the slim-primitive
2599
- * gate).
2600
- *
2601
- * Called by the directive's input listener on numeric clear (commit
2602
- * 5) and by the imperative `setValue(path, unset)` translation
2603
- * (commit 7). Don't call from consumer code.
2604
- * @internal
2605
- */
2606
- markBlank: () => boolean;
2607
- /**
2608
- * Flip this field's sticky `interacted` flag — the signal that the
2609
- * user has issued at least one value edit here (an insert or a
2610
- * delete). Called by the directive's input / change listeners on
2611
- * every genuine user input; never by hydration or programmatic
2612
- * writes. Idempotent (the store skips the write once set). Don't
2613
- * call from consumer code.
2614
- * @internal
2615
- */
2616
- markInteracted: () => void;
2617
- /**
2618
- * `true` when the schema's slim primitive set at this path includes
2619
- * `'undefined'` — i.e. the leaf was declared `.optional()` (or as
2620
- * part of a union admitting `undefined`). Cached at register-time.
2621
- *
2622
- * Read by the directive's text-input listener to map a DOM clear
2623
- * (`el.value === ''`) onto `undefined` storage instead of `''`, so
2624
- * the `.optional()` "absent" semantic survives user interactions.
2625
- * Without this, a user who typed an invalid value into an optional
2626
- * field and then cleared it would be stuck with a permanent
2627
- * validation error (storage holds `''`, which is neither
2628
- * `undefined` nor a valid inner value).
2629
- * @internal
2630
- */
2631
- acceptsUndefined: boolean;
2632
- /**
2633
- * `true` when the schema's slim primitive set at this path includes
2634
- * `'string'`. Cached at register-time alongside
2635
- * [[acceptsUndefined]].
2636
- *
2637
- * Read by the directive's text-input listener so a DOM clear on a
2638
- * numeric-only (or boolean-only, bigint-only) leaf takes the
2639
- * `markBlank` path instead of writing `''` through the assigner:
2640
- * the slim-primitive gate would reject the empty string anyway,
2641
- * and the directive's post-write force-sync would then snap the
2642
- * DOM back to the last accepted value, making the final character
2643
- * undeletable. With `markBlank`, storage holds the slim default
2644
- * with the blank meta and the DOM stays empty.
2645
- * @internal
2646
- */
2647
- acceptsString: boolean;
2648
- /**
2649
- * The field's aria satellite ids, mirroring `FieldState.aria`. The
2650
- * directive points `aria-describedby` at `errorId` while the field
2651
- * is in its error state. Optional so hand-rolled `RegisterValue`
2652
- * mocks don't have to declare it; the directive skips aria wiring
2653
- * when absent.
2654
- * @internal
2655
- */
2656
- aria?: {
2657
- readonly errorId: string;
2658
- readonly descriptionId: string;
2659
- };
2660
- /**
2661
- * Whether the schema marks this path required, from
2662
- * `schema.isRequiredAtPath(segments)`. Drives `aria-required`.
2663
- * Optional for the same mock-tolerance reason as `aria`.
2664
- * @internal
2665
- */
2666
- isRequired?: boolean;
2667
- /**
2668
- * The gated display-state verdict for this path, reusing the same
2669
- * field-state identity as `form.fields`. The directive watches it to
2670
- * keep `aria-invalid` / `aria-busy` / `aria-describedby` in lockstep
2671
- * with the visible error state, even on async ticks with no parent
2672
- * re-render. Optional; the directive skips aria wiring when absent.
2673
- * @internal
2674
- */
2675
- ariaDisplayState?: Readonly<Ref<DisplayState>>;
2676
- }>;
2677
- /**
2678
- * Internal extension of `RegisterValue` that includes directive-private
2679
- * coordination state. Imported by the directive runtime; not part of
2680
- * the public surface.
2681
- *
2682
- * `lastTypedForm` is the user's most recently typed string form for a
2683
- * numeric field while mid-typing, or `null` once the field has been
2684
- * blurred / cleared. The directive populates it on every committable
2685
- * input event and clears it on the change (blur) event so:
2686
- *
2687
- * - Mid-typing: `displayValue` returns the typed form (e.g.
2688
- * `'1e2'`) when it parses back to current storage. Vue's
2689
- * `:value` patch then targets the typed form, which already
2690
- * equals the DOM — idempotent, no cursor reset.
2691
- * - On blur: `displayValue` falls back to `String(storage)`
2692
- * (`'100'`), Vue patches the DOM to match. The user sees
2693
- * exactly what's stored.
2694
- *
2695
- * Why a separate field: JavaScript's Number carries no representation
2696
- * info — `1e2 === 100`, so `String(parseFloat('1e2'))` yields `'100'`.
2697
- * Tracking the typed form lets us avoid Vue's mid-typing DOM yank
2698
- * without lying about storage. Only meaningful for `.number` text
2699
- * inputs and `<input type="number">`; other bindings ignore it.
2700
- *
2701
- * @internal
2702
- */
2703
- /**
2704
- * Mutable holder for an async-transform run's `AbortController`, shared
2705
- * between the directive — which lazily creates the controller the first
2706
- * time a transform reaches for `ctx.signal` — and the store, which
2707
- * aborts it when the run is superseded, cancelled, or reset.
2708
- * `controller` stays `null` until `ctx.signal` is actually touched, so a
2709
- * purely-sync chain never allocates one. `aborted` latches `true` the
2710
- * moment the store tears the run down, so a signal accessed AFTER
2711
- * teardown still resolves to an already-aborted signal rather than a
2712
- * live one.
2713
- */
2714
- type TransformAbortHolder = {
2715
- controller: AbortController | null;
2716
- aborted: boolean;
2717
- };
2718
- /**
2719
- * The second argument handed to every transform in a `transforms: [...]`
2720
- * chain. `signal` is an `AbortSignal` that aborts when the run is
2721
- * superseded by a newer input, or torn down by a reset / cancel — so a
2722
- * transform doing cancellable I/O (a `fetch`, a worker round-trip) can
2723
- * pass `ctx.signal` through and bail the moment its result is no longer
2724
- * wanted.
2725
- *
2726
- * The signal is lazy: the backing `AbortController` is allocated only on
2727
- * first access, so a purely-synchronous chain that never reaches for
2728
- * `ctx.signal` allocates nothing. It is meaningful for async transforms;
2729
- * a sync chain has no in-flight I/O to cancel, so its `signal` simply
2730
- * never aborts.
2731
- */
2732
- type TransformContext = {
2733
- readonly signal: AbortSignal;
2734
- };
2735
- /**
2736
- * Custom assigner installed on an element via the directive's
2737
- * `[assignKey]` slot OR an `@update:registerValue` listener. Called
2738
- * by the directive when a DOM event (input / change / etc.) fires
2739
- * on the bound element.
2740
- *
2741
- * The directive passes the extracted value plus the `RegisterValue`
2742
- * the directive is currently bound to, regardless of install path.
2743
- * The second arg lets a top-level handler write back to form state
2744
- * without having to capture the RV via closure:
2745
- *
2746
- * ```ts
2747
- * function upperCaseAssigner(value: unknown, rv: RegisterValue): void {
2748
- * rv.setValueWithInternalPath(String(value ?? '').toUpperCase())
2749
- * }
2750
- * ```
2751
- *
2752
- * The `registerValue` parameter is typed optional only to keep
2753
- * standalone invocations from outside the directive (rare; manual
2754
- * dispatch in tests, for example) type-checkable; the directive
2755
- * itself always supplies it at fire time.
2756
- *
2757
- * Return `true` when the write was accepted, `false` when it was
2758
- * rejected (e.g. the value didn't match the path's expected type).
2759
- * `undefined` is treated as "succeeded" so simple assigners can
2760
- * just return `void`.
2761
- */
2762
- type CustomDirectiveRegisterAssignerFn = (value: unknown, registerValue?: RegisterValue) => boolean | undefined;
2763
- /**
2764
- * Generic shape of a v-register directive variant. Used by the
2765
- * library's text / checkbox / radio / select directive types and
2766
- * available for custom integrations that need to drop in their own
2767
- * variant.
2768
- *
2769
- * The value generic admits `undefined` because `useRegister()` may
2770
- * return `undefined` (a wrapper component rendered without a parent
2771
- * `registerValue`); binding that value to `v-register` is supported
2772
- * and installs a no-op assigner at runtime.
2773
- */
2774
- type CustomRegisterDirective<T, Modifiers extends string = string> = ObjectDirective<T & {
2775
- _assigning?: boolean;
2776
- /**
2777
- * Snapshot of the last `value.innerRef.value` reference the
2778
- * directive's DOM-sync (setSelected / setChecked / radio
2779
- * `el.checked = …`) was applied for. Used by every input
2780
- * directive's `updated` / `beforeUpdate` to skip the per-render
2781
- * DOM sync when the model is identity-unchanged — preventing
2782
- * parent re-renders (a typed character in a sibling, an async-
2783
- * validation tick, any reactive read) from clobbering an in-
2784
- * progress user interaction. Identity comparison is sound:
2785
- * every form write produces a fresh value at the path (scalars
2786
- * are new primitives; arrays/Sets get fresh references along the
2787
- * spine via diff-apply), so reference equality on
2788
- * `innerRef.value` tracks "did the model move" exactly. The
2789
- * `_assigning` gate stays alongside — it short-circuits the
2790
- * immediate post-write render where the DOM is already in sync
2791
- * from the user's input.
2792
- */
2793
- _lastAppliedModel?: unknown;
2794
- /**
2795
- * Variant-specific "repaint the DOM from current storage" closure,
2796
- * stashed by each input directive's `created` hook (it mirrors that
2797
- * variant's post-write force-sync block). The deferred async-transform
2798
- * orchestrator calls it once the resolved value has committed, so a
2799
- * bare `<input v-register>` with no other reactive reader still paints
2800
- * the normalized result without depending on a parent re-render.
2801
- */
2802
- _syncFromStorage?: () => void;
2803
- [S: symbol]: CustomDirectiveRegisterAssignerFn;
2804
- }, RegisterValue | undefined, Modifiers, string>;
2805
- /**
2806
- * Modifier names supported by `v-register` on `<input type="text">`,
2807
- * `<input type="number">`, and `<textarea>`. Mirrors Vue's
2808
- * `v-model` modifier semantics on the same elements; combine freely
2809
- * (`<input v-register.lazy.trim.number="..." />`).
2810
- */
2811
- type RegisterTextModifier =
2812
- /**
2813
- * Write on `change` (blur) instead of `input`. The reactive
2814
- * model only updates after the user tabs/clicks out of the
2815
- * field. IME composition handlers are skipped under `.lazy` —
2816
- * composition events do not gate writes.
2817
- */
2818
- 'lazy'
2819
- /**
2820
- * Strip leading and trailing whitespace on blur. The form holds
2821
- * the user's raw input (whitespace included) while they're
2822
- * typing; on `change` (blur / commit) the value is trimmed
2823
- * once and written back to both the model and the visible DOM.
2824
- * Combine with `.lazy` to skip the mid-typing writes entirely.
2825
- */
2826
- | 'trim'
2827
- /**
2828
- * Cast the value via `parseFloat` before writing. Values that
2829
- * can't be parsed as a number (e.g. `'abc'`) pass through
2830
- * unchanged — the slim-primitive gate then sees a string
2831
- * heading to a numeric slot and rejects the write. Auto-applied
2832
- * for `<input type="number">`; explicit `.number` is redundant
2833
- * there.
2834
- */
2835
- | 'number';
2836
- /**
2837
- * v-register directive variant for `<input type="text">`,
2838
- * `<input type="number">`, and `<textarea>`. Supports the
2839
- * `.lazy`, `.trim`, and `.number` modifiers — see
2840
- * `RegisterTextModifier` for per-modifier semantics.
2841
- */
2842
- type RegisterTextCustomDirective = CustomRegisterDirective<HTMLInputElement | HTMLTextAreaElement, RegisterTextModifier>;
2843
- /** v-register directive variant for checkboxes. No modifiers. */
2844
- type RegisterCheckboxCustomDirective = CustomRegisterDirective<HTMLInputElement>;
2845
- /** v-register directive variant for radio inputs. No modifiers. */
2846
- type RegisterRadioCustomDirective = CustomRegisterDirective<HTMLInputElement>;
2847
- /**
2848
- * Modifier name supported by `v-register` on `<select>`. Mirrors
2849
- * Vue's `v-model` `.number` on the same element.
2850
- */
2851
- type RegisterSelectModifier =
2852
- /**
2853
- * Cast each selected option's `value` via `parseFloat` before
2854
- * writing. The form state holds numbers, not numeric strings —
2855
- * useful when option values are written as strings in the
2856
- * markup but the schema expects numbers.
2857
- */
2858
- 'number';
2859
- /**
2860
- * v-register directive variant for `<select>`. Supports `.number`
2861
- * — see `RegisterSelectModifier` for semantics.
2862
- */
2863
- type RegisterSelectCustomDirective = CustomRegisterDirective<HTMLSelectElement, RegisterSelectModifier>;
2864
- /** v-register directive variant for the dynamic input/select/textarea bridge. */
2865
- type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, RegisterValue | undefined, string>;
2866
- /**
2867
- * The `v-register` directive. Binds a form field to a native
2868
- * input, select, textarea, checkbox, or radio:
2869
- *
2870
- * ```vue
2871
- * <input v-register="form.register('email')" />
2872
- * <select v-register="form.register('country')">
2873
- * <option value="us">US</option>
2874
- * <option value="uk">UK</option>
2875
- * </select>
2876
- * ```
2877
- *
2878
- * Also works on custom components whose root is NOT a native
2879
- * input — call `useRegister()` in the child's setup to read the
2880
- * parent's binding, then re-bind `v-register` onto an inner native
2881
- * element. (When the wrapper's root IS the input itself, attribute
2882
- * fallthrough handles it; `useRegister` is unnecessary.)
2883
- *
2884
- * ```vue
2885
- * <!-- Parent -->
2886
- * <MyField label="Email" v-register="form.register('email')" />
2887
- *
2888
- * <!-- MyField.vue (root is <label>, not <input>) -->
2889
- * <script setup>
2890
- * import { useRegister } from 'attaform'
2891
- * defineProps<{ label: string }>()
2892
- * const register = useRegister()
2893
- * </script>
2894
- * <template>
2895
- * <label>
2896
- * <span>{{ label }}</span>
2897
- * <input v-register="register" />
2898
- * </label>
2899
- * </template>
2900
- * ```
2901
- *
2902
- * Modifier support varies by element:
2903
- * - text / number / textarea: `.lazy`, `.trim`, `.number`
2904
- * - select: `.number`
2905
- * - checkbox / radio: none
2906
- *
2907
- * See `RegisterTextModifier` / `RegisterSelectModifier` for
2908
- * per-modifier semantics.
2909
- *
2910
- * Delivered by the Vite / Nuxt plugin's compile-time binding, or by
2911
- * `installVRegister(app)` from `attaform/directive` everywhere else.
2912
- * Most consumers don't import the directive itself — it's exposed for
2913
- * integrations that install directives manually.
2914
- */
2915
- type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
2916
- /**
2917
- * Module augmentation: register `v-register` with Vue's template
2918
- * type system. Lives in `types-api` because every public entry
2919
- * (`attaform`, `attaform/zod`, `attaform/zod-v3`, `attaform/zod-v4`)
2920
- * transitively reaches this file via the `useForm` return type, so
2921
- * the augmentation propagates to consumer SFCs regardless of which
2922
- * entry they import from — and regardless of whether they install
2923
- * `attaform/nuxt` or the Vite plugin.
2924
- *
2925
- * Augmentation targets `vue` rather than `@vue/runtime-core`:
2926
- * `GlobalDirectives` is originally declared in `@vue/runtime-core`,
2927
- * but consumers and Volar's strict-template codegen both resolve
2928
- * the interface through `vue`'s `export * from '@vue/runtime-dom'`
2929
- * → `export * from '@vue/runtime-core'` chain. TypeScript merges
2930
- * interfaces across re-exports, so augmenting `'vue'` reaches Volar
2931
- * without needing `@vue/runtime-core` to be hoisted into the
2932
- * library's own `node_modules` for its own typecheck.
2933
- */
2934
- declare module 'vue' {
2935
- interface GlobalDirectives {
2936
- /**
2937
- * The `v-register` directive. Binds a form field to a native
2938
- * input, select, textarea, checkbox, or radio:
2939
- *
2940
- * ```vue
2941
- * <input v-register="form.register('email')" />
2942
- * ```
2943
- *
2944
- * Also works on custom components whose root is NOT a native
2945
- * input — call `useRegister()` in the child's setup to read the
2946
- * parent's binding, then re-bind `v-register` onto an inner
2947
- * native element. (When the wrapper's root IS the input itself,
2948
- * attribute fallthrough handles it; `useRegister` is unnecessary.)
2949
- *
2950
- * Modifier support varies by element:
2951
- * - text / number / textarea: `.lazy`, `.trim`, `.number`
2952
- * - select: `.number`
2953
- * - checkbox / radio: none
2954
- */
2955
- vRegister: RegisterDirective;
2956
- }
2957
- }
2958
- /**
2959
- * Callback form of `setValue`'s value argument. Receives the previous
2960
- * value at the path and returns the next value:
2961
- *
2962
- * ```ts
2963
- * form.setValue('count', (prev) => prev + 1)
2964
- * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
2965
- * ```
2966
- *
2967
- * A whole-form callback replaces the whole form. Any key the return
2968
- * omits is refilled from the SCHEMA's declared defaults (the slim
2969
- * value where none is declared), so an omitted key is not carried
2970
- * over from the previous value or from `defaultValues` — spread
2971
- * `prev` to keep the rest of the form.
2972
- */
2973
- type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
2974
- /**
2975
- * The value argument of `form.setValue`. Either the next value
2976
- * directly, or a callback that derives it from the previous value.
2977
- *
2978
- * Type parameters:
2979
- * - `Write` — what the direct value form accepts (the storable shape
2980
- * at the path).
2981
- * - `Read` — what the callback's `prev` argument exposes (defaults
2982
- * to `Write`). For whole-form callbacks the read shape tags
2983
- * array elements as possibly-undefined to reflect runtime reality.
2984
- */
2985
- type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
2986
- /**
2987
- * Detect `any` distinctly from `unknown`. The trick: `1 & any` is `any`
2988
- * and `0 extends any` is `true`; `1 & unknown` is `1` and `0 extends 1`
2989
- * is `false`. Used to fork `PathSetValuePayload` so `z.any()` paths
2990
- * resolve to `any` (matching the read-side surface) and `z.unknown()` /
2991
- * preprocess paths resolve to `unknown` (matching Zod's input typing).
2992
- */
2993
- type IsAny<T> = 0 extends 1 & T ? true : false;
2994
- /**
2995
- * Resolves `setValue`'s `value` argument type at a single `Path` leaf.
2996
- *
2997
- * Three branches, one per Zod input-typing case:
2998
- *
2999
- * 1. **`any` leaf (`z.any()`)** — schema input type is `any`; the
3000
- * whole form API surface (read, register, fields) is `any` at
3001
- * this path. This branch returns raw `any` so `setValue` stays
3002
- * consistent with the rest. Callsites that pass an unannotated
3003
- * `(prev) => ...` may surface `noImplicitAny` under the
3004
- * consumer's tsconfig — annotate `(prev: any) => ...` to opt
3005
- * into the looser shape explicitly.
3006
- *
3007
- * 2. **`unknown` leaf (`z.unknown()`, `z.preprocess()` input)** —
3008
- * schema input is unconstrained; consumers narrow before use.
3009
- * The branch returns `({} | null | undefined) | ((prev: unknown)
3010
- * => unknown)` instead of a `SetValuePayload<unknown, ...>`-style
3011
- * union for three reasons:
3012
- *
3013
- * a. **Union absorption** — `unknown | X` collapses to `unknown`,
3014
- * erasing the callback union member. With the callback shape
3015
- * gone, TS has no contextual type for `prev` and decays it to
3016
- * implicit `any` under `noImplicitAny`. The triple
3017
- * `{} | null | undefined` is structurally equivalent to
3018
- * `unknown` (covers the same value space) but is NOT subject
3019
- * to absorption — the callback branch survives the union and
3020
- * `prev` infers cleanly to `unknown`.
3021
- *
3022
- * b. **`NonNullable<unknown> = {}`** — applying `NonNullable` to
3023
- * the read slot for an unknown leaf narrows `prev` to `{}`,
3024
- * which is looser than `unknown` (allows ad-hoc property
3025
- * access). This branch keeps the read slot as `unknown`
3026
- * directly so the consumer is forced to narrow.
3027
- *
3028
- * c. **`Unset`-widening doesn't apply** — `DefaultValuesShape`
3029
- * widens primitive leaves to admit `unset`; for an unknown
3030
- * leaf there's no primitive to widen. The open-form triple
3031
- * covers the same value space the runtime accepts (any
3032
- * value, including `unset` — symbols are `{}`).
3033
- *
3034
- * 3. **All other leaves** — flow through unchanged via
3035
- * `SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>`.
3036
- */
3037
- type PathSetValuePayload<Leaf> = IsAny<Leaf> extends true ? any : unknown extends Leaf ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
3038
- ({} | null | undefined) | ((prev: unknown) => unknown) : SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>;
3039
- /**
3040
- * Per-field reactive shape returned by `form.fields.<leaf-path>` and
3041
- * `form.fields(path)`. Slim, readonly across the board. The unified
3042
- * shape replaces the older split between `FieldState` /
3043
- * `FieldStateBranch`: one type lives at every path, with aggregations
3044
- * rolled up at containers.
3045
- *
3046
- * Leaf-aware: this shape only injects these keys at LEAF paths via
3047
- * dot-access. At container paths the proxy descends without
3048
- * injecting, so a schema field literally named `dirty` at depth 2+
3049
- * stays reachable as a descent target — no shadowing. Container
3050
- * call-form (`form.fields('address')`) returns a `FieldState`
3051
- * surface where the keys are aggregations of the descendant leaves.
3052
- */
3053
- type FieldState<Value = unknown> = {
3054
- readonly value: Value;
3055
- readonly original: Value;
3056
- readonly pristine: boolean;
3057
- readonly dirty: boolean;
3058
- readonly focused: boolean | null;
3059
- readonly blurred: boolean | null;
3060
- readonly touched: boolean;
3061
- /**
3062
- * `true` once the user has issued at least one value edit on this
3063
- * field through `v-register` (an insert or a delete), sticky
3064
- * thereafter and preserved across disconnects. Distinct from
3065
- * `dirty`: typing `"a"` then deleting it back to empty leaves the
3066
- * field net-unchanged (`dirty: false`) yet `interacted: true`.
3067
- * Distinct from `touched`: tabbing through a field without editing
3068
- * flips `touched` but never `interacted`. Set only by user input,
3069
- * never by hydration or programmatic `setValue`; cleared by
3070
- * `form.reset()` / `form.resetField(path)`. Containers roll it up as
3071
- * a disjunction (any descendant interacted).
3072
- */
3073
- readonly interacted: boolean;
3074
- /**
3075
- * `true` once the user has blurred this field after editing it: the
3076
- * first time they edit a value and then leave. Sticky thereafter and
3077
- * preserved across disconnects; a tab-through with no edit never sets
3078
- * it (`interacted` is still false at that blur). It composes
3079
- * `interacted` with the departure and drives the default display gate,
3080
- * so errors reveal once the user finishes a pass and leaves, then stay
3081
- * visible through a re-focus to be fixed live. Set only by user
3082
- * input/blur, never by hydration or programmatic writes; cleared by
3083
- * `form.reset()` / `form.resetField(path)`. Containers roll it up as a
3084
- * disjunction.
3085
- */
3086
- readonly blurredAfterInteraction: boolean;
3087
- readonly connected: boolean;
3088
- /**
3089
- * The first DOM element bound to this path via `v-register`, or
3090
- * `null` when none is registered (initial mount, post-unmount,
3091
- * SSR). "First" means first by registration order. Reach for it
3092
- * when you need to call a native DOM method on a field's input —
3093
- * `focus()`, `scrollIntoView()`, `select()`, `setSelectionRange()`,
3094
- * etc. — without the library having to verb every imperative:
3095
- *
3096
- * ```ts
3097
- * form.fields.email.element?.focus()
3098
- * form.fields.email.element?.scrollIntoView({ block: 'center' })
3099
- * ```
3100
- *
3101
- * For paths with multiple bindings (input syncing, mirrored
3102
- * shadow inputs), prefer `elements` and pick the right target
3103
- * yourself. Reactive: register / deregister triggers
3104
- * re-evaluation.
3105
- */
3106
- readonly element: HTMLElement | null;
3107
- /**
3108
- * Every DOM element currently bound to this path via `v-register`,
3109
- * in registration order. Empty array when none is registered.
3110
- * Two bindings to the same path are intentional — input syncing,
3111
- * mirrored shadow inputs:
3112
- *
3113
- * ```ts
3114
- * for (const el of form.fields.email.elements) el.blur()
3115
- * ```
3116
- *
3117
- * For the common single-binding case, reach for `element` — sugar
3118
- * over `elements[0] ?? null`.
3119
- */
3120
- readonly elements: readonly HTMLElement[];
3121
- readonly updatedAt: string | null;
3122
- /**
3123
- * Every `ValidationError` in this path's SUBTREE: the node's own
3124
- * bucket plus every descendant's, sorted by schema-declaration order.
3125
- * On a leaf (no descendants) this equals `ownErrors`. On a container
3126
- * it rolls up the whole fieldset, so a parent's `errors` is non-empty
3127
- * whenever any child is invalid.
3128
- *
3129
- * For the errors pinned at THIS path alone (a container's own
3130
- * cross-field `.refine()`, or `setErrors` at this path), read
3131
- * `ownErrors`.
3132
- */
3133
- readonly errors: readonly ValidationError[];
3134
- /**
3135
- * Every `ValidationError` at THIS exact path's own bucket, excluding
3136
- * descendants: schema errors, blank-required errors, and `setErrors`
3137
- * pinned here, in `schema`, `blank`, `user` order. The exact-path
3138
- * counterpart to the subtree-scoped `errors`.
3139
- *
3140
- * On a leaf this is the same array as `errors` (a leaf has no
3141
- * descendants). On a container it holds only the container's OWN
3142
- * errors, a cross-field `.refine()` on the fieldset or a path-less
3143
- * `setErrors` at this node, without dragging in any child error:
3144
- *
3145
- * ```vue
3146
- * <p v-if="form.fields.address.firstOwnError">
3147
- * {{ form.fields.address.firstOwnError.message }}
3148
- * </p>
3149
- * ```
3150
- *
3151
- * On `form.meta` this is the root `[]` bucket: form-level errors from
3152
- * a root `.refine()` or a path-less `setErrors`. `form.meta.ownErrors`
3153
- * (with `firstOwnError`) is the banner accessor for those.
3154
- */
3155
- readonly ownErrors: readonly ValidationError[];
3156
- /**
3157
- * `true` while a per-field validation run is in flight at this path.
3158
- * Reflects field-level debounced runs (`validate-on-change`) and
3159
- * cross-field re-validations targeting this path. Whole-form
3160
- * `validate()` / `parse()` calls drive `form.meta.validating`
3161
- * only — they don't flip per-field flags.
3162
- *
3163
- * Per-field analogue of `form.meta.validating`. Use for a tight
3164
- * "Checking…" indicator next to a single async-validated input
3165
- * without commandeering the whole-form spinner.
3166
- */
3167
- readonly validating: boolean;
3168
- /**
3169
- * `true` when this field has no errors AND no per-field validation
3170
- * is in flight (`errors.length === 0 && !validating`). Confidence
3171
- * that "we've checked, and we have no problems right now." Use for
3172
- * green-checkmark / `aria-invalid` UX.
3173
- *
3174
- * Validation-only: an in-flight async transform does NOT clamp
3175
- * `valid` to `false`. `valid` is the verdict on the last committed
3176
- * value; `busy` is the union "work in flight" signal.
3177
- */
3178
- readonly valid: boolean;
3179
- /**
3180
- * `true` while an async `register` transform is in flight at this
3181
- * path: a transform returned a thenable and the resolved value has
3182
- * not yet committed to form state. Always `false` for a sync-only
3183
- * chain, which reaches form state in the same tick with no deferral.
3184
- * Containers roll it up as a disjunction (any descendant transforming).
3185
- */
3186
- readonly transforming: boolean;
3187
- /**
3188
- * `transforming || validating` — the union "work is in flight at this
3189
- * path" signal. Drives `aria-busy` through `displayState` on a
3190
- * revealed field, and is the surface to bind for a busy indicator on a
3191
- * field not yet revealed (where `displayState` stays idle by the
3192
- * reveal gate). Containers roll it up as a disjunction.
3193
- */
3194
- readonly busy: boolean;
3195
- /**
3196
- * The `Error` from the most recent async transform that rejected at
3197
- * this path, else `null`. A per-field normalization-failure channel
3198
- * separate from validation `errors`: a transform that rejects (a
3199
- * failed fetch, a parse error) surfaces here instead of crashing the
3200
- * host app or logging. Cleared when a fresh transform starts or a
3201
- * write supersedes it. Leaf-only — containers do not roll it up (it is
3202
- * always `null` at a container path).
3203
- */
3204
- readonly transformError: Error | null;
3205
- /**
3206
- * The single display-state verdict at this path: `'idle'`,
3207
- * `'pending'`, `'error'`, or `'success'`. The source of truth the
3208
- * four `show*` booleans below derive from. Bind it directly when one
3209
- * branch over the set reads cleaner than four flags:
3210
- *
3211
- * ```vue
3212
- * <FieldStatusIcon :state="form.fields.email.displayState" />
3213
- * ```
3214
- *
3215
- * Resolved by the display heuristic: gate closed → `'idle'`; a run
3216
- * in flight → a delayed `'pending'`; an own-path error → `'error'`;
3217
- * otherwise earned `valid` → `'success'`, else `'idle'`. The gate
3218
- * opens after the first submit attempt OR once the field is edited
3219
- * and left.
3220
- *
3221
- * Available on container paths too: `form.fields.users[0].displayState`
3222
- * rolls up over the row's descendants.
3223
- */
3224
- readonly displayState: DisplayState;
3225
- /**
3226
- * `displayState === 'error'`. The centralised "render this field's
3227
- * errors right now?" gate, so templates avoid re-spelling the
3228
- * heuristic at every error site:
3229
- *
3230
- * ```vue
3231
- * <span v-if="form.fields.email.showErrors">
3232
- * {{ form.fields.email.firstError?.message }}
3233
- * </span>
3234
- * ```
3235
- *
3236
- * Kept plural to match `errors` / `firstError`. On container paths it
3237
- * rolls up over descendants (any descendant resolving to `'error'`
3238
- * flips the container on).
3239
- */
3240
- readonly showErrors: boolean;
3241
- /**
3242
- * `displayState === 'pending'`. A per-field validation run is in
3243
- * flight at this path and the prior verdict is stale; drive a spinner
3244
- * or a "Checking…" affordance.
3245
- */
3246
- readonly showPending: boolean;
3247
- /**
3248
- * `displayState === 'success'`. Validation has passed and the timing
3249
- * gate has cleared a positive confirmation; drive the green-check
3250
- * pattern.
3251
- */
3252
- readonly showSuccess: boolean;
3253
- /**
3254
- * `displayState === 'idle'`. Nothing to surface yet — pre-interaction,
3255
- * or gate-open with no verdict worth showing. Read it to suppress
3256
- * helper text the moment any other signal takes over.
3257
- */
3258
- readonly showIdle: boolean;
3259
- /**
3260
- * The first `ValidationError` at this path in the deterministic
3261
- * schema-declaration order — equivalent to `errors[0]`, exposed as
3262
- * a sugar accessor for the common case of "show the highest-priority
3263
- * error message and ignore the rest":
3264
- *
3265
- * ```vue
3266
- * <span v-if="form.fields.email.showErrors">
3267
- * {{ form.fields.email.firstError?.message }}
3268
- * </span>
3269
- * ```
3270
- *
3271
- * `undefined` when no errors exist. Independent of `showErrors` —
3272
- * the data primitive is always available; the heuristic only
3273
- * decides when to render it.
3274
- *
3275
- * On container paths, the first error in the aggregated subtree
3276
- * (descendants sorted by `pathOrdinal`). For the container's OWN
3277
- * first error only, read `firstOwnError`.
3278
- */
3279
- readonly firstError: ValidationError | undefined;
3280
- /**
3281
- * The first `ValidationError` in `ownErrors` (this exact path's own
3282
- * bucket), or `undefined` when that bucket is empty. Equivalent to
3283
- * `ownErrors[0]`, the exact-path counterpart to `firstError`.
3284
- *
3285
- * On a leaf this equals `firstError`. On a container it is the
3286
- * container's own cross-field error (a fieldset `.refine()`), never a
3287
- * child's. `form.meta.firstOwnError` is the form-level banner: the
3288
- * first root `[]` error from a root `.refine()` or a path-less
3289
- * `setErrors`:
3290
- *
3291
- * ```vue
3292
- * <p v-if="form.meta.firstOwnError" role="alert">
3293
- * {{ form.meta.firstOwnError.message }}
3294
- * </p>
3295
- * ```
3296
- */
3297
- readonly firstOwnError: ValidationError | undefined;
3298
- readonly path: ReadonlyArray<string | number>;
3299
- /**
3300
- * Stable, SSR-safe DOM id for this field, unique across every mount
3301
- * on the page. Derived from the form's key and this path, folded with
3302
- * the form's per-mount `instanceId` so two simultaneous mounts of the
3303
- * same keyed form never collide. Bind it to wire a label and its
3304
- * input without inventing your own id:
3305
- *
3306
- * ```vue
3307
- * <label :for="form.fields.email.id">Email</label>
3308
- * <input :id="form.fields.email.id" v-register="form.register('email')" />
3309
- * ```
3310
- *
3311
- * Treat as identity, not state: stable for the path across the form's
3312
- * lifetime, opaque, not meant to be parsed.
3313
- */
3314
- readonly id: string;
3315
- /**
3316
- * Satellite ids derived from {@link id} for the elements that
3317
- * describe this field. Wire them to an error node and a description
3318
- * node so assistive tech announces them with the input. The
3319
- * `v-register` directive points `aria-describedby` at `errorId`
3320
- * automatically while the field is in its error state; you render the
3321
- * matching element and id it:
3322
- *
3323
- * ```vue
3324
- * <input v-register="form.register('email')" />
3325
- * <span :id="form.fields.email.aria.errorId" v-if="form.fields.email.showErrors">
3326
- * {{ form.fields.email.firstError?.message }}
3327
- * </span>
3328
- * ```
3329
- *
3330
- * `descriptionId` is for opt-in help text; chain it into your own
3331
- * `aria-describedby` when you render a persistent description element.
3332
- */
3333
- readonly aria: {
3334
- readonly errorId: string;
3335
- readonly descriptionId: string;
3336
- };
3337
- /**
3338
- * Stable identity for this field as an element of its parent array,
3339
- * suitable as a Vue `:key` when iterating array elements. An allocated
3340
- * token (not derived from the element's value) that follows the
3341
- * element across inserts, removals, moves, and swaps, so a row keeps
3342
- * its component instance across a reorder. Empty for fields that are
3343
- * not array elements. Treat as opaque identity, not state.
3344
- */
3345
- readonly key: string;
3346
- readonly blank: boolean;
3347
- /**
3348
- * `true` when this field's form is frozen via `useForm({ disabled })`.
3349
- * While disabled, value writes no-op and this field's `displayState`
3350
- * is forced to `'idle'` so error, pending, and success signals stand
3351
- * down. A form-level flag: every field of a disabled form reads
3352
- * `true`, and the form root exposes it as `form.meta.disabled`. Bind
3353
- * read-only or styling affordances off this.
3354
- */
3355
- readonly disabled: boolean;
3356
- /**
3357
- * Presentational label for this field. Resolves through the
3358
- * shared cross-adapter field-meta store — written via
3359
- * `schema.register(fieldMeta, {...})` (Zod 4 native chain) or the
3360
- * `withMeta()` helper (works on both majors) — and falls back to
3361
- * a humanized form of the path's last segment when nothing has
3362
- * been registered. Always a string.
3363
- *
3364
- * ```ts
3365
- * z.string().register(fieldMeta, { label: 'Reference' })
3366
- * // template: <label>{{ form.fields.reference.label }}</label>
3367
- * ```
3368
- *
3369
- * Numeric segments (array indices) collapse to the empty string;
3370
- * consumers wanting "Item 3" substitute their own format.
3371
- */
3372
- readonly label: string;
3373
- /**
3374
- * Helper-text description for this field. Reads from the
3375
- * registered `description` first; falls back to the schema's own
3376
- * `.describe('...')` value (both Zod 3 and Zod 4 expose that as
3377
- * `schema.description`); `undefined` when neither is set.
3378
- *
3379
- * Useful for `aria-describedby`-linked help text. Distinct from
3380
- * `label` — descriptions are longer prose, labels are short
3381
- * presentational nouns.
3382
- */
3383
- readonly description: string | undefined;
3384
- /**
3385
- * Placeholder hint for input affordance. Reads from the
3386
- * registered `placeholder`; `undefined` otherwise.
3387
- */
3388
- readonly placeholder: string | undefined;
3389
- /**
3390
- * Full registered metadata payload, frozen — empty object when
3391
- * nothing has been registered. Use as an escape hatch for
3392
- * consumer-augmented keys (declared via TypeScript module
3393
- * augmentation on `FieldMetaPayload`):
3394
- *
3395
- * ```ts
3396
- * declare module 'attaform/zod' {
3397
- * interface FieldMetaPayload { tooltip?: string }
3398
- * }
3399
- * // template: {{ form.fields.email.meta.tooltip }}
3400
- * ```
3401
- */
3402
- readonly meta: Readonly<FieldMetaPayload>;
3403
- };
3404
- /**
3405
- * Recursive type behind `form.fields`. Leaf-aware branching: at
3406
- * primitive paths (string, number, boolean, bigint, Date, …) the
3407
- * proxy returns a `FieldState`; at container paths (object,
3408
- * array, …) the proxy descends without injecting leaf-keys.
3409
- *
3410
- * Field-name collisions at depth 2+ resolve unambiguously: a schema
3411
- * field literally named `dirty` at depth 2 is reachable as a
3412
- * descent target (`form.fields.address.dirty` returns the
3413
- * FieldState for `address.dirty`). Reading `dirty` AT the
3414
- * leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's
3415
- * own dirty boolean — path-segment and leaf-prop occupy different
3416
- * proxy depths.
3417
- *
3418
- * The runtime implementation queries `schema.isLeafAtPath(segments)`
3419
- * at every step; this type approximates that decision using
3420
- * "T extends primitive". The two stay in sync for typical schemas;
3421
- * exotic adapter-defined leaf kinds (custom `Date`-like) may need
3422
- * a runtime check (the runtime is authoritative).
3423
- *
3424
- * The mapped type strips optional flags (`-?:`) because the field-
3425
- * state surface always exposes a record per known leaf, regardless
3426
- * of whether the schema field is declared `.optional()`. Optional
3427
- * schemas mean the VALUE can be undefined — `FieldState<string |
3428
- * undefined>` carries that — but the FieldState wrapper itself
3429
- * always exists. Without the strip, `form.fields.notes` (where
3430
- * `notes?: string`) would type as `FieldState<...> | undefined`,
3431
- * forcing consumers to optional-chain through every reactive read.
3432
- *
3433
- * For discriminated-union containers the object branch uses
3434
- * `[T] extends [object]` (non-distributive) plus
3435
- * `KeyofUnion`/`ValueOfUnion` to merge variant key sets — so
3436
- * `form.fields.cargo.tempMinC` (refrigerated-only) is reachable
3437
- * regardless of the active variant, with the leaf typed as
3438
- * `FieldState<number | undefined>`. Matches the runtime's stub
3439
- * `FieldState` for inactive-variant paths.
3440
- */
3441
- /**
3442
- * Leaf-shape dispatch table for `LeafWalker`. Each entry maps a walker
3443
- * kind to the leaf type that walker produces at primitive / Date /
3444
- * non-recursable positions. The lookup `LeafSchemeFor<T>[Kind]`
3445
- * threads `T` through the leaf type when the kind needs it
3446
- * (`field` carries `FieldState<T>`); kinds that don't depend on
3447
- * `T` simply ignore it (`errors` always produces
3448
- * `readonly ValidationError[] | undefined`).
3449
- *
3450
- * Adding a new walker is one entry here plus a one-line wrapper
3451
- * alias (`type FooShape<T> = LeafWalker<T, 'foo'>`). The walker
3452
- * topology is shared; only the leaf changes.
3453
- *
3454
- * The `errors` entry threads `T` to preserve `| undefined` when the
3455
- * value type itself includes undefined (DU variant-only fields whose
3456
- * lifted shape resolves to `X | undefined`). Statically-known leaves
3457
- * collapse to `readonly ValidationError[]` (no undefined); dynamic-key
3458
- * boundaries (array indices, record keys) re-introduce `| undefined`
3459
- * via the structural index-signature channels.
3460
- *
3461
- * Preprocess / coerce leaves (StorageShape = `unknown`) are
3462
- * statically known too — the IsUnknown filter keeps them on the
3463
- * non-optional branch instead of being swept into the dynamic
3464
- * `| undefined` arm by `undefined extends unknown`.
3465
- *
3466
- * Implementation-detail surface — consumers reach for `FieldStateMap`
3467
- * or `FormErrorsSurface` instead.
3468
- */
3469
- type IsUnknown<T> = IsAny<T> extends true ? false : unknown extends T ? true : false;
3470
- interface LeafSchemeFor<T> {
3471
- field: FieldState<T>;
3472
- errors: IsUnknown<T> extends true ? readonly ValidationError[] : undefined extends T ? readonly ValidationError[] | undefined : readonly ValidationError[];
3473
- }
3474
- /**
3475
- * Generic walk that produces a proxy shape over `T` with leaves
3476
- * dispatched via `LeafSchemeFor<T>[Kind]`. The walk topology
3477
- * (object → mapped homomorphic, object-union → KeyofUnion merge,
3478
- * array → indexed, primitive / Date → terminal) is identical
3479
- * across walker kinds; only the leaf type differs.
3480
- *
3481
- * Replaces the duplicated bodies of `FieldStateMapEntry` and
3482
- * `ErrorsProxyShape`. The prior duplication walked the same shape
3483
- * twice per useForm return type — once for the fields proxy, once
3484
- * for the errors proxy. Factoring lets the bundled `.d.ts` carry one
3485
- * shared walker body plus per-kind one-line wrappers, halving the
3486
- * recursive depth contribution from these two proxies on consumer
3487
- * call sites.
3488
- *
3489
- * `StripOptional` controls whether optional modifiers on input
3490
- * properties are stripped at every recursion level. `true` (default)
3491
- * matches the fields proxy semantics — every known leaf carries a
3492
- * `FieldState` wrapper regardless of source `?`. `false` matches the
3493
- * errors proxy semantics — the proxy shape stays structurally
3494
- * identical to the input form, optional keys included.
3495
- *
3496
- * Implementation-detail surface — consumers reach for `FieldStateMap`
3497
- * or `FormErrorsSurface` instead.
3498
- */
3499
- type LeafWalker<T, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean = true> = [T] extends [string | number | boolean | bigint | symbol | null | undefined | Date | File] ? LeafSchemeFor<T>[Kind] : [T] extends [ReadonlyArray<infer U>] ? {
3500
- readonly [K: number]: LeafWalker<U, Kind, StripOptional>;
3501
- } & ContainerSelfErrorsSlot<T, Kind> : [T] extends [object] ? string extends keyof T ? {
3502
- readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
3503
- } & ContainerSelfErrorsSlot<T, Kind> : [IsUnion<T>] extends [true] ? StripOptional extends true ? {
3504
- readonly [K in KeyofUnion<T>]-?: DiscriminatedLeaf<T, K, Kind, StripOptional>;
3505
- } & ContainerSelfErrorsSlot<T, Kind> : {
3506
- readonly [K in KeyofUnion<T>]: DiscriminatedLeaf<T, K, Kind, StripOptional>;
3507
- } & ContainerSelfErrorsSlot<T, Kind> : StripOptional extends true ? {
3508
- readonly [K in keyof T]-?: LeafWalker<T[K], Kind, StripOptional>;
3509
- } & ContainerSelfErrorsSlot<T, Kind> : {
3510
- readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
3511
- } & ContainerSelfErrorsSlot<T, Kind> : LeafSchemeFor<T>[Kind];
3512
- /**
3513
- * One key of a discriminated-union container in `LeafWalker`. A key
3514
- * present (and required) in EVERY variant is universal — its node is
3515
- * always reachable. A variant-only or optional-in-some key is a dynamic
3516
- * hop: its node is `undefined` when its variant isn't active, so it
3517
- * carries node-optionality (`LeafWalker<…> | undefined`), NOT value-
3518
- * optionality (`LeafWalker<… | undefined>`). `PresentValueOfUnion`
3519
- * strips the synthetic absent-variant `undefined` so the present node
3520
- * resolves to the precise value type; a genuine `undefined` from an
3521
- * `optional` declaration survives.
3522
- *
3523
- * Universality is `[T] extends [Record<K, unknown>]` — true iff `T`
3524
- * (the whole union) satisfies "has K, required", which holds only when
3525
- * every variant declares K as a required property.
3526
- */
3527
- type DiscriminatedLeaf<T, K extends PropertyKey, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean> = [T] extends [Record<K, unknown>] ? LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> : LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> | undefined;
3528
- /**
3529
- * Intersection augmenting every container in the `form.errors` walker
3530
- * with a `''` slot. At a depth >= 1 container it's the container-self
3531
- * sentinel — the home for cross-field refine errors and server-side
3532
- * container marks; at root the `''` property addresses the literal
3533
- * empty-key field. Gated on `Kind extends 'errors'` so `form.values`
3534
- * and `form.fields` surfaces stay untouched. Carve-out for schemas that
3535
- * legitimately declare a `''` field: the declared field type wins; at
3536
- * runtime the two collide harmlessly (errors at the literal leaf and
3537
- * any container-self errors share the slot via array concat).
3538
- *
3539
- * Global (root) errors are NOT this slot: they live at the root `[]`
3540
- * and are read via the `errors([])` call-form, never `errors['']`.
3541
- */
3542
- type ContainerSelfErrorsSlot<T, Kind> = Kind extends 'errors' ? '' extends keyof T ? unknown : {
3543
- readonly ['']: readonly ValidationError[];
3544
- } : unknown;
3545
- type FieldStateMapEntry<T> = LeafWalker<T, 'field'>;
3546
- /**
3547
- * Result of the `form.fields(path)` string call-form. A path the schema
3548
- * declares resolves to its `FieldState` — a leaf's value type, or a
3549
- * container's rolled-up aggregate (every FieldState property exists
3550
- * regardless of the value type). A path the schema lacks resolves to
3551
- * `undefined`, because a typo is not a field and the runtime hands back
3552
- * `undefined` rather than a phantom stub. A non-literal `string` could
3553
- * be either, so it widens to `FieldState<unknown> | undefined`.
3554
- *
3555
- * Named (rather than inlined into the `FieldStateMap` call signature) so
3556
- * the conditional is one cached type — keeps two structurally-identical
3557
- * `FieldStateMap` instantiations (e.g. the unified `attaform/zod` return
3558
- * and `UseFormReturnV4`) relatable instead of collapsing to a nominal
3559
- * "two different types with this name" mismatch.
3560
- */
3561
- type FieldCallResult<Form, P extends string> = [P] extends [FlatPath<Form>] ? FieldState<NestedType<Form, P>> : string extends P ? FieldState<unknown> | undefined : undefined;
3562
- /**
3563
- * Type of `form.fields` — leaf-aware drillable callable Proxy. At
3564
- * a leaf path the proxy resolves to a `FieldState<Value>`; at
3565
- * a container path it returns a sub-proxy you can keep drilling.
3566
- *
3567
- * Augmented with the callable signatures so dot-access and function-
3568
- * call coexist on the same identifier:
3569
- *
3570
- * ```ts
3571
- * form.fields.email.value // string (leaf-prop on FieldState)
3572
- * form.fields('email').value // function-call (dynamic / programmatic)
3573
- * form.fields(['users', 0, 'name']) // path-array form
3574
- * form.fields() // root proxy
3575
- * ```
3576
- *
3577
- * Single-bracket dotted access (`form.fields['address.city']`) is
3578
- * intentionally NOT supported — JS object semantics treat the dotted
3579
- * string as a single key. Use chained dot/bracket or the callable
3580
- * form.
3581
- */
3582
- type FieldStateMap<Form extends GenericForm> = LeafWalker<Form, 'field'> & {
3583
- /**
3584
- * String-path form (dynamic / programmatic). See {@link FieldCallResult}:
3585
- * a path the schema declares resolves to its precise `FieldState`, a
3586
- * path it lacks to `undefined`, and a non-literal `string` to
3587
- * `FieldState<unknown> | undefined`.
3588
- */
3589
- <P extends string>(path: P): FieldCallResult<Form, P>;
3590
- /**
3591
- * Tuple-segment form. The segment-array spelling of the string
3592
- * call-form above, for a path built from variables — the two resolve
3593
- * to the same proxy, so `form.fields(['users', i, 'email'])` and
3594
- * `form.fields(\`users.${i}.email\`)` are interchangeable.
3595
- *
3596
- * Resolves to that path's `FieldState`, which at a container is its
3597
- * rolled-up aggregate (`displayState`, `valid`, `dirty`, ... all
3598
- * exist regardless of the value type), exactly as the string form
3599
- * does. Note this is the CALL form: it hands back the path's state,
3600
- * not a drillable sub-proxy. To walk into a container's children use
3601
- * dot/bracket access (`form.fields.users[i].email`), which is what
3602
- * descends.
3603
- */
3604
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): FieldState<NestedType<Form, JoinSegments<S>>>;
3605
- /**
3606
- * Dynamic-array fallback for callers passing `Path`-typed (runtime)
3607
- * segment arrays — e.g. forwarding `RegisterValue.segments` to
3608
- * resolve a field view. The path may not resolve, so the result
3609
- * widens with `| undefined`; cast when the value type is known.
3610
- */
3611
- (segments: ReadonlyArray<string | number>): FieldState<unknown> | undefined;
3612
- /**
3613
- * No-arg call returns the root FieldState — same as
3614
- * `form.fields([])`. Aggregates over the whole form (one
3615
- * conjunction over every active-variant leaf).
3616
- */
3617
- (): FieldState<Form>;
3618
- };
3619
- /**
3620
- * Untyped error map keyed by dotted-string path. The same data
3621
- * exposed by `form.errors`, but as a plain record — useful when
3622
- * routing API errors that may land on paths the form's TypeScript
3623
- * type doesn't know about.
3624
- */
3625
- type FormErrorRecord = Record<string, ValidationError[]>;
3626
- /**
3627
- * Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
3628
- * statically-known leaf the proxy resolves to `readonly ValidationError[]`
3629
- * (empty array when no errors land); at dynamic boundaries (array
3630
- * indices, record keys, DU variant-only fields) it resolves to
3631
- * `readonly ValidationError[] | undefined`. At a container path it
3632
- * returns a sub-proxy you can keep drilling.
3633
- *
3634
- * Dot/bracket access mirrors the schema shape:
3635
- *
3636
- * ```ts
3637
- * form.errors.email // readonly ValidationError[] (static leaf)
3638
- * form.errors.user.profile.email // readonly ValidationError[] (chained static leaves)
3639
- * form.errors.posts[3]?.title // readonly ValidationError[] | undefined (past array boundary)
3640
- * form.errors.address // sub-proxy (container — descend further)
3641
- * ```
3642
- *
3643
- * Callable form for dynamic / programmatic paths:
3644
- *
3645
- * ```ts
3646
- * form.errors('user.profile.email') // dotted-string
3647
- * form.errors(['user', 'profile', 'email']) // path-array
3648
- * form.errors() // root proxy
3649
- * ```
3650
- *
3651
- * Single-bracket dotted access (`form.errors['user.profile.email']`)
3652
- * is intentionally NOT supported — JS object semantics treat the
3653
- * dotted string as a single key, which would land on a non-existent
3654
- * path. Use chained dot/bracket access or the callable form.
3655
- */
3656
- /**
3657
- * Recursive shape of the `form.errors` proxy. Mirrors the schema:
3658
- * statically-known primitive leaves expose `readonly ValidationError[]`
3659
- * (always an array; empty when no errors); leaves whose value type
3660
- * itself includes `undefined` (DU variant-only fields) keep the
3661
- * `| undefined` branch. Containers expose a sub-shape you can keep
3662
- * drilling. Arrays expose numeric-indexed sub-shapes; reading a
3663
- * numeric index introduces `| undefined` via noUncheckedIndexedAccess.
3664
- *
3665
- * Augmented with the callable signatures so dot-access and function-
3666
- * call coexist on the same identifier.
3667
- */
3668
- type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
3669
- (path: string): readonly ValidationError[];
3670
- /**
3671
- * Tuple-segment form. Validated against `FlatPath<Form>` so literal
3672
- * tuples that don't resolve to a known path fail at the call site.
3673
- * Dynamic `Path`-typed inputs hit the untyped fallback overload below.
3674
- */
3675
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): readonly ValidationError[];
3676
- (segments: ReadonlyArray<string | number>): readonly ValidationError[];
3677
- /**
3678
- * No-arg call returns the whole-form error aggregate, the same as
3679
- * `form.meta.errors` and `form.errors([])`: every field error plus
3680
- * the root bucket. `form.errors(path)` is uniform at every depth,
3681
- * including `[]`, so the root is no exception. For the root bucket
3682
- * alone (a root `.refine()`, a path-less `setErrors`), read
3683
- * `form.meta.ownErrors`. Always a readonly array; empty when the form
3684
- * has no errors.
3685
- */
3686
- (): readonly ValidationError[];
3687
- };
3688
- /**
3689
- * Implementation-detail walker backing `form.errors` typed proxy.
3690
- * Thin alias over `LeafWalker<T, 'errors', false>` — the shared walker
3691
- * topology is defined once at `LeafWalker` and parameterized via
3692
- * `LeafSchemeFor`. `false` preserves optional-key modifiers (errors
3693
- * proxy mirrors the input shape including `?`); contrast with the
3694
- * fields proxy alias which strips them via the default `true`.
3695
- *
3696
- * Exported so the bundled `.d.ts` references a single alias body
3697
- * rather than re-emitting the full union-aware recursion at every
3698
- * consumer call site that types `form.errors`. Multiple useForm
3699
- * instances in one scope otherwise compound this into TS2589
3700
- * territory. Consumers should reach for `FormErrorsSurface` instead.
3701
- */
3702
- type ErrorsProxyShape<T> = LeafWalker<T, 'errors', false>;
3703
- /**
3704
- * Type of `form.values`. Drillable readonly callable proxy. Unlike
3705
- * `form.errors` and `form.fields`, containers are USEFUL terminals:
3706
- * `form.values.address` returns the actual `{ city, … }` subtree
3707
- * (and keeps drilling). Asymmetry justified by density — every
3708
- * container in `values` carries meaningful data; in errors / fields
3709
- * containers are derivations.
3710
- *
3711
- * ```ts
3712
- * form.values.email // string (the value)
3713
- * form.values.address // { city, … } — object (drillable)
3714
- * form.values.address.city // string (chained descent)
3715
- * form.values('address.city') // function-call (dynamic / programmatic)
3716
- * form.values(['address', 'city']) // path-array form
3717
- * form.values() // the whole form value (root)
3718
- * ```
3719
- *
3720
- * The two shapes answer different questions. Dot access is the
3721
- * REACTIVE view: per-key tracking, no copying, always the live value.
3722
- * The call form returns a SNAPSHOT: a detached plain object that keeps
3723
- * what it held at capture time, so it is the shape to hand an async
3724
- * call, a serialiser, or a diff.
3725
- *
3726
- * ```ts
3727
- * const before = form.values() // detached; survives later writes
3728
- * await api.save(before) // cannot mutate underneath the call
3729
- * form.values.email // reactive read; use this in computed
3730
- * ```
3731
- *
3732
- * Prefer dot access inside a `computed`, `watchEffect`, or template:
3733
- * it tracks the one key you read, where the call form depends on the
3734
- * whole form and re-runs the consumer on any change.
3735
- *
3736
- * The copy is deep across plain objects and arrays. Non-plain
3737
- * instances (Map, Set, File, Date) are shared by reference, matching
3738
- * `JSON.stringify` behaviour; `structuredClone(form.values())` gives a
3739
- * fully detached copy when one is needed.
3740
- *
3741
- * Single-bracket dotted access (`form.values['address.city']`) is
3742
- * intentionally NOT supported — JS object semantics treat the dotted
3743
- * string as a single key. Use chained dot/bracket or the callable
3744
- * form.
3745
- *
3746
- * The chained shape applies the discriminated-union lift via
3747
- * `LiftedValueShape<F>` so per-variant keys are reachable without
3748
- * narrowing first (e.g. `form.values.cargo.permitNumber` types as
3749
- * `string | undefined` regardless of which cargo variant is active —
3750
- * matching the runtime, where plain JS object access on a missing
3751
- * variant key returns `undefined`). The strict-variant shape is
3752
- * still required at the WRITE side: `setValue` and `defaultValues`
3753
- * use the un-lifted `WriteShape` so consumers can't accidentally
3754
- * hand the form a partial / cross-variant object.
3755
- */
3756
- type ValuesSurface<F> = Readonly<LiftedValueShape<F>> & {
3757
- (path: string): unknown;
3758
- (path: ReadonlyArray<string | number>): unknown;
3759
- (): Readonly<F>;
3760
- };
3761
- /**
3762
- * Reactive form-level flags, counters, and aggregates returned as
3763
- * `form.meta`. "Meta" because every other surface (`form.values`,
3764
- * `form.errors`, `form.fields`) is data-shaped — `form.meta` holds
3765
- * facts derived ABOUT the form.
3766
- *
3767
- * Read fields directly with no `.value` — they auto-unwrap inside
3768
- * the reactive object:
3769
- *
3770
- * ```vue
3771
- * <button :disabled="form.meta.submitting">Save</button>
3772
- * ```
3773
- *
3774
- * Watch a single field via the getter form:
3775
- *
3776
- * ```ts
3777
- * watch(() => form.meta.submitting, (value) => …)
3778
- * ```
3779
- *
3780
- * Per-field state (touched, dirty, errors) lives behind
3781
- * `form.fields.<path>`; this is the aggregate view across the
3782
- * whole form.
3783
- *
3784
- * Read-only at runtime — an assignment is refused with a dev-console
3785
- * warning and never lands (Attaform does not throw from a read
3786
- * surface). Destructuring snapshots
3787
- * the current values; use `toRefs()` if you need reactive handles
3788
- * to individual fields.
3789
- */
3790
- type FormMeta<F = unknown> = FieldState<F> & {
3791
- /**
3792
- * `true` while a `handleSubmit`-produced submit handler is running.
3793
- * Covers both the validation phase and your async submit callback.
3794
- * Useful for disabling the submit button.
3795
- */
3796
- readonly submitting: boolean;
3797
- /**
3798
- * How many times the submit handler has been invoked, regardless of
3799
- * outcome (validation failure, callback success, callback throw).
3800
- * Useful for "show errors after first submit attempt" UX.
3801
- */
3802
- readonly submissionAttempts: number;
3803
- /**
3804
- * How many times wizard navigation (`wizard.next`, `wizard.back`,
3805
- * `wizard.goTo`) has actually departed this form. Bumped on real
3806
- * departures only: no-ops like `back()` from the first step, a
3807
- * same-key `goTo`, or a `next()` blocked by failed activation leave
3808
- * the counter at its prior value.
3809
- *
3810
- * Pure introspection counter — useful for "this form has been
3811
- * visited and left" UX (analytics, prior-step badges) but does NOT
3812
- * drive the display heuristic. The reveal-on-submit story
3813
- * runs entirely through `submissionAttempts`, which
3814
- * `wizard.handleSubmit` bumps on every form (it always validates the
3815
- * whole step list).
3816
- *
3817
- * Distinct from `submissionAttempts`, which counts `handleSubmit`
3818
- * passes only — wizard departures and form submissions are tracked
3819
- * separately so consumers can introspect each cleanly. Distinct
3820
- * from `form.validate()`, which is a read-only inspection primitive
3821
- * that never bumps any counter.
3822
- *
3823
- * Cleared by `form.reset()`.
3824
- */
3825
- readonly departAttempts: number;
3826
- /**
3827
- * The error thrown or rejected by the most recent submit callback (or
3828
- * its `onError` handler), coerced to a real `Error` (a non-`Error`
3829
- * throw keeps its origin on `.cause`). Cleared to `null` at the start
3830
- * of each new submission attempt; stays `null` on success.
3831
- *
3832
- * The submit handler does NOT re-throw — its returned promise always
3833
- * resolves, so binding it to `@submit` never manufactures a
3834
- * `window` unhandledrejection. This is the channel for an UNEXPECTED
3835
- * submit failure (a thrown exception or rejected promise), read the
3836
- * same way in templates and after an imperative `await submit()`. An
3837
- * EXPECTED rejection handled the documented way (`setErrors(...)` then
3838
- * `return`, no throw) surfaces through the error store and `onError`
3839
- * instead, leaving `submitError` `null`. Like `hydrateError`, it stays
3840
- * distinct from the curated user-error store: render it where you
3841
- * choose:
3842
- *
3843
- * ```vue
3844
- * <p v-if="form.meta.submitError">{{ form.meta.submitError.message }}</p>
3845
- * ```
3846
- */
3847
- readonly submitError: Error | null;
3848
- /**
3849
- * Scalar mirror of `meta.errors.length`. Read it from templates and
3850
- * `watch()` without indexing the underlying array.
3851
- *
3852
- * Always tracks `errors.length` exactly — reactivity is wired through
3853
- * the same computed graph, so a `watch(form.meta.errorCount, ...)`
3854
- * fires when (and only when) the aggregate error count changes.
3855
- */
3856
- readonly errorCount: number;
3857
- /**
3858
- * `true` once a `handleSubmit` callback has resolved without throwing
3859
- * AND left no errors behind. Independent of `submissionAttempts` — a
3860
- * failed submit (validation failure, callback rejection, or a callback
3861
- * that calls `setErrors` and returns) increments attempts but leaves
3862
- * `submitted` at `false`. Templates read it as "the form has been
3863
- * submitted successfully at least once."
3864
- *
3865
- * The error check is scoped to the user-error layer (`setErrors` /
3866
- * `clearErrors`): the documented server-rejection pattern
3867
- * (`setErrors(response.errors); return`) is a failed submit, so it does
3868
- * not flip `submitted`. A background async refinement that writes a
3869
- * schema error mid-submit does not retroactively fail it.
3870
- *
3871
- * Cleared by `form.reset()` alongside `submissionAttempts` and
3872
- * `submitError`. For "the user has attempted a submit," read
3873
- * `submissionAttempts > 0` directly.
3874
- */
3875
- readonly submitted: boolean;
3876
- /**
3877
- * Per-`useForm()`-call identity. Stable for the lifetime of one
3878
- * `useForm()` call; new on every fresh mount. Orthogonal to
3879
- * `form.key`: the key identifies a SHARED FormStore (so two
3880
- * `useForm({ key: 'signup' })` calls return the same store and the
3881
- * same key), while `instanceId` identifies THIS specific callsite —
3882
- * useful when two forms share a key (sidebar + main rendering the
3883
- * same form) and you need to disambiguate which caller is which.
3884
- *
3885
- * Format is opaque (Vue 3.5+ `useId()`-derived). Treat as identity,
3886
- * not state — don't parse, don't compare ordinally, don't persist.
3887
- *
3888
- * Common patterns:
3889
- *
3890
- * - **Devtools panels** disambiguating shared-key form mounts.
3891
- * - **Telemetry / logging hooks** tagging events with which mount
3892
- * triggered them.
3893
- * - **E2E test selectors** stamping `data-form-id={form.meta.instanceId}`
3894
- * onto a wrapper to assert which form was focused.
3895
- * - **Vue `:key`** for keyed lists of dynamically-rendered forms
3896
- * (drag-reorder, etc.) — stable identity per useForm() call.
3897
- */
3898
- readonly instanceId: string;
3899
- };
3900
- /**
3901
- * The object returned by `useForm`. Holds every reactive ref, write
3902
- * helper, and lifecycle method bound to one form.
3903
- *
3904
- * ```ts
3905
- * const form = useForm({ schema })
3906
- * form.register('email') // bind to <input v-register>
3907
- * form.values.email // current value (proxy, no .value)
3908
- * form.fields.email.dirty // per-field flags
3909
- * form.errors.email // readonly ValidationError[]
3910
- * form.setValue('email', 'a@b.c')
3911
- * form.handleSubmit(onSubmit) // returns a submit handler
3912
- * form.meta.submitting // form-level reactive flag
3913
- * ```
3914
- *
3915
- * Three generic slots split the write view, parse view, and read view:
3916
- *
3917
- * - `Form` — the **input / write shape** (`z.input<Schema>`). Used
3918
- * by `setValue`, `defaultValues`, and `register`'s write side.
3919
- * Loose: preprocess paths accept `unknown` at the write boundary,
3920
- * defaulted fields accept their inner type optionally.
3921
- *
3922
- * - `GetValueFormType` — the **output / parsed shape**
3923
- * (`z.output<Schema>`). Used by `handleSubmit`'s `onSubmit`
3924
- * callback and by `form.parse()`'s success payload. This is the
3925
- * shape after refinements have fired and transforms have run.
3926
- *
3927
- * - `ReadForm` — the **read / storage shape**. Used by `values`,
3928
- * `fields`, `register`'s read side, `toRef`. Per-key precise: at
3929
- * the write-boundary wrappers (`default` / `prefault` / `catch` /
3930
- * `readonly` / `preprocess`) the value is `z.output<Inner>`
3931
- * (default has fired, preprocess has normalized); at transforms /
3932
- * pipes the value stays `z.input<Inner>` (transforms are deferred
3933
- * until parse). For schema-agnostic call sites defaults to `Form`.
3934
- *
3935
- * For schemas without write-boundary wrappers or transforms the three
3936
- * shapes coincide.
3937
- */
3938
- /**
3939
- * Read-only view returned by `form.blankPaths.value`. Exposes lookup
3940
- * (`.has`), aggregate (`.size`), and iteration over the form's
3941
- * blank-marked paths.
3942
- *
3943
- * `.has(input)` and the iterator yield consistent results across both
3944
- * input conventions the library accepts everywhere a path is named:
3945
- *
3946
- * - **Dotted string**: `'user.email'`, matching what `register('user.email')`
3947
- * or `setValue('items.0.sku', …)` accept. Convenient when no segment
3948
- * contains a literal dot.
3949
- * - **Array form**: `['user', 'email']`, mirroring `register(['user', 'email'])`.
3950
- * Required when a single segment contains literal dots (e.g.
3951
- * `['address.primary']` for a top-level key named `address.primary` —
3952
- * the dotted form `'address.primary'` would be parsed as two
3953
- * segments).
3954
- *
3955
- * Iteration yields `Path` arrays so the structure is unambiguous —
3956
- * consumers building debug UI or persisting the set never have to guess
3957
- * whether a dot in a segment is a separator or part of the name.
3958
- *
3959
- * Mutating the view does nothing — writes still go through
3960
- * `setValue(path, unset)`, `markBlank()` on a register binding, or the
3961
- * directive's input listener on numeric clear.
3962
- */
3963
- interface BlankPathsView {
3964
- /** Number of blank-marked paths. */
3965
- readonly size: number;
3966
- /**
3967
- * `true` when the path is in the blank set. Accepts dotted-string
3968
- * form (parsed by [[parseDottedPath]]) or the array form.
3969
- */
3970
- has(input: string | Path): boolean;
3971
- /** Snapshot of all blank-marked paths as segment arrays. */
3972
- values(): readonly Path[];
3973
- /** Iterates the blank-marked paths as segment arrays. */
3974
- [Symbol.iterator](): IterableIterator<Path>;
3975
- }
3976
- /**
3977
- * The no-arg `form.record()` call form, present only when the form root
3978
- * is itself an open record (`z.record(K, V)`). `string extends keyof
3979
- * Form` is the open-keyset probe: true for `Record<string, V>`, false
3980
- * for a fixed `z.object` shape. On a fixed object this resolves to
3981
- * `unknown`, which contributes nothing to the intersection in `record`
3982
- * below, so the no-arg call form simply does not exist there (and
3983
- * `form.record()` stays a compile error, as it should when the root has
3984
- * a closed key set). On a record root it mirrors the path-addressed
3985
- * `record(path)` overload: one `FieldState` per entry, keyed by the
3986
- * record's own runtime keys.
3987
- */
3988
- type RootRecordView<Form> = string extends keyof Form ? Form extends Record<string, infer RootValue> ? () => Readonly<Record<string, FieldState<RootValue>>> : unknown : unknown;
3989
- type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form, K extends FormKey = FormKey> = {
3990
- /**
3991
- * Wraps your submit logic with validation and error routing.
3992
- *
3993
- * ```ts
3994
- * const onSubmit = form.handleSubmit(
3995
- * (data) => api.signup(data),
3996
- * (errors) => console.log(errors),
3997
- * )
3998
- * // Bind the returned handler: <form @submit="onSubmit">
3999
- * ```
4000
- *
4001
- * `data` is the strictly-typed parsed value — refinements have
4002
- * fired and `.transform()`s have run, so the payload matches
4003
- * `z.output<Schema>` (the post-parse output shape). For schemas
4004
- * where the input type differs from the output type (e.g.
4005
- * `z.string().transform(v => v.length > 10)`), `data` is the
4006
- * output shape while `form.values` stays the input shape.
4007
- */
4008
- handleSubmit: HandleSubmit<GetValueFormType>;
4009
- /**
4010
- * Reactive readonly proxy over the form's storage value. Read
4011
- * identically in script and template — no `.value`, no auto-unwrap
4012
- * rules. Pinia setup-store pattern.
4013
- *
4014
- * ```vue
4015
- * <script setup>
4016
- * const form = useForm({ schema, key: 'login' })
4017
- * </script>
4018
- *
4019
- * <template>
4020
- * <p>{{ form.values.email }}</p>
4021
- * <p>{{ form.values.address.city }}</p>
4022
- * </template>
4023
- * ```
4024
- *
4025
- * Writes are blocked at the proxy boundary — go through `setValue`,
4026
- * the directive, or one of the field-array helpers. The
4027
- * slim-primitive write gate stays the only path into storage.
4028
- *
4029
- * Reads reflect what's storable: enum-typed slots widen to their
4030
- * primitive supertype (`string`), so refinement-invalid but
4031
- * structurally-valid values are visible. Storage holds the
4032
- * `z.input<Schema>` shape — `.transform()`s have NOT run, so for
4033
- * a schema like `z.string().transform(v => v.length > 10)` the
4034
- * value reads as `string`, not `boolean`. Use `handleSubmit` or
4035
- * `form.parse()` when you need the post-transform output shape.
4036
- */
4037
- values: ValuesSurface<WriteShape<ReadForm>>;
4038
- /**
4039
- * Reactive per-field state proxy. Pinia-style nested object — read
4040
- * leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
4041
- * `focused`, `blank`, …) directly off the field's path:
4042
- *
4043
- * ```vue
4044
- * <p v-if="form.fields.email.touched && form.fields.email.errors.length">
4045
- * {{ form.fields.email.errors[0].message }}
4046
- * </p>
4047
- * <p>City dirty? {{ form.fields.address.city.dirty }}</p>
4048
- * ```
4049
- *
4050
- * The same proxy supports descent at every level — `address` reads
4051
- * the FieldState for the address object, and `address.city`
4052
- * descends into the nested leaf.
4053
- *
4054
- * Leaf values follow the slim WriteShape contract: enum-typed leaves
4055
- * widen to their primitive supertype, and the leaf value reflects
4056
- * the `z.input<Schema>` shape (transforms deferred until parse).
4057
- * The errors array, dirty flag, focus state, etc. are unaffected.
4058
- *
4059
- * Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
4060
- * `errors`, `blank`, `focused`, `blurred`, `value`,
4061
- * `original`, `pristine`, `connected`, `updatedAt`, `path`) win
4062
- * over schema field names. Top-level fields are NOT shadowed.
4063
- * Document edge case; rename the offending schema field if the
4064
- * collision matters.
4065
- */
4066
- fields: FieldStateMap<WriteShape<ReadForm>>;
4067
- /**
4068
- * Write to the form programmatically. Two forms:
4069
- *
4070
- * - `setValue(value)` — replace the whole form.
4071
- * - `setValue(path, value)` — write at a specific path.
4072
- *
4073
- * Either takes a callback in place of `value` to derive the next
4074
- * value from the previous one:
4075
- *
4076
- * ```ts
4077
- * form.setValue('count', (prev) => prev + 1)
4078
- * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
4079
- * ```
4080
- *
4081
- * Returns `true` when the write is accepted. A `false` return
4082
- * means the value didn't match the slot's expected type
4083
- * (e.g. writing a number to a string field) — the form state
4084
- * stays unchanged. Refinement-level mismatches (out-of-enum
4085
- * values, failing format checks, etc.) DO succeed and surface as
4086
- * field errors instead.
4087
- */
4088
- setValue: {
4089
- /**
4090
- * Replace the whole form. Pass a value or a callback receiving
4091
- * the previous form.
4092
- *
4093
- * ```ts
4094
- * form.setValue({ name: 'Ada', email: 'a@b.c' })
4095
- * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
4096
- * ```
4097
- *
4098
- * Returns `true` when the write was accepted, `false` when the
4099
- * value didn't match the expected shape (e.g. wrong primitive
4100
- * type at a leaf). Refinement-level mismatches (out-of-enum
4101
- * values, failing format checks, etc.) succeed and surface as
4102
- * field errors instead.
4103
- */
4104
- <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
4105
- /**
4106
- * Write at a specific path. Pass a value or a callback receiving
4107
- * the previous value at that path.
4108
- *
4109
- * ```ts
4110
- * form.setValue('email', 'a@b.c')
4111
- * form.setValue('count', (prev) => prev + 1)
4112
- * form.setValue('income', unset) // numeric leaf marked displayed-empty
4113
- * ```
4114
- *
4115
- * Returns `true` when the write was accepted, `false` when the
4116
- * value didn't match the slot's expected primitive type.
4117
- * Refinement-level mismatches succeed and surface as field
4118
- * errors. Pass the `unset` symbol at any primitive leaf to mark
4119
- * it blank (storage holds the slim default; UI displays
4120
- * empty; submit raises "No value supplied" for required schemas).
4121
- */
4122
- <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
4123
- /**
4124
- * Tuple-segment form. Equivalent to the dotted-string overload —
4125
- * useful when paths are built from variables or arrays:
4126
- * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
4127
- * type is exact, matching the dotted-string form.
4128
- */
4129
- <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>), value: Value): boolean;
4130
- };
4131
- /**
4132
- * Reactive validation status. Re-runs whenever the form (or the
4133
- * subtree at `path`) mutates. The returned ref carries a `pending`
4134
- * flag — gate on `!status.value.pending` before reading
4135
- * `success` / `errors`.
4136
- *
4137
- * ```ts
4138
- * const status = form.validate()
4139
- * watchEffect(() => {
4140
- * if (status.value.pending) return
4141
- * if (!status.value.success) console.log(status.value.errors)
4142
- * })
4143
- * ```
4144
- *
4145
- * Stale in-flight runs are dropped automatically — the ref only
4146
- * settles to results from the most recent call.
4147
- */
4148
- validate: (path?: FlatPath<Form>) => Readonly<Ref<ReactiveValidationStatus<Form>>>;
4149
- /**
4150
- * Resolve once every in-flight async `register({ transforms })` run
4151
- * has settled — globally, or (with `path`) only at-or-under that path.
4152
- * Resolve-never-reject: a transform that throws still settles the
4153
- * field (its failure lands on `field.transformError`), so the returned
4154
- * promise always resolves.
4155
- *
4156
- * `handleSubmit` awaits this internally before parsing, so a submit
4157
- * fired the instant after an async transform still validates the
4158
- * resolved value. Reach for it directly when you need the same
4159
- * guarantee outside submit — e.g. before reading `form.values` in an
4160
- * imperative flow or a test:
4161
- *
4162
- * ```ts
4163
- * input.value = ' a@b.com '
4164
- * await form.settleTransforms('email')
4165
- * // form.values.email is now the normalized value
4166
- * ```
4167
- */
4168
- settleTransforms: (path?: FlatPath<Form>) => Promise<void>;
4169
- /**
4170
- * Imperative one-shot parse. Runs the full pipeline — refinements,
4171
- * `.transform()`s, blank-required composition — against the current
4172
- * form snapshot and RETAINS the parsed data.
4173
- *
4174
- * Storage holds the "honest input view" — values you wrote, with
4175
- * preprocess normalization applied but `.transform()` deferred. For
4176
- * schemas where the input type differs from the output type (e.g.,
4177
- * `z.string().transform(v => v.length > 10)`), `form.values.X` is
4178
- * the input shape and `(await form.parse()).data?.X` is the
4179
- * output shape.
4180
- *
4181
- * ```ts
4182
- * const result = await form.parse()
4183
- * if (result.success) {
4184
- * // result.data matches z.output<typeof schema>
4185
- * } else {
4186
- * // result.errors is the validation failure list
4187
- * }
4188
- * ```
4189
- *
4190
- * By default the call is a PURE read: nothing is written to
4191
- * `form.errors`, and in-flight per-field validation runs are left
4192
- * alone. Pass `{ commit: true }` to make the run authoritative —
4193
- * the verdict is committed to the error store at the parsed scope
4194
- * and any in-flight per-field runs are cancelled first (mirroring
4195
- * `handleSubmit`), so `await form.parse('email', { commit: true })`
4196
- * lands a deterministic view of `form.errors.email`:
4197
- *
4198
- * ```ts
4199
- * const result = await form.parse({ commit: true })
4200
- * if (!result.success) showErrors(result.errors)
4201
- * ```
4202
- *
4203
- * Always async, and there is no synchronous variant by design: a
4204
- * schema can carry async refinements or transforms, so a sync parse
4205
- * would silently miss them the moment one is added. One always-
4206
- * awaited `parse` closes that category of bug entirely. The returned
4207
- * promise never rejects (a thrown adapter lands as a `success: false`
4208
- * response). Pass a path to parse a subtree only. `meta.validating`
4209
- * flips `true` while the promise is in flight.
4210
- */
4211
- parse: {
4212
- (path?: FlatPath<Form>, options?: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
4213
- (options: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
4214
- };
4215
- /**
4216
- * Bind a path to a native input via `v-register`. Returns a
4217
- * `RegisterValue` carrying the live ref and event handlers the
4218
- * directive needs.
4219
- *
4220
- * ```vue
4221
- * <input v-register="form.register('email')" />
4222
- * <input v-register="form.register('username', { transforms: [trim] })" />
4223
- * ```
4224
- *
4225
- * Also accepts a segment-array form for callers building paths
4226
- * dynamically — particularly inside a `v-for` over a prefix variable
4227
- * where dotted-string concatenation widens the prefix's literal
4228
- * union to plain `string`:
4229
- *
4230
- * ```vue
4231
- * <fieldset v-for="block in [{ prefix: 'pickup' }, { prefix: 'delivery' }] as const">
4232
- * <input v-register="form.register([block.prefix, 'line1'])" />
4233
- * </fieldset>
4234
- * ```
4235
- *
4236
- * Pass `options.transforms` to run a sync normalisation pipeline over
4237
- * user-typed values before they reach form state.
4238
- */
4239
- register: {
4240
- <Path extends RegisterFlatPath<Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
4241
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form>] ? unknown : SegmentRegisterRejection<JoinSegments<S>>), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>;
4242
- };
4243
- /**
4244
- * The form's identifier — either the explicit `key` passed to
4245
- * `useForm` or an auto-generated unique id when `key` was omitted.
4246
- * Every `ValidationError` this form produces carries it as `formKey`,
4247
- * so a shared error list can be routed back to the right form.
4248
- *
4249
- * Typed as the literal `K` when an explicit `key` was passed; falls
4250
- * back to `FormKey` when omitted (auto-generated id).
4251
- */
4252
- key: K;
4253
- /**
4254
- * `true` while a function-form `defaultValues` factory is in flight
4255
- * — between `useForm` construction and the moment the factory
4256
- * resolves (sync function on the next microtask; async function when
4257
- * its promise settles). `false` otherwise, including when
4258
- * `defaultValues` is a plain value.
4259
- *
4260
- * The form is fully usable while `hydrating` is `true` — it holds
4261
- * the schema's slim defaults. The flag exists so templates can show
4262
- * a spinner / dim the form while real data loads:
4263
- *
4264
- * ```vue
4265
- * <div :aria-busy="form.hydrating">…</div>
4266
- * ```
4267
- *
4268
- * Exposed as an auto-unwrapping `boolean` (no `.value`); reactivity
4269
- * is preserved via a getter that tracks the underlying ref at the
4270
- * access site, so `watch(() => form.hydrating, …)` and template
4271
- * reads both fire on change. Reading this property activates the
4272
- * form's factory under the lazy-by-default rule.
4273
- */
4274
- readonly hydrating: boolean;
4275
- /**
4276
- * The error from the most recent function-form `defaultValues` factory,
4277
- * normalized to a `ValidationError` (code `atta:hydration-failed`) so the
4278
- * shape matches every other surface in `form.errors` / `form.meta.errors`.
4279
- * `null` on construction, on successful resolution, and whenever no
4280
- * factory has fired. Updates with each `form.rehydrate()` call.
4281
- *
4282
- * Distinct from `meta.submitError` so retry buttons and recovery UX can
4283
- * stay focused on the load-time failure without entangling the submit
4284
- * pipeline. Read directly in templates and script (no `.value`);
4285
- * reactivity is preserved via a getter:
4286
- *
4287
- * ```vue
4288
- * <p v-if="form.hydrateError">{{ form.hydrateError.message }}</p>
4289
- * ```
4290
- */
4291
- readonly hydrateError: ValidationError | null;
4292
- /**
4293
- * `true` once the form's defaults have been applied — either a plain
4294
- * `defaultValues` value at construction or an async factory whose
4295
- * settle completed successfully. Stays `false` for dormant lazy
4296
- * forms (factory not yet activated) and for failed activations
4297
- * (`hydrateError` set). Once `true`, stays `true` through refetches
4298
- * so stale-while-revalidate UIs can keep rendering the prior values
4299
- * while a `rehydrate()` is in flight.
4300
- *
4301
- * Composes with `hydrating` and `hydrateError`:
4302
- *
4303
- * ```vue
4304
- * <Spinner v-if="!form.ready && form.hydrating" />
4305
- * <ErrorBanner v-if="!form.ready && form.hydrateError" :error="form.hydrateError" />
4306
- * <form v-if="form.ready">…</form>
4307
- * ```
4308
- *
4309
- * Exposed as a reactive `boolean` (no `.value`). Reading it activates
4310
- * the factory under the lazy-by-default rule — observing readiness
4311
- * implies use.
4312
- */
4313
- readonly ready: boolean;
4314
- /**
4315
- * Re-fire the captured `defaultValues` factory and re-apply its
4316
- * payload over the current form values. Useful when the upstream
4317
- * source changes (the user picks a different draft, a background
4318
- * sync indicates fresh server data, etc.).
4319
- *
4320
- * Resolves after `hydrating` flips back to `false`. Throws
4321
- * synchronously when the form was constructed with a plain-value
4322
- * `defaultValues` (nothing to re-fire).
4323
- *
4324
- * The payload becomes the form's defaults, the same way a
4325
- * `reset(next)` argument does, so `form.reset()` afterwards lands on
4326
- * the values just fetched. Does NOT clear touched / submit state;
4327
- * chain `form.reset()` for that.
4328
- */
4329
- rehydrate(): Promise<void>;
4330
- /**
4331
- * Idempotent activation. Forms are lazy-by-default: a function-form
4332
- * `defaultValues` factory fires on the first reactive interaction
4333
- * (reading `form.values`, calling `form.setValue`, etc.). Call
4334
- * `form.activate()` to kick the factory explicitly — typically from
4335
- * `setup` so SSR's `onServerPrefetch` hook awaits the resolution
4336
- * before the page renders. Subsequent calls return the in-flight
4337
- * promise until the factory settles, after which they resolve
4338
- * immediately. Plain-value forms (no factory captured) always
4339
- * return a resolved promise.
4340
- */
4341
- activate(): Promise<void>;
4342
- /**
4343
- * Reactive map of field errors, keyed by dotted path. Populated
4344
- * automatically by `handleSubmit` and per-field validation; cleared
4345
- * on validation success.
4346
- *
4347
- * Read in templates with no `.value`:
4348
- *
4349
- * ```vue
4350
- * <p v-if="form.errors.email">{{ form.errors.email[0].message }}</p>
4351
- * ```
4352
- *
4353
- * Watch from script via the getter form:
4354
- *
4355
- * ```ts
4356
- * watch(() => form.errors.email, (errors) => …)
4357
- * ```
4358
- *
4359
- * Use bracket access for nested dotted keys
4360
- * (`form.errors['user.profile.email']`) — JS dot notation splits
4361
- * on literal dots.
4362
- *
4363
- * Read-only — populate via `setErrors` / `clearErrors`. A server
4364
- * response that already emits `ValidationError[]` pipes straight into
4365
- * `setErrors` with no adapter.
4366
- */
4367
- errors: FormErrorsSurface<Form>;
4368
- /**
4369
- * Escape hatch for the rare case a consumer needs a `Ref<T>` —
4370
- * e.g. handing the value to an external composable that expects a
4371
- * Vue ref, or watching a single path with `watch(formRef, ...)`.
4372
- *
4373
- * ```ts
4374
- * const emailRef = form.toRef('email') // Readonly<Ref<string>>
4375
- * watch(emailRef, (next) => console.log(next))
4376
- * ```
4377
- *
4378
- * Returns `Readonly<Ref<...>>` — writes go through `setValue`,
4379
- * `register()`, or the field-array helpers, never via the ref.
4380
- * Prefer `form.values.email` for direct reads in templates +
4381
- * scripts; `toRef` is for ref-shaped interop only.
4382
- */
4383
- toRef: {
4384
- <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, Path>>>;
4385
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>>;
4386
- };
4387
- /**
4388
- * Set the form's manual error layer. One surface for server-side
4389
- * errors, optimistic-UI errors, and form-level banners: a field error
4390
- * and a global (form-level) error are the same thing at different
4391
- * paths, so there is no separate field/form split.
4392
- *
4393
- * Input is lenient ({@link ErrorInput}): a real `Error`, a partial
4394
- * `{ message?, path?, code?, data? }`, or an array of either. The form
4395
- * stamps its own `formKey`, defaults a missing `code` to
4396
- * `atta:user-error`, and coerces a missing or empty `message` to
4397
- * `"Unknown error"` instead of throwing. A server that already emits
4398
- * `ValidationError[]` satisfies the input shape directly, so
4399
- * `form.setErrors(response.errors)` needs no adapter.
4400
- *
4401
- * Three call forms, mirroring `setValue`:
4402
- *
4403
- * ```ts
4404
- * // Whole-layer replace. An entry with no `path` is a global,
4405
- * // form-level error (path `[]`); add a `path` to target a field.
4406
- * form.setErrors([{ path: ['email'], message: 'Already taken' }])
4407
- * form.setErrors({ message: 'Capacity exceeded' }) // global banner
4408
- * form.setErrors(new Error('Network unreachable')) // message coerced
4409
- *
4410
- * // Functional update. `prev` is the current manual layer, flat.
4411
- * form.setErrors((prev) => [...prev, { message: 'And one more' }])
4412
- *
4413
- * // Path-scoped. The path is stamped onto every entry, and only that
4414
- * // path's bucket is replaced. `prev` is that path's manual errors.
4415
- * form.setErrors('email', [{ message: 'Already taken' }])
4416
- * form.setErrors(['profile', 'handle'], { message: 'Reserved' })
4417
- * form.setErrors('email', (prev) => prev.slice(0, 1))
4418
- * ```
4419
- *
4420
- * Replaces only the manual layer; schema/validation errors live in a
4421
- * separate store and merge on read. Pass `[]` to clear the manual
4422
- * layer (or use `clearErrors`, which also clears the schema layer).
4423
- */
4424
- setErrors: {
4425
- (update: (prev: ValidationError[]) => ErrorInput | ErrorInput[]): void;
4426
- (errors: ErrorInput | ErrorInput[]): void;
4427
- (path: string | (string | number)[], errors: ErrorInput | ErrorInput[] | ((prev: ValidationError[]) => ErrorInput | ErrorInput[])): void;
4428
- };
4429
- /**
4430
- * Clear errors at one path, or everywhere. Clears BOTH the manual
4431
- * layer (set through `setErrors`) and the schema/validation layer at
4432
- * the target. With always-on validation the schema half re-populates
4433
- * on the next mutation if the value is still invalid, so the cleared
4434
- * state is short-lived for a field that is still wrong.
4435
- *
4436
- * ```ts
4437
- * form.clearErrors('email') // one field
4438
- * form.clearErrors([]) // the global, form-level bucket
4439
- * form.clearErrors() // every error on the form
4440
- * ```
4441
- */
4442
- clearErrors: (path?: string | (string | number)[]) => void;
4443
- /**
4444
- * Form-level reactive flags, counters, and aggregates (`dirty`,
4445
- * `valid`, `submitting`, `submissionAttempts`, and the flat `errors`
4446
- * array). See `FormMeta` for the full shape. Read leaves directly
4447
- * with no `.value`.
4448
- *
4449
- * For per-field state (touched, focused, blurred, errors at one
4450
- * path), use `form.fields.<path>` instead. Undo/redo state lives at
4451
- * `form.history` (see `FormHistoryNamespace`).
4452
- */
4453
- meta: FormMeta<Form>;
4454
- /**
4455
- * Restore the form to its defaults. Without arguments, re-applies
4456
- * whatever the defaults currently are: `useForm({ defaultValues })`
4457
- * on a fresh form, or the last values a `reset(next)` or an async
4458
- * `defaultValues` factory settled on.
4459
- *
4460
- * Resets:
4461
- * - the form value back to defaults;
4462
- * - the dirty baseline (so the next edit flips `dirty` correctly);
4463
- * - field errors;
4464
- * - touched / focused / blurred per-field flags;
4465
- * - submission state (`submitting` / `submissionAttempts` /
4466
- * `submitted` / `submitError`).
4467
- *
4468
- * `nextDefaultValues` RE-SEATS the defaults rather than applying for
4469
- * one call, so the form has a single set of defaults that `reset()`,
4470
- * `resetField(path)`, and `dirty` all read. That is what makes the
4471
- * save-then-discard shape work: reset from the resource the server
4472
- * returned, and a later Discard returns to the save instead of
4473
- * rolling back across it.
4474
- *
4475
- * ```ts
4476
- * const saved = await api.save(form.values())
4477
- * form.reset(toValues(saved)) // the saved resource is now the baseline
4478
- * // …later…
4479
- * form.reset() // back to the saved resource
4480
- * ```
4481
- *
4482
- * The argument is sparse: it folds over the defaults already in
4483
- * force, so paths it does not name keep the value they had, and
4484
- * `reset({})` changes nothing. Arrays are replaced wholesale rather
4485
- * than merged element-wise. Pass `unset` at a path to withdraw its
4486
- * value and mark it blank.
4487
- */
4488
- reset: (nextDefaultValues?: DefaultValuesInput<Form>) => void;
4489
- /**
4490
- * Restore a single field (or a sub-tree like `'user'`) to its
4491
- * initial value. Clears errors and touched flags for the field
4492
- * and its descendants; leaves siblings and submission state alone.
4493
- *
4494
- * No-op when the path doesn't exist on the form (e.g. a typo'd
4495
- * dynamic key).
4496
- */
4497
- resetField: (path: FlatPath<Form>) => void;
4498
- /**
4499
- * Wipe a field (or the whole form) to the "appropriate nullish
4500
- * value" for its declared type — the underlying type's empty/falsy
4501
- * concrete, with any `.default(x)` wrapper INTENTIONALLY skipped.
4502
- * Orthogonal to `reset` / `resetField` by design.
4503
- *
4504
- * ```ts
4505
- * const schema = z.object({
4506
- * notify: z.boolean().default(true),
4507
- * count: z.number().default(5),
4508
- * })
4509
- * const form = useForm({ schema })
4510
- *
4511
- * form.reset() // notify → true, count → 5 (defaults)
4512
- * form.clear() // notify → false, count → 0 (falsy-for-type)
4513
- * form.clear('notify') // → false (NOT the declared default true)
4514
- * ```
4515
- *
4516
- * Per-wrapper semantics:
4517
- *
4518
- * - `.default(x)` / `.prefault(x)` / `.catch(x)` → inner-schema
4519
- * empty (default is INTENTIONALLY skipped).
4520
- * - `.optional()` → `undefined` (the wrapper's "absent" marker).
4521
- * - `.nullable()` → `null` (the wrapper's "explicit empty").
4522
- * - Object → recursive (every property gets its own empty).
4523
- * - Array / Set / Record → empty.
4524
- *
4525
- * Returns `true` when the write was accepted, `false` when the
4526
- * adapter couldn't resolve an empty value at the path (e.g. the
4527
- * path doesn't exist in the schema). The form state is unchanged
4528
- * on a `false` return.
4529
- *
4530
- * Sugar over `setValue(path, schema.getEmptyValueAtPath(path))` —
4531
- * no separate bookkeeping. Variant memory, history, and listeners
4532
- * all see this as a regular write at the path.
4533
- *
4534
- * `clear()` (no arg) targets the whole form. `clear('')` targets
4535
- * the empty-string path slot SPECIFICALLY — the two are NOT
4536
- * interchangeable, matching `touch()` / `touch('')` from #184.
4537
- */
4538
- clear: {
4539
- (): boolean;
4540
- <Path extends FlatPath<Form> | ''>(path: Path): boolean;
4541
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form> | ''] ? unknown : SegmentPathRejection<JoinSegments<S>>)): boolean;
4542
- };
4543
- /**
4544
- * Consolidated undo/redo namespace — `form.history.{undo, redo,
4545
- * clear, canUndo, canRedo, size}`. Always present; inert when
4546
- * `useForm({ history })` wasn't configured. See `FormHistoryNamespace`
4547
- * for field-by-field semantics.
4548
- */
4549
- history: FormHistoryNamespace;
4550
- /**
4551
- * Focus the first errored field's first visible element. Returns
4552
- * `true` when an element was focused, `false` when no candidate
4553
- * element exists (no errors, or every errored field is unmounted
4554
- * or hidden).
4555
- *
4556
- * Pass `{ preventScroll: true }` if you're scrolling separately
4557
- * (e.g. via `scrollToFirstError`) and don't want the browser to
4558
- * fight the explicit scroll.
4559
- *
4560
- * Requests `focusVisible: true` so the UA paints a focus ring even
4561
- * though the move is programmatic. Honored where supported; on
4562
- * browsers without it, non-text controls focused after a pointer
4563
- * submit may show no ring, so you may still want an app-side
4564
- * indicator.
4565
- */
4566
- focusFirstError: (options?: {
4567
- preventScroll?: boolean;
4568
- }) => boolean;
4569
- /**
4570
- * Scroll the first errored field's first visible element into
4571
- * view. Returns `true` when the call ran, `false` when no
4572
- * candidate element exists.
4573
- *
4574
- * `options` is forwarded to `Element.scrollIntoView` unchanged.
4575
- */
4576
- scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
4577
- /**
4578
- * Run the form's own invalid-submit nudge imperatively: exactly what
4579
- * `handleSubmit` does after a failed submit, but available standalone
4580
- * and honoring this form's `useForm({ focusOnInvalidSubmit })` choice.
4581
- *
4582
- * Used by `useWizard` after navigating to the first failing form
4583
- * during `wizard.handleSubmit`, so the failing form's own configured
4584
- * behavior fires once its DOM is in view.
4585
- *
4586
- * No-op when this form opted out, or when no errored field is
4587
- * currently registered. Use `focusFirstError()` to focus regardless
4588
- * of the configured choice.
4589
- */
4590
- applyInvalidSubmitPolicy: () => void;
4591
- /**
4592
- * Programmatically mark fields as `touched` — the descriptive
4593
- * "this field was visited" flag.
4594
- *
4595
- * ```ts
4596
- * form.touch('email') // one leaf
4597
- * form.touch('profile') // every leaf under profile
4598
- * form.touch(['profile', 'name']) // segment-array form
4599
- * form.touch() // every leaf in the form
4600
- * ```
4601
- *
4602
- * **This does not reveal errors under the default display
4603
- * heuristic.** `touched` also flips on a bare focus → blur with no
4604
- * edit, so the library-default gate deliberately ignores it and
4605
- * reads `blurredAfterInteraction` instead — the stricter bit that
4606
- * only a blur *following* an edit sets. Reach for `touch()` when
4607
- * your own analytics reads `touched`; reach for
4608
- * {@link UseFormReturnType.interact} to make
4609
- * seeded or imported values surface their errors.
4610
- *
4611
- * Pure flag write — does not mutate value, focused, blurred, or
4612
- * trigger validation. Idempotent: re-calling on an already-touched
4613
- * field is a no-op. Touched is sticky-true; pair with
4614
- * `form.reset()` / `form.resetField()` to clear.
4615
- */
4616
- touch: (path?: FlatPath<Form> | (string | number)[]) => void;
4617
- /**
4618
- * Simulate a complete focus → edit → blur over every leaf under
4619
- * `path` (the whole form when omitted), so values that arrived
4620
- * without a DOM gesture reveal their errors under the default
4621
- * display heuristic.
4622
- *
4623
- * ```ts
4624
- * form.interact('profile') // arm one subtree
4625
- * await form.interact(['members', 2]) // errors committed on resolve
4626
- * form.interact() // every leaf in the form
4627
- * ```
4628
- *
4629
- * Flips the full interaction ladder (`touched`, `interacted`, and
4630
- * `blurredAfterInteraction`) and runs the subtree's validation, so
4631
- * the default gate — `submissionAttempts > 0 ||
4632
- * blurredAfterInteraction` — opens through its front door. Use it
4633
- * to arm one region's errors without a form-wide submit, which
4634
- * would light up every other field on the page: a field-array row
4635
- * edited in a modal, a server-seeded section, a pasted import.
4636
- *
4637
- * Walks schema leaves, so it reaches fields that are currently
4638
- * `v-if`'d away or were never mounted; the flags are sticky, so
4639
- * such a subtree stays revealed when it remounts.
4640
- *
4641
- * Flags land synchronously — the returned promise resolves once the
4642
- * subtree's validation has committed, so an awaiting caller can read
4643
- * `showErrors` / `errors` immediately after. It never rejects, and
4644
- * ignoring it is fine.
4645
- *
4646
- * No-op on a disabled form, matching every other interaction-
4647
- * lifecycle write. Does not mutate value, `focused`, or `blurred`:
4648
- * those are DOM-owned, and the display gate reads none of them.
4649
- * Sticky like `touched`; pair with `form.reset()` /
4650
- * `form.resetField()` to clear.
4651
- */
4652
- interact: (path?: FlatPath<Form> | (string | number)[]) => Promise<void>;
4653
- /**
4654
- * Append `value` to the array at `path`.
4655
- *
4656
- * ```ts
4657
- * form.append('items', { name: 'New' })
4658
- * ```
4659
- */
4660
- append: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
4661
- /** Prepend `value` to the array at `path`. */
4662
- prepend: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
4663
- /**
4664
- * Insert `value` into the array at `path` at the given `index`.
4665
- * Behaves like `Array.prototype.splice`: `index` is clamped into
4666
- * `[0, length]`, and negative indices count from the end.
4667
- */
4668
- insert: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
4669
- /** Remove the element at `index` from the array at `path`. No-op when out of range. */
4670
- remove: <Path extends ArrayPath<Form>>(path: Path, index: number) => void;
4671
- /** Swap the elements at indices `a` and `b`. No-op when either is out of range. */
4672
- swap: <Path extends ArrayPath<Form>>(path: Path, a: number, b: number) => void;
4673
- /**
4674
- * Move the element at `from` to `to`. Useful for drag-and-drop
4675
- * reordering. No-op when either index is out of range.
4676
- */
4677
- move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
4678
- /** Replace the element at `index` with `value`. No-op when out of range. */
4679
- replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
4680
- /**
4681
- * Read-only, reactive view of the array at `path` as one `FieldState`
4682
- * per element, in index order. Each entry carries its element `key`,
4683
- * an allocated identity token, so a `v-for` keyed by it keeps a row's
4684
- * component instance across an insert, removal, move, or swap:
4685
- *
4686
- * ```vue
4687
- * <div v-for="(row, i) in form.list('contacts')" :key="row.key">
4688
- * <input v-register="form.register(`contacts.${i}.name`)" />
4689
- * <p v-if="row.showErrors">{{ row.firstError?.message }}</p>
4690
- * </div>
4691
- * ```
4692
- *
4693
- * Entries are the same field states `form.fields` exposes, so reads
4694
- * stay live. `form.fields(path)` remains the single aggregated
4695
- * container for the whole array; `list` is the per-element view.
4696
- * For a record, reach for `record`, which keys each entry by its own
4697
- * key.
4698
- */
4699
- list: <Path extends ArrayPath<Form>>(path: Path) => readonly FieldState<ArrayItem<Form, Path>>[];
4700
- /**
4701
- * Read-only, reactive view of the record at `path` as one `FieldState`
4702
- * per entry, keyed by the entry's own key. Where `list` hands back an
4703
- * ordered array for an array path, `record` hands back a keyed object
4704
- * for a record path, so you iterate it by key:
4705
- *
4706
- * ```vue
4707
- * <div v-for="(field, key) in form.record('scoresByTeam')" :key="key">
4708
- * <label>{{ key }}</label>
4709
- * <input v-register="form.register(`scoresByTeam.${key}`)" />
4710
- * <p v-if="field.showErrors">{{ field.firstError?.message }}</p>
4711
- * </div>
4712
- * ```
4713
- *
4714
- * Entries are the same field states `form.fields` exposes, so reads
4715
- * stay live, and the keyed shape mirrors the record's own keys: an
4716
- * entry appears once you write its key (`form.setValue`) and drops
4717
- * when the key leaves. `form.fields(path)` remains the single
4718
- * aggregated container for the whole record; `record` is the
4719
- * per-entry view.
4720
- *
4721
- * When the form root is itself a record (`useForm({ schema:
4722
- * z.record(K, V) })` — a dictionary form), call `form.record()` with
4723
- * no argument for the root entry view:
4724
- *
4725
- * ```vue
4726
- * <div v-for="(member, id) in form.record()" :key="id">
4727
- * <input v-register="form.register(id)" />
4728
- * <p v-if="member.showErrors">{{ member.firstError?.message }}</p>
4729
- * </div>
4730
- * ```
4731
- */
4732
- record: RootRecordView<Form> & (<Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>);
4733
- /**
4734
- * Read-only view of the form's blank path set. Reactive — Vue 3.5
4735
- * tracks `.has()` / `for..of` / size accesses, so consumers can drive
4736
- * conditional UI off it directly:
4737
- *
4738
- * ```ts
4739
- * watchEffect(() => {
4740
- * if (form.blankPaths.value.size > 0) {
4741
- * const paths = [...form.blankPaths.value] // Path[][] — array of segments per entry
4742
- * console.warn('unanswered fields:', paths.map((p) => p.join('.')))
4743
- * }
4744
- * })
4745
- * ```
4746
- *
4747
- * `.has(input)` accepts the dotted-string form (`'user.email'`) or
4748
- * the array form (`['user', 'email']`). The array form disambiguates
4749
- * keys with literal dots (e.g. `['address.primary']`). See
4750
- * [[BlankPathsView]] for the full surface.
4751
- *
4752
- * For per-path access, use `form.fields.<path>.blank`.
4753
- * Writes happen through `setValue(path, unset)`,
4754
- * `markBlank()` on a register binding, and the directive's
4755
- * input listener on numeric clear.
4756
- */
4757
- blankPaths: ComputedRef<BlankPathsView>;
4758
- };
4759
-
4760
- export { ROOT_PATH as R, canonicalizePath as aj, isPathPrefix as ak, isUnset as al, parseDottedPath as am, unset as an, ROOT_PATH_KEY as z };
4761
- export type { SchemaDefaultsResult as $, AbstractSchema as A, ReactiveValidationStatus as B, CustomDirectiveRegisterAssignerFn as C, DefaultValuesInput as D, ErrorInput as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, ParseOptions as P, RecordPath as Q, RecordValue as S, RegisterDirective as T, UseFormConfiguration as U, RegisterFlatPath as V, RegisterOptions as W, RegisterSelectModifier as X, RegisterTextModifier as Y, RegisterTransform as Z, RegisterValue as _, UseFormReturnType as a, SchemaParseErrorWithData as a0, SchemaParseErrorWithoutData as a1, SchemaParseResult as a2, SchemaParseSuccess as a3, Segment as a4, SetValueCallback as a5, SetValuePayload as a6, SettledValidationStatus as a7, SlimPrimitiveKind as a8, SubmitHandler as a9, Unset as aa, ValidateOn as ab, ValidateOnConfig as ac, ValidationError as ad, ValidationResponse as ae, ValidationResponseWithoutValue as af, ValueOfUnion as ag, WriteMeta as ah, WriteShape as ai, RegisterModelDynamicCustomDirective as ao, SchemaFactoryOptions as ap, AcceptableDefaults as aq, DisplayCtx as ar, GetDisplayState as as, DisplayMachine as at, FieldRecord as au, ErrorCell as av, OriginalsRecord as aw, AttaformDomBinding as ax, TransformAbortHolder as ay, ArrayItem as b, ArrayPath as c, DeepPartial as d, DefaultValuesShape as e, DisplayState as f, ErrorsProxyShape as g, FieldMetaPayload as h, FieldState as i, FieldStateMap as j, FieldStateMapEntry as k, FlatPath as l, FormErrorRecord as m, FormErrorsSurface as n, FormMeta as o, HistoryPlugin as p, IsUnion as q, Json as r, NestedType as s, OnSubmit as t, PartialFlatPath as u, Path as v, PathKey as w, PendingValidationStatus as x, Primitive as y };