attaform 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/abstract.d.mts +5 -5
  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 +1 -1
  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.oYrkP9dj.mjs → attaform-dev.BK6Ge2qR.mjs} +80 -341
  11. package/dist/dev/shared/{attaform-dev.Dr2YzS5B.mjs → attaform-dev.BQc5QV3Y.mjs} +11 -19
  12. package/dist/dev/shared/{attaform-dev.BR8u0YOZ.mjs → attaform-dev.BcNtyOmf.mjs} +4 -4
  13. package/dist/{shared/attaform.h0i6LijT.mjs → dev/shared/attaform-dev.BcYT6tFh.mjs} +152 -20
  14. package/dist/dev/shared/{attaform-dev.6qxD-Yqo.mjs → attaform-dev.BisFstxh.mjs} +227 -88
  15. package/dist/dev/shared/{attaform-dev.WmuhD_CZ.mjs → attaform-dev.CQ2rJiAX.mjs} +833 -656
  16. package/dist/dev/shared/{attaform-dev.P7cseiEG.mjs → attaform-dev.D6CLvaQT.mjs} +20 -11
  17. package/dist/dev/shared/{attaform-dev.D2I8e_Mm.mjs → attaform-dev.DTrHonpc.mjs} +225 -238
  18. package/dist/dev/shared/{attaform-dev.BXE2cr0L.mjs → attaform-dev.DUZckyV8.mjs} +1 -3
  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 +2 -2
  23. package/dist/directive.mjs +1 -1
  24. package/dist/history.d.mts +1 -1
  25. package/dist/history.mjs +1 -2
  26. package/dist/index.d.mts +9 -7
  27. package/dist/index.mjs +5 -5
  28. package/dist/nuxt.d.mts +11 -20
  29. package/dist/nuxt.mjs +0 -1
  30. package/dist/runtime/plugins/attaform.mjs +3 -3
  31. package/dist/shared/{attaform.uLbun31C.mjs → attaform.B3resMQ1.mjs} +19 -11
  32. package/dist/shared/{attaform.Cs2Rbdlo.mjs → attaform.BK0cgu2K.mjs} +123 -139
  33. package/dist/{dev/shared/attaform-dev.By4PRjt0.mjs → shared/attaform.BRDDQeHV.mjs} +136 -26
  34. package/dist/shared/{attaform.NLmerepI.mjs → attaform.BXneBxEj.mjs} +71 -334
  35. package/dist/shared/{attaform.D6DCnnQu.mjs → attaform.ByWP6kSx.mjs} +4 -4
  36. package/dist/shared/{attaform.CJP3RkGa.mjs → attaform.CJ8DpRGj.mjs} +223 -238
  37. package/dist/shared/{attaform.C09fpdLS.mjs → attaform.CUfrMK7t.mjs} +227 -88
  38. package/dist/shared/{attaform.wfY8XFvm.mjs → attaform.CdZIEf7_.mjs} +11 -19
  39. package/dist/shared/{attaform.JfSWdEcb.d.mts → attaform.Clz0ArIi.d.mts} +1 -1
  40. package/dist/shared/{attaform.B8k8JrmK.mjs → attaform.DNJOWjQw.mjs} +826 -612
  41. package/dist/shared/{attaform.B4VkS43-.d.mts → attaform.Rc9u4Vfw.d.mts} +1 -1
  42. package/dist/shared/{attaform.D5V4tZWq.d.mts → attaform.UGyOK83T.d.mts} +200 -218
  43. package/dist/shared/{attaform.Dq4vWxuG.mjs → attaform.WiGdIqgx.mjs} +1 -3
  44. package/dist/shared/{attaform.Cgu-hdSb.d.mts → attaform.bKLzMU0o.d.mts} +321 -523
  45. package/dist/transforms.d.mts +0 -22
  46. package/dist/transforms.mjs +1 -1
  47. package/dist/vite.d.mts +13 -4
  48. package/dist/vite.mjs +8 -2
  49. package/dist/zod-v3.d.mts +6 -4
  50. package/dist/zod-v3.mjs +6 -6
  51. package/dist/zod-v4.d.mts +18 -31
  52. package/dist/zod-v4.mjs +6 -6
  53. package/dist/zod.d.mts +4 -4
  54. package/dist/zod.mjs +5 -5
  55. package/package.json +19 -15
  56. package/skills/attaform/SKILL.md +6 -3
  57. package/skills/attaform/references/custom-components.md +8 -3
  58. package/skills/attaform/references/errors.md +8 -3
  59. package/skills/attaform/references/saving.md +77 -0
  60. package/skills/attaform/references/wizards.md +10 -8
  61. package/dist/chunks/fingerprint.mjs +0 -183
  62. package/dist/chunks/fingerprint2.mjs +0 -159
  63. package/dist/dev/chunks/fingerprint.mjs +0 -183
  64. package/dist/dev/chunks/fingerprint2.mjs +0 -159
  65. package/dist/dev/shared/attaform-dev.BJGA_UOS.mjs +0 -36
  66. package/dist/shared/attaform.BJGA_UOS.mjs +0 -36
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { G as GenericForm, s as FlatPath, B as NestedType, F as FormKey, U as UseFormConfiguration, A as AbstractSchema, as as AcceptableDefaults, af as ValidateOnConfig, a as UseFormReturnType } from './attaform.Cgu-hdSb.mjs';
2
+ import { G as GenericForm, l as FlatPath, s as NestedType, F as FormKey, U as UseFormConfiguration, A as AbstractSchema, aq as AcceptableDefaults, ac as ValidateOnConfig, a as UseFormReturnType } from './attaform.bKLzMU0o.mjs';
3
3
 
