attaform 0.14.0 → 0.15.1

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 (69) hide show
  1. package/dist/chunks/devtools.cjs +3 -3
  2. package/dist/chunks/devtools.cjs.map +1 -1
  3. package/dist/chunks/devtools.mjs +3 -3
  4. package/dist/chunks/devtools.mjs.map +1 -1
  5. package/dist/chunks/indexeddb.cjs +1 -1
  6. package/dist/chunks/indexeddb.mjs +1 -1
  7. package/dist/chunks/local-storage.cjs +1 -1
  8. package/dist/chunks/local-storage.mjs +1 -1
  9. package/dist/chunks/session-storage.cjs +1 -1
  10. package/dist/chunks/session-storage.mjs +1 -1
  11. package/dist/index.cjs +5 -4
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +4 -4
  14. package/dist/index.d.mts +4 -4
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.mjs +6 -6
  17. package/dist/nuxt.d.cts +1 -1
  18. package/dist/nuxt.d.mts +1 -1
  19. package/dist/nuxt.d.ts +1 -1
  20. package/dist/runtime/plugins/attaform.cjs +1 -1
  21. package/dist/runtime/plugins/attaform.mjs +1 -1
  22. package/dist/shared/{attaform.DDXrY-1Q.d.mts → attaform.0Gxd_OOx.d.cts} +558 -174
  23. package/dist/shared/{attaform.DDXrY-1Q.d.ts → attaform.0Gxd_OOx.d.mts} +558 -174
  24. package/dist/shared/{attaform.DDXrY-1Q.d.cts → attaform.0Gxd_OOx.d.ts} +558 -174
  25. package/dist/shared/{attaform.xKWYHMdq.cjs → attaform.BOi138GE.cjs} +10 -2
  26. package/dist/shared/{attaform.xKWYHMdq.cjs.map → attaform.BOi138GE.cjs.map} +1 -1
  27. package/dist/shared/{attaform.CRgix6_n.cjs → attaform.BgYBU8gV.cjs} +18 -17
  28. package/dist/shared/attaform.BgYBU8gV.cjs.map +1 -0
  29. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  30. package/dist/shared/{attaform.CNJO3mME.cjs → attaform.CDJVeoJU.cjs} +633 -236
  31. package/dist/shared/attaform.CDJVeoJU.cjs.map +1 -0
  32. package/dist/shared/{attaform.DlgKK10S.mjs → attaform.CRk8NhlD.mjs} +18 -17
  33. package/dist/shared/attaform.CRk8NhlD.mjs.map +1 -0
  34. package/dist/shared/{attaform.CXZgUECn.d.cts → attaform.CVv9Oh0a.d.mts} +41 -9
  35. package/dist/shared/{attaform.BYc9kugA.d.ts → attaform.CWCx2r0x.d.ts} +41 -9
  36. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  37. package/dist/shared/{attaform.Cc93zNzD.mjs → attaform.DXye3JKf.mjs} +10 -3
  38. package/dist/shared/{attaform.Cc93zNzD.mjs.map → attaform.DXye3JKf.mjs.map} +1 -1
  39. package/dist/shared/{attaform.DOKOyb3Y.d.mts → attaform.Dq5BabH1.d.cts} +41 -9
  40. package/dist/shared/{attaform.B5GWYl76.cjs → attaform.RypIkgVy.cjs} +38 -7
  41. package/dist/shared/attaform.RypIkgVy.cjs.map +1 -0
  42. package/dist/shared/{attaform.al_rpt7_.mjs → attaform.a99dQV7Q.mjs} +39 -8
  43. package/dist/shared/attaform.a99dQV7Q.mjs.map +1 -0
  44. package/dist/shared/{attaform.BRTxpA3q.mjs → attaform.qxyip_aN.mjs} +634 -238
  45. package/dist/shared/attaform.qxyip_aN.mjs.map +1 -0
  46. package/dist/transforms.d.cts +2 -2
  47. package/dist/transforms.d.mts +2 -2
  48. package/dist/transforms.d.ts +2 -2
  49. package/dist/zod-v3.cjs +55 -3
  50. package/dist/zod-v3.cjs.map +1 -1
  51. package/dist/zod-v3.d.cts +77 -4
  52. package/dist/zod-v3.d.mts +77 -4
  53. package/dist/zod-v3.d.ts +77 -4
  54. package/dist/zod-v3.mjs +56 -6
  55. package/dist/zod-v3.mjs.map +1 -1
  56. package/dist/zod.cjs +372 -5
  57. package/dist/zod.cjs.map +1 -1
  58. package/dist/zod.d.cts +120 -4
  59. package/dist/zod.d.mts +120 -4
  60. package/dist/zod.d.ts +120 -4
  61. package/dist/zod.mjs +371 -8
  62. package/dist/zod.mjs.map +1 -1
  63. package/package.json +3 -1
  64. package/dist/shared/attaform.B5GWYl76.cjs.map +0 -1
  65. package/dist/shared/attaform.BRTxpA3q.mjs.map +0 -1
  66. package/dist/shared/attaform.CNJO3mME.cjs.map +0 -1
  67. package/dist/shared/attaform.CRgix6_n.cjs.map +0 -1
  68. package/dist/shared/attaform.DlgKK10S.mjs.map +0 -1
  69. package/dist/shared/attaform.al_rpt7_.mjs.map +0 -1
@@ -1,5 +1,70 @@
1
1
  import { Ref, ObjectDirective, ComputedRef } from 'vue';
2
2
 
3
+ /**
4
+ * Schema-attached field metadata — the shared types used by both Zod
5
+ * adapters (`attaform/zod` for v4 and `attaform/zod-v3` for v3) so a
6
+ * consumer's data flow reads the same shape regardless of adapter.
7
+ *
8
+ * The Zod 4 adapter creates a typed `z.registry<FieldMetaPayload>()`
9
+ * and writes through `schema.register(fieldMeta, payload)` (native) or
10
+ * the `withMeta(schema, payload)` helper. The Zod 3 adapter has no
11
+ * native registry — it shims a `WeakMap<ZodTypeAny, FieldMetaPayload>`
12
+ * with the same write API via `withMeta`.
13
+ *
14
+ * Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
15
+ * which returns a fully-resolved `ResolvedFieldMeta` (label /
16
+ * description / placeholder / meta) so the per-leaf and per-container
17
+ * `FieldState` producers in core never see the version split.
18
+ */
19
+ /**
20
+ * The metadata a consumer attaches to a schema node — short label
21
+ * (presentational), longer description (helper text), placeholder
22
+ * (input affordance). Declared as `interface` (not `type`) so
23
+ * downstream apps can extend the shape via TypeScript declaration
24
+ * merging when they want to register richer payloads (tooltips,
25
+ * icons, badge counts, etc.):
26
+ *
27
+ * declare module 'attaform/zod' {
28
+ * interface FieldMetaPayload {
29
+ * tooltip?: string
30
+ * }
31
+ * }
32
+ *
33
+ * After augmentation, `withMeta(schema, { tooltip: '…' })` is typed
34
+ * and `state.meta.tooltip` reads back as `string | undefined`.
35
+ *
36
+ * Every key is optional. Empty payloads (no keys registered) are
37
+ * indistinguishable from "not registered at all" — both surface as
38
+ * fallbacks (humanize for label, undefined for the rest).
39
+ */
40
+ interface FieldMetaPayload {
41
+ label?: string;
42
+ description?: string;
43
+ placeholder?: string;
44
+ }
45
+ /**
46
+ * The fully-resolved metadata returned by
47
+ * `AbstractSchema.getFieldMetaAtPath(path)`. Adapters apply the
48
+ * precedence rules:
49
+ *
50
+ * - `label`: registry payload → `humanize(lastSegment)`
51
+ * - `description`: registry payload → schema's `.describe()` value → `undefined`
52
+ * - `placeholder`: registry payload → `undefined`
53
+ * - `meta`: full registered payload, frozen — empty object if nothing registered
54
+ *
55
+ * `label` is always a non-empty string at leaves (humanize fallback
56
+ * guarantees this for any non-numeric segment). For containers it
57
+ * may collapse to the empty string when the path is empty (root) or
58
+ * the segment is a numeric index — callers display "" or substitute
59
+ * a context-appropriate fallback.
60
+ */
61
+ type ResolvedFieldMeta = {
62
+ readonly label: string;
63
+ readonly description: string | undefined;
64
+ readonly placeholder: string | undefined;
65
+ readonly meta: Readonly<FieldMetaPayload>;
66
+ };
67
+
3
68
  /**
4
69
  * Path primitives for advanced integrations. The form library accepts
5
70
  * paths in dotted-string form (`'user.email'`) at every public API.
@@ -60,6 +125,21 @@ declare function canonicalizePath(input: string | Path): {
60
125
  declare const ROOT_PATH: Path;
61
126
  /** Stable string key for the root path. */
