attaform 0.17.2 → 0.18.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 (115) hide show
  1. package/README.md +77 -36
  2. package/dist/chunks/devtools.cjs +10 -37
  3. package/dist/chunks/devtools.cjs.map +1 -1
  4. package/dist/chunks/devtools.mjs +10 -37
  5. package/dist/chunks/devtools.mjs.map +1 -1
  6. package/dist/chunks/indexeddb.cjs +4 -4
  7. package/dist/chunks/indexeddb.cjs.map +1 -1
  8. package/dist/chunks/indexeddb.mjs +1 -1
  9. package/dist/chunks/local-storage.cjs +2 -2
  10. package/dist/chunks/local-storage.cjs.map +1 -1
  11. package/dist/chunks/local-storage.mjs +1 -1
  12. package/dist/chunks/session-storage.cjs +2 -2
  13. package/dist/chunks/session-storage.cjs.map +1 -1
  14. package/dist/chunks/session-storage.mjs +1 -1
  15. package/dist/index.cjs +42 -37
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +159 -196
  18. package/dist/index.d.mts +159 -196
  19. package/dist/index.d.ts +159 -196
  20. package/dist/index.mjs +5 -7
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/nuxt.cjs +31 -40
  23. package/dist/nuxt.cjs.map +1 -1
  24. package/dist/nuxt.d.cts +8 -1
  25. package/dist/nuxt.d.mts +8 -1
  26. package/dist/nuxt.d.ts +8 -1
  27. package/dist/nuxt.mjs +32 -41
  28. package/dist/nuxt.mjs.map +1 -1
  29. package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
  30. package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
  31. package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
  32. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
  33. package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
  34. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
  35. package/dist/runtime/plugins/attaform.cjs +17 -6
  36. package/dist/runtime/plugins/attaform.cjs.map +1 -1
  37. package/dist/runtime/plugins/attaform.mjs +15 -4
  38. package/dist/runtime/plugins/attaform.mjs.map +1 -1
  39. package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.2b7M2mww.d.mts} +57 -23
  40. package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
  41. package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
  42. package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
  43. package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
  44. package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
  45. package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
  46. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  47. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  48. package/dist/shared/{attaform.Drt6fivF.mjs → attaform.CtNUB9nf.mjs} +74 -76
  49. package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
  50. package/dist/shared/{attaform.C5MH4lNh.d.mts → attaform.DF8wo-ry.d.ts} +4 -4
  51. package/dist/shared/attaform.DK9aj0N8.d.ts +1651 -0
  52. package/dist/shared/{attaform.BPRHR3Zs.cjs → attaform.DUHru0OF.cjs} +83 -85
  53. package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
  54. package/dist/shared/{attaform.C6lbmMUe.d.ts → attaform.DVLB6CAn.d.mts} +4 -4
  55. package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.Dj9mwbaV.d.cts} +756 -148
  56. package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.Dj9mwbaV.d.mts} +756 -148
  57. package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.Dj9mwbaV.d.ts} +756 -148
  58. package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
  59. package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
  60. package/dist/shared/attaform.DoSuaKMd.d.cts +1651 -0
  61. package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
  62. package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
  63. package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
  64. package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
  65. package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.M33WKVV4.d.cts} +57 -23
  66. package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
  67. package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
  68. package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
  69. package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
  70. package/dist/shared/attaform.iTqxvl-P.d.mts +1651 -0
  71. package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.tsNFcEW7.d.ts} +57 -23
  72. package/dist/shared/{attaform.DtMN-MAm.d.cts → attaform.tts_OM7j.d.cts} +4 -4
  73. package/dist/vite.cjs +288 -2
  74. package/dist/vite.cjs.map +1 -1
  75. package/dist/vite.mjs +288 -2
  76. package/dist/vite.mjs.map +1 -1
  77. package/dist/zod-v3.cjs +11 -8
  78. package/dist/zod-v3.cjs.map +1 -1
  79. package/dist/zod-v3.d.cts +6 -6
  80. package/dist/zod-v3.d.mts +6 -6
  81. package/dist/zod-v3.d.ts +6 -6
  82. package/dist/zod-v3.mjs +3 -3
  83. package/dist/zod-v4.cjs +11 -8
  84. package/dist/zod-v4.cjs.map +1 -1
  85. package/dist/zod-v4.d.cts +5 -5
  86. package/dist/zod-v4.d.mts +5 -5
  87. package/dist/zod-v4.d.ts +5 -5
  88. package/dist/zod-v4.mjs +3 -3
  89. package/dist/zod.cjs +15 -16
  90. package/dist/zod.cjs.map +1 -1
  91. package/dist/zod.d.cts +127 -40
  92. package/dist/zod.d.mts +127 -40
  93. package/dist/zod.d.ts +127 -40
  94. package/dist/zod.mjs +7 -11
  95. package/dist/zod.mjs.map +1 -1
  96. package/package.json +18 -7
  97. package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
  98. package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
  99. package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
  100. package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
  101. package/dist/shared/attaform.BPRHR3Zs.cjs.map +0 -1
  102. package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
  103. package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
  104. package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
  105. package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
  106. package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
  107. package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
  108. package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
  109. package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
  110. package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
  111. package/dist/shared/attaform.Drt6fivF.mjs.map +0 -1
  112. package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
  113. package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
  114. package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
  115. package/dist/shared/attaform.h1sq3BFu.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Ref, ObjectDirective, ComputedRef } from 'vue';
1
+ import { ObjectDirective, Ref, ComputedRef } from 'vue';
2
2
 
3
3
  /**
4
4
  * Schema-attached field metadata — the shared types used by both Zod
@@ -114,7 +114,7 @@ type Unset = typeof _unsetBrand;
114
114
  * every realm gets the same sentinel.
115
115
  *
116
116
  * @see {@link isUnset} — type guard that narrows a value back to {@link Unset}.
117
- * @see `docs/recipes/blank-inputs.md` — the conceptual model behind blank-aware fields.
117
+ * @see `docs/validation/blank.md` — the conceptual model behind blank-aware fields.
118
118
  */
119
119
  declare const unset: Unset;
120
120
  /**
@@ -136,6 +136,16 @@ declare function isUnset(value: unknown): value is Unset;
136
136
  type GenericForm = Record<string, unknown>;
137
137
  /** Internal helper — `true` when `T` is an object or array. */
138
138
  type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
