attaform 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.5UhpSVFI.cjs +63 -0
  40. package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
  41. package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
  42. package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
  43. package/dist/shared/attaform.Bgu9l6OG.d.cts +1651 -0
  44. package/dist/shared/attaform.BmDBu4ql.d.ts +1651 -0
  45. package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
  46. package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
  47. package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.BsMdl-35.d.cts} +754 -146
  48. package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.BsMdl-35.d.mts} +754 -146
  49. package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.BsMdl-35.d.ts} +754 -146
  50. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  51. package/dist/shared/{attaform.C6lbmMUe.d.ts → attaform.C3x1hKJC.d.mts} +4 -4
  52. package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.CWs1Z3p7.d.ts} +57 -23
  53. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  54. package/dist/shared/{attaform.C5MH4lNh.d.mts → attaform.CjmJpfLH.d.ts} +4 -4
  55. package/dist/shared/{attaform.Drt6fivF.mjs → attaform.CtNUB9nf.mjs} +74 -76
  56. package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
  57. package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.D-hDvb98.d.cts} +57 -23
  58. package/dist/shared/attaform.DAH3kvav.d.mts +1651 -0
  59. package/dist/shared/{attaform.BPRHR3Zs.cjs → attaform.DUHru0OF.cjs} +83 -85
  60. package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
  61. package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
  62. package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
  63. package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
  64. package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
  65. package/dist/shared/{attaform.DtMN-MAm.d.cts → attaform.Dzi89x8N.d.cts} +4 -4
  66. package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
  67. package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
  68. package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
  69. package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
  70. package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
  71. package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
  72. package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.bH7WvNad.d.mts} +57 -23
  73. package/dist/vite.cjs +270 -2
  74. package/dist/vite.cjs.map +1 -1
  75. package/dist/vite.mjs +266 -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 +19 -5
  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