4
4
  /**
5
5
  * The Zod v4 schema roots `useForm` accepts. A fixed-shape
@@ -1,4 +1,4 @@
1
- import { G as GenericForm, F as FormKey, a as UseFormReturnType, a5 as RegisterValue, V as PathKey, k as DisplayCtx, w as GetDisplayState, l as DisplayMachine, aa as SlimPrimitiveKind, C as CoercionEntry, e as CoercionRegistry, au as FieldRecord, av as ErrorCell, ag as ValidationError, aw as OriginalsRecord, A as AbstractSchema, T as Path, ak as WriteMeta, h as DeepPartial, al as WriteShape, ax as AttaformDomBinding, ay as TransformAbortHolder, ae as ValidateOn, d as AttaformDefaults } from './attaform.Cgu-hdSb.mjs';
1
+ import { G as GenericForm, F as FormKey, a as UseFormReturnType, _ as RegisterValue, v as Path, w as PathKey, ar as DisplayCtx, as as GetDisplayState, at as DisplayMachine, au as FieldRecord, av as ErrorCell, ad as ValidationError, aw as OriginalsRecord, A as AbstractSchema, ah as WriteMeta, d as DeepPartial, ai as WriteShape, ax as AttaformDomBinding, ay as TransformAbortHolder } from './attaform.bKLzMU0o.mjs';
2
2
  import { Ref, ComputedRef, ShallowRef, App, InjectionKey, Plugin } from 'vue';
3
3
 
4
4
  /**
@@ -121,6 +121,22 @@ declare function injectForm<Form extends GenericForm, GetValueFormType extends G
121
121
  * composable's `onMounted` warn fires once per instance to surface
122
122
  * the misuse case at runtime.
123
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
+ *
124
140
  * Diagnostic: in dev mode, a single `console.warn` fires per instance
125
141
  * at `onMounted` if the captured value is still `undefined` — by then
126
142
  * the parent has had its full mount lifecycle to bind, so a missing
@@ -149,12 +165,69 @@ declare function injectForm<Form extends GenericForm, GetValueFormType extends G
149
165
  * the hybrid's only `.value`. Older code that read `rv.value?.path`
150
166
  * keeps working; new code can write `rv.path` directly.
151
167
  */
152
- type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined>;
153
- declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
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
+ type DynamicPathSweep = {
212
+ /**
213
+ * Note that a cache has just stored an entry for `segments` under
214
+ * `key`. Paths a schema shape bounds are ignored, so only the
215
+ * candidates the sweep could ever drop are walked.
216
+ */
217
+ readonly track: (segments: Path, key: PathKey) => void;
218
+ /**
219
+ * Register a cache's own eviction. It receives the canonical
220
+ * `PathKey` of a path the form no longer has, and deletes whatever it
221
+ * files under that path. A cache keying by more than the path (a
222
+ * shape sigil, say) deletes every variant; a key it never held is a
223
+ * no-op.
224
+ */
225
+ readonly onEvict: (evict: (key: PathKey) => void) => void;
226
+ };
154
227
 