139
+ /**
140
+ * Implementation detail backing `FlatPath` in its default
141
+ * (partial-path) mode. Exported so `rollup-plugin-dts` preserves it
142
+ * as a named alias in the bundled `.d.ts` rather than inlining the
143
+ * full template-literal recursion body at every reference site
144
+ * (`FlatPath`, `RegisterFlatPath`, every path-addressed API method).
145
+ * Inlining at consumer call sites compounds into TS2589 territory
146
+ * when multiple complex forms share a scope. Consumers should reach
147
+ * for `FlatPath` instead; this alias is not part of the stable surface.
148
+ */
139
149
  type PartialFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}` | `${Key}.${PartialFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${PartialFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
140
150
  type CompleteFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}.${CompleteFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${CompleteFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
141
151
  /**
@@ -259,6 +269,14 @@ type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem>
259
269
  * never reach this depth.
260
270
  */
261
271
  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;
272
+ /**
273
+ * Implementation-detail primitive-leaf marker used by `DeepPartial`
274
+ * and sibling structural walkers. Exported so the bundled `.d.ts`
275
+ * references one alias instead of re-emitting the union at every
276
+ * recursion branch of every walker that depends on it. Not part of
277
+ * the stable consumer-facing surface — reach for `DeepPartial`
278
+ * instead.
279
+ */
262
280
  type Primitive = string | number | boolean | symbol | bigint | null | undefined;
263
281
  /**
264
282
  * Distinguish a tuple from a regular array.
@@ -334,10 +352,14 @@ type ArrayItem<Form, Path extends ArrayPath<Form>> = NestedType<Form, Path> exte
334
352
  * the array-recursion branch so positionally-typed array literals
335
353
  * survive intact.
336
354
  *
337
- * Read-side types (handleSubmit's `data` argument,
338
- * validate*() result payloads) intentionally stay STRICT those
339
- * payloads have been parsed by the schema, so the widened shape
340
- * doesn't apply.
355
+ * `WriteShape<T>` stays STRICT — no `| Unset` widening. It's used for
356
+ * the callback's prev-value argument in `setValue(path, prev => ...)`
357
+ * (which always receives a real value, never the sentinel), for
358
+ * read-side types where consumers expect a structural form shape, and
359
+ * by internal types like `FieldStateMap<T>`. The consumer-facing
360
+ * write-value type is `DefaultValuesShape<T>` (composed via
361
+ * `SetValuePayload`), which adds `| Unset` at every recursable
362
+ * position.
341
363
  */
342
364
  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[]] ? {
343
365
  -readonly [K in keyof T]: WriteShape<T[K]>;
@@ -352,30 +374,75 @@ type WriteShape<T> = T extends string | number | boolean | bigint | symbol | nul
352
374
  * brand-typed sentinel consumers pass to indicate "this leaf starts
353
375
  * displayed-empty" in `defaultValues`, `setValue`, and `reset`.
354
376
  *
355
- * Non-primitive leaves (`Date`, `RegExp`, `Map`, `Set`, functions)
356
- * stay strict `defaultValues: { joinedAt: unset }` against a
357
- * `Date`-typed leaf is a type error.
377
+ * `Unset` is admitted at every position — primitive leaves, opaque
378
+ * leaves (`Date`, `RegExp`, `Map`, `Set`, functions), and containers
379
+ * (objects, arrays, tuples, records, DUs, optional / nullable
380
+ * wrappers). A container `unset` recursively marks every primitive
381
+ * descendant blank, so `defaultValues: { profile: unset }` and
382
+ * `setValue('cargo', unset)` typecheck cleanly.
358
383
  *
359
384
  * The recursion mirrors `WriteShape<T>` exactly so `defaultValues`
360
- * stays compatible at every nested position; the only divergence is
361
- * the leaf widening. Tuple positions, unbounded arrays, and nested
362
- * records all flow through unchanged.
385
+ * stays compatible at every nested position. Tuple positions,
386
+ * unbounded arrays, and nested records all flow through unchanged.
363
387
  *
364
388
  * Example:
365
389
  *
366
390
  * DefaultValuesShape<{ income: number; name: string; age: 21 }>
367
- * // → { income: number | Unset; name: string | Unset; age: number | Unset }
391
+ * // → { income: number | Unset; name: string | Unset; age: number | Unset } | Unset
368
392
  *
369
393
  * Used by `UseFormConfiguration.defaultValues`, `setValue`'s value
370
- * parameter, and `reset`'s parameter (commit 7 widens all three).
394
+ * parameter, and `reset`'s parameter.
371
395
  */
372
- type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string | Unset : T extends number ? number | Unset : T extends boolean ? boolean | Unset : T extends bigint ? bigint | Unset : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
396
+ type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string | Unset : T extends number ? number | Unset : T extends boolean ? boolean | Unset : T extends bigint ? bigint | Unset : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
373
397
  -readonly [K in keyof T]: DefaultValuesShape<T[K]>;
374
- } : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
398
+ } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
375
399
  -readonly [K in keyof T]: DefaultValuesShape<T[K]>;
376
- } : Array<DefaultValuesShape<U>> : T extends object ? {
400
+ } | Unset : Array<DefaultValuesShape<U>> | Unset : T extends object ? {
377
401
  [K in keyof T]: DefaultValuesShape<T[K]>;
378
- } : T;
402
+ } | Unset : T;
403
+ /**
404
+ * Single-walker fusion of `DeepPartial` and `DefaultValuesShape` — the
405
+ * type accepted at `defaultValues`, `reset()`'s parameter, and every
406
+ * partial-shape consumer. Every level is optional and every position
407
+ * — primitive leaf, opaque leaf, or container — admits `| Unset`, in
408
+ * one tree walk where the prior `DeepPartial<DefaultValuesShape<F>>`
409
+ * composition walked twice.
410
+ *
411
+ * Both passes had identical topology (object → mapped, tuple →
412
+ * positional, array → recurse, primitive → terminal) — the doubled
413
+ * recursion exhausted the depth budget at consumer call sites that
414
+ * wire multiple complex forms into one scope. Collapsing them buys
415
+ * back the headroom plus a side fix: opaque leaves (`Date`, `Map`,
416
+ * `Set`, `RegExp`, functions) now stay intact when their containing
417
+ * property is optional, rather than getting structurally destructured
418
+ * by `DeepPartial`'s pass.
419
+ *
420
+ * Tuple positions, array elements, and discriminated-union variants
421
+ * all flow through unchanged from the prior semantics. Container
422
+ * positions widen with `| Unset` so `defaultValues: { profile: unset }`
423
+ * and `reset({ cargo: unset })` typecheck — the runtime recursively
424
+ * marks every primitive descendant blank.
425
+ *
426
+ * ```ts
427
+ * type T = DefaultValuesInput<{
428
+ * email: string
429
+ * joinedAt: Date
430
+ * profile: { name: string; age: number }
431
+ * }>
432
+ * // → {
433
+ * // email?: string | Unset
434
+ * // joinedAt?: Date | Unset
435
+ * // profile?: { name?: string | Unset; age?: number | Unset } | Unset
436
+ * // } | Unset
437
+ * ```
438
+ */
439
+ 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[]] ? {
440
+ -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
441
+ } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
442
+ -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
443
+ } | Unset : Array<DefaultValuesInput<U>> | Unset : T extends object ? {
444
+ [K in keyof T]?: DefaultValuesInput<T[K]>;
445
+ } | Unset : T;
379
446
 
380
447
  /**
381
448
  * Per-form options threaded from `useForm` into the adapter factory.
@@ -469,6 +536,42 @@ declare const ROOT_PATH_KEY: PathKey;
469
536
  */
470
537
  declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
471
538
 
539
+ /**
540
+ * Per-FormStore registry tracking which DOM elements have opted into
541
+ * persistence for which paths. Lives on the FormStore so that two SFCs
542
+ * sharing a key share the registry — opt-ins are per-element, not
543
+ * per-component.
544
+ *
545
+ * The directive's input handler computes `meta.persist` for each write
546
+ * by calling `hasOptIn(elementId, path)` — only THIS element's writes
547
+ * persist if THIS element opted in. Other call sites that aren't tied
548
+ * to a single element (history undo/redo, field-array helpers, devtools
549
+ * edits) use `hasAnyOptInForPath(path)` — persist if any element has
550
+ * opted into that path.
551
+ *
552
+ * Internal data structure: `Map<PathKey, Set<elementId>>`. Small forms
553
+ * have ~10-50 paths; iteration is cheap. All operations are O(1) given
554
+ * (id, path).
555
+ */
556
+ type PersistOptInRegistry = {
557
+ /** Add an opt-in entry; idempotent. */
558
+ add(elementId: string, path: PathKey): void;
559
+ /** Remove a single (element, path) entry. */
560
+ remove(elementId: string, path: PathKey): void;
561
+ /** Remove every opt-in for `elementId`. Called from directive's beforeUnmount. */
562
+ removeAllFor(elementId: string): void;
563
+ /** Check whether THIS element has opted into THIS path. */
564
+ hasOptIn(elementId: string, path: PathKey): boolean;
565
+ /** Check whether ANY element has opted into this path. */
566
+ hasAnyOptInForPath(path: PathKey): boolean;
567
+ /** Iterate every path that currently has at least one opt-in. */
568
+ optedInPaths(): IterableIterator<PathKey>;
569
+ /** True iff no element has opted into any path. */
570
+ isEmpty(): boolean;
571
+ /** Drop every entry. Called from FormStore.dispose. */
572
+ clear(): void;
573
+ };
574
+
472
575
  /**
473
576
  * Identifier for a form. A `FormKey` is the string passed via
474
577
  * `useForm({ key })`, used to look up a form by name from a distant
@@ -699,37 +802,24 @@ type AbstractSchema<Form, GetValueFormType> = {
699
802
  */
700
803
  getEmptyValueAtPath(path: Path): unknown;
701
804
  /**
702
- * Give the schema a chance to normalize the consumer's write value
703
- * before it lands in storage / hits the slim-primitive gate. Each
704
- * schema library exposes this concept differently Zod calls it
705
- * `z.preprocess(fn, inner)`, Yup calls it `.transform()`, Valibot
706
- * spells it `pipe(transform(fn), inner)` but the shape is the
707
- * same: "this input shape gets coerced into that storage shape at
708
- * the boundary."
709
- *
710
- * Runs SYNCHRONOUSLY at the write boundary so storage holds the
711
- * post-normalization shape. Without this, a schema like `notify:
712
- * z.preprocess(v => v == null ? defaultVar : v, innerDU)` would
713
- * let the consumer write `null` and lock storage into `null` —
714
- * because the gate sees the raw input (which the preprocess wrapper
715
- * accepts as `unknown`) and storage holds a shape no variant
716
- * matches.
717
- *
718
- * Adapters MUST:
719
- * - Return `value` unchanged when no normalization is declared at
720
- * the path.
721
- * - Return `value` unchanged when the user's normalization fn
722
- * returns a `Promise` (async coercion can't run at write time —
723
- * validation handles it during parse).
724
- * - Let user-thrown errors propagate (the user wrote the fn; we
725
- * just tag the path in the wrapper error for diagnostics).
726
- *
727
- * Normalization runs when `path` equals the wrapper's exact
728
- * location. Writes deeper than the wrapper bypass it (a wrapper
729
- * over the whole subtree can't be invoked from a partial leaf
730
- * write).
731
- */
732
- normalizeWriteValueAtPath(value: unknown, path: Path): unknown;
805
+ * Reports whether `path` resolves to (or descends through) a
806
+ * schema-side normalizer that runs at parse, not at the write
807
+ * boundary. In Zod v4 that's `z.preprocess(fn, inner)` and
808
+ * `z.coerce.X()` (both desugar to `ZodPipe<ZodTransform, inner>`);
809
+ * in Zod v3 it's `ZodEffects` with `_def.effect.type === 'preprocess'`.
810
+ *
811
+ * Consulted by the slim-primitive write gate. When true at a path,
812
+ * the gate accepts the consumer's raw value verbatim and stops
813
+ * walking children storage holds the user's input, and the
814
+ * normalizer fires during `safeParse` (handleSubmit / validate /
815
+ * validateAsync), not at `setValue` time.
816
+ *
817
+ * Path-prefix semantic: returns true if ANY ancestor of `path`
818
+ * resolves to such a wrapper, so descendants under a preprocess-
819
+ * wrapped container also short-circuit the gate. Adapters cache
820
+ * the result by canonicalized path key.
821
+ */
822
+ isPreprocessOrCoerceLeaf(path: Path): boolean;
733
823
  /**
734
824
  * Distinguish a tuple (fixed-length, position-typed) from an
735
825
  * unbounded array at `path`. The runtime calls this on every
@@ -1021,7 +1111,7 @@ type UnionDiscriminatorContext = {
1021
1111
  * the set of kinds the path's leaf schema accepts. A write is gated
1022
1112
  * by `accepted.has(slimKindOf(value))`.
1023
1113
  */
