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