@@ -0,0 +1,1651 @@
1
+ import { F as FormKey, af as SlimPrimitiveKind, C as CoercionEntry, j as CoercionRegistry, G as GenericForm, Y as PathKey, X as Path, V as ValidationError, a as AbstractSchema, d as ShouldShowErrors, ao as WriteMeta, m as DeepPartial, ap as WriteShape, aj as ValidateOn, aw as PersistOptInRegistry, S as Segment, A as AttaformDefaults, b as UseFormReturnType, R as RegisterValue } from './attaform.BsMdl-35.js';
2
+ import { Ref, ComputedRef, App, InjectionKey } from 'vue';
3
+
4
+ /**
5
+ * Public types for `useWizard` — the multistep-form orchestrator.
6
+ *
7
+ * The wizard is built around an ordered list of step slots. Each slot
8
+ * resolves to a participating form: an existing `useForm` reference, a
9
+ * bare string key (desugared to a noop form so affordance steps
10
+ * participate uniformly), an eagerly-evaluated function slot for
11
+ * runtime branching, or a `lazy()`-wrapped function slot that caches
12
+ * its resolution and re-fires only on its own tracked deps.
13
+ *
14
+ * The wizard surface is loosely keyed (`Record<FormKey, …>`).
15
+ * Cross-component flows threaded through `injectWizard` lose lexical
16
+ * key knowledge anyway, so the public read surface is a string-keyed
17
+ * record. Typed per-form access flows back through the original form
18
+ * refs and through `wizard.handleSubmit`'s `ctx.get(formRef)` accessor.
19
+ */
20
+
21
+ /**
22
+ * Minimum structural shape the wizard requires from a participating
23
+ * form. Constraining to the full `UseFormReturnType` would force
24
+ * contravariant unification of the storage / read shapes across all
25
+ * steps; the wizard does not care about those — it routes by `key` at
26
+ * runtime and exposes the original form objects untouched.
27
+ */
28
+ type AnyForm = {
29
+ readonly key: FormKey;
30
+ };
31
+ /**
32
+ * Per-form summary surface — what `wizard.statuses[key]` exposes (and
33
+ * what `defaultStatuses` seeds). Distinct from `form.meta`: `FormStatus`
34
+ * is the cross-step rollup optimized for template ergonomics
35
+ * (`{{ wizard.statuses.cargo.valid }}`), while `form.meta` carries the
36
+ * full per-form lifecycle surface.
37
+ *
38
+ * Field semantics:
39
+ * - `valid` — `form.meta.valid`. `false` while errors exist or while
40
+ * the first-validation-done gate has not flipped.
41
+ * - `dirty` — `form.meta.dirty`. `true` once any value differs from
42
+ * the original defaults.
43
+ * - `submitted` — `form.meta.submitted`. `true` once a `handleSubmit`
44
+ * callback has resolved without throwing. A failed submit
45
+ * (validation or callback rejection) leaves this `false`;
46
+ * `submissionAttempts > 0` is the "user has tried" signal.
47
+ * - `errorCount` — `form.meta.errorCount`. Count of active validation
48
+ * errors (zero when valid).
49
+ *
50
+ * Noop forms generated for string slots surface as default-valid
51
+ * (`{ valid: true, dirty: false, submitted: false, errorCount: 0 }`).
52
+ */
53
+ type FormStatus = {
54
+ readonly valid: boolean;
55
+ readonly dirty: boolean;
56
+ readonly submitted: boolean;
57
+ readonly errorCount: number;
58
+ };
59
+ /**
60
+ * Flat error shape returned per form by `wizard.allErrors[key]`. Each
61
+ * entry carries the formKey + path tuple so consumers can route to the
62
+ * offending field from a wizard-wide error summary.
63
+ */
64
+ type AggregateError = {
65
+ readonly formKey: FormKey;
66
+ readonly path: ReadonlyArray<string | number>;
67
+ readonly message: string;
68
+ readonly code?: string;
69
+ };
70
+ /**
71
+ * Mirror of `form.values`' call-or-read pattern, one level deep.
72
+ * Drillable as `wizard.statuses.cargo.valid` (readable), as
73
+ * `wizard.statuses('cargo')` (callable single-key), or as
74
+ * `wizard.statuses()` (callable no-arg returns the whole record).
75
+ */
76
+ type WizardStatusesProxy<S extends Record<string, FormStatus>> = ((key?: keyof S) => FormStatus | S) & Readonly<S>;
77
+ /**
78
+ * One compiled position in the wizard's flow. The wizard surface
79
+ * exposes an ordered array of these as `wizard.steps`, plus a
80
+ * `wizard.forms` record keyed by `step.key` for direct lookup.
81
+ *
82
+ * String slots in the source `steps` array desugar to noop forms
83
+ * before compilation, so every compiled step carries a `form`
84
+ * regardless of source kind.
85
+ */
86
+ type CompiledStep = {
87
+ readonly key: FormKey;
88
+ readonly form: AnyForm;
89
+ };
90
+ /**
91
+ * Shape of a participating form as seen from inside a function slot's
92
+ * `ctx.forms[key]` lookup. Adds `values` to the structural `AnyForm`
93
+ * minimum so routing decisions can read live form state.
94
+ *
95
+ * Values are typed loose because the wizard does not generically thread
96
+ * each step's schema through `ctx.forms`. For typed access inside slot
97
+ * bodies, close over the original form ref instead of routing through
98
+ * `ctx.forms`.
99
+ */
100
+ type WizardCtxForm = AnyForm & {
101
+ readonly values: Readonly<Record<string, unknown>>;
102
+ };
103
+ /**
104
+ * Context object passed to function slots in the `steps` array. The
105
+ * `forms` record exposes the wizard's statically-known forms (every
106
+ * top-level `AnyForm` slot plus every noop form synthesized for a
107
+ * top-level string slot). `currentKey` mirrors the live wizard step.
108
+ *
109
+ * Function slots re-evaluate reactively when the values they read
110
+ * mutate (typically `ctx.forms.<key>.values.<path>`). The `forms`
111
+ * accumulator itself is stable across re-evaluations so the slot's
112
+ * lookup identity stays referentially equal. Effectful slot bodies
113
+ * should be avoided; routing decisions live here.
114
+ */
115
+ type WizardCtx = {
116
+ readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
117
+ readonly currentKey: FormKey | undefined;
118
+ };
119
+ /**
120
+ * Internal phantom brand for `LazyMarker`. The runtime brand symbol
121
+ * lives in `core/wizard-lazy.ts`; this declaration keeps the marker
122
+ * type unforgeable without circular module imports.
123
+ */
124
+ declare const _lazyBrand: unique symbol;
125
+ /**
126
+ * Brand-typed marker returned by `lazy((ctx) => …)`. Wrapping a
127
+ * function slot in `lazy()` gives that slot its own memoization cache:
128
+ * the resolver fires once on the first compile pass, and the result
129
+ * stays cached until one of the resolver's own tracked reactive reads
130
+ * changes (or `wizard.reset()` invalidates the cache). Heavy or
131
+ * one-shot lookups (network-backed factories, expensive derivations)
132
+ * do not re-fire because an unrelated slot's deps changed.
133
+ *
134
+ * Construct via the `lazy()` helper exported from the same entry as
135
+ * `useWizard`. The marker is opaque at the type level; consumers do
136
+ * not assemble it directly.
137
+ */
138
+ type LazyMarker<Ctx = WizardCtx> = {
139
+ readonly [_lazyBrand]: true;
140
+ readonly resolve: (ctx: Ctx) => AnyForm | string | undefined;
141
+ };
142
+ /**
143
+ * One position in the source `useWizard({ steps })` array. Each slot
144
+ * resolves to a compiled `{ key, form }` step:
145
+ *
146
+ * - `AnyForm` — a form declared via `useForm`. Surfaced as-is.
147
+ * - `string` — bare key. The wizard generates a noop form
148
+ * under the hood so the external surface stays
149
+ * uniform across affordance positions (intro,
150
+ * terms, congratulations, review surfaces).
151
+ * - function — eager slot, re-evaluates reactively. Returns
152
+ * one of the above, or `undefined` to drop the
153
+ * slot from the compiled list.
154
+ * - `LazyMarker` — memoized function slot (see `lazy`).
155
+ */
156
+ type StepSlot<Ctx = WizardCtx> = AnyForm | string | ((ctx: Ctx) => AnyForm | string | undefined) | LazyMarker<Ctx>;
157
+ /**
158
+ * Shape returned by the `restore` callback. Carries the active step's
159
+ * key; intentionally open-ended (object form) so future additions land
160
+ * without a callback-signature break.
161
+ */
162
+ type WizardRestoreState = {
163
+ readonly step?: FormKey;
164
+ };
165
+ /**
166
+ * `restore` callback signature. Invoked at construction and watched
167
+ * reactively via `watchEffect` so external state changes (browser
168
+ * back/forward, cross-tab events, route changes) re-apply through the
169
+ * wizard. Returning `undefined` falls through to the first step.
170
+ */
171
+ type WizardRestoreFn = () => WizardRestoreState | undefined;
172
+ /**
173
+ * `persist` callback signature. Invoked whenever `wizard.currentStep`
174
+ * changes; the wizard diffs against the last persisted value to break
175
+ * the restore-persist loop, so the callback only fires when the active
176
+ * step actually moves.
177
+ */
178
+ type WizardPersistFn = (state: WizardRestoreState) => void;
179
+ /**
180
+ * Submit context passed to the `onSubmit` callback registered via
181
+ * `wizard.handleSubmit(onSubmit, onError?)`. Same shape on every step;
182
+ * `isFinal` distinguishes intermediate vs final calls.
183
+ *
184
+ * - `values` — namespaced aggregate keyed by form key, mirroring
185
+ * `wizard.allValues`. Reflects parsed output for every
186
+ * form whose validation has settled; noops contribute an
187
+ * empty record.
188
+ * - `get(form)` — typed accessor that reads the parsed output for a
189
+ * specific form ref. Works across cross-component graphs
190
+ * because the form ref carries its schema info.
191
+ * - `currentKey` — key of the step that fired this submission.
192
+ * - `isFinal` — `true` when `currentKey` is the last position in
193
+ * `wizard.steps`. Intermediate calls validate the active
194
+ * form only and advance; final calls validate every form
195
+ * and stay on the terminal step.
196
+ */
197
+ type WizardSubmitContext = {
198
+ readonly values: Readonly<Record<FormKey, unknown>>;
199
+ readonly get: <F extends AnyForm>(form: F) => F extends {
200
+ readonly values: infer V;
201
+ } ? V : unknown;
202
+ readonly currentKey: FormKey;
203
+ readonly isFinal: boolean;
204
+ };
205
+ /**
206
+ * `onSubmit` callback registered via `wizard.handleSubmit`. Sync or
207
+ * async; the returned promise gates `wizard.submitting`.
208
+ */
209
+ type WizardOnSubmit = (ctx: WizardSubmitContext) => void | Promise<void>;
210
+ /**
211
+ * Optional `onError` callback registered via `wizard.handleSubmit`.
212
+ * Receives the aggregate error list — entries originate from per-form
213
+ * validation and activation failures (`atta:activation-failed`). Sync
214
+ * or async; the returned promise gates `wizard.submitting`.
215
+ */
216
+ type WizardOnError = (errors: readonly AggregateError[]) => void | Promise<void>;
217
+ /**
218
+ * Options for `useWizard({ steps, … })`. `steps` is the only required
219
+ * field; the rest are optional and default sensibly for the common
220
+ * URL-synchronized wizard case.
221
+ */
222
+ type WizardOptions = {
223
+ /**
224
+ * Ordered list of slots that compile into the wizard's positional
225
+ * step list. See `StepSlot` for the per-slot shape contract.
226
+ */
227
+ readonly steps: ReadonlyArray<StepSlot>;
228
+ /**
229
+ * Identifier used to register the wizard handle in the per-app
230
+ * registry. Descendant components call `injectWizard(key)` to reach
231
+ * the same wizard without prop-threading. Anonymous wizards (option
232
+ * omitted) get a synthetic `__atta:anon-wizard:<id>` key resolved
233
+ * via `useId()` so SSR-rendered and client-hydrated trees agree on
234
+ * the same registry entry; the synthetic key is opaque and
235
+ * descendants reach an anonymous wizard via ambient `injectWizard()`
236
+ * rather than by key.
237
+ *
238
+ * Duplicate-key registration is first-wins-silently (dev-warn on the
239
+ * second registration) to mirror `useForm`'s shared-key behavior.
240
+ * The dev-warn fires only for explicit keys — two anonymous wizards
241
+ * are guaranteed distinct synthetic keys, so the warning never
242
+ * misfires on independent anonymous wizards on the same page.
243
+ */
244
+ readonly key?: string;
245
+ /**
246
+ * Seed status payload used while a form is pre-resolved (async
247
+ * `defaultValues` in flight, or wizard-deferred non-current).
248
+ * Mirrors `defaultValues`' trichotomy: plain object, sync factory,
249
+ * or async factory.
250
+ *
251
+ * Status resolution priority per form:
252
+ * 1. `store.defaultsResolved === true` → derive from `form.meta`
253
+ * 2. else noop form → built-in always-valid status
254
+ * 3. else seed value for this key → frozen seed
255
+ * 4. else → pending sentinel
256
+ *
257
+ * Unknown keys in the seed object dev-warn so a stale resume payload
258
+ * surfaces at construction.
259
+ */
260
+ readonly defaultStatuses?: Record<string, FormStatus> | (() => Record<string, FormStatus>) | (() => Promise<Record<string, FormStatus>>);
261
+ /**
262
+ * Optional progress override. When omitted, the wizard exposes
263
+ * `progress` as `valid_step_count / count` (normalised to `[0, 1]`).
264
+ * When provided, the returned number is used as-is — the consumer is
265
+ * responsible for any normalisation.
266
+ *
267
+ * The override is invoked inside a Vue `computed` so it must be
268
+ * synchronous and may only read reactive sources.
269
+ */
270
+ readonly progress?: (steps: ReadonlyArray<CompiledStep>) => number;
271
+ /**
272
+ * When `wizard.handleSubmit` finds errors, automatically focus the
273
+ * first failing form: jump to its step and invoke its
274
+ * `applyInvalidSubmitPolicy()` (focus / scroll per the form's own
275
+ * `onInvalidSubmit` configuration). Default `true`; pass `false` to
276
+ * keep the active step where the user left it and handle navigation
277
+ * manually in the `onError` callback.
278
+ */
279
+ readonly focusFirstError?: boolean;
280
+ /**
281
+ * Source of truth for the active step. Invoked at construction and
282
+ * re-evaluated reactively via `watchEffect`. Default callback reads
283
+ * `?step=<key>` from the URL via `wizard-history.ts`; pass `false`
284
+ * to disable URL sync, or provide a custom callback for non-router
285
+ * persistence (localStorage, broadcast channel, etc.).
286
+ */
287
+ readonly restore?: WizardRestoreFn | false;
288
+ /**
289
+ * Destination for the active step. Invoked whenever `currentStep`
290
+ * changes, with a diff check to break the restore-persist loop.
291
+ * Default callback writes `?step=<key>` via `wizard-history.ts`;
292
+ * pass `false` to disable persistence, or provide a custom callback
293
+ * to scope the param name or write to alternate storage.
294
+ */
295
+ readonly persist?: WizardPersistFn | false;
296
+ };
297
+ /**
298
+ * Predicate: is the steps tuple statically guaranteed to compile to a
299
+ * non-empty list? A tuple passes when (a) it's not the empty array
300
+ * literal and (b) it carries no function or `lazy()` slot — those
301
+ * slot kinds can resolve to `undefined` at runtime and drop the
302
+ * compiled position. Form slots and bare-string affordance slots
303
+ * always preserve their position; a tuple made of only those kinds is
304
+ * statically safe.
305
+ *
306
+ * Used to narrow `currentStep` / `activeForm` to their non-`undefined`
307
+ * shapes in the common-case wizard, while keeping the honest union
308
+ * everywhere a runtime drop is reachable.
309
+ */
310
+ type StaticallyNonEmpty<S> = S extends readonly [] ? false : S extends readonly (infer Item)[] ? Item extends LazyMarker | ((...args: unknown[]) => unknown) ? false : true : false;
311
+ /** Active step's key, narrowed to `string` when `S` is statically safe. */
312
+ type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
313
+ /** Active step's form handle, narrowed to `AnyForm` when `S` is statically safe. */
314
+ type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? AnyForm : AnyForm | undefined;
315
+ /**
316
+ * Recursive tuple walk that builds the static portion of
317
+ * `wizard.forms`. Each step slot contributes to the record:
318
+ *
319
+ * - **String slot** (`'review'`): the literal becomes the record key
320
+ * and the value is `AnyForm` (the noop form synthesized for the
321
+ * affordance position is opaque at the type level).
322
+ * - **Form slot** (a `useForm` reference with a literal `key` field):
323
+ * the form's own `key` becomes the record key, and the value is
324
+ * the concrete form handle type — so drilling
325
+ * `wizard.forms.shipping.values.address` carries the schema-derived
326
+ * field types through.
327
+ * - **Function / `lazy()` slot**: contributes nothing to the static
328
+ * map. Runtime-resolved forms are still reachable via the
329
+ * catch-all index signature on `WizardForms` (typed as `AnyForm`).
330
+ *
331
+ * Recursion is bounded by the tuple length; real-world wizards land
332
+ * well below the TS instantiation budget.
333
+ */
334
+ type FormsRecordOf<S> = S extends readonly [
335
+ infer First,
336
+ ...infer Rest extends ReadonlyArray<StepSlot>
337
+ ] ? (First extends string ? {
338
+ readonly [P in First]: AnyForm;
339
+ } : First extends {
340
+ readonly key: infer K extends string;
341
+ } ? {
342
+ readonly [P in K]: First;
343
+ } : unknown) & FormsRecordOf<Rest> : unknown;
344
+ /**
345
+ * `wizard.forms` typed view. Combines the static per-step type map
346
+ * with a catch-all `Record<FormKey, AnyForm>` fallback so:
347
+ *
348
+ * - Statically known slot keys → concrete form type via `FormsRecordOf`
349
+ * - Any other string key → `AnyForm` via the index signature
350
+ *
351
+ * The intersection collapses to the concrete form for statically
352
+ * known keys (because the concrete form type extends `AnyForm`) and
353
+ * to `AnyForm` for unknown keys.
354
+ */
355
+ type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
356
+ /**
357
+ * Return shape of `useWizard({ steps, … })`. Every reactive read is a
358
+ * plain getter (no `.value`) — `wizard.currentStep`, `wizard.progress`,
359
+ * `wizard.allValues` track inside `computed` / template effects
360
+ * directly.
361
+ *
362
+ * Parameterized by the steps tuple `S` so active-position fields
363
+ * (`currentStep`, `activeForm`) narrow to non-undefined for the common
364
+ * case (all positional Form / string slots) and stay as honest unions
365
+ * when a function or `lazy()` slot can drop the compiled position at
366
+ * runtime. The `const` type parameter on `useWizard` preserves literal
367
+ * tuple types without consumer-side `as const`, so the narrowing
368
+ * happens automatically from the call site.
369
+ *
370
+ * - `currentStep` — key of the active step. Narrows to `string` when
371
+ * the steps tuple is statically guaranteed to
372
+ * compile to a non-empty list (all positional
373
+ * Form / string slots, no function or `lazy()`
374
+ * slots). Otherwise reads as `string | undefined`
375
+ * so the degenerate case (empty list at runtime)
376
+ * surfaces honestly.
377
+ * - `activeForm` — the active step's form handle. Same narrowing as
378
+ * `currentStep`. Noop forms cover string slots in
379
+ * the normal path.
380
+ * - `activeIndex` — 0-based position of the active step.
381
+ * - `isFinalStep` — `true` when `currentStep === steps[count - 1].key`.
382
+ * - `steps` — ordered list of compiled `{ key, form }` slots.
383
+ * - `forms` — record indexable by step key; the value is the
384
+ * full form handle resolved for that slot.
385
+ * - `count` — `steps.length`.
386
+ * - `statuses` — callable readonly proxy over the per-key
387
+ * `FormStatus` record. Noop-form keys always read
388
+ * as default-valid.
389
+ * - `allValues` — namespaced aggregate of each form's values, keyed
390
+ * by step key.
391
+ * - `allErrors` — namespaced aggregate of each form's validation
392
+ * errors, keyed by step key. Noop forms map to an
393
+ * empty list.
394
+ * - `progress` — normalised step-validity ratio (or the consumer's
395
+ * `progress` override). Forward-looking: noops count
396
+ * as always-valid.
397
+ * - `canAdvance` — `true` when `activeIndex < count - 1`. Pure
398
+ * positional check; navigation never gates on
399
+ * validity.
400
+ * - `canGoBack` — `true` when `activeIndex > 0`.
401
+ * - `complete` — `isFinalStep && every step's form is valid`.
402
+ * Forward-looking; reactive to current form
403
+ * validity. Gates "Finish button enable" style UI.
404
+ * - `done` — monotonic latch: flips `true` the first time a
405
+ * final-step `handleSubmit` resolves without
406
+ * throwing, and stays `true` through subsequent
407
+ * edits or invalidations. Only `reset()` flips it
408
+ * back. Gates "show success card" style UI that
409
+ * should reflect submission history rather than
410
+ * current validity.
411
+ * - `submitting` — `true` while a `wizard.handleSubmit` call is in
412
+ * flight. Global re-entrance guard: every
413
+ * navigation method also refuses while this is on.
414
+ * - `submissionAttempts` — count of `wizard.handleSubmit` invocations
415
+ * (success or failure). Always bumps, including on
416
+ * noop-form steps.
417
+ * - `visited` — append-only breadcrumb of navigated step keys.
418
+ * `back()` does not pop; the trail is the audit
419
+ * log, not the back-stack.
420
+ * - `next/back/goTo` — pure navigation. Refuses while `submitting`.
421
+ * - `handleSubmit(onSubmit, onError?)` — universal across all steps.
422
+ * Intermediate calls validate the active form and
423
+ * advance; final calls validate every form. Returns
424
+ * an event handler suitable for `<form @submit>` or
425
+ * imperative use.
426
+ * - `reset()` — zeros wizard lifecycle (`submissionAttempts`,
427
+ * `visited`), resets every form, returns
428
+ * `currentStep` to `steps[0].key`, and invokes
429
+ * `persist` with the cleared state.
430
+ */
431
+ type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
432
+ readonly key: string;
433
+ readonly currentStep: CurrentStepOf<S>;
434
+ readonly activeForm: ActiveFormOf<S>;
435
+ readonly activeIndex: number;
436
+ readonly isFinalStep: boolean;
437
+ readonly steps: ReadonlyArray<CompiledStep>;
438
+ readonly forms: WizardForms<S>;
439
+ readonly count: number;
440
+ readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
441
+ readonly allValues: Readonly<Record<FormKey, unknown>>;
442
+ readonly allErrors: Readonly<Record<FormKey, readonly AggregateError[]>>;
443
+ readonly progress: number;
444
+ readonly canAdvance: boolean;
445
+ readonly canGoBack: boolean;
446
+ readonly complete: boolean;
447
+ readonly done: boolean;
448
+ readonly submitting: boolean;
449
+ readonly submissionAttempts: number;
450
+ readonly visited: readonly FormKey[];
451
+ readonly next: () => Promise<void>;
452
+ readonly back: () => void;
453
+ readonly goTo: (key: string) => void;
454
+ readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
455
+ readonly reset: () => void;
456
+ };
457
+
458
+ /**
459
+ * Schema-driven coercion of user-typed DOM values at the v-register
460
+ * directive layer. When the slim schema declares a numeric or
461
+ * boolean type at a path, the directive coerces incoming string
462
+ * values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
463
+ * gate sees the write — making the schema authoritative for storage
464
+ * shape and freeing consumers from sprinkling `.number` modifiers
465
+ * across templates.
466
+ *
467
+ * Coercion is consumer-extensible: a `CoercionRegistry` is just an
468
+ * `Array<CoercionEntry>` keyed at config time by `(input, output)`
469
+ * `SlimPrimitiveKind` literals. The library ships
470
+ * `defaultCoercionRules` (string→number, string→boolean) and
471
+ * `defineCoercion` for type-narrowed authoring; consumers spread the
472
+ * defaults to extend or supply their own array to replace.
473
+ *
474
+ * Coercion applies ONLY to user-typed DOM values flowing through
475
+ * the directive's assigner. Programmatic writes (`form.setValue`,
476
+ * `setValueWithInternalPath`) bypass coercion — they're authoritative
477
+ * writes whose strict typing is on the caller. This mirrors the
478
+ * `transforms` pipeline's user-input-only contract.
479
+ */
480
+
481
+ /**
482
+ * Type-narrowing helper for authoring entries. At runtime it's
483
+ * identity; at compile time it preserves the `input` / `output`
484
+ * literal types so `transform`'s parameter is narrowed to the
485
+ * runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
486
+ *
487
+ * Without this helper, authoring `{ input: 'string', output:
488
+ * 'number', transform: (s) => ... }` against the broader
489
+ * `CoercionEntry` widens `s` to `string | number | boolean | ...`,
490
+ * forcing a cast in every transform body. `defineCoercion` is the
491
+ * opaque-free idiom.
492
+ */
493
+ declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
494
+ /**
495
+ * Internal index built from a `CoercionRegistry` at config-resolve
496
+ * time. Keyed by `${input}->${output}` for O(1) per-keystroke
497
+ * dispatch. The authoring shape (array, ergonomic, type-narrowing-
498
+ * friendly) and the dispatch shape (Map, fast) decouple cleanly.
499
+ */
500
+ type CoercionIndex = ReadonlyMap<`${SlimPrimitiveKind}->${SlimPrimitiveKind}`, CoercionEntry>;
501
+ /**
502
+ * The library's built-in registry. Two cells: string→number and
503
+ * string→boolean. Re-exported so consumers can spread it when
504
+ * supplying a custom registry that extends defaults.
505
+ */
506
+ declare const defaultCoercionRules: CoercionRegistry;
507
+
508
+ /**
509
+ * Per-form closure state — the single store owned by each `useForm` call.
510
+ * Bundles the form value, the summary record, element references, field
511
+ * state, the meta tracker, and the error stores under one keyed-by-
512
+ * `(formKey, path)` instance so cross-form DOM state cannot collide.
513
+ *
514
+ * This is NOT a singleton. Each call to `useForm` creates its own FormStore
515
+ * instance and holds onto it via closure. The registry provides SSR
516
+ * hydration; otherwise the state is per-component-per-form.
517
+ */
518
+ /**
519
+ * Per-path field status. Replaced wholesale (not mutated in place) on
520
+ * every change. Three semantic groups:
521
+ *
522
+ * - `connected` — is a DOM element registered for this path?
523
+ * - `focused` / `blurred` — DOM-state flags. `null` while no element
524
+ * is connected (no DOM means the concepts don't apply); plain
525
+ * booleans once connected, with the invariant `blurred === !focused`
526
+ * enforced by `markFocused`.
527
+ * - `touched` — interaction history, not DOM state. Always a plain
528
+ * boolean: `false` at registration, sticky `true` after first blur,
529
+ * cleared only by `form.reset()` / `form.resetField(path)`. Persists
530
+ * across disconnects so v-if'd-away fields don't lose their touched
531
+ * state on rehide (wizard "show review of touched fields" patterns
532
+ * rely on this).
533
+ */
534
+ type FieldRecord = {
535
+ readonly path: Path;
536
+ readonly updatedAt: string | null;
537
+ readonly connected: boolean;
538
+ readonly focused: boolean | null;
539
+ readonly blurred: boolean | null;
540
+ readonly touched: boolean;
541
+ };
542
+ /** Per-path DOM element tracking. Client-only. */
543
+ type ElementRecord = {
544
+ /**
545
+ * Original Path captured at first registration. Stored alongside the
546
+ * elements Set so the DOM-order sort cache can recover the structured
547
+ * Path without round-tripping through `JSON.parse(pathKey)`.
548
+ */
549
+ readonly path: Path;
550
+ readonly elements: Set<HTMLElement>;
551
+ };
552
+ /**
553
+ * Per-path record stored in `originals`. Pairing `segments` with the tracked
554
+ * value means `dirty` and `resetField`'s container loop don't have to
555
+ * `JSON.parse(pathKey)` on every iteration — the canonical Path is already
556
+ * sitting next to the value it belongs to. PathKey still keys the Map (the
557
+ * stable string is the only collision-free identifier), but downstream
558
+ * iteration reads `segments` directly.
559
+ */
560
+ type OriginalsRecord = {
561
+ readonly segments: Path;
562
+ readonly value: unknown;
563
+ };
564
+ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
565
+ readonly formKey: FormKey;
566
+ readonly form: Ref<F>;
567
+ readonly fields: Map<PathKey, FieldRecord>;
568
+ readonly elements: Map<PathKey, ElementRecord>;
569
+ /**
570
+ * Schema-driven errors. Written ONLY by the schema validation pipeline:
571
+ * `scheduleFieldValidation`, `handleSubmit`, the construction-time seed,
572
+ * history restore, and hydration. Cleared by `reset` / `resetField` and by
573
+ * a successful submit. `setFieldErrors*` APIs do NOT touch this Map.
574
+ */
575
+ readonly schemaErrors: Map<PathKey, ValidationError[]>;
576
+ /**
577
+ * User-injected errors. Written ONLY by the `setFieldErrors*` API surfaces
578
+ * (and history / hydration replay). Survives schema revalidation and
579
+ * successful submits — the consumer owns its lifetime explicitly.
580
+ */
581
+ readonly userErrors: Map<PathKey, ValidationError[]>;
582
+ /**
583
+ * Reactively-derived "No value supplied" errors. Pure function of
584
+ * `(blankPaths, schema.isRequiredAtPath)` — no writers, no clears.
585
+ * Membership tracks `blankPaths` automatically: typing a value into
586
+ * a blank required numeric field removes the path from `blankPaths`
587
+ * and the derived error vanishes; clearing the numeric input re-adds
588
+ * the path and the error reappears. The `errors` proxy and
589
+ * `getErrorsForPath` merge this map in alongside `schemaErrors` and
590
+ * `userErrors`, so consumers see the "this required field is empty"
591
+ * error the moment it's true — no `validate()` / `handleSubmit`
592
+ * call required. Honors the founding principle that
593
+ * `errors = f(schema, state)`.
594
+ *
595
+ * Most entries flow through this map for `number` / `bigint` leaves
596
+ * (where the side-channel is needed to distinguish "user typed 0"
597
+ * from "user supplied nothing"). String / boolean leaves only land
598
+ * here when the consumer explicitly opted in via the `unset`
599
+ * sentinel — see `docs/recipes/blank-inputs.md`.
600
+ */
601
+ readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
602
+ readonly originals: Map<PathKey, OriginalsRecord>;
603
+ /**
604
+ * Reactive set of paths whose displayed state should be EMPTY even
605
+ * though storage holds a real, schema-conformant value (the slim
606
+ * default). It exists exclusively to record **storage / display
607
+ * divergence** — the case where the runtime can't tell "user typed
608
+ * 0" from "user supplied nothing" by looking at storage alone.
609
+ *
610
+ * The mechanism shines for `number` / `bigint`: storage holds the
611
+ * slim default (`0` / `0n`) but the DOM input shows `''`, so the
612
+ * directive's input listener marks the path here on clear. Strings
613
+ * and booleans don't need it — `''` storage equals `''` display,
614
+ * `false` storage equals unchecked display — so they're never
615
+ * auto-marked. Consumers can still mark any primitive leaf
616
+ * explicitly via the `unset` sentinel (`defaultValues: { x: unset }`,
617
+ * `setValue('x', unset)`, `reset({ x: unset })`); the mark is then
618
+ * a documented signal of consumer intent rather than runtime
619
+ * inference.
620
+ *
621
+ * Reads (`displayValue` computed, `fields.<path>.blank`,
622
+ * `derivedBlankErrors` computed) track via Vue 3.5's reactive Set
623
+ * handlers. Writes happen inside `setValueAtPath` (gate-hook
624
+ * bookkeeping: `blank: true` meta adds the path; any other write
625
+ * removes it) and `reset`.
626
+ *
627
+ * Storage NEVER reflects this set — calculations and reads against
628
+ * `form.value` see the slim default. The set is purely a UI/intent
629
+ * channel that `derivedBlankErrors` consults to surface
630
+ * "No value supplied" errors for required schemas.
631
+ *
632
+ * See `docs/recipes/blank-inputs.md` for the conceptual model.
633
+ */
634
+ readonly blankPaths: Set<PathKey>;
635
+ /**
636
+ * Snapshot of `blankPaths` captured at construction (and
637
+ * re-captured on `reset(args)`). Used by dirty calculation: a path
638
+ * whose membership differs from the snapshot is dirty even if
639
+ * storage matches the original. Eagerly populated to avoid a "dirty
640
+ * on first read" race after construction.
641
+ */
642
+ readonly originalBlankPaths: Set<PathKey>;
643
+ readonly schema: AbstractSchema<F, G>;
644
+ /**
645
+ * Server-side flag, plumbed in from `registry.ssr`. The
646
+ * `register()`-returned `markConnectedOptimistically()` reads this
647
+ * before flipping `connected: true`; on the client it's a no-op so
648
+ * the eventual directive lifecycle remains the source of truth.
649
+ */
650
+ readonly ssr: boolean;
651
+ /**
652
+ * Resolved `shouldShowErrors` predicate driving `field.showErrors`
653
+ * and `form.meta.showErrors`. Resolved once at construction via
654
+ * `resolveShouldShowErrors(options.shouldShowErrors)` so the
655
+ * field-state computeds don't repeat the boolean-vs-function
656
+ * branch on every read. Boolean shorthand has already been lifted
657
+ * to a constant predicate by the time it lands here; `undefined`
658
+ * config falls through to `defaultShouldShowErrors`.
659
+ */
660
+ readonly shouldShowErrors: ShouldShowErrors;
661
+ readonly submitting: Ref<boolean>;
662
+ readonly activeSubmissions: Ref<number>;
663
+ readonly submissionAttempts: Ref<number>;
664
+ /**
665
+ * `true` once a `handleSubmit` callback resolved without throwing.
666
+ * Independent of `submissionAttempts` — a failed submit increments
667
+ * attempts but leaves `submitted` at `false`. Cleared by `reset()`
668
+ * alongside the rest of the submission surface.
669
+ */
670
+ readonly submitted: Ref<boolean>;
671
+ readonly submitError: Ref<unknown>;
672
+ readonly departAttempts: Ref<number>;
673
+ /**
674
+ * `true` while a function-form `defaultValues` factory is in flight.
675
+ * Stays `false` for plain-value `defaultValues`. Shared across every
676
+ * `useForm({ key })` call that resolves to this store — the second
677
+ * caller sees the first caller's hydration state.
678
+ */
679
+ readonly hydrating: Ref<boolean>;
680
+ /**
681
+ * Error from the most recent function-form `defaultValues` factory.
682
+ * Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
683
+ * shape matches `form.errors` / `form.meta.errors` entries. `null` when
684
+ * no factory has fired or the last one succeeded.
685
+ */
686
+ readonly hydrateError: Ref<ValidationError | null>;
687
+ /**
688
+ * The function-form `defaultValues` factory, captured at the first
689
+ * `useForm({ key })` call that wired this store. `undefined` for
690
+ * plain-value forms. Read by `form.rehydrate()`.
691
+ */
692
+ readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
693
+ /**
694
+ * `true` once the form's effective defaults have been applied —
695
+ * sync `defaultValues` at construction, or async factory whose
696
+ * settle completed. Stays `false` for dormant lazy forms until they
697
+ * activate. Read by `useWizard` to decide whether seed status or
698
+ * live meta should surface.
699
+ */
700
+ readonly defaultsResolved: Ref<boolean>;
701
+ /**
702
+ * `true` once the captured async factory has been kicked off (set
703
+ * synchronously by `activate()`, before the factory itself resolves).
704
+ * Distinct from `defaultsResolved`, which only flips after the factory
705
+ * settles. The pair lets the API surface tell "we've started" apart
706
+ * from "we're done."
707
+ */
708
+ readonly activated: Ref<boolean>;
709
+ /**
710
+ * In-flight activation promise. Concurrent callers (cross-component
711
+ * SSR consumers, recursive factory reads, parallel `activate()`
712
+ * calls) receive the same promise, ensuring the factory runs once
713
+ * even under contention.
714
+ */
715
+ readonly activationPromise: Ref<Promise<void> | undefined>;
716
+ /**
717
+ * Idempotent activation entrypoint. Fires the captured function-form
718
+ * `defaultValues` factory on first call and stores the in-flight
719
+ * promise. Subsequent calls return the same promise until the factory
720
+ * settles; thereafter calls return `Promise.resolve()`. Plain-value
721
+ * forms (no factory captured) always return a resolved promise. The
722
+ * public API surface routes all reactive interactions (getters and
723
+ * methods, except `key`) through this entrypoint so the form
724
+ * activates on first use.
725
+ */
726
+ activate(): Promise<void>;
727
+ /**
728
+ * Re-fire the captured function-form `defaultValues` factory. Throws
729
+ * synchronously when no factory was captured (plain-value form).
730
+ * Resolves after `hydrating` flips back to `false`; consumers can
731
+ * `await form.rehydrate()` to gate UI on the fresh load.
732
+ *
733
+ * Does NOT touch dirty / touched / submit state — chain
734
+ * `form.reset()` if you want a clean baseline.
735
+ */
736
+ rehydrate(): Promise<void>;
737
+ /**
738
+ * Incremented by every `reset()` call. The submit wrapper captures
739
+ * this at entry and skips writing `submitError` from a catch that
740
+ * fires *after* a reset — otherwise a reset-during-submit would
741
+ * visibly clear `submitError` and then have it reappear when the
742
+ * in-flight promise rejects.
743
+ */
744
+ readonly submissionGeneration: Ref<number>;
745
+ /**
746
+ * Counts in-flight validation calls across every `validate()` ref and
747
+ * every `validateAsync(...)` / `handleSubmit` pre-check. `validating`
748
+ * on the public API mirrors `activeValidations.value > 0`. Tracked
749
+ * separately from submissions because a validate-while-submitting
750
+ * (e.g. a debounced field check overlapping a submit) needs to show
751
+ * the union of both surfaces.
752
+ */
753
+ readonly activeValidations: Ref<number>;
754
+ /**
755
+ * `true` once the form has completed at least one validation pass
756
+ * — flips when `activeValidations` returns to 0 from any positive
757
+ * value. Until that happens, `meta.valid` and `field.valid` report
758
+ * `false` even when `schemaErrors.size === 0`, because the absence
759
+ * of errors at frame 1 is just "we haven't checked yet," not "we
760
+ * checked and it's clean."
761
+ *
762
+ * This closes the brief flash window for schemas where the slim
763
+ * default-derivation parse strips refinements (`.refine`,
764
+ * `.superRefine`, async validators): the slim parse passes, no
765
+ * construction-time errors land, and the queued microtask hasn't
766
+ * run yet — so without the gate, frame 1 paints the form as
767
+ * "valid" before the real verdict arrives a tick later.
768
+ *
769
+ * Initialized to `!strict`: non-strict consumers opt out of the
770
+ * validation pipeline by design, so locking them on
771
+ * `firstValidationDone === false` would defeat the opt-out.
772
+ * Reset is left untouched — the post-reset validation flips it
773
+ * back true on completion, same as the construction-time path.
774
+ */
775
+ readonly firstValidationDone: Ref<boolean>;
776
+ /**
777
+ * `true` when the sub-schema rooted at `path` (or any of its
778
+ * descendants) declares async work — composes
779
+ * `schema.getSchemasAtPath(path)` with each candidate's
780
+ * `needsAsyncValidation()`, memoised per canonical path key for
781
+ * the lifetime of the FormStore. Used by `meta.valid` /
782
+ * `field.valid` to skip the `firstValidationDone` gate on subtrees
783
+ * that are fully synchronous: their verdict resolves at construction
784
+ * (or on the next per-field run) without waiting on a microtask, so
785
+ * honouring the form-wide gate would just play dumb about a known
786
+ * answer.
787
+ */
788
+ pathHasAsyncValidation(path: Path): boolean;
789
+ /**
790
+ * Per-path counter of in-flight field-level validation runs.
791
+ * `field.validating` on `FieldState` mirrors
792
+ * `(fieldValidationCounts.get(key) ?? 0) > 0`.
793
+ *
794
+ * Incremented at the same point as `activeValidations` inside
795
+ * `scheduleFieldValidation`'s `run` closure (right before the schema
796
+ * call) and decremented in the matching `.finally` — so the per-path
797
+ * bookkeeping is exactly co-extensive with the form-wide counter for
798
+ * the field-scheduled branch. Whole-form `validate()` /
799
+ * `validateAsync()` runs touch `activeValidations` only; they don't
800
+ * have a single field path and so don't contribute here.
801
+ *
802
+ * Counter (not Set) because two runs for the same path can briefly
803
+ * overlap: when an in-flight run is aborted and a new run starts,
804
+ * the new run increments before the aborted run's `.finally`
805
+ * decrements. With `> 0` semantics the field stays "validating"
806
+ * across the abort/restart boundary.
807
+ *
808
+ * Reactive Map: Vue 3's `reactive(new Map())` proxy makes `.get()`,
809
+ * `.has()`, and `.size` track per-key, so the FieldState
810
+ * computed only re-runs when the count for ITS key changes.
811
+ */
812
+ readonly fieldValidationCounts: Map<PathKey, number>;
813
+ /**
814
+ * Replace the form value wholesale. Optional `meta` is forwarded to
815
+ * every `onFormChange` listener so they can decide whether THIS write
816
+ * is one they care about — most importantly, the persistence layer
817
+ * only writes when `meta?.persist === true`. Internal callers that
818
+ * don't pass meta default to no-persist.
819
+ */
820
+ applyFormReplacement(next: F, meta?: WriteMeta): void;
821
+ /**
822
+ * Set a single path's value. `meta` is forwarded to listeners via
823
+ * `applyFormReplacement` (see above). The directive's input handler
824
+ * computes `meta.persist` from the per-element opt-in registry; other
825
+ * internal call sites pass `meta.persist = hasAnyOptInForPath(path)`.
826
+ * Public `form.setValue` passes no meta.
827
+ *
828
+ * Returns `false` when the slim-primitive gate rejects the write
829
+ * (the value's primitive shape doesn't match the schema's slim
830
+ * shape at the path). The store is unchanged in that case.
831
+ */
832
+ setValueAtPath(path: Path, value: unknown, meta?: WriteMeta): boolean;
833
+ getValueAtPath(path: Path): unknown;
834
+ reset(nextDefaultValues?: DeepPartial<WriteShape<F>>): void;
835
+ resetField(path: Path): void;
836
+ /**
837
+ * Wipe `path` (or the whole form when `path === ''`) to the
838
+ * schema's "appropriate nullish value" — the underlying type's
839
+ * empty/falsy concrete, with `.default()` / `.catch()` wrappers
840
+ * INTENTIONALLY skipped. Sugar for
841
+ * `setValueAtPath(path, schema.getEmptyValueAtPath(path))`.
842
+ */
843
+ clear(path: Path): boolean;
844
+ setSchemaErrorsForPath(path: Path, errors: ValidationError[]): void;
845
+ setAllSchemaErrors(errors: readonly ValidationError[]): void;
846
+ clearSchemaErrors(path?: Path): void;
847
+ /**
848
+ * Replace `schemaErrors` under `path` with `errors`, keying each
849
+ * error by its OWN absolute path. Used by validation pipelines
850
+ * (scheduleFieldValidation, validateAsync, handleSubmit, reset)
851
+ * to commit a parse result wholesale — entries not in the new
852
+ * pass get dropped from the subtree, surviving keys update in
853
+ * place to preserve insertion order. Pass `path === []` for the
854
+ * whole-form scope.
855
+ */
856
+ applySchemaErrorsForSubtree(path: Path, errors: ValidationError[]): void;
857
+ setAllUserErrors(errors: readonly ValidationError[]): void;
858
+ addUserErrors(errors: readonly ValidationError[]): void;
859
+ clearUserErrors(path?: Path): void;
860
+ /**
861
+ * Merged read — returns `[...schemaErrors[path], ...userErrors[path]]`.
862
+ * Schema errors come first (structural validation before business logic),
863
+ * matching the iteration order for `getFirstErrorElement` and the
864
+ * top-level `errors` drillable Proxy.
865
+ */
866
+ getErrorsForPath(path: Path): ValidationError[];
867
+ /**
868
+ * Returns a stable schema-declaration ordinal for `key`, assigning a
869
+ * fresh one if the path hasn't been seen before. Drives
870
+ * `form.meta.errors` sort order so the aggregate is a function of the
871
+ * SET of errors currently present (not the temporal order their
872
+ * Map keys were last `set`). Construction-time seed walks every leaf
873
+ * in the schema's slim default; runtime callers (DU variant 2, dynamic
874
+ * array indices, refines targeting cross-field paths) pick up
875
+ * first-encounter ordinals and keep them for the form's lifetime.
876
+ */
877
+ ensurePathOrdinal(key: PathKey): number;
878
+ /**
879
+ * Register `element` as a binding for `path`, tagged with the calling
880
+ * `useForm()` instance's `formInstanceId`. The ID is the disambiguator
881
+ * used by `getFirstErrorElement` to scope focus / scroll to elements
882
+ * THIS form instance owns — important when two `useForm()` calls share
883
+ * a `key` (e.g. sidebar + main rendering the same form), since both
884
+ * write into one shared element store.
885
+ */
886
+ registerElement(path: Path, element: HTMLElement, formInstanceId: string): boolean;
887
+ deregisterElement(path: Path, element: HTMLElement): number;
888
+ /**
889
+ * Optional `meta.instance` carries per-`useForm()`-instance overrides
890
+ * for `validateOn` / `debounceMs` so the blur-trigger respects the
891
+ * caller's config when sibling instances share a FormStore.
892
+ */
893
+ markFocused(path: Path, focused: boolean, meta?: {
894
+ readonly instance?: WriteMeta['instance'];
895
+ }): void;
896
+ markTouched(path: Path): void;
897
+ /**
898
+ * Walk every active-variant leaf under `segments` and flip
899
+ * `touched: true`. Powers `form.touch(path?)`. Idempotent;
900
+ * does not mutate value / focused / blurred or trigger validation.
901
+ */
902
+ touchAtPath(segments: Path): void;
903
+ /**
904
+ * SSR-only optimistic mark: flip `connected: true` on the field
905
+ * record without an actual DOM element. Called by the `vRegisterHint`
906
+ * compile-time transform via `RegisterValue.markConnectedOptimistically()`
907
+ * for every element rendered with `v-register`. Idempotent + no-op on
908
+ * the client (the directive's `created` hook is the authoritative
909
+ * source there).
910
+ */
911
+ markConnectedOptimistically(path: Path): void;
912
+ /**
913
+ * Leaf-only pristine check. `originals` is populated via
914
+ * `diffAndApply`'s `added` patches, which fire only on primitive
915
+ * leaves — a container path (e.g. `['profile']`) that isn't in
916
+ * `originals` returns `true` here even when a descendant is dirty.
917
+ * Callers that need container semantics should either loop over
918
+ * leaves or walk `originals` manually. The public `getFieldState`
919
+ * surface is typed to accept leaf paths only, so in practice this
920
+ * isn't exposed to consumers.
921
+ */
922
+ isPristineAtPath(path: Path): boolean;
923
+ getFieldRecord(path: Path): FieldRecord | undefined;
924
+ getOriginalAtPath(path: Path): unknown;
925
+ /**
926
+ * Returns the first errored field's first connected, visible DOM
927
+ * element scoped to `formInstanceId` — the target that
928
+ * `focusFirstError` / `scrollToFirstError` act on. "First" is
929
+ * VISUAL-first (DOM-tree order via `compareDocumentPosition`), not
930
+ * schema-declaration order, so a field rendered above another in the
931
+ * template focuses first regardless of which one the schema declared
932
+ * earlier. CSS `order:` flexbox/grid reordering is NOT respected
933
+ * (DOM-tree order wins) — documented as a tradeoff against forcing
934
+ * sync layout on every comparison.
935
+ *
936
+ * The `formInstanceId` filter scopes focus to elements registered
937
+ * through THIS form instance. When two `useForm({ key })` calls share
938
+ * a key, both register into the same element store; without the
939
+ * filter, the sidebar form's submit could focus the main form's
940
+ * input. With it, each `useForm()` callsite focuses only its own
941
+ * elements.
942
+ *
943
+ * Returns `null` when every errored path has no currently-attached
944
+ * element registered to this instance (fields behind `v-if="false"`,
945
+ * unmounted components, or a hidden `display:none` parent). Callers
946
+ * get the choice of no-op or a dev-only warning.
947
+ */
948
+ getFirstErrorElement(formInstanceId: string): {
949
+ path: Path;
950
+ element: HTMLElement;
951
+ } | null;
952
+ /**
953
+ * Cancel every in-flight field-level validation run — clears timers
954
+ * for debounced 'change' runs that haven't fired, aborts controllers
955
+ * for runs whose async parse is in flight. Called by `handleSubmit`
956
+ * at entry (submit validation is authoritative) and by `reset()`.
957
+ */
958
+ cancelFieldValidation(): void;
959
+ /**
960
+ * Kick off (or schedule) a field-level validation run for `path`. Pass
961
+ * `path = []` to cover the whole form; `applySchemaErrorsForSubtree`
962
+ * then wipes every `schemaErrors` entry and replaces them with the
963
+ * adapter's full async response. Used by persistence's post-hydration
964
+ * revalidation and by the construction-time async-refine seed.
965
+ *
966
+ * `immediate: true` skips the debounce window — the runtime kicks off
967
+ * the adapter call on the next microtask. Internal callsites use this
968
+ * for one-shot triggers; the per-keystroke writers pass `false` to
969
+ * coalesce rapid mutations under the configured debounceMs.
970
+ *
971
+ * `override` carries per-`useForm()`-instance values: when provided,
972
+ * the scheduler honors `override.mode` instead of the store's
973
+ * captured `validateOn`, and `override.debounceMs` instead of the
974
+ * store's captured `debounceMs`. Used so sibling instances sharing a
975
+ * FormStore can each validate on their own cadence.
976
+ */
977
+ scheduleFieldValidation(path: Path, immediate: boolean, override?: {
978
+ readonly mode?: ValidateOn;
979
+ readonly debounceMs?: number;
980
+ }): void;
981
+ /**
982
+ * Subscribe to every `applyFormReplacement`. Fires synchronously
983
+ * after `form.value` has been swapped to `next` and all field /
984
+ * originals bookkeeping has run. Used by persistence + undo/redo
985
+ * to hook the single mutation funnel. The optional `meta` carries
986
+ * the originating call site's intent — the persistence subscription
987
+ * filters on `meta?.persist === true`; subscribers that don't care
988
+ * about meta can ignore the parameter. Returns an unsubscribe
989
+ * function.
990
+ */
991
+ onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
992
+ /**
993
+ * Subscribe to successful submissions. Fires after the consumer's
994
+ * `onSubmit` callback has resolved — not on validation failure,
995
+ * not on callback throw. Used by persistence's `clearOnSubmitSuccess`
996
+ * to drop the stored payload once the form is safely through the
997
+ * server round-trip. Returns an unsubscribe function.
998
+ */
999
+ onSubmitSuccess(listener: () => void): () => void;
1000
+ /**
1001
+ * Subscribe to `reset()` calls. Fires AFTER reset has replaced
1002
+ * the form and cleared errors + lifecycle, so listeners see the
1003
+ * fresh post-reset state. Used by the history module to drop the
1004
+ * undo/redo stack on reset. Returns an unsubscribe function.
1005
+ */
1006
+ onReset(listener: () => void): () => void;
1007
+ /**
1008
+ * Internal: notify submit-success subscribers. Called by
1009
+ * `handleSubmit` in `process-form.ts` once the user callback has
1010
+ * resolved. Consumers shouldn't call this directly.
1011
+ */
1012
+ emitSubmitSuccess(): void;
1013
+ /**
1014
+ * Register a teardown function whose lifetime is bound to the
1015
+ * FormStore itself (not a consumer's Vue effect scope). Called by
1016
+ * `dispose()` when the last consumer unmounts. Used by persistence /
1017
+ * history wiring so their subscribers aren't detached prematurely
1018
+ * when only the first consumer unmounts but others remain.
1019
+ */
1020
+ registerCleanup(fn: () => void): void;
1021
+ /**
1022
+ * Register an async drain function. Called by the registry before
1023
+ * `dispose()` so async background work — chiefly the persistence
1024
+ * layer's debounced storage writes — has a chance to settle without
1025
+ * losing the last keystroke. Each registered function is awaited in
1026
+ * parallel; failures are swallowed to keep eviction reliable.
1027
+ */
1028
+ registerDrain(fn: () => Promise<void>): void;
1029
+ /**
1030
+ * Drain async work registered via `registerDrain`. Resolves once
1031
+ * every registered drain has settled (in parallel). Safe to call
1032
+ * repeatedly — registered drains decide their own idempotency.
1033
+ */
1034
+ awaitPendingWrites(): Promise<void>;
1035
+ /**
1036
+ * Cache for per-state modules (history, persistence) that must
1037
+ * outlive any single consumer. Subsequent `useForm` / `injectForm`
1038
+ * calls for the same key read from this map so the public API shape
1039
+ * is identical regardless of mount order. Keyed by a string identifier
1040
+ * owned by the caller (e.g. `'history'`).
1041
+ */
1042
+ readonly modules: Map<string, unknown>;
1043
+ /**
1044
+ * Per-element persistence opt-in tracker. Empty by default; the
1045
+ * `v-register` directive populates entries on `mount` for each binding
1046
+ * that passed `register('foo', { persist: true })` and clears them on
1047
+ * `beforeUnmount`. Two SFCs sharing a key share this registry — opt-ins
1048
+ * are per-DOM-element, not per-component. Internal to the persistence
1049
+ * subsystem; not part of the consumer API surface.
1050
+ */
1051
+ readonly persistOptIns: PersistOptInRegistry;
1052
+ /**
1053
+ * Resolved sensitive-path predicate for THIS form. Honors the
1054
+ * cascade (`useForm({ sensitiveNames })` > global default >
1055
+ * library `DEFAULT_SENSITIVE_NAMES`). Used by:
1056
+ * - persistence enforcement (`enforceSensitiveCheck` at write time);
1057
+ * - the multi-tab sync module (outbound strip + inbound reject);
1058
+ * - DevTools edit rejection;
1059
+ * - any future surface that needs to flag "this path holds
1060
+ * sensitive data."
1061
+ *
1062
+ * Frozen at FormStore construction. Two callsites sharing a key
1063
+ * share the predicate — consistent with the rest of the per-form
1064
+ * resolved-config surface.
1065
+ */
1066
+ readonly isSensitivePath: (path: Path | PathKey | string) => boolean;
1067
+ /**
1068
+ * Single-segment variant of `isSensitivePath`. Used by the DevTools
1069
+ * redact walk to short-circuit whole subtrees the moment any
1070
+ * ancestor segment matches — saving an O(leaves × ancestors) regex
1071
+ * sweep per timeline event. Resolved from the same `sensitiveNames`
1072
+ * cascade as `isSensitivePath`.
1073
+ */
1074
+ readonly segmentMatchesSensitive: (segment: Segment) => boolean;
1075
+ /**
1076
+ * Canonical path keys explicitly opted OUT of multi-tab sync by
1077
+ * `register(path, { multiTab: false })`. The sync module's outbound
1078
+ * broadcaster strips patches at these paths AND the inbound listener
1079
+ * rejects them — symmetric tab-local behaviour for selected fields.
1080
+ *
1081
+ * Read-only Set view; mutate via `incrementNoSyncOptOut` /
1082
+ * `decrementNoSyncOptOut` which maintain a per-path ref count so
1083
+ * multiple bindings on the same path balance correctly across
1084
+ * dynamic conditional renders. Empty by default.
1085
+ */
1086
+ readonly noSyncPaths: ReadonlySet<PathKey>;
1087
+ /**
1088
+ * Ref-counted "this path is tab-local" registration. Called by
1089
+ * `v-register`'s `created` hook for any binding that declared
1090
+ * `register('x', { multiTab: false })`. The first call for a given
1091
+ * path adds it to `noSyncPaths`; subsequent calls just bump the
1092
+ * ref count. Pair with `decrementNoSyncOptOut`.
1093
+ */
1094
+ incrementNoSyncOptOut(path: PathKey): void;
1095
+ /**
1096
+ * Symmetric companion to `incrementNoSyncOptOut`. Called by
1097
+ * `v-register`'s `beforeUnmount` hook. When the ref count for a
1098
+ * path drops to zero, the path is removed from `noSyncPaths` —
1099
+ * dynamic toggling (the binding rendered conditionally) restores
1100
+ * full sync to the path when the last opt-out unmounts.
1101
+ */
1102
+ decrementNoSyncOptOut(path: PathKey): void;
1103
+ /**
1104
+ * Resolved schema-coercion index — the merged config from
1105
+ * `createAttaform({ defaults: { coerce } })` ∪ `useForm({ coerce })`,
1106
+ * keyed by `${input}->${output}` for O(1) per-keystroke dispatch.
1107
+ * Empty Map when coercion is disabled. Read at `register()` time
1108
+ * by `buildCoerceFn` to bake the per-path coerce closure on
1109
+ * `RegisterValue.coerce`.
1110
+ */
1111
+ readonly coerceIndex: CoercionIndex;
1112
+ /**
1113
+ * Tear down non-reactive resources owned by this FormStore. Invoked
1114
+ * by the registry when the last consumer unmounts. Cancels pending
1115
+ * field-validation timers, drops every subscriber, and fires each
1116
+ * cleanup hook registered via `registerCleanup`.
1117
+ */
1118
+ dispose(): void;
1119
+ };
1120
+
1121
+ /**
1122
+ * Portable SSR detection. The plugin captures this value at install time and
1123
+ * exposes it via the registry so every runtime branch reads a single source
1124
+ * of truth instead of sniffing `import.meta.*` (bundler-specific) at each
1125
+ * call site.
1126
+ *
1127
+ * Consumers can override the heuristic explicitly via
1128
+ * `createAttaform({ ssr: true })`; the default handles the common
1129
+ * Node-vs-browser split without relying on any bundler-injected flag.
1130
+ */
1131
+ interface SSRDetectOptions {
1132
+ /**
1133
+ * Force SSR-vs-client mode, bypassing the `typeof window` heuristic.
1134
+ * `true` activates the SSR code paths (no devtools, no persistence
1135
+ * wiring, payload serialisation enabled); `false` forces client mode.
1136
+ * The Nuxt plugin sets this from `import.meta.server` so SSR detection
1137
+ * never depends on whether `window` is polyfilled. Tests that need to
1138
+ * exercise the SSR code paths under jsdom pass `ssr: true`.
1139
+ */
1140
+ ssr?: boolean;
1141
+ }
1142
+
1143
+ /**
1144
+ * Per-Vue-app container for all form state instances. Each
1145
+ * `app.use(createAttaform())` call gets its own registry,
1146
+ * so the library runs under bare Vue 3 + SSR (via
1147
+ * `@vue/server-renderer`) and Nuxt with the same code path.
1148
+ *
1149
+ * Each form's state lives in `forms: Map<FormKey, FormStore<GenericForm>>`.
1150
+ * The type relaxation at storage time is necessary because different
1151
+ * forms in the same app have different `Form` generics; callers recover
1152
+ * the specific form type via `useForm`'s overloads.
1153
+ */
1154
+ /**
1155
+ * Serialised snapshot of one form's state, captured by
1156
+ * `renderAttaformState` for SSR and replayed by
1157
+ * `hydrateAttaformState` on the client. Round-trips through
1158
+ * JSON-safe tuples; field references are intentionally omitted
1159
+ * (DOM nodes don't survive serialisation).
1160
+ */
1161
+ type SerializedFormData = {
1162
+ /** The form's value at snapshot time. */
1163
+ readonly form: unknown;
1164
+ /**
1165
+ * Errors produced by the schema at snapshot time. Replayed into
1166
+ * the client form's error state at hydration; cleared on
1167
+ * successful re-validation client-side.
1168
+ */
1169
+ readonly schemaErrors: ReadonlyArray<readonly [string, unknown]>;
1170
+ /**
1171
+ * Errors set explicitly via `setFieldErrors` / `addFieldErrors`
1172
+ * (typically from a server response parsed via `parseApiErrors`)
1173
+ * at snapshot time. Replayed at hydration; persists across
1174
+ * client-side re-validation.
1175
+ */
1176
+ readonly userErrors: ReadonlyArray<readonly [string, unknown]>;
1177
+ /** Per-field metadata (timestamps, raw values, connection flags) captured at snapshot time. */
1178
+ readonly fields: ReadonlyArray<readonly [string, unknown]>;
1179
+ /**
1180
+ * Path keys that were in the form's `blankPaths` set at
1181
+ * snapshot time. Round-trips the "displayed empty" UI state across
1182
+ * the SSR boundary — without it, the client briefly renders
1183
+ * `String(slim-default)` (e.g. `'0'`) for fields the server
1184
+ * rendered as blank. Optional in the wire format so older payload
1185
+ * shapes deserialise cleanly.
1186
+ */
1187
+ readonly blankPaths?: ReadonlyArray<string>;
1188
+ };
1189
+ type PendingHydration = Map<FormKey, SerializedFormData>;
1190
+ /**
1191
+ * The library's per-Vue-app container. One `AttaformRegistry` is
1192
+ * created per `app.use(createAttaform())` call.
1193
+ *
1194
+ * Most consumers never touch this directly — `useForm` and
1195
+ * `injectForm` reach the registry on your behalf. Access it
1196
+ * explicitly only when wiring SSR or a custom plugin integration.
1197
+ */
1198
+ type AttaformRegistry = {
1199
+ /**
1200
+ * Live forms keyed by `FormKey`.
1201
+ * @internal
1202
+ */
1203
+ readonly forms: Map<FormKey, FormStore<GenericForm>>;
1204
+ /**
1205
+ * Live wizards keyed by the consumer-supplied `key` option. Populated
1206
+ * by `useWizard(entryForm, { key })`; consulted by `injectWizard(key)` to
1207
+ * resolve cross-component wizard handles. Anonymous wizards (no
1208
+ * `key`) do NOT register here — they're reachable only via ambient
1209
+ * provide/inject.
1210
+ * @internal
1211
+ */
1212
+ readonly wizards: Map<string, UseWizardReturnType>;
1213
+ /**
1214
+ * Snapshots staged by `hydrateAttaformState` waiting to be consumed by the next `useForm` call.
1215
+ * @internal
1216
+ */
1217
+ readonly pendingHydration: PendingHydration;
1218
+ /** `true` while running on the server during SSR; `false` on the client. */
1219
+ readonly ssr: boolean;
1220
+ /** App-level defaults applied to every `useForm` call. */
1221
+ readonly defaults: AttaformDefaults;
1222
+ /**
1223
+ * Track a consumer of `key`. Returns a dispose function — call it
1224
+ * when the consumer unmounts. The form is evicted automatically
1225
+ * when the last consumer disposes, so long-running SPAs don't
1226
+ * leak detached state across navigations.
1227
+ * @internal
1228
+ */
1229
+ readonly trackConsumer: (key: FormKey) => () => void;
1230
+ /**
1231
+ * Track a consumer of wizard `key`. Returns a dispose function — call
1232
+ * it when the consumer unmounts. The wizard handle is evicted from
1233
+ * `wizards` once the last consumer disposes, mirroring the form
1234
+ * consumer-counting mechanics. Anonymous wizards never enter this
1235
+ * counter (they have no key to count under).
1236
+ * @internal
1237
+ */
1238
+ readonly trackWizardConsumer: (key: string) => () => void;
1239
+ /**
1240
+ * Mark a form as eligible for SSR prefetch. The form's
1241
+ * `onServerPrefetch` hook consults `shouldPrefetch(key)` and runs the
1242
+ * captured `defaultValues` factory only when this set contains the
1243
+ * key (and the skip set does not). Set by `form.activate()`,
1244
+ * `useWizard`'s current-step auto-mark, and the future Vite transform.
1245
+ * @internal
1246
+ */
1247
+ readonly enqueuePrefetch: (key: FormKey) => void;
1248
+ /**
1249
+ * Mark a form as ineligible for SSR prefetch. Overrides `enqueuePrefetch`.
1250
+ * Used by `useWizard` to keep non-current steps dormant on the
1251
+ * server even when a transform mark or stray `activate()` would
1252
+ * otherwise enqueue them — the wizard's "user isn't on this step"
1253
+ * signal wins.
1254
+ * @internal
1255
+ */
1256
+ readonly skipPrefetch: (key: FormKey) => void;
1257
+ /**
1258
+ * Whether `key`'s SSR prefetch should run. Returns `true` iff the key
1259
+ * is enqueued AND not skipped.
1260
+ * @internal
1261
+ */
1262
+ readonly shouldPrefetch: (key: FormKey) => boolean;
1263
+ /**
1264
+ * Wait for all pending persistence writes across every live form
1265
+ * to settle. Useful for SSR shutdown and integration tests that
1266
+ * need a deterministic teardown.
1267
+ * @internal
1268
+ */
1269
+ readonly shutdown: () => Promise<void>;
1270
+ };
1271
+ /**
1272
+ * The Vue `InjectionKey` under which the registry is provided on the
1273
+ * app. Most consumers never need this — `useForm` and
1274
+ * `injectForm` resolve the registry automatically.
1275
+ */
1276
+ declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
1277
+ declare module 'vue' {
1278
+ interface App {
1279
+ /** @internal */
1280
+ _attaform?: AttaformRegistry;
1281
+ }
1282
+ }
1283
+ /** Options for `createRegistry`. */
1284
+ type CreateRegistryOptions = SSRDetectOptions & {
1285
+ /**
1286
+ * App-level defaults applied to every `useForm` call. Per-form
1287
+ * options always win. Omitted is equivalent to `{}`.
1288
+ */
1289
+ defaults?: AttaformDefaults;
1290
+ };
1291
+ /**
1292
+ * Create a fresh `AttaformRegistry`. `createAttaform()` calls
1293
+ * this internally — most consumers never need to call it directly.
1294
+ * Use it when building a custom plugin that doesn't want the
1295
+ * `createAttaform` plugin's auto-install behaviour (e.g. test
1296
+ * harnesses, embedded apps).
1297
+ */
1298
+ declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
1299
+ /**
1300
+ * Look up the current app's registry from inside a component's
1301
+ * `setup()` (or any synchronous code on the setup call stack).
1302
+ *
1303
+ * Most consumers don't need this — `useForm` and `injectForm`
1304
+ * call it on your behalf. Reach for it directly when building
1305
+ * custom integrations that need the raw registry.
1306
+ *
1307
+ * Throws:
1308
+ * - `OutsideSetupError` when called outside a Vue setup context
1309
+ * (e.g. from an event handler or async callback). Move the call
1310
+ * into setup, or trigger it from a child component.
1311
+ * - `RegistryNotInstalledError` when called inside setup but the
1312
+ * plugin wasn't installed. Add
1313
+ * `app.use(createAttaform())` to your app entry.
1314
+ */
1315
+ declare function useRegistry(): AttaformRegistry;
1316
+ /**
1317
+ * Look up a Vue app's registry by `App` reference. Used by
1318
+ * SSR helpers (`renderAttaformState`, `hydrateAttaformState`) that
1319
+ * run outside a component setup context.
1320
+ *
1321
+ * Throws `RegistryNotInstalledError` when the app hasn't been wired
1322
+ * with `createAttaform()`.
1323
+ */
1324
+ declare function getRegistryFromApp(app: App): AttaformRegistry;
1325
+
1326
+ /**
1327
+ * Options accepted by `injectForm` when passing an object instead of
1328
+ * a bare key string. `__ssrAccessed: true` is set by the Phase 3
1329
+ * `attaform-vite` transform on descendant calls whose template reads
1330
+ * the injected form's reactive state — it tells the runtime to
1331
+ * enqueue the form for SSR prefetch and register the descendant's
1332
+ * `onServerPrefetch` hook. Consumers may set it manually as the
1333
+ * escape hatch when the transform isn't installed or doesn't see
1334
+ * the reference.
1335
+ */
1336
+ type InjectFormInput = {
1337
+ readonly key?: FormKey;
1338
+ /**
1339
+ * Set by the Vite transform when this `injectForm` call site sits in
1340
+ * a component whose template / script reads the form's reactive
1341
+ * state. On the server, this enqueues the form for SSR prefetch and
1342
+ * wires `onServerPrefetch` so the descendant awaits the activation
1343
+ * promise before its render emits HTML.
1344
+ *
1345
+ * @internal Transform-emitted. Manual use is the documented escape
1346
+ * hatch when the transform can't reach the reference (dynamic
1347
+ * property access, untransformed bundlers).
1348
+ */
1349
+ readonly __ssrAccessed?: boolean;
1350
+ };
1351
+ /**
1352
+ * Access an existing form from a descendant component without passing
1353
+ * it through props. Counterpart to `useForm` — `useForm` creates and
1354
+ * provides; `injectForm` looks up via Vue's inject mechanism.
1355
+ *
1356
+ * Three ways to call it:
1357
+ *
1358
+ * ```ts
1359
+ * // Reach the nearest ancestor's anonymous useForm() call.
1360
+ * const form = injectForm<SignupShape>()
1361
+ *
1362
+ * // Reach a specific form by its key — works from anywhere in the app.
1363
+ * const cart = injectForm<CartShape>('cart')
1364
+ *
1365
+ * // Options form. The Vite transform emits this with `__ssrAccessed: true`
1366
+ * // when the descendant's template / script reads the form's reactive
1367
+ * // state, so the descendant participates in SSR prefetch coordination.
1368
+ * const cart = injectForm<CartShape>({ key: 'cart', __ssrAccessed: true })
1369
+ * ```
1370
+ *
1371
+ * Resolution rules (no-key form):
1372
+ * - Closest ambient ancestor wins.
1373
+ * - Only anonymous `useForm()` (no `key`) fills the ambient slot;
1374
+ * keyed forms are reachable only via `injectForm(key)`.
1375
+ * - Inherits the resolved ancestor's `formInstanceId`.
1376
+ *
1377
+ * Resolution rules (keyed form): registry lookup by string key,
1378
+ * independent of component-tree position.
1379
+ *
1380
+ * Returns `null` when no matching form exists (no ambient ancestor, or
1381
+ * the named key isn't registered). A dev-mode warning points at the
1382
+ * call site to help diagnose typos. Always narrow before using:
1383
+ *
1384
+ * ```ts
1385
+ * const form = injectForm<Shape>('signup')
1386
+ * if (!form) return
1387
+ * form.register('email')
1388
+ * ```
1389
+ *
1390
+ * Pass the `Form` generic explicitly — Vue's provide/inject erases
1391
+ * generics, so the library can't recover the shape automatically.
1392
+ *
1393
+ * The form is kept alive for this component's lifetime; once every
1394
+ * consumer unmounts, the form is cleaned up automatically.
1395
+ */
1396
+ declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
1397
+
1398
+ /**
1399
+ * Multistep-form orchestrator built around an ordered list of step slots.
1400
+ * Each slot resolves to a participating form: an existing `useForm`
1401
+ * reference, a bare string key (desugared to a noop form so affordance
1402
+ * steps participate uniformly), an eagerly-evaluated function slot for
1403
+ * runtime branching, or a `lazy()`-wrapped function slot that caches
1404
+ * its resolution and re-fires only on its own tracked deps.
1405
+ *
1406
+ * The wizard's surface is read-only from the consumer's side:
1407
+ * navigation (`next` / `back` / `goTo`) walks positional indices,
1408
+ * `handleSubmit` validates the active form on intermediate steps and
1409
+ * the whole wizard on the final step, and URL synchronization rides on
1410
+ * `restore` / `persist` callbacks that default to `?step=<key>`.
1411
+ */
1412
+ declare function useWizard<const S extends ReadonlyArray<StepSlot>>(options: WizardOptions & {
1413
+ readonly steps: S;
1414
+ }): UseWizardReturnType<S>;
1415
+
1416
+ /**
1417
+ * Options accepted by `injectWizard` when passing an object instead of
1418
+ * a bare key string. Mirrors `injectForm`'s `InjectFormInput` shape so
1419
+ * the two composables present an identical call surface.
1420
+ */
1421
+ type InjectWizardInput = {
1422
+ readonly key?: string | undefined;
1423
+ };
1424
+ /**
1425
+ * Access an existing wizard handle from a descendant component without
1426
+ * passing it through props. Counterpart to `useWizard` — `useWizard`
1427
+ * creates and provides; `injectWizard` looks up via Vue's inject
1428
+ * mechanism or the per-app registry.
1429
+ *
1430
+ * Three ways to call it:
1431
+ *
1432
+ * ```ts
1433
+ * // Reach the nearest ancestor's useWizard call (ambient).
1434
+ * const wizard = injectWizard()
1435
+ *
1436
+ * // Reach a specific wizard by its key — works from anywhere in the app.
1437
+ * const signup = injectWizard('signup-wizard')
1438
+ *
1439
+ * // Object form (equivalent; convenient for spread).
1440
+ * const signup = injectWizard({ key: 'signup-wizard' })
1441
+ * ```
1442
+ *
1443
+ * Resolution rules (no-key form):
1444
+ * - Closest ambient ancestor wins via `provide(kAttaformAncestorWizard)`.
1445
+ * - Only anonymous (no-`key`) `useWizard()` calls fill the ambient
1446
+ * slot. Descendants of a keyed wizard must address it explicitly
1447
+ * via `injectWizard('the-key')`. Mirrors `useForm`'s ambient gate.
1448
+ *
1449
+ * Resolution rules (keyed form): registry lookup by string key,
1450
+ * independent of component-tree position. The wizard must have been
1451
+ * constructed with `useWizard({ steps, key })` to be reachable.
1452
+ *
1453
+ * Returns `null` when no matching wizard exists (no ambient ancestor,
1454
+ * or the named key isn't registered). A dev-mode warning points at the
1455
+ * call site to help diagnose typos. Always narrow before using:
1456
+ *
1457
+ * ```ts
1458
+ * const wizard = injectWizard('signup')
1459
+ * if (!wizard) return
1460
+ * wizard.next()
1461
+ * ```
1462
+ *
1463
+ * Consumer ref-counting: keyed lookups pin the wizard handle in the
1464
+ * registry for this component's lifetime, so the handle survives even
1465
+ * if the parent `useWizard` component unmounts before the child does.
1466
+ * Once every consumer disposes, the registry evicts the entry on the
1467
+ * next microtask. Ambient lookups don't ref-count — the parent
1468
+ * `useWizard`'s scope owns the lifetime.
1469
+ */
1470
+ declare function injectWizard(input?: string | InjectWizardInput): UseWizardReturnType | null;
1471
+
1472
+ /**
1473
+ * Wrap a function slot in `lazy()` to give that slot its own memoized
1474
+ * cache, distinct from the wizard-wide step compiler.
1475
+ *
1476
+ * Default function slots in `useWizard({ steps })` re-evaluate whenever
1477
+ * the compiled step list re-evaluates, including when an unrelated
1478
+ * slot's reactive deps change. That keeps the rail correct but can
1479
+ * trigger spurious work in a resolver that is expensive (a network
1480
+ * lookup, a heavy schema derivation, an async-derived factory).
1481
+ *
1482
+ * `lazy()` isolates each wrapped slot behind its own cache. The
1483
+ * resolver runs eagerly on the first compile pass; subsequent reads
1484
+ * reuse the cached form. The cache invalidates only when one of the
1485
+ * resolver's own tracked reactive dependencies changes (Vue's
1486
+ * `computed` semantics applied to a slot), or when `wizard.reset()`
1487
+ * triggers a global re-compile:
1488
+ *
1489
+ * const wizard = useWizard({
1490
+ * steps: [
1491
+ * account,
1492
+ * lazy((ctx) => loadShippingForm(ctx.forms.account.values.region)),
1493
+ * confirm,
1494
+ * ],
1495
+ * })
1496
+ *
1497
+ * Resolution semantics:
1498
+ * - Eager at construction so `wizard.steps` and SSR markup are honest
1499
+ * from t=0.
1500
+ * - Memoized: the resolver re-fires only when one of its own tracked
1501
+ * reads changes. An unrelated slot re-evaluating does not re-fire
1502
+ * this one.
1503
+ * - `wizard.reset()` clears every lazy slot's cache so a reboot truly
1504
+ * resolves from scratch.
1505
+ * - Resolving to `undefined` drops the slot from the compiled list
1506
+ * until the resolver next re-fires.
1507
+ *
1508
+ * The runtime brand returned by `lazy()` is opaque. Use
1509
+ * {@link isLazyMarker} to detect it.
1510
+ */
1511
+ declare function lazy<Ctx = WizardCtx>(resolve: (ctx: Ctx) => AnyForm | string | undefined): LazyMarker<Ctx>;
1512
+
1513
+ /**
1514
+ * Re-bind a parent's `v-register` onto an inner native element. Use
1515
+ * inside a component that wraps a single form field whose root is
1516
+ * NOT the input itself (e.g. a labelled-row that renders `<label>`
1517
+ * around the input).
1518
+ *
1519
+ * ```vue
1520
+ * <!-- Parent -->
1521
+ * <MyInput v-register="form.register('email')" />
1522
+ *
1523
+ * <!-- MyInput.vue -->
1524
+ * <script setup lang="ts">
1525
+ * import { useRegister } from 'attaform'
1526
+ * const rv = useRegister()
1527
+ * // rv.path / rv.segments / rv.formKey / rv.formInstanceId / rv.innerRef
1528
+ * // are all reachable directly — no `.value` unwrap.
1529
+ * </script>
1530
+ *
1531
+ * <template>
1532
+ * <label class="field">
1533
+ * <span>Email</span>
1534
+ * <input v-register="rv" />
1535
+ * </label>
1536
+ * </template>
1537
+ * ```
1538
+ *
1539
+ * Returns a hybrid Proxy: it answers `__v_isRef` / `.value` like a
1540
+ * Vue `Ref<RegisterValue | undefined>` (so templates auto-unwrap
1541
+ * correctly and `v-register="rv"` feeds the underlying RV to the
1542
+ * directive — preserving the directive's path-migration diff across
1543
+ * renders), AND every other property read pierces to the captured
1544
+ * RV's field (so `rv.path` works directly in script setup). Reads
1545
+ * inside reactive scopes (`computed` / `watchEffect`) track the
1546
+ * underlying `shallowRef`, so `rv.path` re-runs when the parent
1547
+ * rebinds to a different path.
1548
+ *
1549
+ * Unbound state: when the parent didn't pass `v-register`, every
1550
+ * piercing read returns `undefined` at runtime, and the return type
1551
+ * surfaces this honestly as `UseRegisterReturn<V> | undefined`.
1552
+ * Consumers defend with optional chaining (`rv?.formKey`,
1553
+ * `rv?.segments`); the directive accepts `undefined` peacefully (its
1554
+ * binding value type is already `RegisterValue<V> | undefined`), so
1555
+ * `v-register="rv"` works whether or not a parent has bound. The
1556
+ * composable's `onMounted` warn fires once per instance to surface
1557
+ * the misuse case at runtime.
1558
+ *
1559
+ * Diagnostic: in dev mode, a single `console.warn` fires per instance
1560
+ * at `onMounted` if the captured value is still `undefined` — by then
1561
+ * the parent has had its full mount lifecycle to bind, so a missing
1562
+ * binding is conclusive misuse. The warn does NOT fire on every read
1563
+ * of the proxy, and is intentionally silent under SSR
1564
+ * (`renderToString` skips `onMounted`); the CSR hydration pass
1565
+ * surfaces the same diagnostic without double-counting through Nuxt's
1566
+ * `dev:ssr-logs` channel.
1567
+ *
1568
+ * When the wrapper's root IS the input itself, Vue's attribute
1569
+ * fallthrough handles the binding and `useRegister` is unnecessary.
1570
+ * For wrappers that bind multiple fields (compound forms), use
1571
+ * `injectForm<Form>(key?)` and call `ctx.register(...)` directly.
1572
+ */
1573
+
1574
+ /**
1575
+ * Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
1576
+ * `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
1577
+ * script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
1578
+ * template auto-unwrap surfaces the underlying RV to `v-register`
1579
+ * and the directive's path-migration diff sees the real RV across
1580
+ * renders).
1581
+ *
1582
+ * The two surfaces don't clash at the type level: `RegisterValue`
1583
+ * doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
1584
+ * the hybrid's only `.value`. Older code that read `rv.value?.path`
1585
+ * keeps working; new code can write `rv.path` directly.
1586
+ */
1587
+ type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined>;
1588
+ declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
1589
+
1590
+ /**
1591
+ * Stable identifiers for library-emitted `ValidationError` codes.
1592
+ *
1593
+ * Convention: `<scope>:<kebab-case-identifier>`. Three scopes are
1594
+ * recognised by the library:
1595
+ *
1596
+ * - `atta:` — emitted by the framework-agnostic core (this map).
1597
+ * - `zod:` — emitted by the Zod adapter; computed inline from
1598
+ * `issue.code` (e.g. `zod:too_small`). No enum here because
1599
+ * Zod's code list evolves.
1600
+ * - consumer-defined — anything the consumer's backend / app stamps
1601
+ * onto a `ValidationError` (via the `parseApiErrors` wire payload
1602
+ * or `setFieldErrors` directly). Pick a scope (`api:`, `auth:`,
1603
+ * etc.) and stay consistent.
1604
+ *
1605
+ * Use these constants in tests and error-routing UI:
1606
+ *
1607
+ * ```ts
1608
+ * if (error.code === AttaformErrorCode.NoValueSupplied) {
1609
+ * // user hasn't filled this field
1610
+ * }
1611
+ * ```
1612
+ */
1613
+ declare const AttaformErrorCode: {
1614
+ /** A required field is in the blank set — user hasn't supplied a value. */
1615
+ readonly NoValueSupplied: "atta:no-value-supplied";
1616
+ /** The schema adapter's `validateAtPath` threw synchronously. */
1617
+ readonly AdapterThrew: "atta:adapter-threw";
1618
+ /**
1619
+ * User code inside a `z.preprocess`, `.refine`, or `.transform`
1620
+ * threw (sync or async). The adapter caught the throw and surfaced
1621
+ * it as a `ValidationError` at the field path so the form's normal
1622
+ * error pipeline handles it instead of leaking as an unhandled
1623
+ * rejection or routing through `submitError`.
1624
+ */
1625
+ readonly ValidatorThrew: "atta:validator-threw";
1626
+ /**
1627
+ * A function-form `defaultValues` factory threw or its promise
1628
+ * rejected. The runtime captures the raw error on `form.hydrateError`
1629
+ * and ALSO surfaces a form-level `ValidationError` (path `[]`) so
1630
+ * the standard error pipeline carries the signal. Critical for the
1631
+ * SSR round-trip: `hydrateError` itself does not ride the wire
1632
+ * payload, but `schemaErrors` does, so the client sees the failure
1633
+ * after rehydration without an extra channel.
1634
+ */
1635
+ readonly HydrationFailed: "atta:hydration-failed";
1636
+ /** The supplied path didn't resolve to any node in the schema. */
1637
+ readonly PathNotFound: "atta:path-not-found";
1638
+ /**
1639
+ * A walked form's `activate()` (async `defaultValues` factory) threw
1640
+ * during `wizard.handleSubmit`'s path walk. Surfaced as a synthetic
1641
+ * `ValidationError` at the form-level path (`[]`) so the wizard's
1642
+ * aggregate error pipeline can carry the failure alongside ordinary
1643
+ * validation errors. The raw factory error remains on
1644
+ * `form.hydrateError` for retry UX.
1645
+ */
1646
+ readonly ActivationFailed: "atta:activation-failed";
1647
+ };
1648
+ type AttaformErrorCode = (typeof AttaformErrorCode)[keyof typeof AttaformErrorCode];
1649
+
1650
+ export { AttaformErrorCode as d, createRegistry as p, defaultCoercionRules as q, defineCoercion as r, getRegistryFromApp as s, injectForm as t, injectWizard as u, kAttaformRegistry as v, lazy as w, useRegister as x, useRegistry as y, useWizard as z };
1651
+ export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, InjectWizardInput as I, LazyMarker as L, SSRDetectOptions as S, UseRegisterReturn as U, WizardCtx as W, SerializedFormData as a, AggregateError as b, AnyForm as c, StepSlot as e, UseWizardReturnType as f, WizardCtxForm as g, WizardOnError as h, WizardOnSubmit as i, WizardOptions as j, WizardPersistFn as k, WizardRestoreFn as l, WizardRestoreState as m, WizardStatusesProxy as n, WizardSubmitContext as o };