1024
- type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set';
1114
+ type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set' | 'file';
1025
1115
  /**
1026
1116
  * The "no result yet" status returned by the reactive `validate()` ref
1027
1117
  * while a validation run is in flight.
@@ -1053,15 +1143,23 @@ type SettledValidationStatus<Form> = {
1053
1143
  type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
1054
1144
  /**
1055
1145
  * What to do when a submit attempt fails validation. The library can
1056
- * focus and/or scroll the first errored field into view without
1057
- * wiring an `onError` callback yourself. Off by default.
1058
- *
1059
- * - `'none'` (default): no automatic UI nudge.
1060
- * - `'focus-first-error'`: focus the first errored field's first
1061
- * visible element (with `preventScroll: true` so it doesn't fight
1062
- * any `'scroll-to-first-error'` choice you make).
1063
- * - `'scroll-to-first-error'`: scroll that element into view.
1064
- * - `'both'`: scroll first, then focus.
1146
+ * focus and/or scroll the first errored field into view without you
1147
+ * wiring an `onError` callback yourself. Defaults to
1148
+ * `'focus-first-error'` because moving keyboard / screen-reader focus
1149
+ * to the broken field on submit is an accessibility baseline; opt out
1150
+ * with `'none'` if you're managing focus elsewhere.
1151
+ *
1152
+ * - `'focus-first-error'` (default): focus the first errored field's
1153
+ * first visible element. Modern browsers scroll the focused element
1154
+ * into view by default; pair with `'both'` if you want an explicit
1155
+ * scroll alongside.
1156
+ * - `'scroll-to-first-error'`: scroll that element into view without
1157
+ * moving focus.
1158
+ * - `'both'`: scroll first, then focus (with `preventScroll: true` so
1159
+ * the browser doesn't undo the explicit scroll).
1160
+ * - `'none'`: no automatic UI nudge; the dev handles focus / scroll
1161
+ * manually via `form.focusFirstError()` or `form.scrollToFirstError()`
1162
+ * from an `onError` callback.
1065
1163
  *
1066
1164
  * If no errored field has a currently mounted, visible element, the
1067
1165
  * policy silently no-ops.
@@ -1428,7 +1526,7 @@ type PersistConfig = FormStorageKind | FormStorage | PersistConfigOptions;
1428
1526
  * })
1429
1527
  * ```
1430
1528
  */
1431
- type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DeepPartial<DefaultValuesShape<Form>>> = {
1529
+ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DefaultValuesInput<Form>, K extends FormKey = FormKey> = {
1432
1530
  /**
1433
1531
  * The schema describing the form's shape and validation rules.
1434
1532
  * Typed entry points like `attaform/zod` accept the
@@ -1459,8 +1557,12 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1459
1557
  *
1460
1558
  * Keys starting with `__atta:` are reserved for internal use and
1461
1559
  * throw `ReservedFormKeyError` if passed.
1560
+ *
1561
+ * When passed as a string literal, the literal is preserved on
1562
+ * `form.key` so `useWizard` and other consumers can discriminate
1563
+ * against the union of known keys at compile time.
1462
1564
  */
1463
- key?: FormKey;
1565
+ key?: K;
1464
1566
  /**
1465
1567
  * Initial values applied over the schema's defaults. Each field
1466
1568
  * falls back to the schema default (or the primitive default for
@@ -1472,8 +1574,30 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1472
1574
  * membership, length / range bounds). Refinement-invalid defaults
1473
1575
  * pass through and surface as field errors — this lets you
1474
1576
  * rehydrate stale saved data without losing the user's input.
1577
+ *
1578
+ * Accepts a plain value, a sync function, or an async function:
1579
+ *
1580
+ * ```ts
1581
+ * // Plain value — applies at construction.
1582
+ * defaultValues: { email: '' }
1583
+ *
1584
+ * // Sync function — invoked on a microtask after construction.
1585
+ * defaultValues: () => buildDraft()
1586
+ *
1587
+ * // Async function — form starts with the schema's slim defaults
1588
+ * // and `form.hydrating` flips true while the promise is
1589
+ * // in flight; on resolve the values apply and `hydrating` flips
1590
+ * // false. Under SSR the factory fires via `onServerPrefetch` so
1591
+ * // the resolved payload bakes into hydration transfer state and
1592
+ * // the client never re-fetches.
1593
+ * defaultValues: async () => api.fetchDraft(userId)
1594
+ * ```
1595
+ *
1596
+ * Errors thrown by a function-form factory surface on
1597
+ * `form.hydrateError`; the form stays usable with slim defaults.
1598
+ * Call `form.rehydrate()` to re-fire the factory.
1475
1599
  */
1476
- defaultValues?: DefaultValues;
1600
+ defaultValues?: DefaultValues | (() => DefaultValues) | (() => Promise<DefaultValues>);
1477
1601
  /**
1478
1602
  * Whether to validate default values at construction. Default
1479
1603
  * `true`.
@@ -1481,7 +1605,7 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1481
1605
  * - `true` (default): the schema is run against the derived
1482
1606
  * defaults immediately; any failures populate `form.errors` from
1483
1607
  * the first frame. The UI decides when to *show* errors — gate
1484
- * on `form.fields.<path>.touched`, `form.meta.submitCount`, etc.
1608
+ * on `form.fields.<path>.touched`, `form.meta.submissionAttempts`, etc.
1485
1609
  * - `false`: refinements are stripped during defaults derivation
1486
1610
  * and construction-time validation is skipped. Useful for
1487
1611
  * multi-step wizards, field arrays seeded with placeholder
@@ -1494,12 +1618,16 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1494
1618
  /**
1495
1619
  * Automatic UI nudge on submit-validation failure. Fires after
1496
1620
  * errors are populated and before your `onError` callback runs.
1497
- * Default `'none'`.
1621
+ * Default `'focus-first-error'`, which moves keyboard / screen-reader
1622
+ * focus to the broken field as an accessibility baseline.
1498
1623
  *
1499
- * - `'focus-first-error'`: focus the first errored field's first
1500
- * visible element (without scrolling).
1501
- * - `'scroll-to-first-error'`: scroll it into view.
1624
+ * - `'focus-first-error'` (default): focus the first errored field's
1625
+ * first visible element.
1626
+ * - `'scroll-to-first-error'`: scroll it into view without focusing.
1502
1627
  * - `'both'`: scroll, then focus.
1628
+ * - `'none'`: opt out entirely; handle focus / scroll yourself in an
1629
+ * `onError` callback via `form.focusFirstError()` or
1630
+ * `form.scrollToFirstError()`.
1503
1631
  *
1504
1632
  * If no errored field has a currently-mounted, visible element,
1505
1633
  * the policy silently no-ops.
@@ -1658,31 +1786,53 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1658
1786
  */
1659
1787
  sensitiveNames?: readonly string[];
1660
1788
  /**
1661
- * Cross-tab synchronisation via BroadcastChannel. Defaults to `true`
1662
- * (when the browser supports it and the page is in a secure
1663
- * context): a keyed `useForm` callsite auto-pairs with same-keyed
1664
- * siblings in other same-origin tabs and mirrors their mutations
1665
- * in near real-time.
1789
+ * Cross-tab synchronisation via BroadcastChannel. **Defaults to
1790
+ * `false` (opt-in).** Setting `true` on a keyed `useForm` callsite
1791
+ * auto-pairs the form with same-keyed siblings in other same-origin
1792
+ * tabs and mirrors their mutations in near real-time.
1666
1793
  *
1667
1794
  * **Resolution order (per-register override > per-form > global > library):**
1668
1795
  *
1669
- * register(path, { multiTab }) > useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
1796
+ * register(path, { multiTab }) > useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`false`)
1670
1797
  *
1671
- * **When to set `false`:** forms holding PII / PHI, contexts where
1672
- * tab isolation is required by policy, or any flow where conflicting
1673
- * tab edits could corrupt user intent. Sensitive-named paths (via
1674
- * `sensitiveNames`) are always stripped from outbound broadcasts
1675
- * regardless of this setting.
1798
+ * **Why opt-in.** Same-keyed forms broadcasting by default leaks
1799
+ * surprise: a user editing in one tab sees their values appear in a
1800
+ * sibling tab they forgot was open, including PII / PHI for forms
1801
+ * that don't explicitly use `sensitiveNames`. Mirrors the `persist`
1802
+ * default (also opt-in): both opt-in surfaces compose into one
1803
+ * consistent "richer state needs explicit consent" rule.
1676
1804
  *
1677
- * **Secure-context requirement.** Multi-tab sync is silently disabled
1678
- * outside `window.isSecureContext === true` (HTTPS or localhost). On
1679
- * plain HTTP a one-shot dev warning fires and the module noops.
1805
+ * **What's stripped even when opt-in.** `File` and `Blob` values
1806
+ * never traverse the channel regardless of this flag (security +
1807
+ * `structuredClone` cost). Sensitive-named paths (via
1808
+ * `sensitiveNames`) are stripped both directions. See the
1809
+ * multi-tab sync page for the full security model.
1810
+ *
1811
+ * **Secure-context requirement.** Even with `multiTab: true`, sync
1812
+ * is silently disabled outside `window.isSecureContext === true`
1813
+ * (HTTPS or localhost). On plain HTTP a one-shot dev warning fires
1814
+ * and the module noops.
1680
1815
  *
1681
1816
  * **Anonymous (auto-keyed) forms skip sync entirely** — without a
1682
1817
  * consumer-supplied `key`, cross-tab identity is undefined and the
1683
1818
  * channel would be solo by construction.
1684
1819
  */
1685
1820
  multiTab?: boolean;
1821
+ /**
1822
+ * @internal
1823
+ * SSR prefetch mark — set by the `attaform/vite` compile-time
1824
+ * transform on `useForm` calls whose surrounding SFC template (or a
1825
+ * computed feeding it) reads the form's reactive state. The flag
1826
+ * enqueues the form on the registry's SSR prefetch queue so an
1827
+ * async `defaultValues` factory runs inside `onServerPrefetch` and
1828
+ * the resolved payload bakes into the hydration transfer state.
1829
+ *
1830
+ * Consumers do not write this directly — `form.activate()` is the
1831
+ * documented escape hatch when the transform's static analysis
1832
+ * can't see a reference (cross-module sharing, dynamic property
1833
+ * access, headless contexts).
1834
+ */
1835
+ __ssrAccessed?: boolean;
1686
1836
  };
1687
1837
  /**
1688
1838
  * App-level defaults applied to every `useForm` call. Set these once
@@ -1762,7 +1912,7 @@ type AttaformDefaults = {
1762
1912
  *
1763
1913
  * ```ts
1764
1914
  * (field, formMeta) =>
1765
- * formMeta.submitCount > 0 || (field.touched === true && field.dirty)
1915
+ * formMeta.submissionAttempts > 0 || (field.touched === true && field.dirty)
1766
1916
  * ```
1767
1917
  *
1768
1918
  * Compose with the library default via the public
@@ -1851,25 +2001,28 @@ type AttaformDefaults = {
1851
2001
  */
1852
2002
  sensitiveNames?: readonly string[];
1853
2003
  /**
1854
- * App-wide default for `useForm({ multiTab })`. Default `true` when
1855
- * the runtime supports `BroadcastChannel` AND `window.isSecureContext`
1856
- * is true (HTTPS in production, localhost in development) same gate
1857
- * browsers apply to other sensitive APIs (clipboard, geolocation,
1858
- * push, web crypto subtle).
1859
- *
1860
- * Set to `false` once at the plugin level for a multi-tenant
1861
- * deployment that prefers tab-isolation by default; individual forms
1862
- * can still opt back in via `useForm({ multiTab: true })`.
2004
+ * App-wide default for `useForm({ multiTab })`. Library default is
2005
+ * `false` (opt-in) same posture as `persist`. Set to `true` once
2006
+ * at the plugin level to enable cross-tab sync for every form in
2007
+ * the app by default; individual forms can still opt out via
2008
+ * `useForm({ multiTab: false })`.
1863
2009
  *
1864
2010
  * **Resolution order (per-form wins):**
1865
2011
  *
1866
- * useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`true`)
2012
+ * useForm({ multiTab }) > AttaformDefaults.multiTab > library default (`false`)
1867
2013
  *
1868
- * **Secure-context gate.** Multi-tab sync only activates over HTTPS
1869
- * or localhost. On plain HTTP, the module silently noops with a
1870
- * one-shot dev-mode warning production deployments MUST be served
1871
- * over HTTPS for sync to function. See the multi-tab-sync recipe's
1872
- * Security section for the threat model.
2014
+ * **Why opt-in.** Auto-broadcasting same-keyed forms surprises users
2015
+ * (a value typed in one tab appearing in another they forgot was
2016
+ * open) and leaks state for forms that don't explicitly use
2017
+ * `sensitiveNames`. Paired with `persist` (also opt-in), the two
2018
+ * "richer state" surfaces follow one consistent rule: explicit
2019
+ * consent.
2020
+ *
2021
+ * **Secure-context gate.** Even with `multiTab: true`, sync only
2022
+ * activates over HTTPS or localhost. On plain HTTP, the module
2023
+ * silently noops with a one-shot dev-mode warning — production
2024
+ * deployments MUST be served over HTTPS for sync to function. See
2025
+ * the multi-tab-sync recipe's Security section for the threat model.
1873
2026
  */
1874
2027
  multiTab?: boolean;
1875
2028
  };
