attaform 0.29.0 → 0.30.1

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