62
127
  declare const ROOT_PATH_KEY: PathKey;
128
+ /**
129
+ * `true` when `path` starts with every segment of `prefix` (in order).
130
+ * The empty `prefix` matches every path — ROOT prefix is universal.
131
+ *
132
+ * Walks segments rather than `PathKey` strings because the data this
133
+ * helper operates on (e.g. `meta.errors[].path`) carries segment
134
+ * arrays directly.
135
+ *
136
+ * ```ts
137
+ * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
138
+ * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
139
+ * isPathPrefix([], ['anything']) // true (root prefix)
140
+ * ```
141
+ */
142
+ declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
63
143
 
64
144
  /** Internal brand for the `Unset` type. Never exposed at runtime. */
65
145
  declare const _unsetBrand: unique symbol;
@@ -102,7 +182,7 @@ type Unset = typeof _unsetBrand;
102
182
  * every realm gets the same sentinel.
103
183
  *
104
184
  * @see {@link isUnset} — type guard that narrows a value back to {@link Unset}.
105
- * @see `docs/blank.md` — the conceptual model behind blank-aware fields.
185
+ * @see `docs/recipes/blank-inputs.md` — the conceptual model behind blank-aware fields.
106
186
  */
107
187
  declare const unset: Unset;
108
188
  /**
@@ -140,6 +220,87 @@ type CompleteFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArr
140
220
  * (no intermediate container paths).
141
221
  */
142
222
  type FlatPath<Form, Key extends keyof Form = keyof Form, ForceFullPath extends boolean = false> = ForceFullPath extends true ? CompleteFlatPath<Form, Key> : PartialFlatPath<Form, Key>;
223
+ /**
224
+ * Convert a tuple of path segments to its dotted-string equivalent.
225
+ *
226
+ * `JoinSegments<['cargo', 'items', 0, 'sku']>` → `'cargo.items.0.sku'`
227
+ *
228
+ * Recursion depth is bounded by the tuple length (typically 3–4),
229
+ * not by form depth — the cost does not scale with `FlatPath<Form>`.
230
+ * Template literal types distribute over union members, so segments
231
+ * containing unions like `'pickup' | 'delivery'` propagate through
232
+ * to the joined path's union: `JoinSegments<['pickup' | 'delivery', 'line1']>`
233
+ * → `'pickup.line1' | 'delivery.line1'`. This is what makes
234
+ * tuple-form path APIs work cleanly inside `v-for` over a prefix
235
+ * variable: the joined result is checked against `FlatPath<Form>` /
236
+ * `RegisterFlatPath<Form>` (which already exist), so we don't
237
+ * enumerate a separate tuple-path union.
238
+ */
239
+ type JoinSegments<S extends ReadonlyArray<string | number>, Acc extends string = ''> = S extends readonly [
240
+ infer Head extends string | number,
241
+ ...infer Rest extends ReadonlyArray<string | number>
242
+ ] ? Acc extends '' ? JoinSegments<Rest, `${Head}`> : JoinSegments<Rest, `${Acc}.${Head}`> : Acc;
243
+ /**
244
+ * `true` when `T` is a union (multiple members), `false` when it's a
245
+ * single type. Used to gate non-homomorphic mapped-type forms so
246
+ * single-object types retain their homomorphic `[K in keyof T]`
247
+ * lookup (preserving literal keys instead of widening to an index
248
+ * signature).
249
+ */
250
+ type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
251
+ /**
252
+ * Union of all keys across all members of `T`. For a single object
253
+ * type this equals `keyof T`; for a discriminated union `A | B`, it
254
+ * produces `keyof A | keyof B` (whereas naked `keyof (A | B)` would
255
+ * intersect to common keys only).
256
+ *
257
+ * Paired with `ValueOfUnion` to merge variant key sets in chained
258
+ * metadata proxies (`form.fields`, `form.errors`) so per-variant
259
+ * leaves are addressable through one chained-access shape, regardless
260
+ * of which discriminant is currently active.
261
+ */
262
+ type KeyofUnion<T> = T extends unknown ? keyof T : never;
263
+ /**
264
+ * Value at key `K` across union members of `T`. Members containing
265
+ * `K` contribute `T[K]`; members lacking `K` contribute `undefined`.
266
+ *
267
+ * The resulting union mirrors the runtime semantics of metadata
268
+ * proxies: chained access works at every union member, with the leaf
269
+ * carrying `T | undefined` to reflect that the key is absent in some
270
+ * variants and the runtime returns a stable stub there.
271
+ */
272
+ type ValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : undefined : never;
273
+ /**
274
+ * Apply the discriminated-union "lift" to a value shape (i.e., a
275
+ * shape carrying actual values, not metadata leaves like
276
+ * `FieldState`). Single-object types map homomorphically;
277
+ * discriminated unions of objects merge keys via
278
+ * `KeyofUnion` / `ValueOfUnion` so per-variant fields are reachable
279
+ * through one chained-access shape.
280
+ *
281
+ * Used by `ValuesSurface` to make `form.values.cargo.permitNumber`
282
+ * (oversized-only) typecheck regardless of the active variant —
283
+ * matching the runtime, where plain JS object access on a missing
284
+ * variant key returns `undefined` rather than throwing.
285
+ *
286
+ * Distinct from `FieldStateMapEntry`: that variant carries
287
+ * `FieldState<T>` at the leaf; this one carries the leaf VALUE
288
+ * directly. They share the same union-merging logic but differ in
289
+ * what the recursion bottoms out at.
290
+ *
291
+ * Date / Map / Set / RegExp / function leaves stay opaque (not
292
+ * recursed into) — value reads of those types should preserve the
293
+ * platform shape unchanged.
294
+ */
295
+ type LiftedValueShape<T> = [T] extends [
296
+ string | number | boolean | bigint | symbol | null | undefined
297
+ ] ? T : [T] extends [
298
+ Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown)
299
+ ] ? T : [T] extends [ReadonlyArray<unknown>] ? T : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
300
+ [K in KeyofUnion<T>]: LiftedValueShape<ValueOfUnion<T, K>>;
301
+ } : {
302
+ [K in keyof T]: LiftedValueShape<T[K]>;
303
+ } : T;
143
304
  /**
144
305
  * Recursive `Partial` — every property at every depth is optional.
145
306
  * Used as the parameter type of `defaultValues` and `reset()` so
@@ -154,11 +315,18 @@ type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem>
154
315
  *
155
316
  * `NestedType<{ user: { email: string } }, 'user.email'>` → `string`
156
317
  *
318
+ * On discriminated-union descents (e.g. `cargo` is `A | B | C`), uses
319
+ * `KeyofUnion` / `ValueOfUnion` so per-variant keys resolve to
320
+ * `T | undefined` instead of `never`. This keeps NestedType in lockstep
321
+ * with `FlatPath`: any path FlatPath says is reachable resolves to a
322
+ * useful value type (vs. silently collapsing to `never` because
323
+ * `keyof (A|B|C)` would be the intersection of all variants' keys).
324
+ *
157
325
  * TypeScript caps conditional-type recursion at around 50 levels;
158
326
  * paths deeper than that resolve to `never`. Real form schemas
159
327
  * never reach this depth.
160
328
  */