@@ -2005,7 +2158,7 @@ type MetaTrackerValue = {
2005
2158
  * want pre-error introspection ("the user hasn't decided yet"
2006
2159
  * indicator, "review unanswered fields" hint).
2007
2160
  *
2008
- * See `docs/recipes/blank-inputs.md` for the full conceptual model.
2161
+ * See `docs/validation/blank.md` for the full conceptual model.
2009
2162
  */
2010
2163
  blank: boolean;
2011
2164
  };
@@ -2222,6 +2375,14 @@ type RegisterValue<Value = unknown> = Readonly<{
2222
2375
  * directives signal whether the write should be persisted.
2223
2376
  */
2224
2377
  setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
2378
+ /**
2379
+ * Mark this field as DOM-connected during SSR so a server-rendered
2380
+ * template that reads `form.fields.<path>.connected` doesn't
2381
+ * flicker on hydration. The `v-register` directive calls this for
2382
+ * you; no-op on the client.
2383
+ * @internal
2384
+ */
2385
+ markConnectedOptimistically: () => void;
2225
2386
  /**
2226
2387
  * Canonical, JSON-encoded path key for this binding (e.g.
2227
2388
  * `'["items",0,"name"]'`). Useful for stable Map / Set keys, log
@@ -2260,6 +2421,90 @@ type RegisterValue<Value = unknown> = Readonly<{
2260
2421
  * `key`.
2261
2422
  */
2262
2423
  formInstanceId: string;
2424
+ /**
2425
+ * Whether this binding opted into persistence via `register(path, { persist: true })`.
2426
+ * @internal
2427
+ */
2428
+ persist: boolean;
2429
+ /**
2430
+ * Whether this binding acknowledged a sensitive-name override.
2431
+ * @internal
2432
+ */
2433
+ acknowledgeSensitive: boolean;
2434
+ /**
2435
+ * Per-element persistence opt-in registry. Used by directive integrations.
2436
+ * @internal
2437
+ */
2438
+ persistOptIns: PersistOptInRegistry;
2439
+ /**
2440
+ * Resolved sensitive-path predicate honoring this form's
2441
+ * `sensitiveNames` cascade. The directive calls this through
2442
+ * `enforceSensitiveCheck` when a `register('path', { persist: true })`
2443
+ * binding mounts so a per-form custom list (e.g. extending with
2444
+ * `'mrn'`) gates persistence enrolment correctly.
2445
+ * @internal
2446
+ */
2447
+ isSensitivePath: (path: Path | PathKey | string) => boolean;
2448
+ /**
2449
+ * Whether this binding declared `register('path', { multiTab: false })`.
2450
+ * Drives the directive's mount/unmount lifecycle: when `false`, the
2451
+ * directive's `created` hook bumps `state.noSyncPaths` for this
2452
+ * path, and `beforeUnmount` decrements. When `true` (the default),
2453
+ * the binding rides the form-level cascade.
2454
+ * @internal
2455
+ */
2456
+ multiTab: boolean;
2457
+ /**
2458
+ * Pre-bound mount hook for `multiTab: false` bindings — calls
2459
+ * `state.incrementNoSyncOptOut(path)` with this binding's path.
2460
+ * `undefined` when `multiTab !== false`. The directive invokes
2461
+ * during the mount lifecycle.
2462
+ * @internal
2463
+ */
2464
+ markNoSync?: () => void;
2465
+ /**
2466
+ * Pre-bound unmount hook for `multiTab: false` bindings — calls
2467
+ * `state.decrementNoSyncOptOut(path)`. Paired with `markNoSync`;
2468
+ * the directive invokes on `beforeUnmount` (and on `beforeUpdate`
2469
+ * when the binding transitions out of opt-out).
2470
+ * @internal
2471
+ */
2472
+ unmarkNoSync?: () => void;
2473
+ /**
2474
+ * Sync transform pipeline applied by the directive's assigner to
2475
+ * user-typed values before they reach form state. See
2476
+ * `RegisterOptions.transforms` for the public contract; this is
2477
+ * the readonly internal handle the directive iterates. Optional
2478
+ * so hand-rolled `RegisterValue` mocks (test fixtures, custom
2479
+ * integrations) don't have to declare an empty array — the
2480
+ * directive falls back to a no-op pipeline.
2481
+ * @internal
2482
+ */
2483
+ transforms?: ReadonlyArray<RegisterTransform>;
2484
+ /**
2485
+ * Schema-driven coercion closure baked at register-time. Captures
2486
+ * the path's slim accept set and the resolved coercion index so
2487
+ * the per-event hot path is a single function call. Identity
2488
+ * function when coercion is disabled or the path admits no
2489
+ * coercion target. Optional so hand-rolled `RegisterValue` mocks
2490
+ * (test fixtures, custom integrations) don't have to declare it —
2491
+ * the directive falls back to identity.
2492
+ * @internal
2493
+ */
2494
+ coerce?: (value: unknown) => unknown;
2495
+ /**
2496
+ * Element-level coercion closure for container paths
2497
+ * (`z.array(...)` / `z.set(...)`). Coerces a scalar DOM-side
2498
+ * value (an option's `value` attribute, a checkbox's value)
2499
+ * against the container's element type. `undefined` when the
2500
+ * path isn't a container — scalar paths use `coerce` exclusively.
2501
+ *
2502
+ * Used by the directive's read-side comparisons in setChecked
2503
+ * (array/Set branches) and setSelected (multi-select) to keep
2504
+ * parity with the change handler's WRITE-side path-level coerce.
2505
+ * @internal
2506
+ */
2507
+ coerceElement?: (value: unknown) => unknown;
2263
2508
  /**
2264
2509
  * Read-only, string-form view of the field's current value — what
2265
2510
  * the compile-time `:value` injection reads on every input /
@@ -2272,6 +2517,50 @@ type RegisterValue<Value = unknown> = Readonly<{
2272
2517
  * patching `el.value` back to `'0'` (the slim default).
2273
2518
  */
2274
2519
  displayValue: Readonly<Ref<string>>;
2520
+ /**
2521
+ * Add this field's path to the form's `blankPaths` set,
2522
+ * writing the slim default to storage. Returns the `setValueAtPath`
2523
+ * boolean (`true` accepted, `false` rejected by the slim-primitive
2524
+ * gate). Inherits the binding's `persist` meta so the mark rides
2525
+ * the same persistence channel as user-typed writes.
2526
+ *
2527
+ * Called by the directive's input listener on numeric clear (commit
2528
+ * 5) and by the imperative `setValue(path, unset)` translation
2529
+ * (commit 7). Don't call from consumer code.
2530
+ * @internal
2531
+ */
2532
+ markBlank: () => boolean;
2533
+ /**
2534
+ * `true` when the schema's slim primitive set at this path includes
2535
+ * `'undefined'` — i.e. the leaf was declared `.optional()` (or as
2536
+ * part of a union admitting `undefined`). Cached at register-time.
2537
+ *
2538
+ * Read by the directive's text-input listener to map a DOM clear
2539
+ * (`el.value === ''`) onto `undefined` storage instead of `''`, so
2540
+ * the `.optional()` "absent" semantic survives user interactions.
2541
+ * Without this, a user who typed an invalid value into an optional
2542
+ * field and then cleared it would be stuck with a permanent
2543
+ * validation error (storage holds `''`, which is neither
2544
+ * `undefined` nor a valid inner value).
2545
+ * @internal
2546
+ */
2547
+ acceptsUndefined: boolean;
2548
+ /**
2549
+ * `true` when the schema's slim primitive set at this path includes
2550
+ * `'string'`. Cached at register-time alongside
2551
+ * [[acceptsUndefined]].
2552
+ *
2553
+ * Read by the directive's text-input listener so a DOM clear on a
2554
+ * numeric-only (or boolean-only, bigint-only) leaf takes the
2555
+ * `markBlank` path instead of writing `''` through the assigner:
2556
+ * the slim-primitive gate would reject the empty string anyway,
2557
+ * and the directive's post-write force-sync would then snap the
2558
+ * DOM back to the last accepted value, making the final character
2559
+ * undeletable. With `markBlank`, storage holds the slim default
2560
+ * with the blank meta and the DOM stays empty.
2561
+ * @internal
2562
+ */
2563
+ acceptsString: boolean;
2275
2564
  }>;