155
228
  /**
156
- * Per-form display engine: owns the clock and the timers that the pure
157
- * `getDisplayState` reducer policy needs, so the reducer itself stays a
229
+ * Per-form display engine: owns the clock and the timers the display
230
+ * reducer's timing policy needs, so the reducer itself stays a
158
231
  * deterministic `(prev, ctx) => next` function.
159
232
  *
160
233
  * It keeps a `Map` of the machines that are still *active* (a spinner is
@@ -173,15 +246,15 @@ declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
173
246
  * retained set is bounded by the rendered non-idle fields; none of them
174
247
  * arm a timer, so retention costs memory, never CPU.
175
248
  *
176
- * Untrusted reducer: `getDisplayState` is consumer-overridable, so the
177
- * engine treats the returned `reviewAt` as untrusted. A non-finite deadline
178
- * (NaN / ±Infinity from a custom predicate's bad arithmetic) is ignored
179
- * rather than handed to `setTimeout`, where it coerces to 0 and spins; an
180
- * over-large finite deadline is clamped below the 32-bit `setTimeout`
181
- * overflow; and the timer refuses to re-arm for the exact deadline it just
182
- * fired, so a predicate re-emitting a fixed or past `reviewAt` can't drive an
183
- * infinite fire loop. None of these arise from the library default, which
184
- * always advances its deadline or drops it.
249
+ * `setTimeout` invariants on the returned `reviewAt`, enforced here rather
250
+ * than trusted from the reducer. A non-finite deadline is ignored rather
251
+ * than handed to `setTimeout`, where it coerces to 0 and spins; an over-large
252
+ * finite deadline is clamped below the 32-bit `setTimeout` overflow; and the
253
+ * timer refuses to re-arm for a deadline it just fired that has also already
254
+ * passed. The last of those is not belt-and-braces: the library reducer's own
255
+ * min-visible hold re-emits ONE fixed instant for the length of the window,
256
+ * which is exactly the shape that stranded a field on `'pending'` forever (see
257
+ * `rearm`).
185
258
  *
186
259
  * Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
187
260
  * so a min-visible hold can overshoot. A `visibilitychange` listener bumps
@@ -215,56 +288,64 @@ type DisplayEngine = {
215
288
  };
216
289
 
217
290
  /**
218
- * Schema-driven coercion of user-typed DOM values at the v-register
219
- * directive layer. When the slim schema declares a numeric or
220
- * boolean type at a path, the directive coerces incoming string
221
- * values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
222
- * gate sees the write — making the schema authoritative for storage
223
- * shape and freeing consumers from sprinkling `.number` modifiers
224
- * across templates.
225
- *
226
- * Coercion is consumer-extensible: a `CoercionRegistry` is just an
227
- * `Array<CoercionEntry>` keyed at config time by `(input, output)`
228
- * `SlimPrimitiveKind` literals. The library ships
229
- * `defaultCoercionRules` (string→number, string→boolean) and
230
- * `defineCoercion` for type-narrowed authoring; consumers spread the
231
- * defaults to extend or supply their own array to replace.
232
- *
233
- * Coercion applies ONLY to user-typed DOM values flowing through
234
- * the directive's assigner. Programmatic writes (`form.setValue`,
235
- * `setValueWithInternalPath`) bypass coercion — they're authoritative
236
- * writes whose strict typing is on the caller. This mirrors the
237
- * `transforms` pipeline's user-input-only contract.
291
+ * The error stores, indexed by path prefix.
292
+ *
293
+ * `aggregateErrorsAt(state, prefix)` answers "every active error at or
294
+ * under this path", and it used to answer it by walking every entry in
295
+ * all three error stores, resolving each key back to segments and
296
+ * testing the prefix. That is fine for one call. A table asks for one
297
+ * per row: `form.list('rows')` on 400 rows with 800 errors did 320,000
298
+ * of those tests, and a keystroke in any single row re-did all of them,
299
+ * costing a quarter of a second per character typed.
300
+ *
301
+ * The cost is structural, not incidental: the work is O(rows x errors)
302
+ * where the answer is O(errors). So the keys are collected ONCE per
303
+ * change to the stores, resolved to segments once, and sorted, and a
304
+ * query binary-searches its own window instead of scanning.
305
+ *
306
+ * ## Why a sorted key array is the right index
307
+ *
308
+ * A `PathKey` is `JSON.stringify(segments)`, so a path's descendants
309
+ * all begin with the parent's key minus its closing bracket, plus a
310
+ * comma: every key under `["rows",0]` begins `["rows",0,`. Call that
311
+ * the marker. Two facts make a sorted array enough:
312
+ *
313
+ * - every descendant key is >= the marker, since it begins with it;
314
+ * - every descendant key is <= the parent's own key, since the two
315
+ * agree up to the last character, where `,` (0x2C) sorts before
316
+ * `]` (0x5D).
317
+ *
318
+ * So `[marker, parentKey]` is a contiguous window containing every
319
+ * descendant and the parent itself. It is a SUPERSET, not an exact
320
+ * answer, and the caller still applies `isPathPrefix` to what comes
321
+ * back: the window narrows the scan, it does not decide membership.
322
+ * That split is deliberate. Membership stays where it already was, so
323
+ * the encoding argument above cannot quietly become the definition of
324
+ * a prefix.
325
+ *
326
+ * The root prefix has no marker (`[]` minus its bracket plus a comma is
327
+ * `[,`, which nothing begins with), and the root matches everything
328
+ * anyway, so it is answered by handing back the whole index.
238
329
  */
239
330
 
