attaform 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/abstract.d.mts +15 -21
  2. package/dist/abstract.mjs +3 -3
  3. package/dist/dev/abstract.mjs +4 -4
  4. package/dist/dev/chunks/devtools.mjs +4 -4
  5. package/dist/dev/directive.mjs +1 -1
  6. package/dist/dev/history.mjs +2 -2
  7. package/dist/dev/index.mjs +5 -5
  8. package/dist/dev/runtime/plugins/attaform.mjs +1 -1
  9. package/dist/dev/shared/{attaform-dev.DUZckyV8.mjs → attaform-dev.B1q-x9R9.mjs} +4 -4
  10. package/dist/dev/shared/{attaform-dev.BcYT6tFh.mjs → attaform-dev.CZz4GYZ6.mjs} +21 -9
  11. package/dist/dev/shared/{attaform-dev.BisFstxh.mjs → attaform-dev.Caibt7sb.mjs} +16 -15
  12. package/dist/dev/shared/{attaform-dev.DTrHonpc.mjs → attaform-dev.CtyqGia7.mjs} +176 -260
  13. package/dist/dev/shared/{attaform-dev.BK6Ge2qR.mjs → attaform-dev.DJ3nsWw6.mjs} +86 -122
  14. package/dist/dev/shared/{attaform-dev.BcNtyOmf.mjs → attaform-dev.DlaT4xGd.mjs} +4 -4
  15. package/dist/dev/shared/{attaform-dev.D6CLvaQT.mjs → attaform-dev.k5X47GMD.mjs} +520 -395
  16. package/dist/dev/shared/{attaform-dev.CQ2rJiAX.mjs → attaform-dev.y9x9Vp9F.mjs} +324 -234
  17. package/dist/dev/shared/{attaform-dev.BQc5QV3Y.mjs → attaform-dev.zlvDr0WB.mjs} +2 -2
  18. package/dist/dev/zod-v3.mjs +6 -6
  19. package/dist/dev/zod-v4.mjs +6 -6
  20. package/dist/dev/zod.mjs +5 -5
  21. package/dist/directive.d.mts +4 -4
  22. package/dist/directive.mjs +1 -1
  23. package/dist/esbuild.d.mts +11 -0
  24. package/dist/esbuild.mjs +1 -1
  25. package/dist/history.d.mts +3 -32
  26. package/dist/history.mjs +2 -2
  27. package/dist/index.d.mts +67 -70
  28. package/dist/index.mjs +4 -4
  29. package/dist/nuxt.d.mts +4 -3
  30. package/dist/nuxt.mjs +3 -6
  31. package/dist/rollup.d.mts +11 -0
  32. package/dist/rollup.mjs +1 -1
  33. package/dist/rspack.d.mts +19 -11
  34. package/dist/rspack.mjs +1 -1
  35. package/dist/runtime/components/AttaformDevtoolsPanel.vue +15 -15
  36. package/dist/runtime/components/DevtoolsValueTree.vue +2 -2
  37. package/dist/runtime/plugins/attaform.mjs +1 -1
  38. package/dist/shared/{attaform.BRDDQeHV.mjs → attaform.8mfKZCwH.mjs} +2 -2
  39. package/dist/shared/{attaform.DRUT703x.d.mts → attaform.B-Tb62qV.d.mts} +12 -12
  40. package/dist/shared/{attaform.v0fQtWyA.mjs → attaform.B1rH9ht6.mjs} +1 -1
  41. package/dist/shared/{attaform.DhnPb6nK.mjs → attaform.BAf3UNn4.mjs} +1 -1
  42. package/dist/shared/{attaform.BXneBxEj.mjs → attaform.BWUn8Kjy.mjs} +85 -121
  43. package/dist/shared/{attaform.WiGdIqgx.mjs → attaform.C4shmwyJ.mjs} +2 -2
  44. package/dist/shared/{attaform.Rc9u4Vfw.d.mts → attaform.CRVe8s82.d.mts} +83 -95
  45. package/dist/shared/attaform.D9RqPgGu.d.mts +42 -0
  46. package/dist/shared/attaform.DAo0QBmS.d.mts +2173 -0
  47. package/dist/shared/{attaform.BK0cgu2K.mjs → attaform.DBCbxlyk.mjs} +62 -4
  48. package/dist/shared/{attaform.CUfrMK7t.mjs → attaform.DL2nl_QR.mjs} +15 -14
  49. package/dist/shared/{attaform.DNJOWjQw.mjs → attaform.DNPJPOO7.mjs} +299 -218
  50. package/dist/shared/attaform.DZnrDrNb.d.mts +4657 -0
  51. package/dist/shared/{attaform.B3resMQ1.mjs → attaform.DxKQsqFZ.mjs} +496 -371
  52. package/dist/shared/{attaform.ByWP6kSx.mjs → attaform.L011tHgJ.mjs} +3 -3
  53. package/dist/shared/{attaform.CJ8DpRGj.mjs → attaform.LUPC-2Qg.mjs} +175 -257
  54. package/dist/transforms.d.mts +153 -26
  55. package/dist/transforms.mjs +1 -1
  56. package/dist/vite.d.mts +29 -52
  57. package/dist/vite.mjs +3 -3
  58. package/dist/webpack.d.mts +20 -12
  59. package/dist/webpack.mjs +1 -1
  60. package/dist/zod-v3.d.mts +90 -96
  61. package/dist/zod-v3.mjs +5 -5
  62. package/dist/zod-v4.d.mts +120 -118
  63. package/dist/zod-v4.mjs +5 -5
  64. package/dist/zod.d.mts +5 -5
  65. package/dist/zod.mjs +4 -4
  66. package/package.json +7 -6
  67. package/skills/attaform/SKILL.md +1 -1
  68. package/skills/attaform/references/custom-components.md +4 -0
  69. package/dist/shared/attaform.Clz0ArIi.d.mts +0 -44
  70. package/dist/shared/attaform.UGyOK83T.d.mts +0 -2366
  71. package/dist/shared/attaform.bKLzMU0o.d.mts +0 -4761