2276
2565
  /**
2277
2566
  * Custom assigner installed on an element via the directive's
@@ -2444,6 +2733,48 @@ type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HT
2444
2733
  * directives manually.
2445
2734
  */
2446
2735
  type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
2736
+ /**
2737
+ * Module augmentation: register `v-register` with Vue's template
2738
+ * type system. Lives in `types-api` because every public entry
2739
+ * (`attaform`, `attaform/zod`, `attaform/zod-v3`, `attaform/zod-v4`)
2740
+ * transitively reaches this file via the `useForm` return type, so
2741
+ * the augmentation propagates to consumer SFCs regardless of which
2742
+ * entry they import from — and regardless of whether they install
2743
+ * `attaform/nuxt` or the Vite plugin.
2744
+ *
2745
+ * Augmentation targets `vue` rather than `@vue/runtime-core`:
2746
+ * `GlobalDirectives` is originally declared in `@vue/runtime-core`,
2747
+ * but consumers and Volar's strict-template codegen both resolve
2748
+ * the interface through `vue`'s `export * from '@vue/runtime-dom'`
2749
+ * → `export * from '@vue/runtime-core'` chain. TypeScript merges
2750
+ * interfaces across re-exports, so augmenting `'vue'` reaches Volar
2751
+ * without needing `@vue/runtime-core` to be hoisted into the
2752
+ * library's own `node_modules` for its own typecheck.
2753
+ */
2754
+ declare module 'vue' {
2755
+ interface GlobalDirectives {
2756
+ /**
2757
+ * The `v-register` directive. Binds a form field to a native
2758
+ * input, select, textarea, checkbox, or radio:
2759
+ *
2760
+ * ```vue
2761
+ * <input v-register="form.register('email')" />
2762
+ * ```
2763
+ *
2764
+ * Also works on custom components whose root is NOT a native
2765
+ * input — call `useRegister()` in the child's setup to read the
2766
+ * parent's binding, then re-bind `v-register` onto an inner
2767
+ * native element. (When the wrapper's root IS the input itself,
2768
+ * attribute fallthrough handles it; `useRegister` is unnecessary.)
2769
+ *
2770
+ * Modifier support varies by element:
2771
+ * - text / number / textarea: `.lazy`, `.trim`, `.number`
2772
+ * - select: `.number`
2773
+ * - checkbox / radio: none
2774
+ */
2775
+ vRegister: RegisterDirective;
2776
+ }
2777
+ }
2447
2778
  /**
2448
2779
  * Callback form of `setValue`'s value argument. Receives the previous
2449
2780
  * value at the path and returns the next value:
@@ -2544,7 +2875,7 @@ type FieldState<Value = unknown> = {
2544
2875
  readonly dirty: boolean;
2545
2876
  readonly focused: boolean | null;
2546
2877
  readonly blurred: boolean | null;
2547
- readonly touched: boolean | null;
2878
+ readonly touched: boolean;
2548
2879
  readonly connected: boolean;
2549
2880
  /**
2550
2881
  * The first DOM element bound to this path via `v-register`, or
@@ -2732,15 +3063,89 @@ type FieldState<Value = unknown> = {
2732
3063
  * `FieldState<number | undefined>`. Matches the runtime's stub
2733
3064
  * `FieldState` for inactive-variant paths.
2734
3065
  */