161
- type NestedType<RootValue, FlattenedPath extends string, FilterOutNullishTypesDuringRecursion extends boolean = true, _RootValue = FilterOutNullishTypesDuringRecursion extends false ? RootValue : NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedType<_RootValue[NumericKey], Rest, FilterOutNullishTypesDuringRecursion> : never : never : Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] : never : never : FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : never;
329
+ type NestedType<RootValue, FlattenedPath extends string, FilterOutNullishTypesDuringRecursion extends boolean = true, _RootValue = FilterOutNullishTypesDuringRecursion extends false ? RootValue : NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends KeyofUnion<_RootValue> ? NestedType<ValueOfUnion<_RootValue, Key>, Rest, FilterOutNullishTypesDuringRecursion> : Key extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? NestedType<ValueOfUnion<_RootValue, NumericKey>, Rest, FilterOutNullishTypesDuringRecursion> : never : never : Key extends KeyofUnion<_RootValue> ? NestedType<ValueOfUnion<_RootValue, Key>, Rest, FilterOutNullishTypesDuringRecursion> : never : FlattenedPath extends `${number}` ? FlattenedPath extends KeyofUnion<_RootValue> ? ValueOfUnion<_RootValue, FlattenedPath> : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? ValueOfUnion<_RootValue, NumericKey> : never : never : FlattenedPath extends KeyofUnion<_RootValue> ? ValueOfUnion<_RootValue, FlattenedPath> : never;
162
330
  type Primitive = string | number | boolean | symbol | bigint | null | undefined;
163
331
  /**
164
332
  * Distinguish a tuple from a regular array.
@@ -174,13 +342,15 @@ type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false
174
342
  * Path-resolved type for read-side APIs. Like `NestedType`, but once
175
343
  * the walk crosses an array index segment the resulting type is
176
344
  * tagged `| undefined` (the runtime can return undefined for
177
- * out-of-bounds reads).
345
+ * out-of-bounds reads). Discriminated-union descents follow the same
346
+ * `KeyofUnion`/`ValueOfUnion` rule as `NestedType` — per-variant
347
+ * keys resolve to `T | undefined`, agreeing with `FlatPath`.
178
348
  *
179
349
  * Used by `form.values.<path>` reads, `form.toRef(path)`, and
180
350
  * `register(path).innerRef` so the compile-time type honours the
181
351
  * runtime possibility of a missing array position.
182
352
  */
183
- type NestedReadType<RootValue, FlattenedPath extends string, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, true> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedReadType<_RootValue[NumericKey], Rest, true> : never : never : Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] | undefined : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] | undefined : never : never : FlattenedPath extends keyof _RootValue ? _Tainted extends true ? _RootValue[FlattenedPath] | undefined : _RootValue[FlattenedPath] : never;
353
+ type NestedReadType<RootValue, FlattenedPath extends string, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends KeyofUnion<_RootValue> ? NestedReadType<ValueOfUnion<_RootValue, Key>, Rest, true> : Key extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? NestedReadType<ValueOfUnion<_RootValue, NumericKey>, Rest, true> : never : never : Key extends KeyofUnion<_RootValue> ? NestedReadType<ValueOfUnion<_RootValue, Key>, Rest, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends KeyofUnion<_RootValue> ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? ValueOfUnion<_RootValue, NumericKey> | undefined : never : never : FlattenedPath extends KeyofUnion<_RootValue> ? _Tainted extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : never;
184
354
  /**
185
355
  * Filter FlatPath<Form> down to the subset of paths whose resolved leaf
186
356
  * is an array. Used by the typed field-array helpers (append / remove /
@@ -485,8 +655,8 @@ type AbstractSchema<Form, GetValueFormType> = {
485
655
  * length and reuses one element default for every position.
486
656
  * - `undefined` → the path doesn't resolve to an array OR the
487
657
  * adapter can't determine the shape. The runtime falls back to
488
- * the legacy probe loop in this case (defensive — every built-in
489
- * adapter returns `number` or `null`).
658
+ * a probe loop in this case (defensive — every built-in adapter
659
+ * returns `number` or `null`).
490
660
  *
491
661
  * Wrappers (optional / nullable / default / readonly / catch /
492
662
  * pipe / lazy) are peeled transparently before the type check, so
@@ -572,7 +742,7 @@ type AbstractSchema<Form, GetValueFormType> = {
572
742
  *
573
743
  * The leaf-aware branching is what kills the FIELD_STATE_KEYS
574
744
  * shadowing problem: reserved leaf-prop names (`dirty`, `errors`,
575
- * `isValid`, …) inject only at the FieldStateView terminal, not at
745
+ * `valid`, …) inject only at the FieldState terminal, not at
576
746
  * every depth. A schema field literally named `dirty` at depth ≥ 2
577
747
  * stays reachable as a sub-proxy or leaf in its own right.
578
748
  *
@@ -670,6 +840,34 @@ type AbstractSchema<Form, GetValueFormType> = {
670
840
  * without this hook.
671
841
  */
672
842
  getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
843
+ /**
844
+ * Return the resolved field metadata for the schema node at `path`
845
+ * — label, description, placeholder, plus the full registered
846
+ * payload as `meta` for consumer-augmented keys. Reads through the
847
+ * adapter's metadata mechanism (Zod 4: `z.registry()`; Zod 3:
848
+ * a WeakMap shim) and applies these one-way fallbacks:
849
+ *
850
+ * - `label`: registry payload → `humanize(lastSegment)`
851
+ * - `description`: registry payload → `schema.description`
852
+ * (`.describe()` value) → `undefined`
853
+ * - `placeholder`: registry payload → `undefined`
854
+ * - `meta`: registry payload (frozen) — empty object when
855
+ * nothing was registered
856
+ *
857
+ * `path` is the canonical `Segment[]`. The empty path resolves to
858
+ * the root schema's metadata. Multiple candidates (DU branches)
859
+ * resolve against the first candidate to match the existing
860
+ * first-success precedent in `getDefaultAtPath` /
861
+ * `validateAtPath` — schema authors register on the union root
862
+ * for shared metadata, on individual branches for variant-
863
+ * specific metadata.
864
+ *
865
+ * Optional. The runtime treats a missing implementation as a
866
+ * stub that returns `EMPTY_RESOLVED_FIELD_META` — so adapters
867
+ * that don't model field metadata yet can omit it; consumers
868
+ * see humanized fallbacks for `label`, undefined elsewhere.
869
+ */
870
+ getFieldMetaAtPath?(path: Path): ResolvedFieldMeta;
673
871
  /**
674
872
  * Return `true` if `validateAtPath` MAY have to run asynchronously
675
873
  * to surface every error this schema can produce. The runtime uses
@@ -686,6 +884,12 @@ type AbstractSchema<Form, GetValueFormType> = {
686
884
  * don't yet support detection — can omit it; async-only errors
687
885
  * then fall back to firing on first user mutation, matching the
688
886
  * pre-detection behavior. Detection is best-effort.
887
+ *
888
+ * For per-path queries, compose with `getSchemasAtPath(path)`:
889
+ * each candidate sub-schema exposes its own
890
+ * `needsAsyncValidation`, so a caller asking "does the cargo
891
+ * subtree contain async work?" can union the per-candidate
892
+ * answers without a separate top-level overload.
689
893
  */
690
894
  needsAsyncValidation?(): boolean;
691
895
  };
@@ -1288,14 +1492,18 @@ type SubmitHandler = (event?: Event) => Promise<void>;
1288
1492
  */
1289
1493
  type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
1290
1494
  /**
1291
- * Per-leaf metadata tracked alongside a field's value. Read from
1292
- * `FieldState.meta` when type-narrowing through that surface.
1495
+ * Per-leaf internal tracker record. Distinct from `FieldState.meta`
1496
+ * (which surfaces as `Readonly<FieldMetaPayload>` the registry-
1497
+ * attached label / description / placeholder payload). Surfaced for
1498
+ * custom-adapter authors threading metadata through their own
1499
+ * pipelines; most consumers don't reach for it directly — the
1500
+ * matching fields appear with friendlier shape on `FieldState`.
1293
1501
  *
1294
1502
  * - `updatedAt` — ISO timestamp of the most recent write at this path,
1295
1503
  * or `null` if the field has never been written.
1296
1504
  * - `rawValue` — the value as it arrived (before any transform);
1297
1505
  * useful for distinguishing parse-coerced reads from raw user input.
1298
- * - `isConnected` — whether at least one DOM element bound to this
1506
+ * - `connected` — whether at least one DOM element bound to this
1299
1507
  * path is currently mounted. Flips to `false` when every binding
1300
1508
  * unmounts.
1301
1509
  * - `formKey` — identifier of the form this metadata belongs to.
@@ -1307,7 +1515,7 @@ type MetaTrackerValue = {
1307
1515
  /** Value as it arrived, before any transforms. */