240
- /**
241
- * Type-narrowing helper for authoring entries. At runtime it's
242
- * identity; at compile time it preserves the `input` / `output`
243
- * literal types so `transform`'s parameter is narrowed to the
244
- * runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
245
- *
246
- * Without this helper, authoring `{ input: 'string', output:
247
- * 'number', transform: (s) => ... }` against the broader
248
- * `CoercionEntry` widens `s` to `string | number | boolean | ...`,
249
- * forcing a cast in every transform body. `defineCoercion` is the
250
- * opaque-free idiom.
251
- */
252
- declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
253
- /**
254
- * Internal index built from a `CoercionRegistry` at config-resolve
255
- * time. Keyed by `${input}->${output}` for O(1) per-keystroke
256
- * dispatch. The authoring shape (array, ergonomic, type-narrowing-
257
- * friendly) and the dispatch shape (Map, fast) decouple cleanly.
258
- */
259
- type CoercionIndex = ReadonlyMap<`${SlimPrimitiveKind}->${SlimPrimitiveKind}`, CoercionEntry>;
260
- /**
261
- * The library's built-in registry. Two cells: string→number and
262
- * string→boolean. Re-exported so consumers can spread it when
263
- * supplying a custom registry that extends defaults.
264
- */
265
- declare const defaultCoercionRules: CoercionRegistry;
331
+ /** One indexed path: its key, and the segments that key decodes to. */
332
+ type ErrorPathEntry = {
333
+ readonly key: PathKey;
334
+ readonly segments: Path;
335
+ };
266
336
 