2735
- type FieldStateMapEntry<T> = [T] extends [
2736
- string | number | boolean | bigint | symbol | null | undefined | Date
2737
- ] ? FieldState<T> : [T] extends [ReadonlyArray<infer U>] ? {
2738
- readonly [K: number]: FieldStateMapEntry<U>;
2739
- } : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
2740
- readonly [K in KeyofUnion<T>]-?: FieldStateMapEntry<ValueOfUnion<T, K>>;
2741
- } : {
2742
- readonly [K in keyof T]-?: FieldStateMapEntry<T[K]>;
2743
- } : FieldState<T>;
3066
+ /**
3067
+ * Leaf-shape dispatch table for `LeafWalker`. Each entry maps a walker
3068
+ * kind to the leaf type that walker produces at primitive / Date /
3069
+ * non-recursable positions. The lookup `LeafSchemeFor<T>[Kind]`
3070
+ * threads `T` through the leaf type when the kind needs it
3071
+ * (`field` carries `FieldState<T>`); kinds that don't depend on
3072
+ * `T` simply ignore it (`errors` always produces
3073
+ * `readonly ValidationError[] | undefined`).
3074
+ *
3075
+ * Adding a new walker is one entry here plus a one-line wrapper
3076
+ * alias (`type FooShape<T> = LeafWalker<T, 'foo'>`). The walker
3077
+ * topology is shared; only the leaf changes.
3078
+ *
3079
+ * The `errors` entry threads `T` to preserve `| undefined` when the
3080
+ * value type itself includes undefined (DU variant-only fields whose
3081
+ * lifted shape resolves to `X | undefined`). Statically-known leaves
3082
+ * collapse to `readonly ValidationError[]` (no undefined); dynamic-key
3083
+ * boundaries (array indices, record keys) re-introduce `| undefined`
3084
+ * via the structural index-signature channels.
3085
+ *
3086
+ * Preprocess / coerce leaves (StorageShape = `unknown`) are
3087
+ * statically known too — the IsUnknown filter keeps them on the
3088
+ * non-optional branch instead of being swept into the dynamic
3089
+ * `| undefined` arm by `undefined extends unknown`.
3090
+ *
3091
+ * Implementation-detail surface — consumers reach for `FieldStateMap`
3092
+ * or `FormErrorsSurface` instead.
3093
+ */
3094
+ type IsUnknown<T> = IsAny<T> extends true ? false : unknown extends T ? true : false;
3095
+ interface LeafSchemeFor<T> {
3096
+ field: FieldState<T>;
3097
+ errors: IsUnknown<T> extends true ? readonly ValidationError[] : undefined extends T ? readonly ValidationError[] | undefined : readonly ValidationError[];
3098
+ }
3099
+ /**
3100
+ * Generic walk that produces a proxy shape over `T` with leaves
3101
+ * dispatched via `LeafSchemeFor<T>[Kind]`. The walk topology
3102
+ * (object → mapped homomorphic, object-union → KeyofUnion merge,
3103
+ * array → indexed, primitive / Date → terminal) is identical
3104
+ * across walker kinds; only the leaf type differs.
3105
+ *
3106
+ * Replaces the duplicated bodies of `FieldStateMapEntry` and
3107
+ * `ErrorsProxyShape`. The prior duplication walked the same shape
3108
+ * twice per useForm return type — once for the fields proxy, once
3109
+ * for the errors proxy. Factoring lets the bundled `.d.ts` carry one
3110
+ * shared walker body plus per-kind one-line wrappers, halving the
3111
+ * recursive depth contribution from these two proxies on consumer
3112
+ * call sites.
3113
+ *
3114
+ * `StripOptional` controls whether optional modifiers on input
3115
+ * properties are stripped at every recursion level. `true` (default)
3116
+ * matches the fields proxy semantics — every known leaf carries a
3117
+ * `FieldState` wrapper regardless of source `?`. `false` matches the
3118
+ * errors proxy semantics — the proxy shape stays structurally
3119
+ * identical to the input form, optional keys included.
3120
+ *
3121
+ * Implementation-detail surface — consumers reach for `FieldStateMap`
3122
+ * or `FormErrorsSurface` instead.
3123
+ */
3124
+ 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>] ? {
3125
+ readonly [K: number]: LeafWalker<U, Kind, StripOptional>;
3126
+ } & ContainerSelfErrorsSlot<T, Kind> : [T] extends [object] ? [IsUnion<T>] extends [true] ? StripOptional extends true ? {
3127
+ readonly [K in KeyofUnion<T>]-?: LeafWalker<ValueOfUnion<T, K>, Kind, StripOptional>;
3128
+ } & ContainerSelfErrorsSlot<T, Kind> : {
3129
+ readonly [K in KeyofUnion<T>]: LeafWalker<ValueOfUnion<T, K>, Kind, StripOptional>;
3130
+ } & ContainerSelfErrorsSlot<T, Kind> : StripOptional extends true ? {
3131
+ readonly [K in keyof T]-?: LeafWalker<T[K], Kind, StripOptional>;
3132
+ } & ContainerSelfErrorsSlot<T, Kind> : {
3133
+ readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
3134
+ } & ContainerSelfErrorsSlot<T, Kind> : LeafSchemeFor<T>[Kind];
3135
+ /**
3136
+ * Intersection augmenting every container in the `form.errors` walker
3137
+ * with a `''` sentinel slot — the per-container home for cross-field
3138
+ * refine errors, server-side container marks, and (at root) form-level
3139
+ * errors. Gated on `Kind extends 'errors'` so `form.values` and
3140
+ * `form.fields` surfaces stay untouched. Carve-out for schemas that
3141
+ * legitimately declare a `''` field: the declared field type wins; at
3142
+ * runtime the two collide harmlessly (errors at the literal leaf and
3143
+ * any container-self errors share the slot via array concat).
3144
+ */
3145
+ type ContainerSelfErrorsSlot<T, Kind> = Kind extends 'errors' ? '' extends keyof T ? unknown : {
3146
+ readonly ['']: readonly ValidationError[];
3147
+ } : unknown;
3148
+ type FieldStateMapEntry<T> = LeafWalker<T, 'field'>;
2744
3149
  /**
2745
3150
  * Type of `form.fields` — leaf-aware drillable callable Proxy. At
2746
3151
  * a leaf path the proxy resolves to a `FieldState<Value>`; at
@@ -2803,14 +3208,18 @@ type FieldStateMap<Form extends GenericForm> = ([IsUnion<Form>] extends [true] ?
2803
3208
  type FormErrorRecord = Record<string, ValidationError[]>;
2804
3209
  /**
2805
3210
  * Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
2806
- * leaf path the proxy resolves to `ValidationError[] | undefined`;
2807
- * at a container path it returns a sub-proxy you can keep drilling.
3211
+ * statically-known leaf the proxy resolves to `readonly ValidationError[]`
3212
+ * (empty array when no errors land); at dynamic boundaries (array
3213
+ * indices, record keys, DU variant-only fields) it resolves to
3214
+ * `readonly ValidationError[] | undefined`. At a container path it
3215
+ * returns a sub-proxy you can keep drilling.
2808
3216
  *
2809
3217
  * Dot/bracket access mirrors the schema shape:
2810
3218
  *
2811
3219
  * ```ts
2812
- * form.errors.email // ValidationError[] | undefined (leaf)
2813
- * form.errors.user.profile.email // ValidationError[] | undefined (chained leaves)
3220
+ * form.errors.email // readonly ValidationError[] (static leaf)
3221
+ * form.errors.user.profile.email // readonly ValidationError[] (chained static leaves)
3222
+ * form.errors.posts[3]?.title // readonly ValidationError[] | undefined (past array boundary)
2814
3223
  * form.errors.address // sub-proxy (container — descend further)
2815
3224
  * ```
2816
3225
  *
@@ -2829,38 +3238,47 @@ type FormErrorRecord = Record<string, ValidationError[]>;
2829
3238
  */
2830
3239
  /**
2831
3240
  * Recursive shape of the `form.errors` proxy. Mirrors the schema:
2832
- * primitive leaves expose `ValidationError[] | undefined` directly;
2833
- * containers expose a sub-shape you can keep drilling. Arrays expose
2834
- * numeric-indexed sub-shapes.
3241
+ * statically-known primitive leaves expose `readonly ValidationError[]`
3242
+ * (always an array; empty when no errors); leaves whose value type
3243
+ * itself includes `undefined` (DU variant-only fields) keep the
3244
+ * `| undefined` branch. Containers expose a sub-shape you can keep
3245
+ * drilling. Arrays expose numeric-indexed sub-shapes; reading a
3246
+ * numeric index introduces `| undefined` via noUncheckedIndexedAccess.
2835
3247
  *
2836
3248
  * Augmented with the callable signatures so dot-access and function-
2837
3249
  * call coexist on the same identifier.
2838
3250
  */
2839
3251
  type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
2840
- (path: string): readonly ValidationError[] | undefined;
3252
+ (path: string): readonly ValidationError[];
2841
3253
  /**
2842
3254
  * Tuple-segment form. Validated against `FlatPath<Form>` so literal
2843
3255
  * tuples that don't resolve to a known path fail at the call site.
2844
3256
  * Dynamic `Path`-typed inputs hit the untyped fallback overload below.
2845
3257
  */
2846
- <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): readonly ValidationError[] | undefined;
2847
- (segments: ReadonlyArray<string | number>): readonly ValidationError[] | undefined;
3258
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never)): readonly ValidationError[];
3259
+ (segments: ReadonlyArray<string | number>): readonly ValidationError[];
2848
3260
  /**
2849
3261
  * No-arg call returns the form-level error aggregate — same as
2850
- * `form.errors([])` and `form.meta.errors`. `undefined` when the
2851
- * form has no errors; readonly array otherwise.
3262
+ * `form.errors([])` and `form.meta.errors`. Always a readonly array;
3263
+ * empty when the form has no errors.
2852
3264
  */
2853
- (): readonly ValidationError[] | undefined;
3265
+ (): readonly ValidationError[];
2854
3266
  };
2855
- type ErrorsProxyShape<T> = [T] extends [
2856
- string | number | boolean | bigint | symbol | null | undefined | Date
2857
- ] ? readonly ValidationError[] | undefined : [T] extends [ReadonlyArray<infer U>] ? {
2858
- readonly [K: number]: ErrorsProxyShape<U>;
2859
- } : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
2860
- readonly [K in KeyofUnion<T>]: ErrorsProxyShape<ValueOfUnion<T, K>>;
2861
- } : {
2862
- readonly [K in keyof T]: ErrorsProxyShape<T[K]>;
2863
- } : readonly ValidationError[] | undefined;
3267
+ /**
3268
+ * Implementation-detail walker backing `form.errors` typed proxy.
3269
+ * Thin alias over `LeafWalker<T, 'errors', false>` the shared walker
3270
+ * topology is defined once at `LeafWalker` and parameterized via
3271
+ * `LeafSchemeFor`. `false` preserves optional-key modifiers (errors
3272
+ * proxy mirrors the input shape including `?`); contrast with the
3273
+ * fields proxy alias which strips them via the default `true`.
3274
+ *
3275
+ * Exported so the bundled `.d.ts` references a single alias body
3276
+ * rather than re-emitting the full union-aware recursion at every
3277
+ * consumer call site that types `form.errors`. Multiple useForm
3278
+ * instances in one scope otherwise compound this into TS2589
3279
+ * territory. Consumers should reach for `FormErrorsSurface` instead.
3280
+ */
3281
+ type ErrorsProxyShape<T> = LeafWalker<T, 'errors', false>;
2864
3282
  /**
2865
3283
  * Type of `form.values`. Drillable readonly callable proxy. Unlike
2866
3284
  * `form.errors` and `form.fields`, containers are USEFUL terminals:
@@ -2994,7 +3412,31 @@ type FormMeta<F = unknown> = FieldState<F> & {
2994
3412
  * outcome (validation failure, callback success, callback throw).
2995
3413
  * Useful for "show errors after first submit attempt" UX.
2996
3414
  */