1308
1516
  rawValue: unknown;
1309
1517
  /** `true` while at least one binding to this path is currently mounted. */
1310
- isConnected: boolean;
1518
+ connected: boolean;
1311
1519
  /** Form this metadata belongs to. */
1312
1520
  formKey: FormKey;
1313
1521
  /** Dotted-string path to this leaf. */
@@ -1336,7 +1544,7 @@ type MetaTrackerValue = {
1336
1544
  * want pre-error introspection ("the user hasn't decided yet"
1337
1545
  * indicator, "review unanswered fields" hint).
1338
1546
  *
1339
- * See `docs/blank.md` for the full conceptual model.
1547
+ * See `docs/recipes/blank-inputs.md` for the full conceptual model.
1340
1548
  */
1341
1549
  blank: boolean;
1342
1550
  };
@@ -1500,11 +1708,17 @@ type RegisterOptions = {
1500
1708
  *
1501
1709
  * Or read `innerRef` directly when integrating with custom components.
1502
1710
  *
1503
- * The remaining fields support advanced bindings (custom assigners,
1504
- * SSR optimistic marking, persistence opt-ins). Most consumers only
1505
- * touch `innerRef`.
1711
+ * The returned value is a `shallowReadonly` reactive proxy: top-level
1712
+ * reads (`rv.path`, `rv.formKey`, `rv.persist`, ) track in reactive
1713
+ * scopes, mutations are blocked, and inner refs (`innerRef`,
1714
+ * `displayValue`) keep their `Ref` shape.
1715
+ *
1716
+ * `path`, `formKey`, and `formInstanceId` are the wrapper-component
1717
+ * primitives — a generic component using `useRegister()` can derive
1718
+ * field state and form identity from them without re-threading props
1719
+ * from the parent.
1506
1720
  */
1507
- type RegisterValue<Value = unknown> = {
1721
+ type RegisterValue<Value = unknown> = Readonly<{
1508
1722
  /**
1509
1723
  * Live, read-only reactive value at this path. Watch it to drive
1510
1724
  * UI that depends on the field's current value.
@@ -1528,6 +1742,44 @@ type RegisterValue<Value = unknown> = {
1528
1742
  * directives signal whether the write should be persisted.
1529
1743
  */
1530
1744
  setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
1745
+ /**
1746
+ * Canonical, JSON-encoded path key for this binding (e.g.
1747
+ * `'["items",0,"name"]'`). Useful for stable Map / Set keys, log
1748
+ * messages, and equality checks against another `RegisterValue`'s
1749
+ * path. Treat as opaque — for `form.fields(...)` / `form.values(...)`
1750
+ * lookups inside wrapper components, use `segments` instead.
1751
+ */
1752
+ path: PathKey;
1753
+ /**
1754
+ * Structured path segments for this binding (e.g.
1755
+ * `['items', 0, 'name']`). The consumer-friendly form for
1756
+ * `form.fields(...)` / `form.values(...)` lookups in generic
1757
+ * wrapper components:
1758
+ *
1759
+ * ```ts
1760
+ * const rv = useRegister()
1761
+ * const form = injectForm()
1762
+ * const field = computed(() => form.fields(rv.value?.segments ?? []))
1763
+ * ```
1764
+ *
1765
+ * Frozen at runtime so wrapper components can read it without
1766
+ * defensive copying.
1767
+ */
1768
+ segments: Path;
1769
+ /**
1770
+ * The form's user-supplied (or auto-allocated) `key`, mirroring
1771
+ * `form.key` on the public form API. Useful in wrapper components
1772
+ * that target a specific form by key without prop-drilling.
1773
+ */
1774
+ formKey: string;
1775
+ /**
1776
+ * Per-mount runtime identifier for the form instance. Stable across
1777
+ * the form's lifetime. Used by the directive to scope element
1778
+ * registrations to a single mount and exposed here for wrapper
1779
+ * components that need to disambiguate sibling forms with the same
1780
+ * `key`.
1781
+ */
1782
+ formInstanceId: string;
1531
1783
  /**
1532
1784
  * Read-only, string-form view of the field's current value — what
1533
1785
  * the compile-time `:value` injection reads on every input /
@@ -1540,7 +1792,7 @@ type RegisterValue<Value = unknown> = {
1540
1792
  * patching `el.value` back to `'0'` (the slim default).
1541
1793
  */
1542
1794
  displayValue: Readonly<Ref<string>>;
1543
- };
1795
+ }>;
1544
1796
  /**
1545
1797
  * Custom assigner installed on an element via the directive's
1546
1798
  * `[assignKey]` slot OR an `@update:registerValue` listener. Called
@@ -1738,78 +1990,21 @@ type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
1738
1990
  * array elements as possibly-undefined to reflect runtime reality.
1739
1991
  */
1740
1992
  type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
1741
- type DeepFlatten<T> = T extends object ? {
1742
- [K in keyof T]: DeepFlatten<T[K]>;
1743
- } : T;
1744
- /**
1745
- * Focus / blur / touched flags for a registered field.
1746
- *
1747
- * - `focused` — `true` while the user is interacting with the field;
1748
- * `false` after blur. `null` until the field has ever been focused.
1749
- * - `blurred` — `true` after the field has lost focus at least once.
1750
- * `null` before any blur event.
1751
- * - `touched` — flips to `true` on the first blur after a focus and
1752
- * stays `true` thereafter. Useful for "show errors only after the
1753
- * user has interacted" UX.
1754
- */
1755
- type DOMFieldState = {
1756
- /** `true` while focused; `false` after blur; `null` before first focus. */
1757
- focused: boolean | null;
1758
- /** `true` once the field has lost focus at least once; `null` before. */
1759
- blurred: boolean | null;
1760
- /** Flips to `true` on the first blur after a focus and stays there. */
1761
- touched: boolean | null;
1762
- };
1763
1993
  /**
1764
- * Richer per-field type kept for type-level utility code (e.g.
1765
- * higher-order helpers that pass field state around). Carries
1766
- * `currentValue` / `originalValue` / `previousValue` (typed `Value`),
1767
- * the same flag set as `FieldStateLeaf`, plus `meta`
1768
- * (`MetaTrackerValue`).
1994
+ * Per-field reactive shape returned by `form.fields.<leaf-path>` and
1995
+ * `form.fields(path)`. Slim, readonly across the board. The unified
1996
+ * shape replaces the older split between `FieldState` /
1997
+ * `FieldStateBranch`: one type lives at every path, with aggregations
1998
+ * rolled up at containers.
1769
1999
  *
1770
- * `form.fields.<path>` returns the slim `FieldStateLeaf` shape;
1771
- * pick `FieldState<Value>` for code that needs `meta` or the historical
1772
- * `previousValue` slot.
2000
+ * Leaf-aware: this shape only injects these keys at LEAF paths via
2001
+ * dot-access. At container paths the proxy descends without
2002
+ * injecting, so a schema field literally named `dirty` at depth 2+
2003
+ * stays reachable as a descent target — no shadowing. Container
2004
+ * call-form (`form.fields('address')`) returns a `FieldState`
2005
+ * surface where the keys are aggregations of the descendant leaves.
1773
2006
  */
1774
- type FieldState<Value = unknown> = DeepFlatten<DOMFieldState & {
1775
- /** Per-field metadata (timestamps, raw value, connection state). */
1776
- meta: MetaTrackerValue;
1777
- /**
1778
- * Validation errors for this path. Populated automatically by
1779
- * `handleSubmit` and per-field validation; also writable via
1780
- * `setFieldErrors` / `addFieldErrors`. Empty when valid — safe
1781
- * to read without a null check.
1782
- */
1783
- errors: ValidationError[];
1784
- /** The value the field was initialised with. */
1785
- originalValue: Value;
1786
- /** The value before the most recent write. */
1787
- previousValue: Value;
1788
- /** The current value at this path. */
1789
- currentValue: Value;
1790
- /** `true` when `currentValue` matches `originalValue`. */
1791
- pristine: boolean;
1792
- /** `true` when `currentValue` differs from `originalValue`. */
1793
- dirty: boolean;
1794
- /**
1795
- * `true` when this field is **blank** — the side-channel for
1796
- * storage / display divergence (numeric leaves where storage
1797
- * holds `0` / `0n` but the DOM shows `''`, plus any primitive
1798
- * leaf the consumer explicitly opted in via `unset`). Surfaces
1799
- * both as a top-level field here AND via `meta.blank` (the meta
1800
- * projection mirrors the same value). See `docs/blank.md`.
1801
- */
1802
- blank: boolean;
1803
- }>;
1804
- /**
1805
- * Per-field reactive shape returned by `form.fields.<leaf-path>`.
1806
- * Slim, readonly across the board. Leaf-aware: this shape only
1807
- * appears at LEAF paths (primitives, dates). At container paths
1808
- * the proxy descends without injecting these keys, so a schema
1809
- * field literally named `dirty` at depth 2+ stays reachable as a
1810
- * descent target — no shadowing.
1811
- */
1812
- type FieldStateLeaf<Value = unknown> = {
2007
+ type FieldState<Value = unknown> = {
1813
2008
  readonly value: Value;
1814
2009
  readonly original: Value;
1815
2010
  readonly pristine: boolean;
@@ -1817,22 +2012,121 @@ type FieldStateLeaf<Value = unknown> = {
1817
2012
  readonly focused: boolean | null;
1818
2013
  readonly blurred: boolean | null;
1819
2014
  readonly touched: boolean | null;
1820
- readonly isConnected: boolean;
2015
+ readonly connected: boolean;
2016
+ /**
2017
+ * The first DOM element bound to this path via `v-register`, or
2018
+ * `null` when none is registered (initial mount, post-unmount,
2019
+ * SSR). "First" means first by registration order. Reach for it
2020
+ * when you need to call a native DOM method on a field's input —
2021
+ * `focus()`, `scrollIntoView()`, `select()`, `setSelectionRange()`,
2022
+ * etc. — without the library having to verb every imperative:
2023
+ *
2024
+ * ```ts
2025
+ * form.fields.email.element?.focus()
2026
+ * form.fields.email.element?.scrollIntoView({ block: 'center' })
2027
+ * ```
2028
+ *
2029
+ * For paths with multiple bindings (input syncing, mirrored
2030
+ * shadow inputs), prefer `elements` and pick the right target
2031
+ * yourself. Reactive: register / deregister triggers
2032
+ * re-evaluation.
2033
+ */
2034
+ readonly element: HTMLElement | null;
2035
+ /**
2036
+ * Every DOM element currently bound to this path via `v-register`,
2037
+ * in registration order. Empty array when none is registered.
2038
+ * Two bindings to the same path are intentional — input syncing,
2039
+ * mirrored shadow inputs:
2040
+ *
2041
+ * ```ts
2042
+ * for (const el of form.fields.email.elements) el.blur()
2043
+ * ```
2044
+ *
2045
+ * For the common single-binding case, reach for `element` — sugar
2046
+ * over `elements[0] ?? null`.
2047
+ */
2048
+ readonly elements: readonly HTMLElement[];
1821
2049
  readonly updatedAt: string | null;
1822
2050
  readonly errors: readonly ValidationError[];
2051
+ /**
2052
+ * `true` while a per-field validation run is in flight at this path.
2053
+ * Reflects field-level debounced runs (`validate-on-change`) and
2054
+ * cross-field re-validations targeting this path. Whole-form
2055
+ * `validate()` / `validateAsync()` calls drive `form.meta.validating`
2056
+ * only — they don't flip per-field flags.
2057
+ *
2058
+ * Per-field analogue of `form.meta.validating`. Use for a tight
2059
+ * "Checking…" indicator next to a single async-validated input
2060
+ * without commandeering the whole-form spinner.
2061
+ */
2062
+ readonly validating: boolean;
2063
+ /**
2064
+ * `true` when this field has no errors AND no per-field validation
2065
+ * is in flight (`errors.length === 0 && !validating`). Confidence
2066
+ * that "we've checked, and we have no problems right now." Use for
2067
+ * green-checkmark / `aria-invalid` UX.
2068
+ */
2069
+ readonly valid: boolean;
1823
2070
  readonly path: ReadonlyArray<string | number>;
1824
2071
  readonly blank: boolean;
2072
+ /**
2073
+ * Presentational label for this field. Resolves through the
2074
+ * adapter's metadata mechanism — Zod 4's `z.registry()` (typed
2075
+ * payload via `schema.register(fieldMeta, {...})` or the
2076
+ * `withMeta()` helper); Zod 3's WeakMap shim — and falls back to
2077
+ * a humanized form of the path's last segment when nothing has
2078
+ * been registered. Always a string.
2079
+ *
2080
+ * ```ts
2081
+ * z.string().register(fieldMeta, { label: 'Reference' })
2082
+ * // template: <label>{{ form.fields.reference.label }}</label>
2083
+ * ```
2084
+ *
2085
+ * Numeric segments (array indices) collapse to the empty string;
2086
+ * consumers wanting "Item 3" substitute their own format.
2087
+ */
2088
+ readonly label: string;
2089
+ /**
2090
+ * Helper-text description for this field. Reads from the
2091
+ * registered `description` first; falls back to the schema's own
2092
+ * `.describe('...')` value (both Zod 3 and Zod 4 expose that as
2093
+ * `schema.description`); `undefined` when neither is set.
2094
+ *
2095
+ * Useful for `aria-describedby`-linked help text. Distinct from
2096
+ * `label` — descriptions are longer prose, labels are short
2097
+ * presentational nouns.
2098
+ */
2099
+ readonly description: string | undefined;
2100
+ /**
2101
+ * Placeholder hint for input affordance. Reads from the
2102
+ * registered `placeholder`; `undefined` otherwise.
2103
+ */
2104
+ readonly placeholder: string | undefined;
2105
+ /**
2106
+ * Full registered metadata payload, frozen — empty object when
2107
+ * nothing has been registered. Use as an escape hatch for
2108
+ * consumer-augmented keys (declared via TypeScript module
2109
+ * augmentation on `FieldMetaPayload`):
2110
+ *
2111
+ * ```ts
2112
+ * declare module 'attaform/zod' {
2113
+ * interface FieldMetaPayload { tooltip?: string }
2114
+ * }
2115
+ * // template: {{ form.fields.email.meta.tooltip }}
2116
+ * ```
2117
+ */
2118
+ readonly meta: Readonly<FieldMetaPayload>;
1825
2119
  };
1826
2120
  /**
1827
2121
  * Recursive type behind `form.fields`. Leaf-aware branching: at
1828
2122
  * primitive paths (string, number, boolean, bigint, Date, …) the
1829
- * proxy returns a `FieldStateLeaf`; at container paths (object,
2123
+ * proxy returns a `FieldState`; at container paths (object,
1830
2124
  * array, …) the proxy descends without injecting leaf-keys.
1831
2125
  *
1832
2126
  * Field-name collisions at depth 2+ resolve unambiguously: a schema
1833
2127
  * field literally named `dirty` at depth 2 is reachable as a
1834
2128
  * descent target (`form.fields.address.dirty` returns the
1835
- * FieldStateView for `address.dirty`). Reading `dirty` AT the
2129
+ * FieldState for `address.dirty`). Reading `dirty` AT the
1836
2130
  * leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's
1837
2131
  * own dirty boolean — path-segment and leaf-prop occupy different
1838
2132
  * proxy depths.
@@ -1842,22 +2136,43 @@ type FieldStateLeaf<Value = unknown> = {
1842
2136
  * "T extends primitive". The two stay in sync for typical schemas;
1843
2137
  * exotic adapter-defined leaf kinds (custom `Date`-like) may need
1844
2138
  * a runtime check (the runtime is authoritative).
1845
- */
1846
- type FieldStateMapEntry<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? FieldStateLeaf<T> : T extends ReadonlyArray<infer U> ? {
2139
+ *
2140
+ * The mapped type strips optional flags (`-?:`) because the field-
2141
+ * state surface always exposes a record per known leaf, regardless
2142
+ * of whether the schema field is declared `.optional()`. Optional
2143
+ * schemas mean the VALUE can be undefined — `FieldState<string |
2144
+ * undefined>` carries that — but the FieldState wrapper itself
2145
+ * always exists. Without the strip, `form.fields.notes` (where
2146
+ * `notes?: string`) would type as `FieldState<...> | undefined`,
2147
+ * forcing consumers to optional-chain through every reactive read.
2148
+ *
2149
+ * For discriminated-union containers the object branch uses
2150
+ * `[T] extends [object]` (non-distributive) plus
2151
+ * `KeyofUnion`/`ValueOfUnion` to merge variant key sets — so
2152
+ * `form.fields.cargo.tempMinC` (refrigerated-only) is reachable
2153
+ * regardless of the active variant, with the leaf typed as
2154
+ * `FieldState<number | undefined>`. Matches the runtime's stub
2155
+ * `FieldState` for inactive-variant paths.
2156
+ */
2157
+ type FieldStateMapEntry<T> = [T] extends [
2158
+ string | number | boolean | bigint | symbol | null | undefined | Date
2159
+ ] ? FieldState<T> : [T] extends [ReadonlyArray<infer U>] ? {
1847
2160
  readonly [K: number]: FieldStateMapEntry<U>;
1848
- } : T extends object ? {
1849
- readonly [K in keyof T]: FieldStateMapEntry<T[K]>;
1850
- } : FieldStateLeaf<T>;
2161
+ } : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
2162
+ readonly [K in KeyofUnion<T>]-?: FieldStateMapEntry<ValueOfUnion<T, K>>;
2163
+ } : {
2164
+ readonly [K in keyof T]-?: FieldStateMapEntry<T[K]>;
2165
+ } : FieldState<T>;
1851
2166
  /**
1852
2167
  * Type of `form.fields` — leaf-aware drillable callable Proxy. At
1853
- * a leaf path the proxy resolves to a `FieldStateLeaf<Value>`; at
2168
+ * a leaf path the proxy resolves to a `FieldState<Value>`; at
1854
2169
  * a container path it returns a sub-proxy you can keep drilling.
1855
2170
  *
1856
2171
  * Augmented with the callable signatures so dot-access and function-
1857
2172
  * call coexist on the same identifier:
1858
2173
  *
1859
2174
  * ```ts
1860
- * form.fields.email.value // string (leaf-prop on FieldStateView)
2175
+ * form.fields.email.value // string (leaf-prop on FieldState)
1861
2176
  * form.fields('email').value // function-call (dynamic / programmatic)
1862
2177
  * form.fields(['users', 0, 'name']) // path-array form
1863
2178
  * form.fields() // root proxy
@@ -1868,12 +2183,38 @@ type FieldStateMapEntry<T> = T extends string | number | boolean | bigint | symb
1868
2183
  * string as a single key. Use chained dot/bracket or the callable
1869
2184
  * form.
1870
2185
  */
1871
- type FieldStateMap<Form extends GenericForm> = {
1872
- readonly [K in keyof Form]: FieldStateMapEntry<Form[K]>;
1873
- } & {
1874
- (path: string): unknown;
1875
- (path: ReadonlyArray<string | number>): unknown;
1876
- (): FieldStateMap<Form>;
2186
+ type FieldStateMap<Form extends GenericForm> = ([IsUnion<Form>] extends [true] ? {
2187
+ readonly [K in KeyofUnion<Form>]-?: FieldStateMapEntry<ValueOfUnion<Form, K>>;
2188
+ } : {
2189
+ readonly [K in keyof Form]-?: FieldStateMapEntry<Form[K]>;
2190
+ }) & {
2191
+ /**
2192
+ * Dotted-string fallback for dynamic paths. Returns
2193
+ * `FieldState<unknown>` — the runtime always lands on a FieldState
2194
+ * terminal at any depth (leaf or container). Cast to
2195
+ * `FieldState<TypedValue>` when the caller knows the leaf type.
2196
+ */
2197
+ (path: string): FieldState<unknown>;
2198
+ /**
2199
+ * Tuple-segment form. Returns the typed `FieldStateMapEntry` for
2200
+ * the resolved path when the tuple resolves to a known path.
2201
+ * Equivalent to `form.fields[a][b][...]` but useful when the path
2202
+ * is built from variables.
2203
+ */
2204
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): FieldStateMapEntry<NestedType<Form, JoinSegments<S>>>;
2205
+ /**
2206
+ * Dynamic-array fallback for callers passing `Path`-typed (runtime)
2207
+ * segment arrays — e.g. forwarding `RegisterValue.segments` to
2208
+ * resolve a field view. Returns `FieldState<unknown>`; cast when
2209
+ * the value type is known.
2210
+ */
2211
+ (segments: ReadonlyArray<string | number>): FieldState<unknown>;
2212
+ /**
2213
+ * No-arg call returns the root FieldState — same as
2214
+ * `form.fields([])`. Aggregates over the whole form (one
2215
+ * conjunction over every active-variant leaf).
2216
+ */
2217
+ (): FieldState<Form>;
1877
2218
  };
1878
2219
  /**
1879
2220
  * Untyped error map keyed by dotted-string path. The same data
@@ -1919,12 +2260,27 @@ type FormErrorRecord = Record<string, ValidationError[]>;
1919
2260
  */
1920
2261
  type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
1921
2262
  (path: string): readonly ValidationError[] | undefined;
1922
- (path: ReadonlyArray<string | number>): readonly ValidationError[] | undefined;
1923
- (): FormErrorsSurface<Form>;
2263
+ /**
2264
+ * Tuple-segment form. Validated against `FlatPath<Form>` so literal
2265
+ * tuples that don't resolve to a known path fail at the call site.
2266
+ * Dynamic `Path`-typed inputs hit the untyped fallback overload below.
2267
+ */
2268
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): readonly ValidationError[] | undefined;
2269
+ (segments: ReadonlyArray<string | number>): readonly ValidationError[] | undefined;
2270
+ /**
2271
+ * No-arg call returns the form-level error aggregate — same as
2272
+ * `form.errors([])` and `form.meta.errors`. `undefined` when the
2273
+ * form has no errors; readonly array otherwise.
2274
+ */
2275
+ (): readonly ValidationError[] | undefined;
1924
2276
  };