@@ -0,0 +1,2173 @@
1
+ import { G as GenericForm, F as FormKey, a as UseFormReturnType, _ as RegisterValue, v as Path, w as PathKey, aq as DisplayCtx, ar as GetDisplayState, as as DisplayMachine, at as FieldRecord, au as ErrorCell, ac as ValidationError, av as OriginalsRecord, A as AbstractSchema, ag as WriteMeta, d as DeepPartial, ah as WriteShape, aw as AttaformDomBinding, ax as TransformAbortHolder } from './attaform.DZnrDrNb.mjs';
2
+ import { Ref, ComputedRef, ShallowRef, App, InjectionKey, Plugin } from 'vue';
3
+
4
+ /** Options accepted by `injectForm` in place of a bare key string. */
5
+ type InjectFormInput = {
6
+ readonly key?: FormKey;
7
+ /**
8
+ * Set by the Vite transform when this `injectForm` call site sits in
9
+ * a component whose template / script reads the form's reactive
10
+ * state. On the server, this enqueues the form for SSR prefetch and
11
+ * wires `onServerPrefetch` so the descendant awaits the activation
12
+ * promise before its render emits HTML.
13
+ *
14
+ * @internal Transform-emitted. Manual use is the documented escape
15
+ * hatch when the transform can't reach the reference (dynamic
16
+ * property access, untransformed bundlers).
17
+ */
18
+ readonly __ssrAccessed?: boolean;
19
+ };
20
+ /**
21
+ * Reach an existing form from a descendant component without threading
22
+ * it through props. The counterpart to `useForm`: `useForm` creates and
23
+ * provides, `injectForm` looks up.
24
+ *
25
+ * ```ts
26
+ * // The nearest ancestor's anonymous useForm() call.
27
+ * const form = injectForm<SignupShape>()
28
+ *
29
+ * // A specific form by key, from anywhere in the app.
30
+ * const cart = injectForm<CartShape>('cart')
31
+ * ```
32
+ *
33
+ * A keyed call is a registry lookup, so it is independent of
34
+ * component-tree position. A no-key call takes the closest ambient
35
+ * ancestor and inherits its `formInstanceId`; only an anonymous
36
+ * `useForm()` fills that ambient slot, so a keyed form is reachable
37
+ * only by its key.
38
+ *
39
+ * Returns `null` when nothing matches, so narrow before use:
40
+ *
41
+ * ```ts
42
+ * const form = injectForm<Shape>('signup')
43
+ * if (!form) return
44
+ * form.register('email')
45
+ * ```
46
+ *
47
+ * A keyed miss warns in dev with the registered keys and the call site.
48
+ * Pass the `Form` generic explicitly, since Vue's provide/inject erases
49
+ * generics. The form stays alive for this component's lifetime and is
50
+ * cleaned up once every consumer has unmounted.
51
+ *
52
+ * Components whose nearest `useForm`-calling ancestor is the same one get
53
+ * the same form object for the same form: `injectForm('cart')` in two of
54
+ * its descendants returns one form, with one set of field caches.
55
+ */
56
+ declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
57
+
58
+ /**
59
+ * Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
60
+ * `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
61
+ * script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
62
+ * template auto-unwrap surfaces the underlying RV to `v-register`
63
+ * and the directive's path-migration diff sees the real RV across
64
+ * renders).
65
+ *
66
+ * The two surfaces don't clash at the type level: `RegisterValue`
67
+ * doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
68
+ * the hybrid's only `.value`. Older code that read `rv.value?.path`
69
+ * keeps working; new code can write `rv.path` directly.
70
+ */
71
+ type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined> & {
72
+ /**
73
+ * Whether a parent has actually bound this wrapper. `false` until a
74
+ * `RegisterValue` lands, `true` once one has, and reactive in both
75
+ * directions: a parent that binds on a later render flips it, and
76
+ * so does one that stops binding.
77
+ *
78
+ * This is the test a dual-mode wrapper reaches for, and the reason
79
+ * it exists as a field rather than a truthiness check on the
80
+ * return: the composable cannot answer "is this bound" at setup
81
+ * time. A parent is free to bind on a later render
82
+ * (`v-register="ready ? form.register('x') : undefined"`), and
83
+ * `useRegister` has to keep serving that, so it can never return a
84
+ * plain `undefined` for "unbound right now". Script-setup code
85
+ * reading `rv` directly sees the hybrid Proxy, which is an object
86
+ * and therefore always truthy; a template sees the unwrapped ref,
87
+ * which IS `undefined` when unbound. `rv?.isBound` reads the same
88
+ * on both sides (#620).
89
+ */
90
+ readonly isBound: boolean;
91
+ };
92
+ /**
93
+ * Options for `useRegister`.
94
+ */
95
+ type UseRegisterOptions = {
96
+ /**
97
+ * Declares that this wrapper is used both with and without a form, so
98
+ * a render with no parent binding is intended rather than a mistake.
99
+ * Silences the unbound diagnostic; nothing else changes.
100
+ *
101
+ * The diagnostic exists because a single-mode wrapper whose parent
102
+ * forgot `v-register` renders a field that looks fine and stores
103
+ * nothing, which is an expensive afternoon. Once a wrapper is
104
+ * deliberately dual-mode, the same signal fires on correct code and
105
+ * tells the author to do something wrong, so this is the one fact
106
+ * that separates the two cases and only the author has it (#620).
107
+ *
108
+ * Pair it with `rv?.isBound` to branch on what actually happened.
109
+ */
110
+ readonly optional?: boolean;
111
+ };
112
+ /**
113
+ * Re-bind a parent's `v-register` onto an inner native element. Use it
114
+ * inside a component that wraps a single form field whose root is NOT
115
+ * the input itself, such as a labelled row that renders `<label>`
116
+ * around the input.
117
+ *
118
+ * ```vue
119
+ * <!-- Parent -->
120
+ * <MyInput v-register="form.register('email')" />
121
+ *
122
+ * <!-- MyInput.vue -->
123
+ * <script setup lang="ts">
124
+ * import { useRegister } from 'attaform'
125
+ * const rv = useRegister()
126
+ * </script>
127
+ *
128
+ * <template>
129
+ * <label class="field">
130
+ * <span>Email</span>
131
+ * <input v-register="rv" />
132
+ * </label>
133
+ * </template>
134
+ * ```
135
+ *
136
+ * The return is a hybrid: `v-register="rv"` hands the directive the
137
+ * parent's own `RegisterValue`, and `rv.path` / `rv.segments` /
138
+ * `rv.formKey` / `rv.innerRef` read directly in script setup with no
139
+ * `.value` unwrap. Reads inside a `computed` or `watchEffect` re-run
140
+ * when the parent rebinds to a different path.
141
+ *
142
+ * Modifiers on the parent's binding apply at the inner control, so
143
+ * `<MyInput v-register.trim="form.register('email')" />` trims there,
144
+ * together with any the wrapper writes on its own `v-register`.
145
+ *
146
+ * When no parent bound, every field reads `undefined`, so reach for
147
+ * `rv?.path`. `v-register="rv"` is still safe. A wrapper that is meant
148
+ * to render both with and without a form declares it with
149
+ * `useRegister({ optional: true })` and branches on `rv?.isBound`.
150
+ *
151
+ * When the wrapper's root IS the input, Vue's attribute fallthrough
152
+ * already binds it and `useRegister` is unnecessary. For a wrapper
153
+ * that binds several fields, use `injectForm<Form>(key?)` and call
154
+ * `ctx.register(...)` directly.
155
+ */
156
+ declare function useRegister<V = unknown>(options?: UseRegisterOptions): UseRegisterReturn<V> | undefined;
157
+
158
+ type DynamicPathSweep = {
159
+ /**
160
+ * Note that a cache has just stored an entry for `segments` under
161
+ * `key`. Paths a schema shape bounds are ignored, so only the
162
+ * candidates the sweep could ever drop are walked.
163
+ */
164
+ readonly track: (segments: Path, key: PathKey) => void;
165
+ /**
166
+ * Register a cache's own eviction. It receives the canonical
167
+ * `PathKey` of a path the form no longer has, and deletes whatever it
168
+ * files under that path. A cache keying by more than the path (a
169
+ * shape sigil, say) deletes every variant; a key it never held is a
170
+ * no-op. Returns the release, which a registrant that does not live
171
+ * as long as the form must call.
172
+ */
173
+ readonly onEvict: (evict: (key: PathKey) => void) => () => void;
174
+ };
175
+
176
+ /**
177
+ * Per-form display engine: owns the clock and the timers the display
178
+ * reducer's timing policy needs, so the reducer itself stays a
179
+ * deterministic `(prev, ctx) => next` function.
180
+ *
181
+ * It keeps a `Map` of the machines that are still *active* (a spinner is
182
+ * showing, a verdict is being held, or a `reviewAt` deadline is pending),
183
+ * a single reactive `tick` ref, and a single `setTimeout` aimed at the
184
+ * nearest `reviewAt` across every active field. When a field-state
185
+ * computed reads `resolve`, it subscribes to `tick`; when the timer
186
+ * fires, `tick` bumps and every dependent computed re-runs the reducer,
187
+ * so a field whose deadline elapsed transitions (verdict → spinner, or
188
+ * spinner → settled verdict) without any per-field watcher.
189
+ *
190
+ * Eviction: a machine is dropped once it reaches a terminal *idle* state
191
+ * with no pending review. Error / success / pending machines are retained
192
+ * so the reducer can hold the prior verdict under the show-delay window of
193
+ * the *next* validation streak (no success → idle → success flicker). The
194
+ * retained set is bounded by the rendered non-idle fields; none of them
195
+ * arm a timer, so retention costs memory, never CPU.
196
+ *
197
+ * The `setTimeout` invariants on a returned `reviewAt` are enforced here
198
+ * rather than trusted from the reducer. A non-finite deadline is ignored
199
+ * instead of handed to `setTimeout`, where it coerces to 0 and spins; an
200
+ * over-large finite deadline is clamped below the 32-bit `setTimeout`
201
+ * overflow; and the timer refuses to re-arm for a deadline it just fired
202
+ * that has also already passed. That last one is load-bearing, not
203
+ * belt-and-braces: Attaform's own reducer re-emits ONE fixed instant for
204
+ * the length of a min-visible hold, which is exactly the shape that can
205
+ * strand a field on `'pending'` forever (see `rearm`).
206
+ *
207
+ * Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
208
+ * so a min-visible hold can overshoot. A `visibilitychange` listener bumps
209
+ * the clock on return to the foreground, so any overdue deadline resolves at
210
+ * once instead of lingering.
211
+ *
212
+ * SSR: no clock, no timers, no listener. With `now` frozen and nothing
213
+ * validating at render, the reducer returns the plain verdict (never
214
+ * pending) and the engine stores nothing, so the server HTML and the
215
+ * client's first render agree, with no hydration mismatch on the display
216
+ * projection.
217
+ */
218
+ type DisplayEngine = {
219
+ /**
220
+ * Resolve a path's next `DisplayMachine`. Subscribes the calling
221
+ * computed to the engine clock, threads the path's previous machine
222
+ * through `reducer`, persists or evicts the result, and re-arms the
223
+ * single timer to the nearest deadline.
224
+ */
225
+ resolve(key: PathKey, ctx: DisplayCtx, reducer: GetDisplayState): DisplayMachine;
226
+ /** Drop every retained machine and cancel the timer (used by `reset()`). */
227
+ clear(): void;
228
+ /** Tear down for good: `clear()` plus detaching the visibility listener. */
229
+ dispose(): void;
230
+ /** Introspection for tests: count of retained machines. Dev builds only. */
231
+ size?(): number;
232
+ /** Introspection for tests: whether a path has a retained machine. Dev builds only. */
233
+ has?(key: PathKey): boolean;
234
+ /** Introspection for tests: whether a deadline timer is armed. Dev builds only. */
235
+ hasTimer?(): boolean;
236
+ };
237
+
238
+ /**
239
+ * The error stores, indexed by path prefix.
240
+ *
241
+ * `aggregateErrorsAt(state, prefix)` answers "every active error at or
242
+ * under this path". Scanning all three stores per call makes a table's
243
+ * reads O(rows x errors) where the answer is O(errors), so keys are
244
+ * collected once per change, resolved to segments once and sorted, and
245
+ * a query binary-searches its own window.
246
+ *
247
+ * A `PathKey` is `JSON.stringify(segments)`, which is what makes a
248
+ * sorted array enough. Every key under `["rows",0]` begins
249
+ * `["rows",0,`: the parent's key minus its closing bracket, plus a
250
+ * comma. Call that the marker. Every descendant is >= the marker, and
251
+ * every descendant is <= the parent's own key, because the two agree
252
+ * up to the last character and `,` (0x2C) sorts before `]` (0x5D). So
253
+ * `[marker, parentKey]` is a contiguous window holding every
254
+ * descendant and the parent itself.
255
+ *
256
+ * The window is a SUPERSET: callers still apply `isPathPrefix`, so this
257
+ * encoding argument cannot quietly become the definition of a prefix.
258
+ * The root has no marker (`[,` begins nothing) and matches everything,
259
+ * so it is answered with the whole index.
260
+ */
261
+
262
+ /** One indexed path: its key, and the segments that key decodes to. */
263
+ type ErrorPathEntry = {
264
+ readonly key: PathKey;
265
+ readonly segments: Path;
266
+ };
267
+
268
+ /**
269
+ * The per-form kernel each `useForm` call owns: form value, field records,
270
+ * element references, the error stores and the submission, validation and
271
+ * transform lifecycles, all keyed by `(formKey, path)` so two forms cannot
272
+ * collide over shared DOM state.
273
+ *
274
+ * Not a singleton. Every `useForm` call builds one; the registry supplies SSR
275
+ * hydration, and otherwise the state is per component per form.
276
+ */
277
+ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
278
+ /**
279
+ * The form's single liveness sweep over every per-path cache. It lives on
280
+ * the store, not on `buildFormApi`, because the store's own maps (`fields`,
281
+ * `originals`, `authoredPaths`, `fieldValidationState`) are the largest
282
+ * thing it evicts and they exist whether or not a form API was ever built
283
+ * around them. Read surfaces register their evictions into it, so one
284
+ * subscription and one walk serve all of them. See `dynamic-path-sweep.ts`.
285
+ */
286
+ readonly pathSweep: DynamicPathSweep;
287
+ readonly formKey: FormKey;
288
+ readonly form: Ref<F>;
289
+ readonly fields: Map<PathKey, FieldRecord>;
290
+ /**
291
+ * The tagged error store: one cell per error-bearing path, each segregating
292
+ * its two sources. The `schema` side is written only by the validation
293
+ * pipeline (`scheduleFieldValidation`, `handleSubmit`, the construction
294
+ * seed, history restore, hydration) and cleared by `reset` / `resetField`
295
+ * and a successful submit. The `user` side is written only by `setErrors` /
296
+ * `clearErrors` (plus history / hydration replay) and survives both schema
297
+ * revalidation and a successful submit, so the consumer owns its lifetime.
298
+ * A key exists iff a side is non-empty, and cells are replaced rather than
299
+ * mutated so Vue's per-key Map tracking fires for either side. Derived blank
300
+ * entries are not stored here; they synthesize at read
301
+ * (`derivedBlankErrors`).
302
+ *
303
+ * A method, not a property, because it is also the gate on the
304
+ * construction seed: the first call runs it (see `createErrorCells`). A
305
+ * getter on the store object would gate a property read the same way,
306
+ * but V8 builds an object literal holding an accessor in dictionary mode,
307
+ * and every kernel read of the store would then pay a hash lookup.
308
+ */
309
+ readonly errorCells: () => Map<PathKey, ErrorCell>;
310
+ /**
311
+ * Reactively-derived "No value supplied" errors: a pure function of
312
+ * `(blankPaths, schema.isRequiredAtPath)` with no writers and no clears.
313
+ * Membership follows `blankPaths`, so typing into a blank required numeric
314
+ * field makes the error vanish and clearing the input brings it back. The
315
+ * `errors` proxy and `getErrorsForPath` merge this alongside the schema and
316
+ * user sides, so the "this required field is empty" error is true the moment
317
+ * it is true, with no `validate()` / `handleSubmit` call.
318
+ *
319
+ * Most entries are `number` / `bigint` leaves, where the side channel is the
320
+ * only way to tell "user typed 0" from "user supplied nothing". String and
321
+ * boolean leaves land here only when the consumer opts in with the `unset`
322
+ * sentinel. See `docs/validation/blank.md`.
323
+ */
324
+ readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
325
+ /**
326
+ * Every path carrying an error at or under `prefix`, sorted by key.
327
+ * `aggregateErrorsAt` takes its candidates from here rather than rescanning
328
+ * all three error stores per call, which is what keeps a table of N rows at
329
+ * O(errors) instead of O(N x errors). See `error-path-index.ts`.
330
+ *
331
+ * Going through this rather than the form-global index is also what isolates
332
+ * containers from each other: the index is one value for the whole form, so
333
+ * any path gaining or losing an error gives it a fresh identity. Each prefix
334
+ * gets a memoised `computed` that hands back its previous array when its own
335
+ * window is unchanged, so the global change stops there. See `errorWindowAt`.
336
+ */
337
+ readonly errorWindowAt: (prefix: Path, prefixKey: PathKey) => readonly ErrorPathEntry[];
338
+ readonly originals: Map<PathKey, OriginalsRecord>;
339
+ /**
340
+ * Reactive set of paths whose displayed state should be EMPTY even though
341
+ * storage holds a real, schema-conformant value (the slim default). It
342
+ * records storage / display divergence, the case where storage alone cannot
343
+ * tell "user typed 0" from "user supplied nothing".
344
+ *
345
+ * `number` / `bigint` are the reason it exists: storage holds `0` / `0n`
346
+ * while the DOM input shows `''`, so the directive's input listener marks
347
+ * the path on clear. Strings and booleans need no mark (`''` storage is `''`
348
+ * display, `false` is unchecked), so they are never auto-marked. Consumers
349
+ * can still mark any primitive leaf through the `unset` sentinel
350
+ * (`defaultValues: { x: unset }`, `setValue('x', unset)`,
351
+ * `reset({ x: unset })`), where the mark is declared intent rather than
352
+ * runtime inference.
353
+ *
354
+ * Reads (the `displayValue` computed, `fields.<path>.blank`,
355
+ * `derivedBlankErrors`) track through Vue's reactive Set handlers; writes
356
+ * happen in `setValueAtPath` (`blank: true` meta adds, any other write
357
+ * removes) and `reset`.
358
+ *
359
+ * Storage never reflects this set: reads against `form.value` see the slim
360
+ * default. It is a display / intent channel that `derivedBlankErrors`
361
+ * consults. See `docs/validation/blank.md`.
362
+ */
363
+ readonly blankPaths: Set<PathKey>;
364
+ /**
365
+ * Snapshot of `blankPaths` taken at construction and re-taken on
366
+ * `reset(args)`. Dirty calculation compares against it, so a path whose
367
+ * membership diverges is dirty even when storage matches the original.
368
+ * Populated eagerly, so there is no "dirty on first read" window.
369
+ */
370
+ readonly originalBlankPaths: Set<PathKey>;
371
+ readonly schema: AbstractSchema<F, G>;
372
+ /**
373
+ * Server-side flag, plumbed in from `registry.ssr`.
374
+ * `markConnectedOptimistically()` reads it before flipping
375
+ * `connected: true`; on the client it is a no-op, leaving the directive
376
+ * lifecycle as the source of truth.
377
+ */
378
+ readonly ssr: boolean;
379
+ /**
380
+ * Per-form display engine. It owns the clock and the single timer the timed
381
+ * display policy needs, which is what keeps the reducer a pure
382
+ * `(prev, ctx) => next`. Field-state computeds route every `displayState`
383
+ * read through `displayEngine.resolve(...)`, which threads the path's
384
+ * previous machine, persists or evicts the result, and re-arms the
385
+ * nearest-deadline timer. Torn down through `registerCleanup` on eviction.
386
+ */
387
+ readonly displayEngine: DisplayEngine;
388
+ readonly submitting: Ref<boolean>;
389
+ readonly activeSubmissions: Ref<number>;
390
+ readonly submissionAttempts: Ref<number>;
391
+ /**
392
+ * `true` once a `handleSubmit` callback resolved without throwing.
393
+ * Independent of `submissionAttempts`: a failed submit increments attempts
394
+ * and leaves `submitted` at `false`. Cleared by `reset()`.
395
+ */
396
+ readonly submitted: Ref<boolean>;
397
+ readonly submitError: Ref<Error | null>;
398
+ readonly departAttempts: Ref<number>;
399
+ /**
400
+ * Effective data-freeze state: `true` when either the form's own
401
+ * `disabled` config resolves truthy or `externalLock` is set. Read at
402
+ * the write chokepoint (`setValueAtPath`), the register value's
403
+ * `disabled`, and the field display predicate.
404
+ */
405
+ readonly effectiveDisabled: ComputedRef<boolean>;
406
+ /**
407
+ * Wizard-driven freeze channel. `useWizard` sets it `true` for a locked
408
+ * step's member form, and `effectiveDisabled` ORs it in, so a member form
409
+ * cannot escape the lock by passing `disabled: false`. Default `false`.
410
+ */
411
+ readonly externalLock: Ref<boolean>;
412
+ /**
413
+ * `true` while a function-form `defaultValues` factory is in flight, and
414
+ * always `false` for plain-value `defaultValues`. Shared across every
415
+ * `useForm({ key })` call resolving to this store, so the second caller
416
+ * sees the first caller's hydration state.
417
+ */
418
+ readonly hydrating: Ref<boolean>;
419
+ /**
420
+ * Error from the most recent function-form `defaultValues` factory.
421
+ * Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
422
+ * shape matches `form.errors` / `form.meta.errors` entries. `null` when
423
+ * no factory has fired or the last one succeeded.
424
+ */
425
+ readonly hydrateError: Ref<ValidationError | null>;
426
+ /**
427
+ * The function-form `defaultValues` factory, captured at the first
428
+ * `useForm({ key })` call that wired this store. `undefined` for
429
+ * plain-value forms. Read by `form.rehydrate()`.
430
+ */
431
+ readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
432
+ /**
433
+ * `true` when this store carries an SSR prefetch queue, the server path
434
+ * where `activate()` must enqueue intent before deciding whether to fire.
435
+ * `buildFormApi` uses it to skip the lazy activation gate for forms with
436
+ * neither a factory nor a prefetch, the common client case where `gated()`
437
+ * is pure overhead on every public method call.
438
+ */
439
+ readonly hasSsrPrefetch: boolean;
440
+ /**
441
+ * `true` once the form's effective defaults have been applied, whether from
442
+ * a sync `defaultValues` at construction or an async factory that settled.
443
+ * Stays `false` for a dormant lazy form until it activates. `useWizard`
444
+ * reads it to decide between surfacing seed status and live meta.
445
+ */
446
+ readonly defaultsResolved: Ref<boolean>;
447
+ /**
448
+ * `true` once the captured async factory has been kicked off, set
449
+ * synchronously by `activate()` before the factory resolves. Paired with
450
+ * `defaultsResolved` (which flips only on settle) so the API surface can
451
+ * tell "started" from "done".
452
+ */
453
+ readonly activated: Ref<boolean>;
454
+ /**
455
+ * In-flight activation promise. Concurrent callers (cross-component SSR
456
+ * consumers, recursive factory reads, parallel `activate()` calls) all
457
+ * receive this one, so the factory runs once under contention.
458
+ */
459
+ readonly activationPromise: Ref<Promise<void> | undefined>;
460
+ /**
461
+ * Idempotent activation entrypoint. Fires the captured function-form
462
+ * `defaultValues` factory on the first call and publishes the in-flight
463
+ * promise; later calls return that promise until it settles and
464
+ * `Promise.resolve()` afterwards. A plain-value form always resolves
465
+ * immediately. Every public getter and method except `key` routes through
466
+ * here, so the form activates on first use.
467
+ */
468
+ activate(): Promise<void>;
469
+ /**
470
+ * Re-fire the captured function-form `defaultValues` factory. Throws
471
+ * synchronously on a plain-value form, where no factory was captured.
472
+ * Resolves once `hydrating` is back to `false`, so `await form.rehydrate()`
473
+ * can gate UI on the fresh load.
474
+ *
475
+ * Leaves touched / submit state alone; chain `form.reset()` for a clean
476
+ * surface. It does re-seat the defaults (see `adoptResolvedDefaults`), so
477
+ * afterwards dirty means "differs from what the factory just returned" and
478
+ * `form.reset()` lands on the fresh values instead of discarding them.
479
+ */
480
+ rehydrate(): Promise<void>;
481
+ /**
482
+ * Adopt a resolved async-defaults value as the form's durable defaults and
483
+ * re-seed the dirty baseline from it. Called by the activate / rehydrate
484
+ * orchestrator once its factory settles. It hangs off the store rather than
485
+ * being imported because `form-activation` imports this module, and a value
486
+ * import back the other way would close a cycle.
487
+ */
488
+ adoptResolvedDefaults(value: unknown): void;
489
+ /**
490
+ * Incremented by every `reset()`. The submit wrapper captures it at entry
491
+ * and skips writing `submitError` from a catch that fires after a reset,
492
+ * which is what stops a reset-during-submit clearing `submitError` only for
493
+ * it to reappear when the in-flight promise rejects.
494
+ */
495
+ readonly submissionGeneration: Ref<number>;
496
+ /**
497
+ * Counts in-flight validation calls across every `validate()` ref and every
498
+ * committing `parse(...)` / `handleSubmit` pre-check; public `validating`
499
+ * mirrors `> 0`. Separate from the submission counters because a debounced
500
+ * field check overlapping a submit must show the union of both.
501
+ */
502
+ readonly activeValidations: Ref<number>;
503
+ /**
504
+ * `true` once the form has completed at least one validation pass, flipping
505
+ * when `activeValidations` returns to 0 from a positive value. Until then
506
+ * `meta.valid` and `field.valid` report `false` even with an empty schema
507
+ * side, because no errors at frame 1 means "not checked yet", not "checked
508
+ * and clean".
509
+ *
510
+ * The window it closes: a schema with async work gets no verdict from the
511
+ * construction parse (v4 skips it for an async refine or transform; v3 skips
512
+ * it for an async transform and mounts clean when it throws over an async
513
+ * refine), so no construction errors land, and until the queued async pass
514
+ * runs, frame 1 would paint the form valid a tick before the real verdict.
515
+ * A sync-only schema's construction parse seeds its verdict synchronously,
516
+ * before the first read of its errors returns, so its gate starts open. v3
517
+ * reports every `.refine` as async, so a v3 schema with a sync refine
518
+ * starts gated and seeded.
519
+ *
520
+ * `reset()` restores it to the same construction-time seed, so the post-reset
521
+ * window gates exactly like the post-mount one.
522
+ */
523
+ readonly firstValidationDone: Ref<boolean>;
524
+ /**
525
+ * Async-gate verdict for a path whose canonical key the caller already
526
+ * holds. `key` is required and must correspond to `segments`; skipping the
527
+ * `canonicalizePath` round-trip is what lets a descendant walk, whose Map
528
+ * iteration already yields the key, avoid a canonicalize per leaf.
529
+ */
530
+ pathHasAsyncValidationByKey(key: PathKey, segments: Path): boolean;
531
+ /**
532
+ * Per-path counter of in-flight field-level validation runs;
533
+ * `FieldState.validating` mirrors `> 0`. Incremented and decremented in
534
+ * lockstep with `activeValidations` inside `scheduleFieldValidation`'s `run`
535
+ * closure, so the two are co-extensive on the field-scheduled branch.
536
+ * Whole-form `validate()` / `parse()` runs have no single path and touch
537
+ * only `activeValidations`.
538
+ *
539
+ * A counter rather than a Set because two runs at one path can overlap: an
540
+ * aborted run's `.finally` lands after its replacement's increment, and
541
+ * `> 0` keeps the field validating across that boundary.
542
+ *
543
+ * Reactive Map, so `.get()` / `.has()` / `.size` track per key and a
544
+ * FieldState computed re-runs only for its own key.
545
+ */
546
+ readonly fieldValidationCounts: Map<PathKey, number>;
547
+ /**
548
+ * Per-path `Date.now()` stamp for the start of the field's latest validation
549
+ * run, re-anchored on every run start and deleted on the edge back to 0. The
550
+ * display reducer reads it as `ctx.validatingSince` and times the anti-flash
551
+ * spinner off `now - validatingSince`.
552
+ *
553
+ * Re-anchoring per run, not per streak, is the point: a burst of keystrokes
554
+ * keeps pushing the stamp forward and the spinner stays suppressed until the
555
+ * user pauses. Anchoring at the streak start would pin it to the first
556
+ * keystroke, because at `debounceMs: 0` an aborted run's decrement lands
557
+ * after the next run's increment and the count never reaches 0 between fast
558
+ * keystrokes. The container walk takes the descendant minimum, so a row
559
+ * spinner anchors at its earliest still-active leaf.
560
+ *
561
+ * Reactive, and that is load-bearing: the display computed reads this stamp
562
+ * but not the `validating` flag, and a long run that settles on an unchanged
563
+ * verdict leaves `errors` / `valid` untouched. Without reactivity a held
564
+ * `pending` spinner would strand after the run ended, waiting on some
565
+ * unrelated change to re-run the computed. Runtime-only, never hydrated.
566
+ */
567
+ readonly fieldValidatingSince: Map<PathKey, number>;
568
+ /**
569
+ * Per-path counter of in-flight async-transform runs, the async branch of
570
+ * the `register({ transforms })` pipeline; `> 0` drives
571
+ * `field.transforming` / `field.busy`. A counter for the same overlap reason
572
+ * as `fieldValidationCounts`, except a superseding input releases the prior
573
+ * run synchronously first, so in practice the value is 0 or 1.
574
+ */
575
+ readonly fieldTransformCounts: Map<PathKey, number>;
576
+ /**
577
+ * Per-path `ssr ? 0 : Date.now()` stamp for the opening of the path's latest
578
+ * async transform, read by the display reducer as `ctx.transformingSince` to
579
+ * time the gated busy spinner. Mirrors `fieldValidatingSince` exactly, down
580
+ * to why it is reactive.
581
+ */
582
+ readonly fieldTransformingSince: Map<PathKey, number>;
583
+ /**
584
+ * Per-path latest async-transform failure, either a rejected transform or a
585
+ * resolved value the write gate refused, surfaced as `field.transformError`.
586
+ * Cleared when a fresh run opens at the path and on `reset()`. A channel of
587
+ * its own, separate from validation `errors`.
588
+ */
589
+ readonly transformErrors: Map<PathKey, Error | null>;
590
+ /**
591
+ * Form-wide count of in-flight async-transform runs, behind the
592
+ * `settleTransforms` quiescence guard and the `handleSubmit` drain barrier.
593
+ * Clamped at 0 on release, so a run's own late `endTransform` after a
594
+ * synchronous cancel cannot drive it negative.
595
+ */
596
+ readonly activeTransforms: Ref<number>;
597
+ /**
598
+ * Replace the form value wholesale. `meta` is forwarded to every
599
+ * `onFormChange` listener so each can decide whether this write is one it
600
+ * cares about, the way history tags a hydration replay.
601
+ */
602
+ applyFormReplacement(next: F, meta?: WriteMeta): void;
603
+ /**
604
+ * Set a single path's value. `meta` reaches listeners through
605
+ * `applyFormReplacement`; public `form.setValue` passes none.
606
+ *
607
+ * Returns `false` when the slim-primitive gate rejects the write, meaning
608
+ * the value's primitive shape does not match the schema's slim shape at the
609
+ * path. The store is unchanged in that case.
610
+ */
611
+ setValueAtPath(path: Path, value: unknown, meta?: WriteMeta): boolean;
612
+ getValueAtPath(path: Path): unknown;
613
+ /**
614
+ * Stable identity for the array element at `path`, the token the array
615
+ * engine maintains across structural mutations. Empty for anything that is
616
+ * not an array element (a record entry, a fixed-object field, a container,
617
+ * the root). Backs `FieldState.key`.
618
+ */
619
+ arrayElementKey(path: Path): string;
620
+ reset(nextDefaultValues?: DeepPartial<WriteShape<F>>): void;
621
+ resetField(path: Path): void;
622
+ setSchemaErrorsForPath(path: Path, errors: ValidationError[]): void;
623
+ setAllSchemaErrors(errors: readonly ValidationError[]): void;
624
+ clearSchemaErrors(path?: Path): void;
625
+ /**
626
+ * Replace the schema side under `path` with `errors`, keying each error by
627
+ * its OWN absolute path. The validation pipelines
628
+ * (`scheduleFieldValidation`, the committing parse, `handleSubmit`, `reset`)
629
+ * commit a parse result through here: entries missing from the new pass drop
630
+ * out of the subtree, and surviving keys update in place so insertion order
631
+ * holds. Pass `[]` for whole-form scope.
632
+ */
633
+ applySchemaErrorsForSubtree(path: Path, errors: ValidationError[]): void;
634
+ setAllUserErrors(errors: readonly ValidationError[]): void;
635
+ setUserErrorsForPath(path: Path, errors: readonly ValidationError[]): void;
636
+ clearUserErrors(path?: Path): void;
637
+ /**
638
+ * Rebuild the whole tagged store from a snapshot's `[key, cell]` entries,
639
+ * the history ring buffer's restore road. Entry arrays are cloned in so the
640
+ * snapshot stays detached from the live store, and a cell with both sides
641
+ * empty is skipped.
642
+ */
643
+ restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
644
+ /**
645
+ * Merged read of the cell at `path`, in schema -> blank -> user order:
646
+ * structural validation first, the synthesized blank entry between, user
647
+ * entries last. Same order as `getFirstErrorElement` and the top-level
648
+ * `errors` proxy iterate in.
649
+ */
650
+ getErrorsForPath(path: Path): ValidationError[];
651
+ /**
652
+ * A stable schema-declaration ordinal for `key`, assigned on first sight.
653
+ * `form.meta.errors` sorts by it, so the aggregate is a function of the SET
654
+ * of errors present rather than of the order their Map keys were last `set`.
655
+ * The construction seed walks every leaf of the schema's slim default;
656
+ * runtime paths (a second DU variant, a dynamic array index, a refine
657
+ * targeting a cross-field path) take first-encounter ordinals and keep them
658
+ * for the form's lifetime.
659
+ */
660
+ ensurePathOrdinal(key: PathKey): number;
661
+ /**
662
+ * The store's DOM slice, `null` until the directive cluster or `useRegister`
663
+ * arms it through `RegisterValue.ensureDomBinding`. Element registration,
664
+ * host anchors and first-error focus resolution live behind it; eager
665
+ * readers treat `null` as an empty registry.
666
+ */
667
+ readonly domBinding: ShallowRef<AttaformDomBinding | null>;
668
+ /**
669
+ * Field-record connect transition, driven by the DOM binding on element
670
+ * attach or host connect, and by the SSR-only
671
+ * `markConnectedOptimistically`. Sets `connected: true` and lifts
672
+ * `focused` / `blurred` from `null` to optimistic booleans only while they
673
+ * are null, so a boolean from an early focus event is never clobbered.
674
+ */
675
+ noteDomConnected(path: Path): void;
676
+ /**
677
+ * Field-record disconnect transition, driven by the DOM binding when a
678
+ * path's last element detaches or its host disconnects. `connected: false`,
679
+ * and `focused` / `blurred` back to `null`, since those describe an element
680
+ * that no longer exists. Interaction history stays.
681
+ */
682
+ noteDomDisconnected(path: Path): void;
683
+ /**
684
+ * `meta.instance` carries per-`useForm()`-instance overrides for
685
+ * `validateOn` / `debounceMs`, so the blur trigger honours the caller's own
686
+ * config when sibling instances share a FormStore.
687
+ */
688
+ markFocused(path: Path, focused: boolean, meta?: {
689
+ readonly instance?: WriteMeta['instance'];
690
+ }): void;
691
+ /**
692
+ * Flip the sticky `interacted` bit on a leaf. Driven by the directive's
693
+ * input listeners through `RegisterValue.markInteracted`. Idempotent, and
694
+ * never set by a programmatic write.
695
+ */
696
+ markInteracted(path: Path): void;
697
+ /**
698
+ * Walk every active-variant leaf under `segments` and flip `touched: true`.
699
+ * Powers `form.touch(path?)`. Idempotent, and touches neither value nor
700
+ * `focused` / `blurred`, nor does it trigger validation.
701
+ */
702
+ touchAtPath(segments: Path): void;
703
+ /**
704
+ * Walk every active-variant leaf under `segments` and flip the whole
705
+ * interaction ladder (`touched` / `interacted` / `blurredAfterInteraction`),
706
+ * as though the user had focused, edited and left each one. Powers
707
+ * `form.interact(path?)`. Idempotent, and leaves value / `focused` /
708
+ * `blurred` alone. Returns whether any leaf resolved, so the caller can skip
709
+ * validation and dev-warn on an empty path.
710
+ */
711
+ interactAtPath(segments: Path): boolean;
712
+ /**
713
+ * SSR-only optimistic mark: `connected: true` with no DOM element behind it.
714
+ * The `vRegisterHintTransform` compile-time transform calls it through
715
+ * `RegisterValue.markConnectedOptimistically()` for every element rendered
716
+ * with `v-register`. Idempotent, and a no-op on the client, where the
717
+ * directive's `created` hook is authoritative.
718
+ */
719
+ markConnectedOptimistically(path: Path): void;
720
+ /**
721
+ * Pristine verdict for a path whose canonical key the caller already holds.
722
+ * `key` is required and must correspond to `segments`; skipping the
723
+ * `canonicalizePath` round-trip is what lets a descendant walk, whose Map
724
+ * iteration already yields the key, avoid a canonicalize per leaf.
725
+ */
726
+ isPristineAtPathByKey(key: PathKey, segments: Path): boolean;
727
+ /**
728
+ * Whether any tracked array under `path` has changed shape (a reorder,
729
+ * insert or removal) against its construction / reset baseline. The
730
+ * structural half of `dirty`: per-element baselines travel with their
731
+ * element across a mutation, so a positional value comparison alone cannot
732
+ * see the shape change.
733
+ */
734
+ hasStructuralChangeUnder(path: Path): boolean;
735
+ /**
736
+ * Whether a baseline-present container under `path` was replaced wholesale
737
+ * by a non-container (`setValue('profile', undefined)` and the like) and is
738
+ * still absent. The other half of removal-driven `dirty`: every leaf in such
739
+ * a subtree vanishes at once, so neither the present-leaf walk nor the array
740
+ * tracker sees the loss. Self-filters by current liveness, so a refilled
741
+ * path stops counting.
742
+ */
743
+ hasRemovedSubtreeUnder(path: Path): boolean;
744
+ getFieldRecord(path: Path): FieldRecord | undefined;
745
+ /**
746
+ * Cancel every in-flight field-level validation run: clear the timers of
747
+ * debounced 'change' runs that have not fired, and latch `aborted` on runs
748
+ * whose async parse is in flight. Called by `handleSubmit` at entry, where
749
+ * submit validation is authoritative, and by `reset()`.
750
+ */
751
+ cancelFieldValidation(): void;
752
+ /**
753
+ * Open an async-transform run at `key`: bump the run token, increment the
754
+ * in-flight counters, stamp `transformingSince`, clear any prior
755
+ * `transformError` and register `holder` for a later abort. Returns the run
756
+ * token. See `InternalRegisterValue.beginTransform`.
757
+ */
758
+ beginTransform(key: PathKey, holder: TransformAbortHolder): number;
759
+ /** `true` while `token` is the live async-transform run at `key`. */
760
+ isCurrentTransform(key: PathKey, token: number): boolean;
761
+ /**
762
+ * Close the run `token` at `key`: release the counters, a no-op when a
763
+ * supersede or cancel already released them, and flush settled
764
+ * `settleTransforms` waiters.
765
+ */
766
+ endTransform(key: PathKey, token: number): void;
767
+ /** Record a per-field normalization failure at `key` (`field.transformError`). */
768
+ setTransformError(key: PathKey, err: Error): void;
769
+ /**
770
+ * Path-scoped counterpart to `cancelTransforms`: abort + release only
771
+ * the runs at-or-under `prefix`, clearing their `transformError`.
772
+ * Called by `resetField`.
773
+ */
774
+ cancelTransformsUnder(prefix: Path): void;
775
+ /**
776
+ * Resolve once async transforms are quiescent, either form-wide (`path`
777
+ * omitted) or at and under `path`. Resolves, never rejects. See
778
+ * `UseFormReturnType.settleTransforms`.
779
+ */
780
+ settleTransforms(path?: string | Path): Promise<void>;
781
+ /**
782
+ * Kick off, or schedule, a field-level validation run for `path`. Pass `[]`
783
+ * for whole-form scope, where `applySchemaErrorsForSubtree` replaces every
784
+ * schema-side entry with the adapter's full response. That is the road the
785
+ * construction-time async-refine seed takes.
786
+ *
787
+ * `immediate: true` skips the debounce window and calls the adapter on the
788
+ * next microtask; one-shot triggers use it, while the per-keystroke writers
789
+ * pass `false` so rapid mutations coalesce under `debounceMs`.
790
+ *
791
+ * `instance` carries per-`useForm()`-instance overrides for `validateOn` and
792
+ * `debounceMs`, so sibling instances sharing a FormStore each validate on
793
+ * their own cadence.
794
+ */
795
+ scheduleFieldValidation(path: Path, immediate: boolean, instance?: WriteMeta['instance']): void;
796
+ /**
797
+ * Subscribe to every `applyFormReplacement`. Fires synchronously once
798
+ * `form.value` holds `next` and all field / originals bookkeeping has run,
799
+ * which is how undo/redo hooks the single mutation funnel. `meta` carries
800
+ * the originating call site's intent and can be ignored. Returns an
801
+ * unsubscribe function.
802
+ */
803
+ onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
804
+ /**
805
+ * Subscribe to successful submissions. Fires once the consumer's `onSubmit`
806
+ * callback has resolved, so neither a validation failure nor a callback
807
+ * throw reaches it. The DevTools panel rides it to surface a submit event.
808
+ * Returns an unsubscribe function.
809
+ */
810
+ onSubmitSuccess(listener: () => void): () => void;
811
+ /**
812
+ * Subscribe to `reset()`. Fires after reset has replaced the form and
813
+ * cleared errors and lifecycle, so a listener sees the post-reset state.
814
+ * The history module uses it to drop the undo/redo stack. Returns an
815
+ * unsubscribe function.
816
+ */
817
+ onReset(listener: () => void): () => void;
818
+ /**
819
+ * Notify submit-success subscribers. `handleSubmit` in `process-form.ts`
820
+ * calls it once the consumer callback has resolved. Not for consumer use.
821
+ */
822
+ emitSubmitSuccess(): void;
823
+ /**
824
+ * Register a teardown bound to the FormStore's own lifetime rather than to a
825
+ * consumer's Vue effect scope; `dispose()` runs it when the last consumer
826
+ * unmounts. The history wiring needs this, so its subscribers survive one
827
+ * consumer unmounting while others remain.
828
+ */
829
+ registerCleanup(fn: () => void): void;
830
+ /**
831
+ * Cache for per-state modules, chiefly history, that must outlive any single
832
+ * consumer. Later `useForm` / `injectForm` calls for the same key read from
833
+ * here, so the public API shape does not depend on mount order. Keyed by a
834
+ * caller-owned string such as `'history'`.
835
+ */
836
+ readonly modules: Map<string, unknown>;
837
+ /**
838
+ * Whether schema-driven coercion runs for this form, `false` only when the
839
+ * consumer passed `useForm({ coerce: false })`. `buildCoerceFn` reads it at
840
+ * `register()` time to bake the per-path closure on `RegisterValue.coerce`.
841
+ */
842
+ readonly coerceEnabled: boolean;
843
+ /**
844
+ * Tear down the non-reactive resources this FormStore owns. The registry
845
+ * calls it when the last consumer unmounts: pending field-validation timers
846
+ * are cancelled, every subscriber is dropped, and each `registerCleanup`
847
+ * hook fires.
848
+ */
849
+ dispose(): void;
850
+ };
851
+
852
+ /**
853
+ * Public types for `useWizard`, the multistep-form orchestrator.
854
+ *
855
+ * A wizard is an ordered list of step slots. Each slot resolves to a
856
+ * participating form: a `useForm` reference, a bare string key
857
+ * (desugared to a noop form so affordance steps participate
858
+ * uniformly), a function slot for runtime branching, or a `lazy()`
859
+ * function slot that caches its resolution.
860
+ *
861
+ * The read surface is string-keyed (`Record<FormKey, ...>`), because a
862
+ * flow threaded through `injectWizard` has no lexical key knowledge.
863
+ * Typed per-form access flows back through the original form refs and
864
+ * through `wizard.handleSubmit`'s `ctx.get(formRef)` accessor.
865
+ */
866
+
867
+ /**
868
+ * Minimum shape the wizard requires from a participating form. The
869
+ * wizard routes by `key` at runtime and hands back the original form
870
+ * objects untouched, so it never constrains their value shapes.
871
+ */
872
+ type AnyForm = {
873
+ readonly key: FormKey;
874
+ };
875
+ /**
876
+ * Per-form summary at `wizard.statuses[key]`, and what `defaultStatuses`
877
+ * seeds. This is the cross-step rollup built for templates
878
+ * (`{{ wizard.statuses.cargo.valid }}`); `form.meta` carries the full
879
+ * per-form lifecycle surface.
880
+ *
881
+ * A noop form generated for a string slot reads as default-valid,
882
+ * unless that slot is a `gate('...')`, whose `gate` tracks the
883
+ * in-session acknowledgment.
884
+ */
885
+ type FormStatus = {
886
+ /** `form.meta.valid`. `false` while errors exist, or before the first validation pass. */
887
+ readonly valid: boolean;
888
+ /** `form.meta.dirty`. `true` once any value differs from the original defaults. */
889
+ readonly dirty: boolean;
890
+ /**
891
+ * `form.meta.submitted`. `true` once a `handleSubmit` callback has
892
+ * resolved without throwing. A failed submit leaves it `false`;
893
+ * `submissionAttempts > 0` is the "user has tried" signal.
894
+ */
895
+ readonly submitted: boolean;
896
+ /** `form.meta.errorCount`. Active validation errors, zero when valid. */
897
+ readonly errorCount: number;
898
+ /**
899
+ * `true` when this step sits after an uncleared gate: its form is
900
+ * frozen and navigation to it is refused, so a stepper can render it
901
+ * as unreachable.
902
+ *
903
+ * Orthogonal to `gate`. The first uncleared gate reads
904
+ * `locked: false`, since you must reach it to clear it; a later gate
905
+ * sealed behind it reads both `gate: 'uncleared'` and `locked: true`.
906
+ */
907
+ readonly locked: boolean;
908
+ /**
909
+ * This step's own role as a hard prerequisite. `null` when the step is
910
+ * not a `gate()`, `'uncleared'` while its form is unconfirmed (so it
911
+ * seals every later step), `'cleared'` once confirmed by a clean
912
+ * submit or seeded via `defaultStatuses`.
913
+ *
914
+ * Reflects the live compiled shape, so a conditional `gate()` that a
915
+ * function slot drops resolves to `null`.
916
+ */
917
+ readonly gate: 'cleared' | 'uncleared' | null;
918
+ };
919
+ /**
920
+ * Seed accepted by `useWizard({ defaultStatuses })`. An omitted field
921
+ * falls back to the pending sentinel on read.
922
+ *
923
+ * `locked` is not accepted: it is derived live from the wizard's gates
924
+ * and overlaid on every status read, so a seeded value would be ignored.
925
+ */
926
+ type FormStatusSeed = {
927
+ readonly valid?: boolean;
928
+ readonly dirty?: boolean;
929
+ readonly submitted?: boolean;
930
+ readonly errorCount?: number;
931
+ /**
932
+ * Write-only, and not echoed back on read. `'cleared'` latches the
933
+ * gate's cleared state once at construction, for restoring a
934
+ * confirmed prerequisite; the live `gate` overlay then owns the read.
935
+ * `'uncleared'` is the default and seeds nothing. Only a step that
936
+ * compiles to a `gate()` honors it.
937
+ */
938
+ readonly gate?: 'cleared' | 'uncleared';
939
+ };
940
+ /**
941
+ * One error from `wizard.allErrors[key]`. Carries the formKey and path
942
+ * so a wizard-wide error summary can route to the offending field.
943
+ */
944
+ type WizardAggregateError = {
945
+ readonly formKey: FormKey;
946
+ readonly path: ReadonlyArray<string | number>;
947
+ readonly message: string;
948
+ readonly code?: string;
949
+ };
950
+ /**
951
+ * The per-key `FormStatus` record, readable three ways:
952
+ * `wizard.statuses.cargo.valid`, `wizard.statuses('cargo')`, or
953
+ * `wizard.statuses()` for the whole record.
954
+ */
955
+ type WizardStatusesProxy<S extends Record<string, FormStatus>> = ((key?: keyof S) => FormStatus | S) & Readonly<S>;
956
+ /**
957
+ * One compiled position in the flow, as exposed by `wizard.steps`.
958
+ * String slots desugar to noop forms before compilation, so every
959
+ * compiled step carries a `form` whatever its source kind.
960
+ */
961
+ type CompiledStep = {
962
+ readonly key: FormKey;
963
+ readonly form: AnyForm;
964
+ };
965
+ /**
966
+ * A participating form as seen from a function slot's `ctx.forms[key]`.
967
+ * Adds `values` to `AnyForm` so routing decisions can read live state.
968
+ *
969
+ * Values are loose here. For typed access inside a slot body, close
970
+ * over the original form ref instead of routing through `ctx.forms`.
971
+ */
972
+ type WizardCtxForm = AnyForm & {
973
+ readonly values: Readonly<Record<string, unknown>>;
974
+ };
975
+ /**
976
+ * Context passed to function slots in the `steps` array. `forms` holds
977
+ * the statically-known forms: every top-level `AnyForm` slot plus every
978
+ * noop form synthesized for a top-level string slot.
979
+ *
980
+ * Function slots re-evaluate reactively when the values they read
981
+ * mutate, typically `ctx.forms.<key>.values.<path>`. The `forms`
982
+ * accumulator keeps its identity across re-evaluations. Keep slot
983
+ * bodies free of side effects; they are routing decisions.
984
+ */
985
+ type WizardCtx = {
986
+ readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
987
+ readonly currentKey: FormKey | undefined;
988
+ };
989
+ /**
990
+ * What a function slot or a `lazy()` / `gate()` resolver may yield: a
991
+ * participating form, a bare affordance key, a nested `lazy()` /
992
+ * `gate()` wrapper (so the two compose in either order), or `null` /
993
+ * `undefined` to drop the position from the compiled list.
994
+ */
995
+ type SlotResolution<Ctx = WizardCtx> = AnyForm | string | null | undefined | LazyMarker<Ctx> | GateMarker;
996
+ /**
997
+ * Phantom brand for `LazyMarker`. The runtime brand symbol lives in
998
+ * `core/wizard-lazy.ts`; this declaration keeps the marker unforgeable
999
+ * without a circular import.
1000
+ */
1001
+ declare const _lazyBrand: unique symbol;
1002
+ /**
1003
+ * Marker returned by `lazy((ctx) => ...)`, which gives a function slot
1004
+ * its own memoization cache: the resolver fires once on the first
1005
+ * compile pass and stays cached until one of its own tracked reactive
1006
+ * reads changes, or `wizard.reset()` invalidates it. An unrelated
1007
+ * slot's deps changing does not re-fire it, so heavy or one-shot
1008
+ * lookups stay one-shot.
1009
+ *
1010
+ * Construct via the `lazy()` helper exported alongside `useWizard`. The
1011
+ * marker is opaque; consumers do not assemble it directly.
1012
+ */
1013
+ type LazyMarker<Ctx = WizardCtx> = {
1014
+ readonly [_lazyBrand]: true;
1015
+ readonly resolve: (ctx: Ctx) => SlotResolution<Ctx>;
1016
+ };
1017
+ /**
1018
+ * Phantom brand for `GateMarker`. The runtime brand symbol lives in
1019
+ * `core/wizard-gate.ts`; this declaration keeps the marker unforgeable
1020
+ * without a circular import.
1021
+ */
1022
+ declare const _gateBrand: unique symbol;
1023
+ /**
1024
+ * Marker returned by `gate(step)`, which makes a slot a hard
1025
+ * prerequisite: an uncleared gate seals every step after it, and the
1026
+ * gate's own form is the only way past. A gate clears on a member
1027
+ * form's clean submit (confirmation), never on a value merely going
1028
+ * valid (intent), and its form freezes once cleared so a back
1029
+ * navigation is a read-only review.
1030
+ *
1031
+ * `Inner` carries the wrapped slot's static type, so `gate` is
1032
+ * transparent to the type machinery: `gate(form)` still contributes the
1033
+ * form's key to `wizard.forms` and still counts as a guaranteed step,
1034
+ * while `gate(lazy(fn))` stays maybe-absent exactly like the lazy slot
1035
+ * it wraps. See {@link UnwrapGate}.
1036
+ *
1037
+ * Construct via the `gate()` helper exported alongside `useWizard`. The
1038
+ * marker is opaque; consumers do not assemble it directly.
1039
+ */
1040
+ type GateMarker<Inner = unknown> = {
1041
+ readonly [_gateBrand]: true;
1042
+ readonly inner: Inner;
1043
+ };
1044
+ /**
1045
+ * Strip every `gate()` wrapper off a slot type. A gate changes runtime
1046
+ * reachability, never the compiled type, so the forms map and the
1047
+ * non-empty-tuple predicate both unwrap before inspecting a slot.
1048
+ * Recursive, which is what makes `gate(lazy(s))` and
1049
+ * `lazy((ctx) => gate(s))` agree at the type level as they do at
1050
+ * runtime.
1051
+ */
1052
+ type UnwrapGate<T> = T extends GateMarker<infer Inner> ? UnwrapGate<Inner> : T;
1053
+ /**
1054
+ * One position in the source `useWizard({ steps })` array. Each slot
1055
+ * compiles to a `{ key, form }` step, or drops out:
1056
+ *
1057
+ * - `AnyForm`: a form declared via `useForm`. Surfaced as-is.
1058
+ * - `string`: a bare key. The wizard generates a noop form so
1059
+ * affordance positions (intro, terms, review, congratulations) look
1060
+ * the same from outside as real steps.
1061
+ * - `null` / `undefined`: dropped from the compiled list, so a
1062
+ * conditional step reads inline as `cond ? form : null`.
1063
+ * - function: eager slot, re-evaluated reactively. Returns a
1064
+ * `SlotResolution`, or nullish to drop the position.
1065
+ * - `LazyMarker`: memoized function slot, see `lazy`.
1066
+ * - `GateMarker`: hard-prerequisite wrapper, see `gate`. Wraps any of
1067
+ * the above.
1068
+ */
1069
+ type StepSlot<Ctx = WizardCtx> = AnyForm | string | null | undefined | ((ctx: Ctx) => SlotResolution<Ctx>) | LazyMarker<Ctx> | GateMarker;
1070
+ /** What the `restore` callback returns: the key of the step to activate. */
1071
+ type WizardRestoreState = {
1072
+ readonly step?: FormKey;
1073
+ };
1074
+ /**
1075
+ * `restore` callback. Invoked at construction and watched reactively,
1076
+ * so browser back/forward, cross-tab events and route changes re-apply
1077
+ * through the wizard. Returning `undefined` falls through to the first
1078
+ * step.
1079
+ */
1080
+ type WizardRestoreFn = () => WizardRestoreState | undefined;
1081
+ /**
1082
+ * `persist` callback. Invoked when `wizard.currentStep` actually moves;
1083
+ * the wizard diffs against the last persisted value to break the
1084
+ * restore-persist loop.
1085
+ */
1086
+ type WizardPersistFn = (state: WizardRestoreState) => void;
1087
+ /**
1088
+ * Context passed to the `onSubmit` registered via
1089
+ * `wizard.handleSubmit`. That call always processes the whole step
1090
+ * list, so `values` carries every form's parsed output no matter which
1091
+ * step fired the submit.
1092
+ */
1093
+ type WizardSubmitContext = {
1094
+ /**
1095
+ * Namespaced aggregate keyed by form key, mirroring
1096
+ * `wizard.allValues`. Noop forms contribute an empty record.
1097
+ */
1098
+ readonly values: Readonly<Record<FormKey, unknown>>;
1099
+ /**
1100
+ * Typed parsed output for a specific form ref. Works across
1101
+ * cross-component graphs, because the ref carries its own schema.
1102
+ */
1103
+ readonly get: <F extends AnyForm>(form: F) => F extends {
1104
+ readonly values: infer V;
1105
+ } ? V : unknown;
1106
+ /** Key of the step that fired this submission. */
1107
+ readonly currentKey: FormKey;
1108
+ /**
1109
+ * `true` when `currentKey` is the last position in `wizard.steps`.
1110
+ * Positional only: it reports where the submit fired, never what got
1111
+ * validated. A user who steps back, edits and submits from the middle
1112
+ * sees `false`, yet the whole list is still processed and `done` still
1113
+ * latches on success.
1114
+ */
1115
+ readonly isFinal: boolean;
1116
+ };
1117
+ /**
1118
+ * `onSubmit` registered via `wizard.handleSubmit`. Sync or async; the
1119
+ * returned promise gates `wizard.submitting`.
1120
+ */
1121
+ type WizardOnSubmit = (ctx: WizardSubmitContext) => void | Promise<void>;
1122
+ /**
1123
+ * Optional `onError` registered via `wizard.handleSubmit`. Receives
1124
+ * errors spanning EVERY step, since `handleSubmit` validates the whole
1125
+ * wizard, so a failed submit surfaces every form's errors at once
1126
+ * rather than only the active step's. Entries come from per-form
1127
+ * validation, activation failures (`atta:activation-failed`), and a
1128
+ * submit callback that left errors on a processed step via the
1129
+ * `setErrors(...); return` server-rejection path. Sync or async; the
1130
+ * returned promise gates `wizard.submitting`.
1131
+ */
1132
+ type WizardOnError = (errors: readonly WizardAggregateError[]) => void | Promise<void>;
1133
+ /** Options for `useWizard({ steps, ... })`. Only `steps` is required. */
1134
+ type WizardOptions = {
1135
+ /**
1136
+ * Ordered list of slots that compile into the positional step list.
1137
+ * See `StepSlot` for the per-slot contract.
1138
+ */
1139
+ readonly steps: ReadonlyArray<StepSlot>;
1140
+ /**
1141
+ * Identifier this wizard registers under, so descendants reach it
1142
+ * with `injectWizard(key)` instead of prop-threading. Omit it and the
1143
+ * wizard gets a synthetic key from `useId()`, so the server-rendered
1144
+ * and client-hydrated trees agree on one registry entry; that key is
1145
+ * opaque, and descendants reach an anonymous wizard through ambient
1146
+ * `injectWizard()`.
1147
+ *
1148
+ * Registering a key twice is first-wins, with a dev warning on the
1149
+ * second, mirroring `useForm`. The warning fires only for explicit
1150
+ * keys, since two anonymous wizards are always given distinct keys.
1151
+ */
1152
+ readonly key?: string;
1153
+ /**
1154
+ * Seed statuses used while a form is still pre-resolved: async
1155
+ * `defaultValues` in flight, or a wizard-deferred non-current step.
1156
+ * Mirrors `defaultValues`' trichotomy of plain object, sync factory
1157
+ * or async factory.
1158
+ *
1159
+ * Per form, the first of these that applies wins:
1160
+ * 1. defaults resolved, so derive from `form.meta`
1161
+ * 2. noop form, so the built-in always-valid status
1162
+ * 3. a seed for this key
1163
+ * 4. the pending sentinel
1164
+ *
1165
+ * `gate: 'cleared'` is honored as a one-shot seed of the gate's
1166
+ * cleared latch at construction, for restoring a confirmed
1167
+ * prerequisite, and only on a step that compiles to a `gate()`. See
1168
+ * `FormStatusSeed` for what the seed does and does not carry.
1169
+ *
1170
+ * SSR note: an async factory resolves after hydration, so a gate
1171
+ * seeded through it clears client-side. For a gate that must render
1172
+ * open on the first server byte, use the plain or sync-factory form
1173
+ * and resolve server truth in the page's `setup` await.
1174
+ *
1175
+ * Unknown keys dev-warn, so a stale resume payload surfaces at
1176
+ * construction.
1177
+ *
1178
+ * A factory that throws, or a promise that rejects, seeds nothing and
1179
+ * reports once in development, the same as omitting the option.
1180
+ */
1181
+ readonly defaultStatuses?: Record<string, FormStatusSeed> | (() => Record<string, FormStatusSeed>) | (() => Promise<Record<string, FormStatusSeed>>);
1182
+ /**
1183
+ * Override for `wizard.progress`. Omitted, progress is
1184
+ * `valid_step_count / count`, normalised to `[0, 1]`. Provided, the
1185
+ * returned number is used as-is and normalising it is yours to do.
1186
+ *
1187
+ * Invoked inside a `computed`, so it must be synchronous and may only
1188
+ * read reactive sources. A throw falls back to the built-in ratio and
1189
+ * reports once in development.
1190
+ */
1191
+ readonly progress?: (steps: ReadonlyArray<CompiledStep>) => number;
1192
+ /**
1193
+ * When `wizard.handleSubmit` finds errors, jump to the first failing
1194
+ * form's step and invoke its `applyInvalidSubmitPolicy()`, which
1195
+ * honors that form's own `focusOnInvalidSubmit` choice. Default
1196
+ * `true`. Pass `false` to leave the active step where the user left
1197
+ * it and navigate yourself from `onError`.
1198
+ */
1199
+ readonly focusFirstError?: boolean;
1200
+ /**
1201
+ * Source of truth for the active step, invoked at construction and
1202
+ * re-evaluated reactively. The default reads `?step=<key>` from the
1203
+ * URL. Pass `false` to disable URL sync, or your own callback for
1204
+ * non-router persistence.
1205
+ *
1206
+ * A throw yields no step, so the wizard stays where it is, and reports
1207
+ * once in development.
1208
+ */
1209
+ readonly restore?: WizardRestoreFn | false;
1210
+ /**
1211
+ * Destination for the active step, invoked whenever `currentStep`
1212
+ * moves. The default writes `?step=<key>`. Pass `false` to disable
1213
+ * persistence, or your own callback to scope the param name or write
1214
+ * elsewhere.
1215
+ *
1216
+ * A throw does not block the navigation that triggered it, so the step
1217
+ * is live but unrecorded, and reports once in development.
1218
+ */
1219
+ readonly persist?: WizardPersistFn | false;
1220
+ };
1221
+ /**
1222
+ * True when a slot is guaranteed to contribute exactly one compiled
1223
+ * step: a bare form or affordance string, neither nullish nor a
1224
+ * maybe-absent function / `lazy()` slot. One of these anywhere in the
1225
+ * tuple proves the compiled list is non-empty.
1226
+ */
1227
+ type IsGuaranteedStep<T, U = UnwrapGate<T>> = [null] extends [U] ? false : [undefined] extends [U] ? false : U extends LazyMarker | ((...args: unknown[]) => unknown) ? false : U extends string ? true : U extends {
1228
+ readonly key: string;
1229
+ } ? true : false;
1230
+ /**
1231
+ * Is the steps tuple statically guaranteed to compile to a non-empty
1232
+ * list? True when at least one element is an `IsGuaranteedStep`.
1233
+ * Function and `lazy()` slots may resolve to nothing, and nullish slots
1234
+ * are an explicit drop, so a tuple of only those stays honestly
1235
+ * `| undefined`.
1236
+ *
1237
+ * This is what narrows `currentStep` and `activeForm` in the
1238
+ * common-case wizard while keeping the union wherever a runtime drop is
1239
+ * reachable.
1240
+ */
1241
+ type StaticallyNonEmpty<S> = S extends readonly [infer First, ...infer Rest] ? IsGuaranteedStep<First> extends true ? true : StaticallyNonEmpty<Rest> : false;
1242
+ /** Active step's key, narrowed to `string` when `S` is statically safe. */
1243
+ type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
1244
+ /**
1245
+ * Active step's form handle, schema-erased to
1246
+ * `UseFormReturnType<GenericForm>` and narrowed to non-`undefined` when
1247
+ * `S` is statically safe. Values are loose because the active step's
1248
+ * schema is not statically known; reach for the original form ref or
1249
+ * `ctx.get(ref)` for typed per-step values.
1250
+ */
1251
+ type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? UseFormReturnType<GenericForm> : UseFormReturnType<GenericForm> | undefined;
1252
+ /**
1253
+ * One slot's contribution to {@link FormsRecordOf}. Unwraps `gate()`
1254
+ * and strips nullish off the slot first, so a conditionally-present
1255
+ * form and a `gate(form)` both still map to their key and stay
1256
+ * concretely typed on `wizard.forms`. A purely nullish slot contributes
1257
+ * nothing.
1258
+ */
1259
+ type SlotRecord<First, U = UnwrapGate<First>, F = Exclude<U, null | undefined>> = [F] extends [
1260
+ never
1261
+ ] ? unknown : F extends string ? {
1262
+ readonly [P in F]: AnyForm;
1263
+ } : F extends {
1264
+ readonly key: infer K extends string;
1265
+ } ? {
1266
+ readonly [P in K]: F;
1267
+ } : unknown;
1268
+ /**
1269
+ * Builds the static portion of `wizard.forms` by walking the steps
1270
+ * tuple. Per slot kind:
1271
+ *
1272
+ * - **string** (`'review'`): the literal is the record key, and the
1273
+ * value is `AnyForm`, since the synthesized noop form is opaque at
1274
+ * the type level.
1275
+ * - **form**: the form's own `key` is the record key and the value is
1276
+ * the concrete handle, so `wizard.forms.shipping.values.address`
1277
+ * carries schema-derived field types through. A form behind a
1278
+ * `cond ? form : null` still maps to its key.
1279
+ * - **nullish**: contributes nothing.
1280
+ * - **function / `lazy()`**: contributes nothing statically.
1281
+ * Runtime-resolved forms stay reachable as `AnyForm` through the
1282
+ * catch-all index signature on `WizardForms`.
1283
+ * - **`gate()`**: unwrapped, so it contributes exactly what the slot
1284
+ * it wraps would.
1285
+ */
1286
+ type FormsRecordOf<S> = S extends readonly [
1287
+ infer First,
1288
+ ...infer Rest extends ReadonlyArray<StepSlot>
1289
+ ] ? SlotRecord<First> & FormsRecordOf<Rest> : unknown;
1290
+ /**
1291
+ * `wizard.forms` typed view: the static per-step map, intersected with
1292
+ * a catch-all so a statically known key resolves to its concrete form
1293
+ * type and any other string key resolves to `AnyForm`.
1294
+ */
1295
+ type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
1296
+ /**
1297
+ * What `useWizard({ steps, ... })` returns. Every reactive read is a
1298
+ * plain getter, no `.value`, so `wizard.currentStep`, `wizard.progress`
1299
+ * and `wizard.allValues` track inside `computed` and templates
1300
+ * directly.
1301
+ *
1302
+ * Parameterized by the steps tuple `S` so the active-position fields
1303
+ * narrow to non-`undefined` when every slot is positional, and stay
1304
+ * honest unions when a function or `lazy()` slot can drop the position
1305
+ * at runtime. `useWizard`'s `const` type parameter preserves the
1306
+ * literal tuple, so that narrowing happens without a consumer-side
1307
+ * `as const`.
1308
+ */
1309
+ type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
1310
+ readonly key: string;
1311
+ /**
1312
+ * Key of the active step. Narrows to `string` when the steps tuple is
1313
+ * statically guaranteed non-empty; otherwise `string | undefined`, so
1314
+ * an empty compiled list surfaces honestly.
1315
+ */
1316
+ readonly currentStep: CurrentStepOf<S>;
1317
+ /**
1318
+ * A live view of the active step's form, so operating through it
1319
+ * always targets the current step: a handler captured once at setup
1320
+ * (`wizard.activeForm.handleSubmit(() => wizard.next())`) retargets as
1321
+ * the wizard advances. It is a facade, not the raw per-step handle;
1322
+ * reach for `wizard.forms[key]` when you need identity. Same
1323
+ * `undefined` narrowing as `currentStep`.
1324
+ */
1325
+ readonly activeForm: ActiveFormOf<S>;
1326
+ /** 0-based position of the active step. */
1327
+ readonly activeIndex: number;
1328
+ /** `true` when `currentStep` is the last compiled step. */
1329
+ readonly isFinalStep: boolean;
1330
+ /** The ordered compiled `{ key, form }` slots. */
1331
+ readonly steps: ReadonlyArray<CompiledStep>;
1332
+ /** Every step's form, indexable by step key. */
1333
+ readonly forms: WizardForms<S>;
1334
+ /** `steps.length`. */
1335
+ readonly count: number;
1336
+ /**
1337
+ * Callable readonly proxy over the per-key `FormStatus` record.
1338
+ * Noop-form keys always read as default-valid.
1339
+ */
1340
+ readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
1341
+ /** Each form's values, keyed by step key. */
1342
+ readonly allValues: Readonly<Record<FormKey, unknown>>;
1343
+ /** Each form's validation errors, keyed by step key. Noop forms map to an empty list. */
1344
+ readonly allErrors: Readonly<Record<FormKey, readonly WizardAggregateError[]>>;
1345
+ /**
1346
+ * Normalised step-validity ratio, or the `progress` override's
1347
+ * return. Forward-looking: noop steps count as valid.
1348
+ */
1349
+ readonly progress: number;
1350
+ /**
1351
+ * `true` when `activeIndex < count - 1`. Purely positional;
1352
+ * navigation never gates on validity.
1353
+ */
1354
+ readonly canAdvance: boolean;
1355
+ /** `true` when `activeIndex > 0`. */
1356
+ readonly canGoBack: boolean;
1357
+ /**
1358
+ * `isFinalStep` and every step's form valid. Reactive to current
1359
+ * validity, so this is the "enable the Finish button" signal.
1360
+ */
1361
+ readonly complete: boolean;
1362
+ /**
1363
+ * Monotonic latch: `true` the first time a `handleSubmit` resolves
1364
+ * without throwing AND leaves no errors on any step, and stays `true`
1365
+ * through later edits. A callback that calls `setErrors` and returns
1366
+ * is a failed submit, so it does not flip this. Only `reset()` clears
1367
+ * it. Gates success UI that should reflect submission history rather
1368
+ * than current validity.
1369
+ */
1370
+ readonly done: boolean;
1371
+ /**
1372
+ * `true` while a `wizard.handleSubmit` call is in flight. Every
1373
+ * navigation method refuses while it is on.
1374
+ */
1375
+ readonly submitting: boolean;
1376
+ /** `wizard.handleSubmit` invocations, success or failure. */
1377
+ readonly submissionAttempts: number;
1378
+ /**
1379
+ * The error THROWN by the most recent `handleSubmit` callback or its
1380
+ * `onError`, coerced to a real `Error`, and `null` on success. Like
1381
+ * `form.meta.submitError`, this is the unexpected-throw channel: an
1382
+ * expected rejection handled through `setErrors` surfaces on the
1383
+ * error surface and in `onError` instead, leaving this `null`. It is
1384
+ * parked here rather than re-thrown, so the handler always resolves
1385
+ * and never manufactures an unhandled rejection. Cleared at submit
1386
+ * entry and by `reset()`.
1387
+ */
1388
+ readonly submitError: Error | null;
1389
+ /**
1390
+ * Append-only breadcrumb of navigated step keys. `back()` does not
1391
+ * pop: the trail is an audit log, not a back-stack.
1392
+ */
1393
+ readonly visited: readonly FormKey[];
1394
+ /**
1395
+ * Advance one step. Refuses while `submitting`. On an UNCLEARED
1396
+ * `gate()` step it behaves as `tryNext()`, so wiring a Next button
1397
+ * straight to it can never skip the gate's confirmation; once that
1398
+ * gate clears it is plain navigation again and does not re-submit.
1399
+ */
1400
+ readonly next: () => Promise<void>;
1401
+ /** Go back one step. Refuses while `submitting`. */
1402
+ readonly back: () => void;
1403
+ /** Jump to a step by key. Refuses while `submitting`. */
1404
+ readonly goTo: (key: string) => void;
1405
+ /**
1406
+ * Submit the active step, and advance once that submit resolves
1407
+ * clean. Invalid input keeps the pin put under the form's standard
1408
+ * error reveal, with the first error focused and display state
1409
+ * advanced. The advance runs after the submit settles, so a `gate()`
1410
+ * on the active step clears in a single call.
1411
+ *
1412
+ * Inline-bindable (`@click="wizard.tryNext()"`), and resolves to
1413
+ * whether the pin moved. `false` on a degenerate or final-step wizard;
1414
+ * finish through `handleSubmit` instead.
1415
+ *
1416
+ * Called while the active step already has a submit in flight, the
1417
+ * usual case being from inside that form's own callback, it rides
1418
+ * that submit rather than starting a second one, so one action costs
1419
+ * one submission. The pin has not moved by the time it resolves, so
1420
+ * the answer is `false` and the advance lands with the in-flight
1421
+ * submit.
1422
+ */
1423
+ readonly tryNext: () => Promise<boolean>;
1424
+ /**
1425
+ * Validate the entire step list, from any step, and never advance the
1426
+ * pin. On success `done` latches; on any error the first failing step
1427
+ * is focused and `onError` fires with errors spanning every step.
1428
+ *
1429
+ * To gate advancing a step on its own validity, compose with the
1430
+ * active form's submit instead:
1431
+ * `activeForm.handleSubmit(() => wizard.next())`.
1432
+ *
1433
+ * Returns a handler for `<form @submit>` or for imperative use.
1434
+ */
1435
+ readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
1436
+ /**
1437
+ * Zero the wizard lifecycle (`submissionAttempts`, `visited`, `done`),
1438
+ * reset every form, return to `steps[0].key`, and invoke `persist`
1439
+ * with the cleared state. Re-applies the `defaultStatuses` gate seed,
1440
+ * so a reboot returns to the seeded clearance rather than fully
1441
+ * sealed.
1442
+ */
1443
+ readonly reset: () => void;
1444
+ /**
1445
+ * Re-seal a cleared gate, contingent on `commit`. Awaits `commit`, a
1446
+ * server-side revoke, and re-seals only if it resolves clean, so the
1447
+ * gate reflects server-confirmed truth the same way a clearing submit
1448
+ * does; a thrown `commit` leaves the gate as-is.
1449
+ *
1450
+ * Seal-only, so it can never become the leading-signal foot-gun
1451
+ * `gate()` exists to prevent. `commit` is required: pass `() => {}`
1452
+ * for a deliberate client-only re-seal. Resolves whether the gate
1453
+ * ended up sealed and never rejects; resolves `false` with a dev
1454
+ * warning on a non-gate key.
1455
+ */
1456
+ readonly relock: (key: FormKey, commit: () => void | Promise<void>) => Promise<boolean>;
1457
+ };
1458
+
1459
+ /**
1460
+ * Portable SSR detection. The plugin captures this value at install time and
1461
+ * exposes it via the registry so every runtime branch reads a single source
1462
+ * of truth instead of sniffing `import.meta.*` (bundler-specific) at each
1463
+ * call site.
1464
+ *
1465
+ * Consumers can override the heuristic explicitly via
1466
+ * `createAttaform({ ssr: true })`; the default handles the common
1467
+ * Node-vs-browser split without relying on any bundler-injected flag.
1468
+ */
1469
+ interface SSRDetectOptions {
1470
+ /**
1471
+ * Force SSR-vs-client mode, bypassing the `typeof window` heuristic.
1472
+ * `true` activates the SSR code paths (no devtools, no persistence
1473
+ * wiring, payload serialisation enabled); `false` forces client mode.
1474
+ * The Nuxt plugin sets this from `import.meta.server` so SSR detection
1475
+ * never depends on whether `window` is polyfilled. Tests that need to
1476
+ * exercise the SSR code paths under jsdom pass `ssr: true`.
1477
+ */
1478
+ ssr?: boolean;
1479
+ }
1480
+
1481
+ /**
1482
+ * Per-Vue-app container for every form state instance. Each
1483
+ * `app.use(createAttaform())` call gets its own registry, so Attaform runs
1484
+ * under bare Vue 3, SSR through `@vue/server-renderer`, and Nuxt on one
1485
+ * code path.
1486
+ *
1487
+ * Forms are stored as `Map<FormKey, FormStore<GenericForm>>`. The generic
1488
+ * has to relax at storage time because two forms in one app have different
1489
+ * `Form` generics; a caller recovers the specific type through `useForm`'s
1490
+ * overloads.
1491
+ */
1492
+
1493
+ /**
1494
+ * Serialised snapshot of one form's state, captured by
1495
+ * `renderAttaformState` for SSR and replayed by
1496
+ * `hydrateAttaformState` on the client. Round-trips through
1497
+ * JSON-safe tuples; field references are intentionally omitted
1498
+ * (DOM nodes don't survive serialisation).
1499
+ */
1500
+ type SerializedFormData = {
1501
+ /** The form's value at snapshot time. */
1502
+ readonly form: unknown;
1503
+ /**
1504
+ * Errors produced by the schema at snapshot time. Replayed into
1505
+ * the client form's error state at hydration; cleared on
1506
+ * successful re-validation client-side.
1507
+ */
1508
+ readonly schemaErrors: ReadonlyArray<readonly [string, unknown]>;
1509
+ /**
1510
+ * Errors set explicitly via `setErrors` (typically a server
1511
+ * response) at snapshot time. Replayed at hydration; persists across
1512
+ * client-side re-validation.
1513
+ */
1514
+ readonly userErrors: ReadonlyArray<readonly [string, unknown]>;
1515
+ /** Per-field metadata (timestamps, raw values, connection flags) captured at snapshot time. */
1516
+ readonly fields: ReadonlyArray<readonly [string, unknown]>;
1517
+ /**
1518
+ * Path keys that were in the form's `blankPaths` set at snapshot time,
1519
+ * carrying the "displayed empty" UI state across the SSR boundary.
1520
+ * Without it the client briefly renders `String(slim-default)` (`'0'`,
1521
+ * say) for a field the server rendered blank. Optional in the wire
1522
+ * format, so an older payload shape still deserialises.
1523
+ */
1524
+ readonly blankPaths?: ReadonlyArray<string>;
1525
+ };
1526
+ type PendingHydration = Map<FormKey, SerializedFormData>;
1527
+ /**
1528
+ * Attaform's per-Vue-app container, one per `app.use(createAttaform())`
1529
+ * call.
1530
+ *
1531
+ * Most consumers never touch it: `useForm` and `injectForm` reach the
1532
+ * registry for you. Access it explicitly only when wiring SSR or a custom
1533
+ * plugin integration.
1534
+ */
1535
+ type AttaformRegistry = {
1536
+ /**
1537
+ * Live forms keyed by `FormKey`.
1538
+ * @internal
1539
+ */
1540
+ readonly forms: Map<FormKey, FormStore<GenericForm>>;
1541
+ /**
1542
+ * Live wizards keyed by the consumer-supplied `key` option. Populated by
1543
+ * `useWizard(entryForm, { key })` and read by `injectWizard(key)` to
1544
+ * resolve a cross-component wizard handle. An anonymous wizard does NOT
1545
+ * register here, and is reachable only through ambient provide/inject.
1546
+ * @internal
1547
+ */
1548
+ readonly wizards: Map<string, UseWizardReturnType>;
1549
+ /**
1550
+ * Snapshots staged by `hydrateAttaformState` waiting to be consumed by the next `useForm` call.
1551
+ * @internal
1552
+ */
1553
+ readonly pendingHydration: PendingHydration;
1554
+ /** `true` while running on the server during SSR; `false` on the client. */
1555
+ readonly ssr: boolean;
1556
+ /**
1557
+ * Track a consumer of `key`, returning a dispose function to call when
1558
+ * that consumer unmounts. The form is evicted once the last consumer
1559
+ * disposes, so a long-running SPA does not leak detached state across
1560
+ * navigations.
1561
+ * @internal
1562
+ */
1563
+ readonly trackConsumer: (key: FormKey) => () => void;
1564
+ /**
1565
+ * Track a consumer of wizard `key`, returning a dispose function to call
1566
+ * when that consumer unmounts. The handle leaves `wizards` once the last
1567
+ * consumer disposes, mirroring the form consumer counting. An anonymous
1568
+ * wizard never enters this counter, having no key to count under.
1569
+ * @internal
1570
+ */
1571
+ readonly trackWizardConsumer: (key: string) => () => void;
1572
+ /**
1573
+ * Mark a form as eligible for SSR prefetch. The form's
1574
+ * `onServerPrefetch` hook reads `shouldPrefetch(key)` and runs the
1575
+ * captured `defaultValues` factory only when this set holds the key and
1576
+ * the skip set does not. Set by `form.activate()` and by `useWizard`'s
1577
+ * current-step auto-mark.
1578
+ * @internal
1579
+ */
1580
+ readonly enqueuePrefetch: (key: FormKey) => void;
1581
+ /**
1582
+ * Mark a form as ineligible for SSR prefetch, overriding
1583
+ * `enqueuePrefetch`. `useWizard` uses it to keep a non-current step
1584
+ * dormant on the server even when a stray `activate()` enqueued it: the
1585
+ * wizard's "the user is not on this step" signal wins.
1586
+ * @internal
1587
+ */
1588
+ readonly skipPrefetch: (key: FormKey) => void;
1589
+ /**
1590
+ * Whether `key`'s SSR prefetch should run. Returns `true` iff the key
1591
+ * is enqueued AND not skipped.
1592
+ * @internal
1593
+ */
1594
+ readonly shouldPrefetch: (key: FormKey) => boolean;
1595
+ };
1596
+ /**
1597
+ * The Vue `InjectionKey` the registry is provided under on the app. Most
1598
+ * consumers never need it: `useForm` and `injectForm` resolve the registry
1599
+ * automatically.
1600
+ */
1601
+ declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
1602
+ declare module 'vue' {
1603
+ interface App {
1604
+ /** @internal */
1605
+ _attaform?: AttaformRegistry;
1606
+ }
1607
+ }
1608
+ /** Options for `createRegistry`. */
1609
+ type CreateRegistryOptions = SSRDetectOptions;
1610
+ /**
1611
+ * Create a fresh `AttaformRegistry`. `createAttaform()` calls it
1612
+ * internally, so reach for it directly only when building a custom plugin
1613
+ * that does not want `createAttaform`'s auto-install behaviour: a test
1614
+ * harness, or an embedded app.
1615
+ */
1616
+ declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
1617
+ /**
1618
+ * Look up the current app's registry from inside a component's `setup()`,
1619
+ * or any synchronous code on the setup call stack.
1620
+ *
1621
+ * Most consumers do not need it, since `useForm` and `injectForm` call it
1622
+ * for you. Reach for it when building a custom integration that needs the
1623
+ * raw registry.
1624
+ *
1625
+ * Throws:
1626
+ * - `OutsideSetupError` when called outside a Vue setup context, from an
1627
+ * event handler or an async callback. Move the call into setup, or
1628
+ * trigger it from a child component.
1629
+ * - `RegistryNotInstalledError` when called inside setup with the plugin
1630
+ * not installed. Add `app.use(createAttaform())` to your app entry.
1631
+ */
1632
+ declare function useRegistry(): AttaformRegistry;
1633
+ /**
1634
+ * Look up a Vue app's registry by `App` reference, for the SSR helpers
1635
+ * (`renderAttaformState`, `hydrateAttaformState`) that run outside a
1636
+ * component setup context.
1637
+ *
1638
+ * Throws `RegistryNotInstalledError` when the app was never wired with
1639
+ * `createAttaform()`.
1640
+ */
1641
+ declare function getRegistryFromApp(app: App): AttaformRegistry;
1642
+
1643
+ /**
1644
+ * Multistep-form orchestrator built around an ordered list of step
1645
+ * slots. A slot resolves to a participating form four ways: an existing
1646
+ * `useForm` reference, a bare string key (desugared to a noop form, so
1647
+ * an affordance step participates like any other), an eager function
1648
+ * slot for runtime branching, or a `lazy()`-wrapped function slot that
1649
+ * caches its resolution and re-fires only on its own tracked deps.
1650
+ *
1651
+ * Navigation (`next` / `back` / `goTo`) walks positional indices.
1652
+ * `handleSubmit` validates the whole step list from any step and never
1653
+ * advances; to gate an advance, write
1654
+ * `activeForm.handleSubmit(() => wizard.next())`. URL sync rides on the
1655
+ * `restore` / `persist` callbacks, which default to `?step=<key>`.
1656
+ */
1657
+ declare function useWizard<const S extends ReadonlyArray<StepSlot>>(options: WizardOptions & {
1658
+ readonly steps: S;
1659
+ }): UseWizardReturnType<S>;
1660
+
1661
+ /** Options accepted by `injectWizard` in place of a bare key string. */
1662
+ type InjectWizardInput = {
1663
+ readonly key?: string | undefined;
1664
+ };
1665
+ /**
1666
+ * Reach an existing wizard from a descendant component without threading
1667
+ * it through props. The counterpart to `useWizard`: `useWizard` creates
1668
+ * and provides, `injectWizard` looks up.
1669
+ *
1670
+ * ```ts
1671
+ * // The nearest ancestor's useWizard call.
1672
+ * const wizard = injectWizard()
1673
+ *
1674
+ * // A specific wizard by key, from anywhere in the app.
1675
+ * const signup = injectWizard('signup-wizard')
1676
+ * ```
1677
+ *
1678
+ * A keyed call is a registry lookup, independent of component-tree
1679
+ * position, and reaches any wizard built with `useWizard({ steps, key
1680
+ * })`. A no-key call takes the closest ambient ancestor; only an
1681
+ * anonymous `useWizard()` fills that slot, so a keyed wizard has to be
1682
+ * addressed by its key, exactly as on the form side.
1683
+ *
1684
+ * Returns `null` when nothing matches, so narrow before use:
1685
+ *
1686
+ * ```ts
1687
+ * const wizard = injectWizard('signup')
1688
+ * if (!wizard) return
1689
+ * wizard.next()
1690
+ * ```
1691
+ *
1692
+ * A keyed miss warns in dev with the registered keys and the call site.
1693
+ * A keyed lookup also pins the handle for this component's lifetime, so
1694
+ * it outlives a parent `useWizard` that unmounts first, and the registry
1695
+ * evicts the entry a microtask after the last consumer disposes. An
1696
+ * ambient lookup does not pin: the parent's scope owns that lifetime.
1697
+ */
1698
+ declare function injectWizard(input?: string | InjectWizardInput): UseWizardReturnType | null;
1699
+
1700
+ /**
1701
+ * Wrap a function slot in `lazy()` to give that slot its own memoized
1702
+ * cache, distinct from the wizard-wide step compiler.
1703
+ *
1704
+ * Default function slots in `useWizard({ steps })` re-evaluate whenever
1705
+ * the compiled step list re-evaluates, including when an unrelated
1706
+ * slot's reactive deps change. That keeps the rail correct but can
1707
+ * trigger spurious work in a resolver that is expensive (a network
1708
+ * lookup, a heavy schema derivation, an async-derived factory).
1709
+ *
1710
+ * `lazy()` isolates each wrapped slot behind its own cache. The
1711
+ * resolver runs eagerly on the first compile pass; subsequent reads
1712
+ * reuse the cached form. The cache invalidates only when one of the
1713
+ * resolver's own tracked reactive dependencies changes (Vue's
1714
+ * `computed` semantics applied to a slot), or when `wizard.reset()`
1715
+ * triggers a global re-compile:
1716
+ *
1717
+ * const wizard = useWizard({
1718
+ * steps: [
1719
+ * account,
1720
+ * lazy((ctx) => loadShippingForm(ctx.forms.account.values.region)),
1721
+ * confirm,
1722
+ * ],
1723
+ * })
1724
+ *
1725
+ * Resolution semantics:
1726
+ * - Eager at construction so `wizard.steps` and SSR markup are honest
1727
+ * from t=0.
1728
+ * - Memoized: the resolver re-fires only when one of its own tracked
1729
+ * reads changes. An unrelated slot re-evaluating does not re-fire
1730
+ * this one.
1731
+ * - `wizard.reset()` clears every lazy slot's cache so a reboot truly
1732
+ * resolves from scratch.
1733
+ * - Resolving to `null` or `undefined` drops the slot from the compiled
1734
+ * list until the resolver next re-fires.
1735
+ * - The resolver may return a `gate()` wrapper, so a lazy slot can gate
1736
+ * what it resolves to; `lazy((ctx) => gate(s))` and `gate(lazy(s))`
1737
+ * resolve identically.
1738
+ *
1739
+ * The runtime brand returned by `lazy()` is opaque. Use
1740
+ * {@link isLazyMarker} to detect it.
1741
+ */
1742
+ declare function lazy<Ctx = WizardCtx>(resolve: (ctx: Ctx) => SlotResolution<Ctx>): LazyMarker<Ctx>;
1743
+
1744
+ /**
1745
+ * Wrap a step slot in `gate()` to make it a hard prerequisite: while the
1746
+ * gate is uncleared, every step positioned after it is inaccessible, and
1747
+ * the gate's own form is the only way past.
1748
+ *
1749
+ * A gate is safe by construction. It does not hand the consumer a
1750
+ * predicate to key on; it bakes the correct semantics in:
1751
+ *
1752
+ * - **Confirmation, not intent.** The gate clears only when a member
1753
+ * form submits clean, never when a value merely goes valid. Checking
1754
+ * the box does nothing; submitting the consent step is what opens the
1755
+ * rail. This is the whole reason `gate()` exists: a leading value
1756
+ * signal lets a downstream step collect data before the prerequisite
1757
+ * is confirmed.
1758
+ * - **Linear-forward.** An uncleared gate seals every step after it.
1759
+ * Order places it: put `gate(step)` immediately before what it
1760
+ * guards. Everything downstream is both un-navigable and, through the
1761
+ * `disabled` data freeze, un-fillable, so a deep link, the browser
1762
+ * back button, or a stray `goTo` all redirect to the gate.
1763
+ * - **Freeze-after-clear.** A cleared gate's own form freezes, so
1764
+ * navigating back to it is a read-only review with no withdrawal
1765
+ * path. There is no re-lock dance.
1766
+ *
1767
+ * Wrap any slot kind:
1768
+ *
1769
+ * const wizard = useWizard({ steps: [gate(consent), shipping, payment] })
1770
+ *
1771
+ * A **form** gate (`gate(consentForm)`) clears when its form submits
1772
+ * clean, and is treated as pre-cleared on reload when its member form
1773
+ * rehydrates already valid (a persisted consent), so a seeded-valid gate
1774
+ * SSR-renders open. A bare-string **affordance** gate (`gate('terms')`)
1775
+ * clears on acknowledgment (advancing past it), and re-prompts each
1776
+ * session because that acknowledgment is ephemeral.
1777
+ *
1778
+ * Conditional gates come from composition, not an options bag:
1779
+ *
1780
+ * () => (transfer.values.amount > 10_000 ? gate(kyc) : kyc)
1781
+ *
1782
+ * `gate()` composes with `lazy()` in either order:
1783
+ * `gate(lazy(fn))` and `lazy((ctx) => gate(fn(ctx)))` resolve
1784
+ * identically.
1785
+ *
1786
+ * The runtime brand returned by `gate()` is opaque. Use
1787
+ * {@link isGateMarker} to detect it.
1788
+ */
1789
+ declare function gate<const Inner extends StepSlot>(step: Inner): GateMarker<Inner>;
1790
+
1791
+ /**
1792
+ * Stable identifiers for library-emitted `ValidationError` codes.
1793
+ *
1794
+ * Convention: `<scope>:<kebab-case-identifier>`. Three scopes are
1795
+ * recognised by the library:
1796
+ *
1797
+ * - `atta:`: emitted by the framework-agnostic core (this map).
1798
+ * - `zod:`: emitted by the Zod adapter, computed inline from
1799
+ * `issue.code` (e.g. `zod:too_small`). No enum here because
1800
+ * Zod's code list evolves.
1801
+ * - consumer-defined: anything the consumer's backend or app stamps
1802
+ * onto a `ValidationError` (a server response handed to `setErrors`,
1803
+ * or a code passed inline). Pick a scope (`api:`, `auth:`, etc.) and
1804
+ * stay consistent.
1805
+ *
1806
+ * Use these constants in tests and error-routing UI:
1807
+ *
1808
+ * ```ts
1809
+ * if (error.code === AttaformErrorCode.NoValueSupplied) {
1810
+ * // user hasn't filled this field
1811
+ * }
1812
+ * ```
1813
+ */
1814
+ declare const AttaformErrorCode: {
1815
+ /** A required field is in the blank set: no value was supplied. */
1816
+ readonly NoValueSupplied: "atta:no-value-supplied";
1817
+ /** The schema adapter's `validateAtPath` threw synchronously. */
1818
+ readonly AdapterThrew: "atta:adapter-threw";
1819
+ /**
1820
+ * User code inside a `z.preprocess`, `.refine`, or `.transform`
1821
+ * threw (sync or async). The adapter caught the throw and surfaced
1822
+ * it as a `ValidationError` at the field path so the form's normal
1823
+ * error pipeline handles it instead of leaking as an unhandled
1824
+ * rejection or routing through `submitError`.
1825
+ */
1826
+ readonly ValidatorThrew: "atta:validator-threw";
1827
+ /**
1828
+ * A function-form `defaultValues` factory threw or its promise
1829
+ * rejected. The runtime captures the raw error on `form.hydrateError`
1830
+ * and ALSO surfaces a form-level `ValidationError` (path `[]`) so
1831
+ * the standard error pipeline carries the signal. Critical for the
1832
+ * SSR round-trip: `hydrateError` itself does not ride the wire
1833
+ * payload, but `schemaErrors` does, so the client sees the failure
1834
+ * after rehydration without an extra channel.
1835
+ */
1836
+ readonly HydrationFailed: "atta:hydration-failed";
1837
+ /** The supplied path didn't resolve to any node in the schema. */
1838
+ readonly PathNotFound: "atta:path-not-found";
1839
+ /**
1840
+ * A walked form's `activate()` (async `defaultValues` factory) threw
1841
+ * during `wizard.handleSubmit`'s path walk. Surfaced as a synthetic
1842
+ * `ValidationError` at the form-level path (`[]`) so the wizard's
1843
+ * aggregate error pipeline can carry the failure alongside ordinary
1844
+ * validation errors. The raw factory error remains on
1845
+ * `form.hydrateError` for retry UX.
1846
+ */
1847
+ readonly ActivationFailed: "atta:activation-failed";
1848
+ /**
1849
+ * Default code stamped on a manual error set through `form.setErrors`
1850
+ * when the caller omits an explicit `code`. The `setErrors` input is
1851
+ * lenient (`code` optional), so this is the fallback that keeps every
1852
+ * produced `ValidationError` carrying a stable, branchable identifier.
1853
+ * Override it per error by passing your own `code` (`api:…`, `auth:…`).
1854
+ */
1855
+ readonly UserError: "atta:user-error";
1856
+ /**
1857
+ * Stamped on a `ValidationError` synthesized from a throw or rejection
1858
+ * out of a `handleSubmit` `onSubmit` callback. The raw error also lands
1859
+ * on `form.meta.submitError` (a raw-`Error` inspection channel); this
1860
+ * code marks the copy piped into the user-error layer so the failure
1861
+ * also surfaces on `form.errors` / `meta.ownErrors` / `firstOwnError`
1862
+ * where the UI already reads. A throw carrying its own `path` / `code`
1863
+ * keeps them; a bare `Error` or a non-Error throw defaults here at the
1864
+ * form-level path `[]`.
1865
+ */
1866
+ readonly SubmitError: "atta:submit-error";
1867
+ /**
1868
+ * Synthesized by `wizard.handleSubmit` when the whole-wizard submit
1869
+ * would otherwise succeed but a `gate()` step is still uncleared. A
1870
+ * gate clears only on its member form's clean submit (confirmation), so
1871
+ * a downstream step that happens to be valid-by-default must not let the
1872
+ * wizard finish past an unconfirmed prerequisite. Emitted at the gate
1873
+ * form's key (path `[]`) so the aggregate error pipeline routes focus to
1874
+ * the gate, and `done` never latches.
1875
+ */
1876
+ readonly GateNotCleared: "atta:gate-not-cleared";
1877
+ };
1878
+ /** Every `atta:`-prefixed code Attaform itself can put on an error. */
1879
+ type AttaformErrorCode = (typeof AttaformErrorCode)[keyof typeof AttaformErrorCode];
1880
+
1881
+ /**
1882
+ * Options for `createAttaform()`.
1883
+ */
1884
+ type AttaformPluginOptions = SSRDetectOptions & {
1885
+ /**
1886
+ * Whether to install the Vue DevTools integration. Default `true`.
1887
+ * The integration is dev-only: the `import('./devtools')` sits behind
1888
+ * the `__DEV__` flag, so a consumer's production build folds it out
1889
+ * entirely (no devtools chunk shipped, no fetch attempted). In
1890
+ * development the DevTools peer is loaded lazily and a missing peer
1891
+ * fails silently. Pass `false` to skip the integration even in dev.
1892
+ */
1893
+ devtools?: boolean;
1894
+ };
1895
+ /**
1896
+ * Create the Vue plugin that installs the form library on a Vue
1897
+ * application. Required only when you want app-wide options
1898
+ * (`devtools: false`, `ssr: true`). For the default setup,
1899
+ * `useForm` / `injectForm` / `useRegister` lazy-install the registry
1900
+ * on first use.
1901
+ *
1902
+ * ```ts
1903
+ * import { createApp } from 'vue'
1904
+ * import { createAttaform } from 'attaform'
1905
+ *
1906
+ * createApp(App)
1907
+ * .use(createAttaform({ devtools: false }))
1908
+ * .mount('#app')
1909
+ * ```
1910
+ *
1911
+ * Under SSR with bare Vue 3, install explicitly with `{ ssr: true }`
1912
+ * from your server entry: the SSR serialization helpers
1913
+ * (`renderAttaformState` / `hydrateAttaformState`) require an
1914
+ * already-attached registry and don't trigger lazy install. Under
1915
+ * Nuxt, install via `attaform/nuxt` instead; the Nuxt module wires
1916
+ * both server and client automatically.
1917
+ *
1918
+ * Installing more than once on the same app is a no-op (the second
1919
+ * call logs a dev-mode warning).
1920
+ */
1921
+ declare function createAttaform(options?: AttaformPluginOptions): Plugin;
1922
+
1923
+ /**
1924
+ * Serialised snapshot of every form in a Vue app, produced by
1925
+ * `renderAttaformState` and consumed by `hydrateAttaformState`.
1926
+ *
1927
+ * JSON-safe: pass it to `JSON.stringify`, `devalue`, or any other
1928
+ * serialiser before embedding in your SSR payload.
1929
+ */
1930
+ type SerializedAttaformState = {
1931
+ /** Envelope version; see {@link ATTAFORM_STATE_VERSION}. */
1932
+ readonly v: number;
1933
+ /** Tuples of `[formKey, snapshot]` for every form in the app. */
1934
+ readonly forms: ReadonlyArray<readonly [FormKey, SerializedFormData]>;
1935
+ };
1936
+ /**
1937
+ * Snapshot every form on a Vue app for SSR. Call from your server
1938
+ * entry after rendering the app:
1939
+ *
1940
+ * ```ts
1941
+ * import { renderToString } from '@vue/server-renderer'
1942
+ * import { renderAttaformState, escapeForInlineScript } from 'attaform'
1943
+ *
1944
+ * const html = await renderToString(app)
1945
+ * const state = renderAttaformState(app)
1946
+ * const payload = escapeForInlineScript(JSON.stringify(state))
1947
+ *
1948
+ * return `
1949
+ * ${html}
1950
+ * <script>window.__ATTAFORM_STATE__ = ${payload}</script>
1951
+ * `
1952
+ * ```
1953
+ *
1954
+ * Pair with `hydrateAttaformState` on the client to restore the
1955
+ * forms in their server-rendered state. Nuxt users do not need it:
1956
+ * `attaform/nuxt` wires SSR automatically.
1957
+ */
1958
+ declare function renderAttaformState(app: App): SerializedAttaformState;
1959
+ /**
1960
+ * Restore forms from a server-rendered snapshot on the client. Call
1961
+ * from your client entry before mounting:
1962
+ *
1963
+ * ```ts
1964
+ * import { createApp } from 'vue'
1965
+ * import { createAttaform, hydrateAttaformState } from 'attaform'
1966
+ *
1967
+ * const app = createApp(App).use(createAttaform())
1968
+ * hydrateAttaformState(app, window.__ATTAFORM_STATE__)
1969
+ * app.mount('#app')
1970
+ * ```
1971
+ *
1972
+ * Accepts `unknown` because the payload crosses the wire untyped
1973
+ * (an inline `<script>` tag, `JSON.parse`): the envelope is
1974
+ * validated at runtime, and anything that isn't a version-matched
1975
+ * `renderAttaformState` snapshot is skipped wholesale, so forms
1976
+ * construct fresh from their schemas instead.
1977
+ *
1978
+ * The next `useForm({ key })` call for each serialised form picks up
1979
+ * the snapshot transparently, with no further action required.
1980
+ */
1981
+ declare function hydrateAttaformState(app: App, payload: unknown): void;
1982
+
1983
+ /**
1984
+ * Escape a JSON string so it's safe to embed inside an inline
1985
+ * `<script>` tag during SSR. Plain `JSON.stringify` is not safe: a
1986
+ * form value containing the literal substring `</script>` would
1987
+ * break out of the script tag.
1988
+ *
1989
+ * ```ts
1990
+ * const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))
1991
+ * // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.
1992
+ * ```
1993
+ *
1994
+ * Output remains valid JSON, so `JSON.parse` round-trips back to the
1995
+ * original value on the client.
1996
+ */
1997
+ declare function escapeForInlineScript(json: string): string;
1998
+
1999
+ /**
2000
+ * Symbol slot used by custom directive integrations to install an
2001
+ * assigner on the bound element. Read by the v-register directive
2002
+ * when a DOM event fires:
2003
+ *
2004
+ * ```ts
2005
+ * import { assignKey } from 'attaform'
2006
+ * el[assignKey] = (value) => myCustomWriter(value)
2007
+ * ```
2008
+ *
2009
+ * Most consumers never need this: the built-in directives wire default
2010
+ * assigners for text inputs, checkboxes, radios, and selects.
2011
+ */
2012
+ declare const assignKey: unique symbol;
2013
+
2014
+ /**
2015
+ * Type guard for a `RegisterValue`. Returns `true` when `val` looks
2016
+ * like the object returned from `form.register(path)`.
2017
+ *
2018
+ * ```ts
2019
+ * if (isRegisterValue(slotValue)) {
2020
+ * // slotValue.innerRef is now a Ref<unknown>
2021
+ * }
2022
+ * ```
2023
+ *
2024
+ * Useful when building wrapper components that accept either a
2025
+ * `RegisterValue` or a plain ref via the same prop.
2026
+ */
2027
+ declare function isRegisterValue<Value = unknown>(val: unknown): val is RegisterValue<Value>;
2028
+
2029
+ /**
2030
+ * Shared building blocks for Attaform's two devtools surfaces: the Vue
2031
+ * DevTools (Chrome-extension) inspector wired up in `./devtools.ts`, and
2032
+ * the Nuxt DevTools (overlay) panel wired up via `../../nuxt.ts` +
2033
+ * `../pages/_attaform_devtools.vue`.
2034
+ *
2035
+ * Houses the window-bridge contract both surfaces consume so a new
2036
+ * bridge field lands in one file. Both surfaces render RAW form values
2037
+ * by design. DevTools is a dev-only surface, and redaction across every
2038
+ * place a value surfaces is impractical security theater rather than a
2039
+ * real safeguard.
2040
+ */
2041
+
2042
+ /**
2043
+ * Property key on `window` that the Nuxt-side dev plugin attaches the
2044
+ * bridge object to. The iframe-mounted overlay panel reads
2045
+ * `window.parent[DEVTOOLS_WINDOW_KEY]` to reach the host app's registry.
2046
+ *
2047
+ * Underscored + namespaced to make accidental collision with consumer
2048
+ * globals vanishingly unlikely. It must stay stable across versions:
2049
+ * would silently disconnect older library builds from newer overlay
2050
+ * panels in the same browser tab during a library upgrade.
2051
+ */
2052
+ declare const DEVTOOLS_WINDOW_KEY = "__attaform_devtools__";
2053
+ /**
2054
+ * Shape of the object the host plugin attaches to `window` in dev mode.
2055
+ * The iframe overlay panel reads this to discover the live registry and
2056
+ * render its forms.
2057
+ *
2058
+ * Single-registry assumption: the latest `createAttaform()` install
2059
+ * wins. Multi-app pages (rare; typically only seen in micro-frontend
2060
+ * setups) will only see one app's forms in the panel. Documented but
2061
+ * not actively supported. The alternative (a Set of registries with
2062
+ * union-rendering) is a future call if a real consumer hits it.
2063
+ */
2064
+ interface AttaformDevtoolsBridge {
2065
+ registry: AttaformRegistry;
2066
+ /**
2067
+ * The library version, surfaced in the panel's footer for support /
2068
+ * bug-report context. Read from `package.json` at host-plugin init.
2069
+ */
2070
+ version: string;
2071
+ }
2072
+ declare global {
2073
+ interface Window {
2074
+ [DEVTOOLS_WINDOW_KEY]?: AttaformDevtoolsBridge;
2075
+ }
2076
+ }
2077
+
2078
+ /**
2079
+ * Typed error classes thrown by the form library. Each one signals a
2080
+ * distinct misuse so calling code can branch on `instanceof` instead
2081
+ * of pattern-matching error messages.
2082
+ *
2083
+ * Every class extends `AttaformError`, so consumers can write a single
2084
+ * polymorphic catch (`catch (e) { if (e instanceof AttaformError) ... }`)
2085
+ * instead of OR-chaining checks for each subclass. `AttaformError` itself
2086
+ * extends the standard `Error`, so existing `instanceof Error` usage
2087
+ * keeps working.
2088
+ */
2089
+
2090
+ /**
2091
+ * Base for every error class thrown by `attaform`. Sets
2092
+ * `this.name` from the constructor's `new.target.name`, so subclasses
2093
+ * don't have to redeclare their own name override.
2094
+ */
2095
+ declare class AttaformError extends Error {
2096
+ constructor(message: string, options?: ErrorOptions);
2097
+ }
2098
+ /**
2099
+ * Thrown when a path string is malformed, typically a dotted path
2100
+ * with empty segments (e.g. `'a..b'`, leading or trailing dots).
2101
+ * Use array form (`['a', 'b']`) for keys that contain literal dots.
2102
+ */
2103
+ declare class InvalidPathError extends AttaformError {
2104
+ }
2105
+ /**
2106
+ * Thrown when `useForm` receives an invalid configuration, most often
2107
+ * a schema passed directly as the first argument, or no argument at
2108
+ * all. The configuration is an options bag; the schema is one of
2109
+ * several fields, even when it's the only one in use.
2110
+ *
2111
+ * ```ts
2112
+ * // ✗ Crashes deep inside the validator with an opaque message:
2113
+ * const form = useForm(z.object({ ... }))
2114
+ * // ✗ Same:
2115
+ * const form = useForm()
2116
+ * // ✓ Pass the schema as the `schema` field:
2117
+ * const form = useForm({ schema: z.object({ ... }) })
2118
+ * ```
2119
+ *
2120
+ * The same shape applies to every entry point: `attaform/zod`,
2121
+ * `attaform/zod-v3`, `attaform/zod-v4`, and the schema-agnostic
2122
+ * `attaform` root.
2123
+ */
2124
+ declare class InvalidUseFormConfigError extends AttaformError {
2125
+ constructor();
2126
+ }
2127
+ /**
2128
+ * Thrown when a `handleSubmit`-supplied `onError` callback itself
2129
+ * throws or rejects. Wraps the inner failure so both the original
2130
+ * cause (via `error.cause`) and the propagation site are visible.
2131
+ */
2132
+ declare class SubmitErrorHandlerError extends AttaformError {
2133
+ }
2134
+ /**
2135
+ * Thrown when an `attaform` API needs the registry attached to a Vue
2136
+ * app but it isn't there yet. Component-level entry points (`useForm`,
2137
+ * `injectForm`, `useRegister`) lazy-install the registry on first use,
2138
+ * so this error is mostly reached by the SSR helpers `renderAttaformState`
2139
+ * and `hydrateAttaformState`, which run outside a setup context and
2140
+ * have no current instance to install against.
2141
+ *
2142
+ * Fix: add `app.use(createAttaform())` (or `app.use(createAttaform({
2143
+ * ssr: true }))` on the server) to your SSR entry, before
2144
+ * `renderToString` / hydration. Under Nuxt, `attaform/nuxt` already
2145
+ * does this; the error usually points at a non-Nuxt SSR setup that
2146
+ * hasn't installed explicitly.
2147
+ */
2148
+ declare class RegistryNotInstalledError extends AttaformError {
2149
+ constructor();
2150
+ }
2151
+ /**
2152
+ * Thrown when `useForm` / `injectForm` is called outside of a
2153
+ * Vue `setup()` function, typically from an event handler, watcher,
2154
+ * or async callback that runs after mount.
2155
+ *
2156
+ * Fix: move the call into `setup()`, or trigger it from a child
2157
+ * component whose `setup()` runs the composable.
2158
+ */
2159
+ declare class OutsideSetupError extends AttaformError {
2160
+ constructor();
2161
+ }
2162
+ /**
2163
+ * Thrown when a `useForm({ key })` call uses a key starting with
2164
+ * `__atta:`. That prefix is reserved for keys the library generates
2165
+ * internally (e.g. for anonymous `useForm()` calls without an
2166
+ * explicit key). Pick a different prefix for your form.
2167
+ */
2168
+ declare class ReservedFormKeyError extends AttaformError {
2169
+ constructor(key: string);
2170
+ }
2171
+
2172
+ export { createRegistry as B, DEVTOOLS_WINDOW_KEY as D, escapeForInlineScript as E, gate as H, getRegistryFromApp as J, hydrateAttaformState as K, injectForm as M, injectWizard as N, OutsideSetupError as O, isRegisterValue as P, kAttaformRegistry as Q, RegistryNotInstalledError as R, lazy as T, renderAttaformState as V, useRegister as X, useRegistry as Y, useWizard as Z, AttaformError as c, AttaformErrorCode as d, InvalidPathError as g, InvalidUseFormConfigError as h, ReservedFormKeyError as i, SubmitErrorHandlerError as l, assignKey as y, createAttaform as z };
2173
+ export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, GateMarker as G, InjectWizardInput as I, LazyMarker as L, SerializedAttaformState as S, UseRegisterOptions as U, WizardAggregateError as W, AnyForm as a, AttaformDevtoolsBridge as b, AttaformPluginOptions as e, FormStatusSeed as f, SerializedFormData as j, StepSlot as k, UseRegisterReturn as m, UseWizardReturnType as n, WizardCtx as o, WizardCtxForm as p, WizardOnError as q, WizardOnSubmit as r, WizardOptions as s, WizardPersistFn as t, WizardRestoreFn as u, WizardRestoreState as v, WizardStatusesProxy as w, WizardSubmitContext as x };