2997
- readonly submitCount: number;
3415
+ readonly submissionAttempts: number;
3416
+ /**
3417
+ * How many times wizard navigation (`wizard.next`, `wizard.back`,
3418
+ * `wizard.goTo`) has actually departed this form. Bumped on real
3419
+ * departures only: no-ops like `back()` from the first step, a
3420
+ * same-key `goTo`, or a `next()` blocked by failed activation leave
3421
+ * the counter at its prior value.
3422
+ *
3423
+ * Pure introspection counter — useful for "this form has been
3424
+ * visited and left" UX (analytics, prior-step badges, layered
3425
+ * `shouldShowErrors` predicates) but does NOT drive the library's
3426
+ * default `shouldShowErrors` heuristic. The reveal-on-submit story
3427
+ * runs entirely through `submissionAttempts`, which
3428
+ * `wizard.handleSubmit` bumps on the active form at intermediate
3429
+ * steps and on every form at the final step.
3430
+ *
3431
+ * Distinct from `submissionAttempts`, which counts `handleSubmit`
3432
+ * passes only — wizard departures and form submissions are tracked
3433
+ * separately so consumers can introspect each cleanly. Distinct
3434
+ * from `form.validate()`, which is a read-only inspection primitive
3435
+ * that never bumps any counter.
3436
+ *
3437
+ * Cleared by `form.reset()`.
3438
+ */
3439
+ readonly departAttempts: number;
2998
3440
  /**
2999
3441
  * The error thrown or rejected by the most recent submit callback
3000
3442
  * (or its `onError` handler). Cleared to `null` at the start of
@@ -3005,6 +3447,27 @@ type FormMeta<F = unknown> = FieldState<F> & {
3005
3447
  * `try { await onSubmit() }` instead.
3006
3448
  */
3007
3449
  readonly submitError: unknown;
3450
+ /**
3451
+ * Scalar mirror of `meta.errors.length`. Read it from templates and
3452
+ * `watch()` without indexing the underlying array.
3453
+ *
3454
+ * Always tracks `errors.length` exactly — reactivity is wired through
3455
+ * the same computed graph, so a `watch(form.meta.errorCount, ...)`
3456
+ * fires when (and only when) the aggregate error count changes.
3457
+ */
3458
+ readonly errorCount: number;
3459
+ /**
3460
+ * `true` once a `handleSubmit` callback has resolved without
3461
+ * throwing. Independent of `submissionAttempts` — a failed submit
3462
+ * (validation failure or callback rejection) increments attempts but
3463
+ * leaves `submitted` at `false`. Templates read it as "the form has
3464
+ * been submitted successfully at least once."
3465
+ *
3466
+ * Cleared by `form.reset()` alongside `submissionAttempts` and
3467
+ * `submitError`. For "the user has attempted a submit," read
3468
+ * `submissionAttempts > 0` directly.
3469
+ */
3470
+ readonly submitted: boolean;
3008
3471
  /**
3009
3472
  * Per-`useForm()`-call identity. Stable for the lifetime of one
3010
3473
  * `useForm()` call; new on every fresh mount. Orthogonal to
@@ -3038,7 +3501,7 @@ type FormMeta<F = unknown> = FieldState<F> & {
3038
3501
  * form.register('email') // bind to <input v-register>
3039
3502
  * form.values.email // current value (proxy, no .value)
3040
3503
  * form.fields.email.dirty // per-field flags
3041
- * form.errors.email // ValidationError[] | undefined
3504
+ * form.errors.email // readonly ValidationError[]
3042
3505
  * form.setValue('email', 'a@b.c')
3043
3506
  * form.handleSubmit(onSubmit) // returns a submit handler
3044
3507
  * form.meta.submitting // form-level reactive flag
@@ -3067,7 +3530,45 @@ type FormMeta<F = unknown> = FieldState<F> & {
3067
3530
  * For schemas without write-boundary wrappers or transforms the three
3068
3531
  * shapes coincide.
3069
3532
  */