1925
- type ErrorsProxyShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? readonly ValidationError[] | undefined : T extends ReadonlyArray<infer U> ? {
2277
+ type ErrorsProxyShape<T> = [T] extends [
2278
+ string | number | boolean | bigint | symbol | null | undefined | Date
2279
+ ] ? readonly ValidationError[] | undefined : [T] extends [ReadonlyArray<infer U>] ? {
1926
2280
  readonly [K: number]: ErrorsProxyShape<U>;
1927
- } : T extends object ? {
2281
+ } : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
2282
+ readonly [K in KeyofUnion<T>]: ErrorsProxyShape<ValueOfUnion<T, K>>;
2283
+ } : {
1928
2284
  readonly [K in keyof T]: ErrorsProxyShape<T[K]>;
1929
2285
  } : readonly ValidationError[] | undefined;
1930
2286
  /**
@@ -1948,8 +2304,18 @@ type ErrorsProxyShape<T> = T extends string | number | boolean | bigint | symbol
1948
2304
  * intentionally NOT supported — JS object semantics treat the dotted
1949
2305
  * string as a single key. Use chained dot/bracket or the callable
1950
2306
  * form.
1951
- */
1952
- type ValuesSurface<F> = Readonly<F> & {
2307
+ *
2308
+ * The chained shape applies the discriminated-union lift via
2309
+ * `LiftedValueShape<F>` so per-variant keys are reachable without
2310
+ * narrowing first (e.g. `form.values.cargo.permitNumber` types as
2311
+ * `string | undefined` regardless of which cargo variant is active —
2312
+ * matching the runtime, where plain JS object access on a missing
2313
+ * variant key returns `undefined`). The strict-variant shape is
2314
+ * still required at the WRITE side: `setValue` and `defaultValues`
2315
+ * use the un-lifted `WriteShape` so consumers can't accidentally
2316
+ * hand the form a partial / cross-variant object.
2317
+ */
2318
+ type ValuesSurface<F> = Readonly<LiftedValueShape<F>> & {
1953
2319
  (path: string): unknown;
1954
2320
  (path: ReadonlyArray<string | number>): unknown;
1955
2321
  (): Readonly<F>;
@@ -2021,13 +2387,13 @@ type ApiErrorEnvelope = {
2021
2387
  * the reactive object:
2022
2388
  *
2023
2389
  * ```vue
2024
- * <button :disabled="form.meta.isSubmitting">Save</button>
2390
+ * <button :disabled="form.meta.submitting">Save</button>
2025
2391
  * ```
2026
2392
  *
2027
2393
  * Watch a single field via the getter form:
2028
2394
  *
2029
2395
  * ```ts
2030
- * watch(() => form.meta.isSubmitting, (value) => …)
2396
+ * watch(() => form.meta.submitting, (value) => …)
2031
2397
  * ```
2032
2398
  *
2033
2399
  * Per-field state (touched, dirty, errors) lives behind
@@ -2038,33 +2404,13 @@ type ApiErrorEnvelope = {
2038
2404
  * the current values; use `toRefs()` if you need reactive handles
2039
2405
  * to individual fields.
2040
2406
  */
2041
- interface FormMeta {
2042
- /**
2043
- * `true` when any field's current value differs from its initial
2044
- * value. `false` for a pristine form and for one where every change
2045
- * has been undone. Restore the pristine baseline via `reset()`.
2046
- *
2047
- * Note: object/array leaves are compared by reference, so replacing
2048
- * an array with an equal copy still reads as dirty.
2049
- */
2050
- readonly isDirty: boolean;
2051
- /**
2052
- * `true` when the form currently has no validation errors. Flips
2053
- * with every `validate()` / `handleSubmit` outcome.
2054
- */
2055
- readonly isValid: boolean;
2407
+ type FormMeta<F = unknown> = FieldState<F> & {
2056
2408
  /**
2057
2409
  * `true` while a `handleSubmit`-produced submit handler is running.
2058
2410
  * Covers both the validation phase and your async submit callback.
2059
2411
  * Useful for disabling the submit button.
2060
2412
  */
2061
- readonly isSubmitting: boolean;
2062
- /**
2063
- * `true` while any validation run is in flight (the reactive
2064
- * `validate()` re-run, an imperative `validateAsync()`, or the
2065
- * pre-submit validation inside `handleSubmit`).
2066
- */
2067
- readonly isValidating: boolean;
2413
+ readonly submitting: boolean;
2068
2414
  /**
2069
2415
  * How many times the submit handler has been invoked, regardless of
2070
2416
  * outcome (validation failure, callback success, callback throw).
@@ -2091,33 +2437,6 @@ interface FormMeta {
2091
2437
  * `canUndo` / `canRedo` instead.
2092
2438
  */
2093
2439
  readonly historySize: number;
2094
- /**
2095
- * Flat aggregate of EVERY validation error in the form — schema-
2096
- * keyed entries, form-level errors (path: []), unmapped server
2097
- * errors (paths not in `FlatPath`), and cross-field-refine errors
2098
- * (paths at containers). Reads as English: "the form's errors."
2099
- *
2100
- * Unlike `form.errors.<path>` (per-leaf, active-path-filtered),
2101
- * `form.meta.errors` is unfiltered — inactive-variant errors stay
2102
- * in the array. Consumers who want only addressable errors filter
2103
- * the array themselves (`form.meta.errors.filter(e => …)`).
2104
- *
2105
- * Common patterns:
2106
- *
2107
- * ```vue
2108
- * <p v-if="form.meta.errors.length">{{ form.meta.errors.length }} issue(s)</p>
2109
- * <ul>
2110
- * <li v-for="err in form.meta.errors" :key="err.path.join('.')">
2111
- * {{ err.path.join('.') || 'form' }}: {{ err.message }}
2112
- * </li>
2113
- * </ul>
2114
- * ```
2115
- *
2116
- * The array re-allocates on any underlying store change (schema /
2117
- * derived-blank / user); reactivity propagates through the standard
2118
- * Vue computed graph.
2119
- */
2120
- readonly errors: readonly ValidationError[];
2121
2440
  /**
2122
2441
  * Per-`useForm()`-call identity. Stable for the lifetime of one
2123
2442
  * `useForm()` call; new on every fresh mount. Orthogonal to
@@ -2141,7 +2460,7 @@ interface FormMeta {
2141
2460
  * (drag-reorder, etc.) — stable identity per useForm() call.
2142
2461
  */
2143
2462
  readonly instanceId: string;
2144
- }
2463
+ };
2145
2464
  /**
2146
2465
  * The object returned by `useForm`. Holds every reactive ref, write
2147
2466
  * helper, and lifecycle method bound to one form.
@@ -2154,7 +2473,7 @@ interface FormMeta {
2154
2473
  * form.errors.email // ValidationError[] | undefined
2155
2474
  * form.setValue('email', 'a@b.c')
2156
2475
  * form.handleSubmit(onSubmit) // returns a submit handler
2157
- * form.meta.isSubmitting // form-level reactive flag
2476
+ * form.meta.submitting // form-level reactive flag
2158
2477
  * ```
2159
2478
  */
2160
2479
  type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form> = {
@@ -2212,16 +2531,16 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2212
2531
  * ```
2213
2532
  *
2214
2533
  * The same proxy supports descent at every level — `address` reads
2215
- * the FieldStateLeaf for the address object, and `address.city`
2534
+ * the FieldState for the address object, and `address.city`
2216
2535
  * descends into the nested leaf.
2217
2536
  *
2218
2537
  * Leaf values follow the slim WriteShape contract: enum-typed leaves
2219
2538
  * widen to their primitive supertype. The errors array, dirty flag,
2220
2539
  * focus state, etc. are unaffected.
2221
2540
  *
2222
- * Shadowing: at depth 2+, FieldStateLeaf keys (`dirty`, `touched`,
2541
+ * Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
2223
2542
  * `errors`, `blank`, `focused`, `blurred`, `value`,
2224
- * `original`, `pristine`, `isConnected`, `updatedAt`, `path`) win
2543
+ * `original`, `pristine`, `connected`, `updatedAt`, `path`) win
2225
2544
  * over schema field names. Top-level fields are NOT shadowed.
2226
2545
  * Document edge case; rename the offending schema field if the
2227
2546
  * collision matters.
@@ -2283,6 +2602,13 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2283
2602
  * empty; submit raises "No value supplied" for required schemas).
2284
2603
  */
2285
2604
  <Path extends FlatPath<Form>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, Path>>, NonNullable<WriteShape<NestedType<Form, Path>>>>>(path: Path, value: Value): boolean;
2605
+ /**
2606
+ * Tuple-segment form. Equivalent to the dotted-string overload —
2607
+ * useful when paths are built from variables or arrays:
2608
+ * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
2609
+ * type is exact, matching the dotted-string form.
2610
+ */
2611
+ <const S extends ReadonlyArray<string | number>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, JoinSegments<S>>>, NonNullable<WriteShape<NestedType<Form, JoinSegments<S>>>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value): boolean;
2286
2612
  };
2287
2613
  /**
2288
2614
  * Reactive validation status. Re-runs whenever the form (or the
@@ -2311,7 +2637,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2311
2637
  * if (!result.success) showErrors(result.errors)
2312
2638
  * ```
2313
2639
  *
2314
- * Pass a path to validate a subtree. `state.isValidating` flips
2640
+ * Pass a path to validate a subtree. `state.validating` flips
2315
2641
  * `true` while the promise is in flight.
2316
2642
  */
2317
2643
  validateAsync: (path?: FlatPath<Form>) => Promise<ValidationResponseWithoutValue<Form>>;
@@ -2328,11 +2654,25 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2328
2654
  * />
2329
2655
  * ```
2330
2656
  *
2657
+ * Also accepts a segment-array form for callers building paths
2658
+ * dynamically — particularly inside a `v-for` over a prefix variable
2659
+ * where dotted-string concatenation widens the prefix's literal
2660
+ * union to plain `string`:
2661
+ *
2662
+ * ```vue
2663
+ * <fieldset v-for="block in [{ prefix: 'pickup' }, { prefix: 'delivery' }] as const">
2664
+ * <input v-register="form.register([block.prefix, 'line1'])" />
2665
+ * </fieldset>
2666
+ * ```
2667
+ *
2331
2668
  * Pass `options.persist` to opt into the form's persistence
2332
2669
  * pipeline. Persistence requires `useForm({ persist })` configured
2333
2670
  * for storage activity to actually happen.
2334
2671
  */
2335
- register: <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions) => RegisterValue<NestedReadType<WriteShape<Form>, Path>>;
2672
+ register: {
2673
+ <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<Form>, Path>>;
2674
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form, keyof Form>] ? unknown : never), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<Form>, JoinSegments<S>>>;
2675
+ };
2336
2676
  /**
2337
2677
  * The form's identifier — either the explicit `key` passed to
2338
2678
  * `useForm` or an auto-generated unique id when `key` was omitted.
@@ -2385,7 +2725,10 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2385
2725
  * Prefer `form.values.email` for direct reads in templates +
2386
2726
  * scripts; `toRef` is for ref-shaped interop only.
2387
2727
  */
2388
- toRef: <Path extends FlatPath<Form>>(path: Path) => Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
2728
+ toRef: {
2729
+ <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
2730
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, JoinSegments<S>>>>;
2731
+ };
2389
2732
  /**
2390
2733
  * Replace every field error for this form with the provided list.
2391
2734
  * Useful after `parseApiErrors` produces a fresh batch from a
@@ -2414,15 +2757,56 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2414
2757
  */
2415
2758
  clearFieldErrors: (path?: string | (string | number)[]) => void;
2416
2759
  /**
2417
- * Form-level reactive flags, counters, and aggregates (`isDirty`,
2418
- * `isValid`, `isSubmitting`, `submitCount`, `canUndo`,
2760
+ * Replace the form-level errors the entries at the empty path
2761
+ * (`path: []`) without disturbing any field-level errors. Pass an
2762
+ * empty array to clear them all.
2763
+ *
2764
+ * ```ts
2765
+ * form.setFormErrors([{ message: 'Capacity exceeded' }])
2766
+ * form.setFormErrors([
2767
+ * { message: 'Capacity exceeded', code: 'capacity:exceeded' },
2768
+ * { message: 'Pickup window full' },
2769
+ * ])
2770
+ * form.setFormErrors([]) // clear
2771
+ * ```
2772
+ *
2773
+ * Only `message` is required. `code` defaults to `'atta:form-error'`.
2774
+ * Any caller-provided `path` or `formKey` is ignored — `path` is
2775
+ * always forced to `[]` (this API is form-level-only by definition)
2776
+ * and `formKey` is filled in from the form instance. The lenient
2777
+ * input shape lets you pipe `parseApiErrors` output (or any
2778
+ * `ValidationError[]`) straight in:
2779
+ *
2780
+ * ```ts
2781
+ * const result = parseApiErrors(payload, { formKey: form.key })
2782
+ * if (result.ok) form.setFormErrors(result.errors)
2783
+ * ```
2784
+ *
2785
+ * Form-level errors surface in `form.meta.errors` (alongside field
2786
+ * errors) but are intentionally excluded from the path-keyed
2787
+ * `form.errors` proxy (no key represents `[]` in a nested object) —
2788
+ * read them via `meta.errors.filter(e => e.path.length === 0)` or
2789
+ * `form.errors([])` (the call-form aggregates everywhere, including
2790
+ * form-level errors at `path: []`).
2791
+ */
2792
+ setFormErrors: (errors: ReadonlyArray<Partial<ValidationError> & {
2793
+ message: string;
2794
+ }>) => void;
2795
+ /**
2796
+ * Clear every form-level error. Equivalent to `setFormErrors([])`;
2797
+ * field errors are untouched.
2798
+ */
2799
+ clearFormErrors: () => void;
2800
+ /**
2801
+ * Form-level reactive flags, counters, and aggregates (`dirty`,
2802
+ * `valid`, `submitting`, `submitCount`, `canUndo`,
2419
2803
  * `historySize`, and the flat `errors` array). See `FormMeta` for
2420
2804
  * the full shape. Read leaves directly with no `.value`.
2421
2805
  *
2422
2806
  * For per-field state (touched, focused, blurred, errors at one
2423
2807
  * path), use `form.fields.<path>` instead.
2424
2808
  */
2425
- meta: FormMeta;
2809
+ meta: FormMeta<Form>;
2426
2810
  /**
2427
2811
  * Restore the form to its initial state. Without arguments,
2428
2812
  * re-applies the schema defaults (and any `defaultValues` passed
@@ -2431,10 +2815,10 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2431
2815
  *
2432
2816
  * Resets:
2433
2817
  * - the form value back to defaults;
2434
- * - the dirty baseline (so the next edit flips `isDirty` correctly);
2818
+ * - the dirty baseline (so the next edit flips `dirty` correctly);
2435
2819
  * - field errors;
2436
2820
  * - touched / focused / blurred per-field flags;
2437
- * - submission state (`isSubmitting` / `submitCount` / `submitError`);
2821
+ * - submission state (`submitting` / `submitCount` / `submitError`);
2438
2822
  * - the persisted draft, if persistence is configured.
2439
2823
  *
2440
2824
  * The next edit on a still-mounted opted-in input will start
@@ -2564,5 +2948,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2564
2948
  blankPaths: ComputedRef<ReadonlySet<string>>;
2565
2949
  };
2566
2950
 
2567
- export { ROOT_PATH as L, ROOT_PATH_KEY as Q, canonicalizePath as ac, isUnset as ad, parseDottedPath as ae, unset as af };
2568
- export type { RegisterTransform as $, AttaformDefaults as A, PendingValidationStatus as B, CoercionRegistry as C, DeepPartial as D, PersistConfig as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, PersistConfigOptions as J, PersistIncludeMode as K, MetaTrackerValue as M, NestedReadType as N, OnError as O, Path as P, RegisterValue as R, SlimPrimitiveKind as S, ReactiveValidationStatus as T, UseFormConfiguration as U, ValidationError as V, RegisterDirective as W, RegisterFlatPath as X, RegisterOptions as Y, RegisterSelectModifier as Z, RegisterTextModifier as _, CoercionEntry as a, Segment as a0, SetValueCallback as a1, SetValuePayload as a2, SettledValidationStatus as a3, SlimRuntimeOf as a4, SubmitHandler as a5, Unset as a6, ValidateOn as a7, ValidateOnConfig as a8, ValidationResponse as a9, ValidationResponseWithoutValue as aa, WriteMeta as ab, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, CoercionResult as i, CustomDirectiveRegisterAssignerFn as j, DefaultValuesResponse as k, FieldState as l, FieldStateLeaf as m, FieldStateMap as n, FieldStateMapEntry as o, FlatPath as p, FormErrorRecord as q, FormErrorsSurface as r, FormMeta as s, FormStorage as t, FormStorageKind as u, HistoryConfig as v, NestedType as w, OnInvalidSubmitPolicy as x, OnSubmit as y, PathKey as z };
2951
+ export { ROOT_PATH as Z, ROOT_PATH_KEY as _, canonicalizePath as ak, isPathPrefix as al, isUnset as am, parseDottedPath as an, unset as ao };
2952
+ export type { ReactiveValidationStatus as $, AttaformDefaults as A, OnInvalidSubmitPolicy as B, CoercionRegistry as C, DeepPartial as D, OnSubmit as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, Path as P, PathKey as Q, RegisterValue as R, SlimPrimitiveKind as S, PendingValidationStatus as T, UseFormConfiguration as U, ValidationError as V, PersistConfig as W, PersistConfigOptions as X, PersistIncludeMode as Y, CoercionEntry as a, RegisterDirective as a0, RegisterFlatPath as a1, RegisterOptions as a2, RegisterSelectModifier as a3, RegisterTextModifier as a4, RegisterTransform as a5, Segment as a6, SetValueCallback as a7, SetValuePayload as a8, SettledValidationStatus as a9, SlimRuntimeOf as aa, SubmitHandler as ab, Unset as ac, ValidateOn as ad, ValidateOnConfig as ae, ValidationResponse as af, ValidationResponseWithoutValue as ag, ValueOfUnion as ah, WriteMeta as ai, WriteShape as aj, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, ArrayItem as i, ArrayPath as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DefaultValuesResponse as m, FieldMetaPayload as n, FieldState as o, FieldStateMap as p, FieldStateMapEntry as q, FlatPath as r, FormErrorRecord as s, FormErrorsSurface as t, FormMeta as u, FormStorage as v, FormStorageKind as w, HistoryConfig as x, IsUnion as y, NestedType as z };