267
337
  type FormStore<F extends GenericForm, G extends GenericForm = F> = {
338
+ /**
339
+ * The form's single liveness sweep over every per-path cache. Owned by
340
+ * the store rather than by `buildFormApi` because the store's own
341
+ * per-path maps (`fields`, `originals`, `authoredPaths`,
342
+ * `fieldValidationState`) are the largest thing it evicts, and they
343
+ * exist whether or not a form API was ever built around them. Read
344
+ * surfaces register their own evictions into it, so one subscription
345
+ * and one liveness walk serve all of them. See
346
+ * `dynamic-path-sweep.ts`.
347
+ */
348
+ readonly pathSweep: DynamicPathSweep;
268
349
  readonly formKey: FormKey;
269
350
  readonly form: Ref<F>;
270
351
  readonly fields: Map<PathKey, FieldRecord>;
@@ -303,6 +384,22 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
303
384
  * sentinel — see `docs/validation/blank.md`.
304
385
  */
305
386
  readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
387
+ /**
388
+ * Every path carrying an error at or under `prefix`, sorted by key.
389
+ * `aggregateErrorsAt` takes its candidates from here instead of
390
+ * re-scanning all three error stores per call, which is what turns a
391
+ * table of N rows from O(N x errors) back into O(errors). See
392
+ * `error-path-index.ts`.
393
+ *
394
+ * Reading this rather than the form-global index is also what keeps
395
+ * containers isolated from each other. The index is one value for the
396
+ * whole form, so any path gaining or losing an error gives it a new
397
+ * identity; a container reading it directly woke on every other
398
+ * container's errors. Each prefix gets its own memoised `computed`
399
+ * that holds its previous array when its own window is unchanged, so
400
+ * the global change stops there. See `errorWindowAt` in the store.
401
+ */
402
+ readonly errorWindowAt: (prefix: Path, prefixKey: PathKey) => readonly ErrorPathEntry[];
306
403
  readonly originals: Map<PathKey, OriginalsRecord>;
307
404
  /**
308
405
  * Reactive set of paths whose displayed state should be EMPTY even
@@ -352,18 +449,9 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
352
449
  * the eventual directive lifecycle remains the source of truth.
353
450
  */
354
451
  readonly ssr: boolean;
355
- /**
356
- * Resolved `getDisplayState` predicate driving `field.displayState`,
357
- * the `show*` booleans, and their `form.meta` rollups. Resolved once
358
- * at construction via `resolveGetDisplayState(options.getDisplayState)`;
359
- * `undefined` config falls through to `defaultDisplayState`. The
360
- * field-state computeds read the resolved function directly on every
361
- * read.
362
- */
363
- readonly getDisplayState: GetDisplayState;
364
452
  /**
365
453
  * Per-form display engine: owns the clock and the single timer the timed
366
- * `getDisplayState` reducer policy needs, keeping the reducer itself a
454
+ * display-reducer policy needs, keeping the reducer itself a
367
455
  * pure `(prev, ctx) => next` function. The field-state computeds route
368
456
  * every `displayState` read through `displayEngine.resolve(...)`, which
369
457
  * threads the path's previous machine, persists or evicts the result, and
@@ -466,10 +554,22 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
466
554
  * Resolves after `hydrating` flips back to `false`; consumers can
467
555
  * `await form.rehydrate()` to gate UI on the fresh load.
468
556
  *
469
- * Does NOT touch dirty / touched / submit state — chain
470
- * `form.reset()` if you want a clean baseline.
557
+ * Does NOT touch touched / submit state; chain `form.reset()` if you
558
+ * want a clean surface. It DOES re-seat the defaults (see
559
+ * `adoptResolvedDefaults`), so dirty afterwards means "differs from
560
+ * what the factory just returned", and `form.reset()` lands on the
561
+ * fresh values rather than throwing them away.
471
562
  */
472
563
  rehydrate(): Promise<void>;
564
+ /**
565
+ * Adopt a resolved async-defaults value as the form's durable
566
+ * defaults and re-seed the dirty baseline from it. Called by the
567
+ * activate / rehydrate orchestrator once its factory settles.
568
+ * Exposed on the store (rather than imported) because
569
+ * `form-activation` is imported BY this module; a value import back
570
+ * the other way would close a cycle.
571
+ */
572
+ adoptResolvedDefaults(value: unknown): void;
473
573
  /**
474
574
  * Incremented by every `reset()` call. The submit wrapper captures
475
575
  * this at entry and skips writing `submitError` from a catch that
@@ -502,26 +602,10 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
502
602
  * run yet — so without the gate, frame 1 paints the form as
503
603
  * "valid" before the real verdict arrives a tick later.
504
604
  *
505
- * Initialized to `!strict`: non-strict consumers opt out of the
506
- * validation pipeline by design, so locking them on
507
- * `firstValidationDone === false` would defeat the opt-out.
508
605
  * Reset is left untouched — the post-reset validation flips it
509
606
  * back true on completion, same as the construction-time path.
510
607
  */
511
608
  readonly firstValidationDone: Ref<boolean>;
512
- /**
513
- * `true` when the sub-schema rooted at `path` (or any of its
514
- * descendants) declares async work — composes
515
- * `schema.getSchemasAtPath(path)` with each candidate's
516
- * `needsAsyncValidation()`, memoised per canonical path key for
517
- * the lifetime of the FormStore. Used by `meta.valid` /
518
- * `field.valid` to skip the `firstValidationDone` gate on subtrees
519
- * that are fully synchronous: their verdict resolves at construction
520
- * (or on the next per-field run) without waiting on a microtask, so
521
- * honouring the form-wide gate would just play dumb about a known
522
- * answer.
523
- */
524
- pathHasAsyncValidation(path: Path): boolean;
525
609
  /**
526
610
  * Precomputed-key shortcut for `pathHasAsyncValidation`. The
527
611
  * canonical key is required and must correspond to `segments`; the
@@ -741,17 +825,6 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
741
825
  * source there).
742
826
  */
743
827
  markConnectedOptimistically(path: Path): void;
744
- /**
745
- * Leaf-only pristine check. `originals` is populated via
746
- * `diffAndApply`'s `added` patches, which fire only on primitive
747
- * leaves — a container path (e.g. `['profile']`) that isn't in
748
- * `originals` returns `true` here even when a descendant is dirty.
749
- * Callers that need container semantics should either loop over
750
- * leaves or walk `originals` manually. The public `getFieldState`
751
- * surface is typed to accept leaf paths only, so in practice this
752
- * isn't exposed to consumers.
753
- */
754
- isPristineAtPath(path: Path): boolean;
755
828
  /**
756
829
  * Precomputed-key shortcut for `isPristineAtPath`. The canonical
757
830
  * key is required and must correspond to `segments`; the helper
@@ -778,7 +851,6 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
778
851
  */
779
852
  hasRemovedSubtreeUnder(path: Path): boolean;
780
853
  getFieldRecord(path: Path): FieldRecord | undefined;
781
- getOriginalAtPath(path: Path): unknown;
782
854
  /**
783
855
  * Cancel every in-flight field-level validation run — clears timers
784
856
  * for debounced 'change' runs that haven't fired, latches `aborted`
@@ -803,12 +875,6 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
803
875
  endTransform(key: PathKey, token: number): void;
804
876
  /** Record a per-field normalization failure at `key` (`field.transformError`). */
805
877
  setTransformError(key: PathKey, err: Error): void;
806
- /**
807
- * Abort + release every in-flight async-transform run (all paths) and
808
- * clear `transformErrors`. Mirrors `cancelFieldValidation`; called by
809
- * `reset()` and store teardown.
810
- */
811
- cancelTransforms(): void;
812
878
  /**
813
879
  * Path-scoped counterpart to `cancelTransforms`: abort + release only
814
880
  * the runs at-or-under `prefix`, clearing their `transformError`.
@@ -839,10 +905,7 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
839
905
  * store's captured `debounceMs`. Used so sibling instances sharing a
840
906
  * FormStore can each validate on their own cadence.
841
907
  */
842
- scheduleFieldValidation(path: Path, immediate: boolean, override?: {
843
- readonly mode?: ValidateOn;
844
- readonly debounceMs?: number;
845
- }): void;
908
+ scheduleFieldValidation(path: Path, immediate: boolean, instance?: WriteMeta['instance']): void;
846
909
  /**
847
910
  * Subscribe to every `applyFormReplacement`. Fires synchronously
848
911
  * after `form.value` has been swapped to `next` and all field /
@@ -889,14 +952,12 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
889
952
  */
890
953
  readonly modules: Map<string, unknown>;
891
954
  /**
892
- * Resolved schema-coercion index — the merged config from
893
- * `createAttaform({ defaults: { coerce } })` ∪ `useForm({ coerce })`,
894
- * keyed by `${input}->${output}` for O(1) per-keystroke dispatch.
895
- * Empty Map when coercion is disabled. Read at `register()` time
896
- * by `buildCoerceFn` to bake the per-path coerce closure on
897
- * `RegisterValue.coerce`.
955
+ * Whether schema-driven coercion runs for this form — `false` only
956
+ * when the consumer passed `useForm({ coerce: false })`. Read at
957
+ * `register()` time by `buildCoerceFn` to bake the per-path coerce
958
+ * closure on `RegisterValue.coerce`.
898
959
  */
899
- readonly coerceIndex: CoercionIndex;
960
+ readonly coerceEnabled: boolean;
900
961
  /**
901
962
  * Tear down non-reactive resources owned by this FormStore. Invoked
902
963
  * by the registry when the last consumer unmounts. Cancels pending
@@ -1286,8 +1347,8 @@ type WizardOptions = {
1286
1347
  /**
1287
1348
  * When `wizard.handleSubmit` finds errors, automatically focus the
1288
1349
  * first failing form: jump to its step and invoke its
1289
- * `applyInvalidSubmitPolicy()` (focus / scroll per the form's own
1290
- * `onInvalidSubmit` configuration). Default `true`; pass `false` to
1350
+ * `applyInvalidSubmitPolicy()` (which honors that form's own
1351
+ * `focusOnInvalidSubmit` choice). Default `true`; pass `false` to
1291
1352
  * keep the active step where the user left it and handle navigation
1292
1353
  * manually in the `onError` callback.
1293
1354
  */
@@ -1486,6 +1547,12 @@ type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
1486
1547
  * `back()` does not pop; the trail is the audit
1487
1548
  * log, not the back-stack.
1488
1549
  * - `next/back/goTo` — pure navigation. Refuses while `submitting`.
1550
+ * The one exception is `next()` on an UNCLEARED
1551
+ * `gate()` step, which behaves as `tryNext()` so
1552
+ * wiring Next straight to it can never skip the
1553
+ * gate's confirmation. Once that gate clears, `next()`
1554
+ * is plain navigation again and does not re-submit to
1555
+ * re-confirm.
1489
1556
  * - `tryNext()` — submit the active step, and once that submit
1490
1557
  * resolves clean, advance; invalid input keeps the pin
1491
1558
  * put under the form's standard error reveal (first
@@ -1495,6 +1562,13 @@ type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
1495
1562
  * (`@click="wizard.tryNext()"`); resolves to whether
1496
1563
  * the pin moved. No-ops to `false` on a degenerate or
1497
1564
  * final-step wizard (finish via `handleSubmit`).
1565
+ * Called while the active step's form already has a
1566
+ * submit in flight (the usual case being from inside
1567
+ * that form's own callback), it rides that submit
1568
+ * rather than starting a second one, so one action
1569
+ * costs one submission. The pin has not moved by the
1570
+ * time it resolves, so the answer is `false`; the
1571
+ * advance lands with the in-flight submit.
1498
1572
  * - `handleSubmit(onSubmit, onError?)` — always validates the entire
1499
1573
  * step list, from any step, and never advances the
1500
1574
  * pin: on success it latches `done`; on any error it
@@ -1649,8 +1723,6 @@ type AttaformRegistry = {
1649
1723
  readonly pendingHydration: PendingHydration;
1650
1724
  /** `true` while running on the server during SSR; `false` on the client. */
1651
1725
  readonly ssr: boolean;
1652
- /** App-level defaults applied to every `useForm` call. */
1653
- readonly defaults: AttaformDefaults;
1654
1726
  /**
1655
1727
  * Track a consumer of `key`. Returns a dispose function — call it
1656
1728
  * when the consumer unmounts. The form is evicted automatically
@@ -1706,13 +1778,7 @@ declare module 'vue' {
1706
1778
  }
1707
1779
  }
1708
1780
  /** Options for `createRegistry`. */
1709
- type CreateRegistryOptions = SSRDetectOptions & {
1710
- /**
1711
- * App-level defaults applied to every `useForm` call. Per-form
1712
- * options always win. Omitted is equivalent to `{}`.
1713
- */
1714
- defaults?: AttaformDefaults;
1715
- };
1781
+ type CreateRegistryOptions = SSRDetectOptions;
1716
1782
  /**
1717
1783
  * Create a fresh `AttaformRegistry`. `createAttaform()` calls
1718
1784
  * this internally — most consumers never need to call it directly.
@@ -2018,34 +2084,20 @@ type AttaformPluginOptions = SSRDetectOptions & {
2018
2084
  * fails silently. Pass `false` to skip the integration even in dev.
2019
2085
  */
2020
2086
  devtools?: boolean;
2021
- /**
2022
- * App-level defaults applied to every `useForm` call in this app.
2023
- * Per-form options always win. See `AttaformDefaults` for
2024
- * the supported option set and the merge rules.
2025
- *
2026
- * ```ts
2027
- * app.use(
2028
- * createAttaform({
2029
- * defaults: { debounceMs: 100 },
2030
- * })
2031
- * )
2032
- * ```
2033
- */
2034
- defaults?: AttaformDefaults;
2035
2087
  };
2036
2088
  /**
2037
2089
  * Create the Vue plugin that installs the form library on a Vue
2038
2090
  * application. Required only when you want app-wide options
2039
- * (`defaults`, `devtools: false`, `ssr: true`) — for the default
2040
- * setup, `useForm` / `injectForm` / `useRegister` lazy-install the
2041
- * registry on first use.
2091
+ * (`devtools: false`, `ssr: true`) — for the default setup,
2092
+ * `useForm` / `injectForm` / `useRegister` lazy-install the registry
2093
+ * on first use.
2042
2094
  *
2043
2095
  * ```ts
2044
2096
  * import { createApp } from 'vue'
2045
2097
  * import { createAttaform } from 'attaform'
2046
2098
  *
2047
2099
  * createApp(App)
2048
- * .use(createAttaform({ defaults: { debounceMs: 100 } }))
2100
+ * .use(createAttaform({ devtools: false }))
2049
2101
  * .mount('#app')
2050
2102
  * ```
2051
2103
  *
@@ -2310,75 +2362,5 @@ declare class ReservedFormKeyError extends AttaformError {
2310
2362
  constructor(key: string);
2311
2363
  }
2312
2364
 
2313
- /**
2314
- * Anti-flash timing for the library-default display reducer.
2315
- *
2316
- * - `showDelay` — how long in-flight work (a validation run or an async
2317
- * register transform) may run before its spinner is allowed to show. Work
2318
- * that settles inside this window never reveals `'pending'` at all, so a
2319
- * fast (often synchronous) check does not flash a spinner on every keystroke.
2320
- * - `minVisible` — once shown, the minimum time the spinner stays up. Work
2321
- * that lands just past `showDelay` is held here so the spinner does not
2322
- * itself flash on and immediately off.
2323
- *
2324
- * Both are milliseconds.
2325
- */
2326
- type DisplayTimings = {
2327
- readonly showDelay: number;
2328
- readonly minVisible: number;
2329
- };
2330
- /**
2331
- * Library-default anti-flash timings. `showDelay: 120` cleanly swallows
2332
- * synchronous, microtask-resolved, and tiny-async work (validation runs and
2333
- * async register transforms alike) so none of them flash a spinner;
2334
- * `minVisible: 120` keeps a shown spinner snappy once one does land.
2335
- * Retune via {@link makeDefaultDisplayState} without touching the engine.
2336
- */
2337
- declare const DEFAULT_TIMINGS: DisplayTimings;
2338
- /**
2339
- * Build a default `getDisplayState` reducer with custom anti-flash timing.
2340
- * Power users who want a tighter or looser spinner than {@link DEFAULT_TIMINGS}
2341
- * pass their own `{ showDelay, minVisible }`:
2342
- *
2343
- * ```ts
2344
- * import { makeDefaultDisplayState } from 'attaform'
2345
- *
2346
- * useForm({
2347
- * schema,
2348
- * getDisplayState: makeDefaultDisplayState({ showDelay: 50, minVisible: 200 }),
2349
- * })
2350
- * ```
2351
- *
2352
- * The returned reducer is pure: the engine injects `now` and threads the
2353
- * previous machine, so the same `(prev, ctx)` always yields the same next
2354
- * machine. It shapes only the display projection — `errors`, `valid`,
2355
- * `validating`, and the underlying validation run exactly as before.
2356
- */
2357
- declare function makeDefaultDisplayState({ showDelay, minVisible, }: DisplayTimings): GetDisplayState;
2358
- /**
2359
- * Library-default `getDisplayState` reducer. Resolves every path's
2360
- * `field.displayState` — and thus `field.show*` and the `form.meta`
2361
- * rollups — whenever the consumer has not configured an override at the
2362
- * per-form or plugin level. Built from {@link DEFAULT_TIMINGS}; publicly
2363
- * re-exported so an override can compose with it (a layered reducer that
2364
- * special-cases a subtree but otherwise defers picks up future
2365
- * refinements for free):
2366
- *
2367
- * ```ts
2368
- * import { defaultDisplayState } from 'attaform'
2369
- *
2370
- * useForm({
2371
- * schema,
2372
- * getDisplayState: (prev, ctx) => {
2373
- * const next = defaultDisplayState(prev, ctx)
2374
- * return next.display === 'success' && ctx.field.path[0] === 'username'
2375
- * ? { display: 'idle' }
2376
- * : next
2377
- * },
2378
- * })
2379
- * ```
2380
- */
2381
- declare const defaultDisplayState: GetDisplayState;
2382
-
2383
- export { renderAttaformState as $, createAttaform as B, DEFAULT_TIMINGS as D, createRegistry as E, defaultCoercionRules as H, defaultDisplayState as J, defineCoercion as K, escapeForInlineScript as M, gate as N, OutsideSetupError as O, getRegistryFromApp as P, hydrateAttaformState as Q, RegistryNotInstalledError as R, injectForm as T, injectWizard as V, isRegisterValue as X, kAttaformRegistry as Y, lazy as Z, makeDefaultDisplayState as _, useRegister as a0, useRegistry as a1, useWizard as a2, AttaformError as c, AttaformErrorCode as d, DEVTOOLS_WINDOW_KEY as f, InvalidPathError as i, InvalidUseFormConfigError as j, ReservedFormKeyError as k, SubmitErrorHandlerError as n, assignKey as z };
2384
- export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, GateMarker as G, InjectWizardInput as I, LazyMarker as L, SerializedAttaformState as S, UseRegisterReturn 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, UseWizardReturnType as o, WizardCtx as p, WizardCtxForm as q, WizardOnError as r, WizardOnSubmit as s, WizardOptions as t, WizardPersistFn as u, WizardRestoreFn as v, WizardRestoreState as w, WizardStatusesProxy as x, WizardSubmitContext as y };
2365
+ export { createRegistry as B, DEVTOOLS_WINDOW_KEY as D, escapeForInlineScript as E, gate as H, getRegistryFromApp as J, hydrateAttaformState as K, injectForm as M, injectWizard as N, OutsideSetupError as O, isRegisterValue as P, kAttaformRegistry as Q, RegistryNotInstalledError as R, lazy as T, renderAttaformState as V, useRegister as X, useRegistry as Y, useWizard as Z, AttaformError as c, AttaformErrorCode as d, InvalidPathError as g, InvalidUseFormConfigError as h, ReservedFormKeyError as i, SubmitErrorHandlerError as l, assignKey as y, createAttaform as z };
2366
+ export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, GateMarker as G, InjectWizardInput as I, LazyMarker as L, SerializedAttaformState as S, UseRegisterOptions as U, WizardAggregateError as W, AnyForm as a, AttaformDevtoolsBridge as b, AttaformPluginOptions as e, FormStatusSeed as f, SerializedFormData as j, StepSlot as k, UseRegisterReturn as m, UseWizardReturnType as n, WizardCtx as o, WizardCtxForm as p, WizardOnError as q, WizardOnSubmit as r, WizardOptions as s, WizardPersistFn as t, WizardRestoreFn as u, WizardRestoreState as v, WizardStatusesProxy as w, WizardSubmitContext as x };
@@ -1,4 +1,4 @@
1
- import { c as RegistryNotInstalledError, O as OutsideSetupError, g as pathKeyToDotted } from './attaform.wfY8XFvm.mjs';
1
+ import { c as RegistryNotInstalledError, O as OutsideSetupError, g as pathKeyToDotted } from './attaform.CdZIEf7_.mjs';
2
2
  import { shallowReactive, getCurrentInstance, inject } from 'vue';
3
3
 
4
4
  function detectSSR(options = {}) {
@@ -17,7 +17,6 @@ const kFormContext = /* @__PURE__ */ Symbol.for("attaform:form-context");
17
17
  const kFormInstanceId = /* @__PURE__ */ Symbol.for("attaform:form-instance-id");
18
18
  function createRegistry(options = {}) {
19
19
  const ssr = detectSSR(options);
20
- const defaults = Object.freeze({ ...options.defaults ?? {} });
21
20
  const forms = shallowReactive(/* @__PURE__ */ new Map());
22
21
  const wizards = shallowReactive(/* @__PURE__ */ new Map());
23
22
  const pendingHydration = shallowReactive(/* @__PURE__ */ new Map());
@@ -102,7 +101,6 @@ function createRegistry(options = {}) {
102
101
  wizards,
103
102
  pendingHydration,
104
103
  ssr,
105
- defaults,
106
104
  trackConsumer,
107
105
  trackWizardConsumer,
108
106
  enqueuePrefetch,