3070
- type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form> = {
3533
+ /**
3534
+ * Read-only view returned by `form.blankPaths.value`. Exposes lookup
3535
+ * (`.has`), aggregate (`.size`), and iteration over the form's
3536
+ * blank-marked paths.
3537
+ *
3538
+ * `.has(input)` and the iterator yield consistent results across both
3539
+ * input conventions the library accepts everywhere a path is named:
3540
+ *
3541
+ * - **Dotted string**: `'user.email'`, matching what `register('user.email')`
3542
+ * or `setValue('items.0.sku', …)` accept. Convenient when no segment
3543
+ * contains a literal dot.
3544
+ * - **Array form**: `['user', 'email']`, mirroring `register(['user', 'email'])`.
3545
+ * Required when a single segment contains literal dots (e.g.
3546
+ * `['address.primary']` for a top-level key named `address.primary` —
3547
+ * the dotted form `'address.primary'` would be parsed as two
3548
+ * segments).
3549
+ *
3550
+ * Iteration yields `Path` arrays so the structure is unambiguous —
3551
+ * consumers building debug UI or persisting the set never have to guess
3552
+ * whether a dot in a segment is a separator or part of the name.
3553
+ *
3554
+ * Mutating the view does nothing — writes still go through
3555
+ * `setValue(path, unset)`, `markBlank()` on a register binding, or the
3556
+ * directive's input listener on numeric clear.
3557
+ */
3558
+ interface BlankPathsView {
3559
+ /** Number of blank-marked paths. */
3560
+ readonly size: number;
3561
+ /**
3562
+ * `true` when the path is in the blank set. Accepts dotted-string
3563
+ * form (parsed by [[parseDottedPath]]) or the array form.
3564
+ */
3565
+ has(input: string | Path): boolean;
3566
+ /** Snapshot of all blank-marked paths as segment arrays. */
3567
+ values(): readonly Path[];
3568
+ /** Iterates the blank-marked paths as segment arrays. */
3569
+ [Symbol.iterator](): IterableIterator<Path>;
3570
+ }
3571
+ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form, K extends FormKey = FormKey> = {
3071
3572
  /**
3072
3573
  * Wraps your submit logic with validation and error routing.
3073
3574
  *
@@ -3306,8 +3807,96 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3306
3807
  * const result = parseApiErrors(serverPayload, { formKey: form.key })
3307
3808
  * if (result.ok) form.setFieldErrors(result.errors)
3308
3809
  * ```
3810
+ *
3811
+ * Typed as the literal `K` when an explicit `key` was passed; falls
3812
+ * back to `FormKey` when omitted (auto-generated id).
3813
+ */
3814
+ key: K;
3815
+ /**
3816
+ * `true` while a function-form `defaultValues` factory is in flight
3817
+ * — between `useForm` construction and the moment the factory
3818
+ * resolves (sync function on the next microtask; async function when
3819
+ * its promise settles). `false` otherwise, including when
3820
+ * `defaultValues` is a plain value.
3821
+ *
3822
+ * The form is fully usable while `hydrating` is `true` — it holds
3823
+ * the schema's slim defaults. The flag exists so templates can show
3824
+ * a spinner / dim the form while real data loads:
3825
+ *
3826
+ * ```vue
3827
+ * <div :aria-busy="form.hydrating">…</div>
3828
+ * ```
3829
+ *
3830
+ * Exposed as an auto-unwrapping `boolean` (no `.value`); reactivity
3831
+ * is preserved via a getter that tracks the underlying ref at the
3832
+ * access site, so `watch(() => form.hydrating, …)` and template
3833
+ * reads both fire on change. Reading this property activates the
3834
+ * form's factory under the lazy-by-default rule.
3835
+ */
3836
+ readonly hydrating: boolean;
3837
+ /**
3838
+ * The error from the most recent function-form `defaultValues` factory,
3839
+ * normalized to a `ValidationError` (code `atta:hydration-failed`) so the
3840
+ * shape matches every other surface in `form.errors` / `form.meta.errors`.
3841
+ * `null` on construction, on successful resolution, and whenever no
3842
+ * factory has fired. Updates with each `form.rehydrate()` call.
3843
+ *
3844
+ * Distinct from `meta.submitError` so retry buttons and recovery UX can
3845
+ * stay focused on the load-time failure without entangling the submit
3846
+ * pipeline. Read directly in templates and script (no `.value`);
3847
+ * reactivity is preserved via a getter:
3848
+ *
3849
+ * ```vue
3850
+ * <p v-if="form.hydrateError">{{ form.hydrateError.message }}</p>
3851
+ * ```
3309
3852
  */
3310
- key: FormKey;
3853
+ readonly hydrateError: ValidationError | null;
3854
+ /**
3855
+ * `true` once the form's defaults have been applied — either a plain
3856
+ * `defaultValues` value at construction or an async factory whose
3857
+ * settle completed successfully. Stays `false` for dormant lazy
3858
+ * forms (factory not yet activated) and for failed activations
3859
+ * (`hydrateError` set). Once `true`, stays `true` through refetches
3860
+ * so stale-while-revalidate UIs can keep rendering the prior values
3861
+ * while a `rehydrate()` is in flight.
3862
+ *
3863
+ * Composes with `hydrating` and `hydrateError`:
3864
+ *
3865
+ * ```vue
3866
+ * <Spinner v-if="!form.ready && form.hydrating" />
3867
+ * <ErrorBanner v-if="!form.ready && form.hydrateError" :error="form.hydrateError" />
3868
+ * <form v-if="form.ready">…</form>
3869
+ * ```
3870
+ *
3871
+ * Exposed as a reactive `boolean` (no `.value`). Reading it activates
3872
+ * the factory under the lazy-by-default rule — observing readiness
3873
+ * implies use.
3874
+ */
3875
+ readonly ready: boolean;
3876
+ /**
3877
+ * Re-fire the captured `defaultValues` factory and re-apply its
3878
+ * payload over the current form values. Useful when the upstream
3879
+ * source changes (the user picks a different draft, a background
3880
+ * sync indicates fresh server data, etc.).
3881
+ *
3882
+ * Resolves after `hydrating` flips back to `false`. Throws
3883
+ * synchronously when the form was constructed with a plain-value
3884
+ * `defaultValues` (nothing to re-fire). Does NOT clear dirty /
3885
+ * touched / submit state — chain `form.reset()` for that.
3886
+ */
3887
+ rehydrate(): Promise<void>;
3888
+ /**
3889
+ * Idempotent activation. Forms are lazy-by-default: a function-form
3890
+ * `defaultValues` factory fires on the first reactive interaction
3891
+ * (reading `form.values`, calling `form.setValue`, etc.). Call
3892
+ * `form.activate()` to kick the factory explicitly — typically from
3893
+ * `setup` so SSR's `onServerPrefetch` hook awaits the resolution
3894
+ * before the page renders. Subsequent calls return the in-flight
3895
+ * promise until the factory settles, after which they resolve
3896
+ * immediately. Plain-value forms (no factory captured) always
3897
+ * return a resolved promise.
3898
+ */
3899
+ activate(): Promise<void>;
3311
3900
  /**
3312
3901
  * Reactive map of field errors, keyed by dotted path. Populated
3313
3902
  * automatically by `handleSubmit` and per-field validation; cleared
@@ -3426,7 +4015,7 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3426
4015
  clearFormErrors: () => void;
3427
4016
  /**
3428
4017
  * Form-level reactive flags, counters, and aggregates (`dirty`,
3429
- * `valid`, `submitting`, `submitCount`, and the flat `errors`
4018
+ * `valid`, `submitting`, `submissionAttempts`, and the flat `errors`
3430
4019
  * array). See `FormMeta` for the full shape. Read leaves directly
3431
4020
  * with no `.value`.
3432
4021
  *
@@ -3446,13 +4035,14 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3446
4035
  * - the dirty baseline (so the next edit flips `dirty` correctly);
3447
4036
  * - field errors;
3448
4037
  * - touched / focused / blurred per-field flags;
3449
- * - submission state (`submitting` / `submitCount` / `submitError`);
4038
+ * - submission state (`submitting` / `submissionAttempts` /
4039
+ * `submitted` / `submitError`);
3450
4040
  * - the persisted draft, if persistence is configured.
3451
4041
  *
3452
4042
  * The next edit on a still-mounted opted-in input will start
3453
4043
  * persisting again automatically.
3454
4044
  */
3455
- reset: (nextDefaultValues?: DeepPartial<DefaultValuesShape<Form>>) => void;
4045
+ reset: (nextDefaultValues?: DefaultValuesInput<Form>) => void;
3456
4046
  /**
3457
4047
  * Restore a single field (or a sub-tree like `'user'`) to its
3458
4048
  * initial value. Clears errors and touched flags for the field
@@ -3565,6 +4155,21 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3565
4155
  * `options` is forwarded to `Element.scrollIntoView` unchanged.
3566
4156
  */
3567
4157
  scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
4158
+ /**
4159
+ * Drive the form's `onInvalidSubmit` policy imperatively. The same
4160
+ * focus/scroll behavior `handleSubmit` runs after a failed submit,
4161
+ * but available standalone. Defaults to the policy configured via
4162
+ * `useForm({ onInvalidSubmit })` (or `'focus-first-error'` when
4163
+ * omitted). Pass an explicit policy to override for one call.
4164
+ *
4165
+ * Used by `useWizard` after navigating to the first failing form
4166
+ * during `wizard.handleSubmit`, so the failing form's own configured
4167
+ * policy fires once its DOM is in view.
4168
+ *
4169
+ * No-op when no errored field is currently registered or when the
4170
+ * resolved policy is `'none'`.
4171
+ */
4172
+ applyInvalidSubmitPolicy: (policy?: OnInvalidSubmitPolicy) => void;
3568
4173
  /**
3569
4174
  * Programmatically mark fields as touched — the sticky flag the
3570
4175
  * standard "show errors after interaction" pattern reads. Closes
@@ -3613,28 +4218,31 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
3613
4218
  /** Replace the element at `index` with `value`. No-op when out of range. */
3614
4219
  replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
3615
4220
  /**
3616
- * Read-only view of the form's blank path set. Each entry
3617
- * is a canonical `PathKey` (the `JSON.stringify(segments)` form
3618
- * `canonicalizePath` produces). The set is reactive — Vue 3.5
3619
- * tracks `.has()` / `for..of` / size accesses, so consumers can
3620
- * drive conditional UI off it directly:
4221
+ * Read-only view of the form's blank path set. Reactive — Vue 3.5
4222
+ * tracks `.has()` / `for..of` / size accesses, so consumers can drive
4223
+ * conditional UI off it directly:
3621
4224
  *
3622
4225
  * ```ts
3623
4226
  * watchEffect(() => {
3624
4227
  * if (form.blankPaths.value.size > 0) {
3625
- * console.warn('unanswered fields:', [...form.blankPaths.value])
4228
+ * const paths = [...form.blankPaths.value] // Path[][] — array of segments per entry
4229
+ * console.warn('unanswered fields:', paths.map((p) => p.join('.')))
3626
4230
  * }
3627
4231
  * })
3628
4232
  * ```
3629
4233
  *
4234
+ * `.has(input)` accepts the dotted-string form (`'user.email'`) or
4235
+ * the array form (`['user', 'email']`). The array form disambiguates
4236
+ * keys with literal dots (e.g. `['address.primary']`). See
4237
+ * [[BlankPathsView]] for the full surface.
4238
+ *
3630
4239
  * For per-path access, use `form.fields.<path>.blank`.
3631
4240
  * Writes happen through `setValue(path, unset)`,
3632
4241
  * `markBlank()` on a register binding, and the directive's
3633
- * input listener on numeric clear. Mutating the snapshot returned
3634
- * here does nothing — it's `Object.freeze`-d.
4242
+ * input listener on numeric clear.
3635
4243
  */
3636
- blankPaths: ComputedRef<ReadonlySet<string>>;
4244
+ blankPaths: ComputedRef<BlankPathsView>;
3637
4245
  };
3638
4246
 
3639
- export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
3640
- export type { AttaformDefaults as A, NestedType as B, CoercionRegistry as C, DeepPartial as D, OnInvalidSubmitPolicy 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, OnSubmit as P, Path as Q, RegisterValue as R, SlimPrimitiveKind as S, PathKey as T, UseFormConfiguration as U, ValidationError as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, CoercionEntry as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterOptions as a3, RegisterSelectModifier as a4, RegisterTextModifier as a5, RegisterTransform as a6, Segment as a7, SetValueCallback as a8, SetValuePayload as a9, SettledValidationStatus as aa, ShouldShowErrorsConfig as ab, SlimRuntimeOf as ac, SubmitHandler as ad, Unset as ae, ValidateOn as af, ValidateOnConfig as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, SchemaFactoryOptions as ar, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ShouldShowErrors as f, ApiErrorEnvelope as g, ApiErrorDetails as h, ApiErrorEntry as i, ArrayItem as j, ArrayPath as k, CoercionResult as l, CustomDirectiveRegisterAssignerFn as m, DefaultValuesResponse 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, FormStorage as w, FormStorageKind as x, HistoryConfig as y, IsUnion as z };
4247
+ export { ROOT_PATH as a2, ROOT_PATH_KEY as a3, canonicalizePath as aq, isPathPrefix as ar, isUnset as as, parseDottedPath as at, unset as au };
4248
+ export type { PersistConfigOptions as $, AttaformDefaults as A, IsUnion as B, CoercionEntry as C, DefaultValuesInput as D, ErrorsProxyShape as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, NestedType as O, OnError as P, OnInvalidSubmitPolicy as Q, RegisterValue as R, Segment as S, OnSubmit as T, UseFormConfiguration as U, ValidationError as V, PartialFlatPath as W, Path as X, PathKey as Y, PendingValidationStatus as Z, PersistConfig as _, AbstractSchema as a, PersistIncludeMode as a0, Primitive as a1, ReactiveValidationStatus as a4, RegisterDirective as a5, RegisterFlatPath as a6, RegisterOptions as a7, RegisterSelectModifier as a8, RegisterTextModifier as a9, RegisterTransform as aa, SetValueCallback as ab, SetValuePayload as ac, SettledValidationStatus as ad, ShouldShowErrorsConfig as ae, SlimPrimitiveKind as af, SlimRuntimeOf as ag, SubmitHandler as ah, Unset as ai, ValidateOn as aj, ValidateOnConfig as ak, ValidationResponse as al, ValidationResponseWithoutValue as am, ValueOfUnion as an, WriteMeta as ao, WriteShape as ap, SchemaFactoryOptions as av, PersistOptInRegistry as aw, UseFormReturnType as b, RegisterModelDynamicCustomDirective as c, ShouldShowErrors as d, ApiErrorEnvelope as e, ApiErrorDetails as f, ApiErrorEntry as g, ArrayItem as h, ArrayPath as i, CoercionRegistry as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DeepPartial as m, DefaultValuesResponse as n, DefaultValuesShape as o, FieldMetaPayload as p, FieldState as q, FieldStateMap as r, FieldStateMapEntry as s, FlatPath as t, FormErrorRecord as u, FormErrorsSurface as v, FormMeta as w, FormStorage as x, FormStorageKind as y, HistoryConfig as z };