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