attaform 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +77 -36
  2. package/dist/chunks/devtools.cjs +10 -37
  3. package/dist/chunks/devtools.cjs.map +1 -1
  4. package/dist/chunks/devtools.mjs +10 -37
  5. package/dist/chunks/devtools.mjs.map +1 -1
  6. package/dist/chunks/indexeddb.cjs +4 -4
  7. package/dist/chunks/indexeddb.cjs.map +1 -1
  8. package/dist/chunks/indexeddb.mjs +1 -1
  9. package/dist/chunks/local-storage.cjs +2 -2
  10. package/dist/chunks/local-storage.cjs.map +1 -1
  11. package/dist/chunks/local-storage.mjs +1 -1
  12. package/dist/chunks/session-storage.cjs +2 -2
  13. package/dist/chunks/session-storage.cjs.map +1 -1
  14. package/dist/chunks/session-storage.mjs +1 -1
  15. package/dist/index.cjs +42 -37
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +159 -196
  18. package/dist/index.d.mts +159 -196
  19. package/dist/index.d.ts +159 -196
  20. package/dist/index.mjs +5 -7
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/nuxt.cjs +31 -40
  23. package/dist/nuxt.cjs.map +1 -1
  24. package/dist/nuxt.d.cts +8 -1
  25. package/dist/nuxt.d.mts +8 -1
  26. package/dist/nuxt.d.ts +8 -1
  27. package/dist/nuxt.mjs +32 -41
  28. package/dist/nuxt.mjs.map +1 -1
  29. package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
  30. package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
  31. package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
  32. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
  33. package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
  34. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
  35. package/dist/runtime/plugins/attaform.cjs +17 -6
  36. package/dist/runtime/plugins/attaform.cjs.map +1 -1
  37. package/dist/runtime/plugins/attaform.mjs +15 -4
  38. package/dist/runtime/plugins/attaform.mjs.map +1 -1
  39. package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
  40. package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
  41. package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
  42. package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
  43. package/dist/shared/attaform.Bgu9l6OG.d.cts +1651 -0
  44. package/dist/shared/attaform.BmDBu4ql.d.ts +1651 -0
  45. package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
  46. package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
  47. package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.BsMdl-35.d.cts} +754 -146
  48. package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.BsMdl-35.d.mts} +754 -146
  49. package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.BsMdl-35.d.ts} +754 -146
  50. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  51. package/dist/shared/attaform.C3x1hKJC.d.mts +53 -0
  52. package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.CWs1Z3p7.d.ts} +57 -23
  53. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  54. package/dist/shared/attaform.CjmJpfLH.d.ts +53 -0
  55. package/dist/shared/{attaform.CVCmBKZX.mjs → attaform.CtNUB9nf.mjs} +74 -72
  56. package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
  57. package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.D-hDvb98.d.cts} +57 -23
  58. package/dist/shared/attaform.DAH3kvav.d.mts +1651 -0
  59. package/dist/shared/{attaform.B5qiXQwN.cjs → attaform.DUHru0OF.cjs} +83 -81
  60. package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
  61. package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
  62. package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
  63. package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
  64. package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
  65. package/dist/shared/attaform.Dzi89x8N.d.cts +53 -0
  66. package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
  67. package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
  68. package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
  69. package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
  70. package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
  71. package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
  72. package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.bH7WvNad.d.mts} +57 -23
  73. package/dist/vite.cjs +270 -2
  74. package/dist/vite.cjs.map +1 -1
  75. package/dist/vite.mjs +266 -2
  76. package/dist/vite.mjs.map +1 -1
  77. package/dist/zod-v3.cjs +11 -8
  78. package/dist/zod-v3.cjs.map +1 -1
  79. package/dist/zod-v3.d.cts +36 -84
  80. package/dist/zod-v3.d.mts +36 -84
  81. package/dist/zod-v3.d.ts +36 -84
  82. package/dist/zod-v3.mjs +3 -3
  83. package/dist/zod-v4.cjs +11 -8
  84. package/dist/zod-v4.cjs.map +1 -1
  85. package/dist/zod-v4.d.cts +5 -5
  86. package/dist/zod-v4.d.mts +5 -5
  87. package/dist/zod-v4.d.ts +5 -5
  88. package/dist/zod-v4.mjs +3 -3
  89. package/dist/zod.cjs +13 -10
  90. package/dist/zod.cjs.map +1 -1
  91. package/dist/zod.d.cts +127 -15
  92. package/dist/zod.d.mts +127 -15
  93. package/dist/zod.d.ts +127 -15
  94. package/dist/zod.mjs +5 -5
  95. package/dist/zod.mjs.map +1 -1
  96. package/package.json +19 -5
  97. package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
  98. package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
  99. package/dist/shared/attaform.B5qiXQwN.cjs.map +0 -1
  100. package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
  101. package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
  102. package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
  103. package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
  104. package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
  105. package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
  106. package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
  107. package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
  108. package/dist/shared/attaform.CVCmBKZX.mjs.map +0 -1
  109. package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
  110. package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
  111. package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
  112. package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
  113. package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
  114. package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
  115. package/dist/shared/attaform.h1sq3BFu.mjs.map +0 -1
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const vue = require('vue');
4
- const plugin = require('./attaform.Dee2rU1P.cjs');
5
- const paths = require('./attaform.BBM2muQ9.cjs');
4
+ const paths = require('./attaform.BqK_L4gK.cjs');
6
5
 
7
6
  const NOT_FOUND = Symbol("NOT_FOUND");
8
7
  function descendStep(value, segment) {
@@ -123,7 +122,12 @@ function mergeStructural(schema, path, consumer, defaultValue = schema.getDefaul
123
122
  return mergeStructuralImpl(schema, scratch, consumer, defaultValue);
124
123
  }
125
124
  function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
126
- if (consumer === void 0) return defaultValue;
125
+ if (consumer === void 0) {
126
+ if (schema.getSlimPrimitiveTypesAtPath?.(scratch).has("undefined") === true) {
127
+ return void 0;
128
+ }
129
+ return defaultValue;
130
+ }
127
131
  if (consumer === null) return null;
128
132
  if (Array.isArray(consumer)) {
129
133
  const shape = resolveArrayShape(schema, scratch);
@@ -163,7 +167,7 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
163
167
  let mutated = false;
164
168
  const out = { ...consumer };
165
169
  for (const key of Object.keys(defaultValue)) {
166
- if (!(key in consumer) || consumer[key] === void 0) {
170
+ if (!(key in consumer)) {
167
171
  const defAtKey = defaultValue[key];
168
172
  scratch.push(key);
169
173
  const filled = mergeStructuralImpl(schema, scratch, void 0, defAtKey);
@@ -175,10 +179,12 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
175
179
  }
176
180
  }
177
181
  for (const key of Object.keys(consumer)) {
182
+ const cVal = consumer[key];
183
+ if (cVal === void 0) continue;
178
184
  scratch.push(key);
179
- const merged = mergeStructuralImpl(schema, scratch, consumer[key], defaultValue[key]);
185
+ const merged = mergeStructuralImpl(schema, scratch, cVal, defaultValue[key]);
180
186
  scratch.pop();
181
- if (merged !== consumer[key]) {
187
+ if (merged !== cVal) {
182
188
  out[key] = merged;
183
189
  mutated = true;
184
190
  }
@@ -506,7 +512,7 @@ function buildLeafFieldStateBase(state, segments, key) {
506
512
  dirty: !pristine,
507
513
  focused: record?.focused ?? null,
508
514
  blurred: record?.blurred ?? null,
509
- touched: record?.touched ?? null,
515
+ touched: record?.touched ?? false,
510
516
  connected: record?.connected ?? false,
511
517
  element: firstElement,
512
518
  elements: elementsArr,
@@ -648,10 +654,12 @@ function buildSurfaceProxy(opts) {
648
654
  if (opts.leafKeys !== void 0) return leafViewProxyAt(segs);
649
655
  return opts.resolveLeaf(segs);
650
656
  }
657
+ if (opts.isTerminalAt?.(segs) === true) return opts.resolveLeaf(segs);
651
658
  return containerProxyAt(segs);
652
659
  }
653
660
  function containerProxyAt(segments) {
654
- const cacheKey = JSON.stringify(segments);
661
+ const isArrayLike = opts.isArrayContainer?.(segments) === true;
662
+ const cacheKey = `${JSON.stringify(segments)}+${isArrayLike ? "A" : "O"}`;
655
663
  const existing = containerCache.get(cacheKey);
656
664
  if (existing !== void 0) return existing;
657
665
  const snapshotContainer = () => opts.materializeContainer === void 0 ? {} : opts.materializeContainer(segments);
@@ -661,7 +669,7 @@ function buildSurfaceProxy(opts) {
661
669
  return this;
662
670
  }
663
671
  const containerToPrimitive = (hint) => hint === "number" ? NaN : containerToString();
664
- const target = (() => {
672
+ const target = isArrayLike ? [] : (() => {
665
673
  });
666
674
  const proxy = new Proxy(target, {
667
675
  apply(_, __, args) {
@@ -676,7 +684,14 @@ function buildSurfaceProxy(opts) {
676
684
  return Reflect.get(target, key);
677
685
  }
678
686
  if (typeof key !== "string") return void 0;
687
+ if (key === "__v_skip") return true;
688
+ if (key === "__v_isReactive" || key === "__v_isReadonly" || key === "__v_isShallow" || key === "__v_isRef" || key === "__v_raw") {
689
+ return void 0;
690
+ }
679
691
  if (key === "toJSON") return containerToJSON;
692
+ if (key === "length" && (isArrayLike || opts.isArrayContainer?.(segments) === true)) {
693
+ return opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
694
+ }
680
695
  const childSegs = [...segments, keyToSegment(key)];
681
696
  if (key === "toString" || key === "valueOf") {
682
697
  if (!schemaHasPath(childSegs)) {
@@ -689,11 +704,38 @@ function buildSurfaceProxy(opts) {
689
704
  if (typeof key === "symbol") return Reflect.has(target, key);
690
705
  return true;
691
706
  },
692
- // Containers are descend-only `JSON.stringify(form.fields.address)`
693
- // returns `{}` (no leaf keys to enumerate). Consumers who want
694
- // structural data use `form.values.<container>` instead.
695
- ownKeys: () => [],
696
- getOwnPropertyDescriptor: () => void 0,
707
+ // Live enumeration. When the surface provides `containerOwnKeys`,
708
+ // `Object.keys(proxy)` / `Object.entries(proxy)` /
709
+ // `v-for="(child, key) in proxy"` reflect whichever keys the
710
+ // underlying form data currently holds at this path: array
711
+ // indices (`'0'`, `'1'`, …) when the live value is an array,
712
+ // object keys when it's a record. `getOwnPropertyDescriptor`
713
+ // descends to the per-key sub-proxy so iteration yields the
714
+ // same objects dot-access would. Without `containerOwnKeys`,
715
+ // the container stays non-enumerable for callers that don't
716
+ // need iteration — `JSON.stringify` still serialises through
717
+ // the `toJSON` trap above either way.
718
+ ownKeys: () => {
719
+ const liveKeys = opts.containerOwnKeys === void 0 ? [] : opts.containerOwnKeys(segments);
720
+ if (isArrayLike) return ["length", ...liveKeys];
721
+ return [...liveKeys];
722
+ },
723
+ getOwnPropertyDescriptor(_, key) {
724
+ if (typeof key !== "string") return void 0;
725
+ if (isArrayLike && key === "length") {
726
+ const length = opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
727
+ return { configurable: false, enumerable: false, value: length, writable: true };
728
+ }
729
+ if (opts.containerOwnKeys === void 0) return void 0;
730
+ const liveKeys = opts.containerOwnKeys(segments);
731
+ if (!liveKeys.includes(key)) return void 0;
732
+ return {
733
+ configurable: true,
734
+ enumerable: true,
735
+ value: descendOrTerminate([...segments, keyToSegment(key)]),
736
+ writable: false
737
+ };
738
+ },
697
739
  // Block writes at the proxy boundary. Mutations go through
698
740
  // `setValue`, the directive, or the field-array helpers.
699
741
  set: () => false,
@@ -784,22 +826,47 @@ function buildErrorsProxy(state) {
784
826
  return buildSurfaceProxy({
785
827
  schema: state.schema,
786
828
  resolveLeaf: (path) => {
787
- const { key } = paths.canonicalizePath(path);
788
- const userForKey = state.userErrors.get(key);
789
- const isActive = hasAtPath(state.form.value, path);
829
+ const isContainerSelfAccess = path.length > 1 && path[path.length - 1] === "";
830
+ const collectAtKey = (key2, active2, into) => {
831
+ if (active2) {
832
+ const s = state.schemaErrors.get(key2);
833
+ const b = state.derivedBlankErrors.value.get(key2);
834
+ if (s !== void 0) into.push(...s);
835
+ if (b !== void 0) into.push(...b);
836
+ }
837
+ const u = state.userErrors.get(key2);
838
+ if (u !== void 0) into.push(...u);
839
+ };
790
840
  const merged = [];
791
- if (isActive) {
792
- const schemaForKey = state.schemaErrors.get(key);
793
- const blankForKey = state.derivedBlankErrors.value.get(key);
794
- if (schemaForKey !== void 0) merged.push(...schemaForKey);
795
- if (blankForKey !== void 0) merged.push(...blankForKey);
841
+ if (isContainerSelfAccess) {
842
+ const containerPath = path.slice(0, -1);
843
+ const containerKey = paths.canonicalizePath(containerPath).key;
844
+ const literalKey = paths.canonicalizePath(path).key;
845
+ const active2 = hasAtPath(state.form.value, containerPath);
846
+ collectAtKey(containerKey, active2, merged);
847
+ if (literalKey !== containerKey) collectAtKey(literalKey, active2, merged);
848
+ return merged;
796
849
  }
797
- if (userForKey !== void 0) merged.push(...userForKey);
798
- return merged.length === 0 ? void 0 : merged;
850
+ const { key } = paths.canonicalizePath(path);
851
+ const isFormLevel = key === paths.FORM_ERRORS_PATH_KEY;
852
+ const active = isFormLevel || hasAtPath(state.form.value, path);
853
+ collectAtKey(key, active, merged);
854
+ return merged;
799
855
  },
800
856
  // No leafKeys — at a leaf, the resolved value (the merged array or
801
857
  // undefined) IS the terminal.
802
858
  materializeContainer: (segments) => materializeErrors(state, segments),
859
+ // Any path ending in `''` is a meaningful terminal at the proxy
860
+ // layer: at root it's the form-level bucket; at depth >= 1 it's
861
+ // the container-self sentinel that surfaces cross-field refines
862
+ // and container-targeted marks. `resolveLeaf` translates `[...,
863
+ // '']` lookups to the parent container path before querying the
864
+ // stores. When a schema legitimately owns a `''` field, the
865
+ // literal leaf and any container-self errors share the slot
866
+ // (errors concatenate) — vanishingly rare, accepted as the
867
+ // ergonomic cost of one unified sentinel convention at every
868
+ // depth.
869
+ isTerminalAt: (segs) => segs.length >= 1 && segs[segs.length - 1] === "",
803
870
  // Call-form aggregates: `form.errors(path)` returns a single
804
871
  // `ValidationError[]` for any depth (leaf or container) — same
805
872
  // shared `aggregateErrorsAt` helper that `form.meta.errors` and
@@ -809,12 +876,31 @@ function buildErrorsProxy(state) {
809
876
  // when valid) so consumer code that branches on truthiness keeps
810
877
  // working — the call-form just extends that semantic to
811
878
  // containers and dynamic paths.
812
- resolveCallTarget: (path) => {
813
- const errs = aggregateErrorsAt(state, path);
814
- return errs.length === 0 ? void 0 : errs;
815
- }
879
+ resolveCallTarget: (path) => aggregateErrorsAt(state, path),
880
+ // Mirror `form.fields` enumeration: `Object.keys(form.errors.items)`
881
+ // and `v-for="(errs, idx) in form.errors.items"` walk the live
882
+ // array indices / object keys at the path. Iteration yields the
883
+ // descended sub-proxies (one per live key), so consumers can
884
+ // `form.errors.items[idx]` straight from the entry.
885
+ containerOwnKeys: (segments) => liveKeysAtPath$1(state, segments),
886
+ isArrayContainer: (segments) => isArrayPath$1(state, segments)
816
887
  });
817
888
  }
889
+ function liveKeysAtPath$1(state, segments) {
890
+ const value = getAtPath(state.form.value, segments);
891
+ if (value === null || value === void 0) return [];
892
+ if (Array.isArray(value)) {
893
+ const keys = new Array(value.length);
894
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
895
+ return keys;
896
+ }
897
+ if (typeof value === "object") return Object.keys(value);
898
+ return [];
899
+ }
900
+ function isArrayPath$1(state, segments) {
901
+ if (segments.length === 0) return false;
902
+ return Array.isArray(getAtPath(state.form.value, segments));
903
+ }
818
904
  function materializeErrors(state, containerSegments) {
819
905
  const liveContainer = getAtPath(state.form.value, containerSegments);
820
906
  const tree = Array.isArray(liveContainer) ? [] : {};
@@ -823,12 +909,33 @@ function materializeErrors(state, containerSegments) {
823
909
  if (errors.length === 0) continue;
824
910
  const fullPath = paths.segmentsForPathKey(pathKey);
825
911
  if (fullPath === null) continue;
826
- if (fullPath.length <= containerSegments.length) continue;
827
- for (let i = 0; i < containerSegments.length; i++) {
828
- if (fullPath[i] !== containerSegments[i]) continue entries;
912
+ const isSyntheticFormLevel = fullPath.length === 1 && fullPath[0] === "";
913
+ if (!isSyntheticFormLevel) {
914
+ if (fullPath.length < containerSegments.length) continue;
915
+ for (let i = 0; i < containerSegments.length; i++) {
916
+ if (fullPath[i] !== containerSegments[i]) continue entries;
917
+ }
918
+ } else if (containerSegments.length !== 0) {
919
+ continue;
920
+ }
921
+ if (applyActivePathFilter && !isSyntheticFormLevel && !hasAtPath(state.form.value, fullPath))
922
+ continue;
923
+ let placePath;
924
+ if (isSyntheticFormLevel) {
925
+ placePath = [""];
926
+ } else {
927
+ const relativePath = fullPath.slice(containerSegments.length);
928
+ if (relativePath.length === 0) {
929
+ placePath = [""];
930
+ } else if (state.schema.isLeafAtPath(fullPath)) {
931
+ placePath = relativePath;
932
+ } else if (state.schema.getSlimPrimitiveTypesAtPath(fullPath).size > 0) {
933
+ placePath = [...relativePath, ""];
934
+ } else {
935
+ placePath = relativePath;
936
+ }
829
937
  }
830
- if (applyActivePathFilter && !hasAtPath(state.form.value, fullPath)) continue;
831
- placeAt(tree, fullPath.slice(containerSegments.length), errors);
938
+ placeAt(tree, placePath, errors);
832
939
  }
833
940
  };
834
941
  collect(state.schemaErrors, true);
@@ -1016,9 +1123,26 @@ function buildFieldStateProxy(state, getFormMetaBase, options) {
1016
1123
  leafKeys: FIELD_STATE_KEYS,
1017
1124
  readLeafKey: (computed, key) => computed.value[key],
1018
1125
  materializeContainer: (segments) => materializeFields(state, segments, snapshotFieldStateAt),
1019
- resolveCallTarget: (path) => fieldStateTerminalAt(path)
1126
+ resolveCallTarget: (path) => fieldStateTerminalAt(path),
1127
+ containerOwnKeys: (segments) => liveKeysAtPath(state, segments),
1128
+ isArrayContainer: (segments) => isArrayPath(state, segments)
1020
1129
  });
1021
1130
  }
1131
+ function liveKeysAtPath(state, segments) {
1132
+ const value = getAtPath(state.form.value, segments);
1133
+ if (value === null || value === void 0) return [];
1134
+ if (Array.isArray(value)) {
1135
+ const keys = new Array(value.length);
1136
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
1137
+ return keys;
1138
+ }
1139
+ if (typeof value === "object") return Object.keys(value);
1140
+ return [];
1141
+ }
1142
+ function isArrayPath(state, segments) {
1143
+ if (segments.length === 0) return false;
1144
+ return Array.isArray(getAtPath(state.form.value, segments));
1145
+ }
1022
1146
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
1023
1147
  const liveValue = getAtPath(state.form.value, containerSegments);
1024
1148
  return walk$2(liveValue, containerSegments, state.schema, snapshotFieldStateAt);
@@ -1050,12 +1174,13 @@ const DEFAULT_HISTORY_MAX_SNAPSHOTS = 128;
1050
1174
  const PERSISTENCE_KEY_PREFIX = "attaform:";
1051
1175
  const RESERVED_KEY_PREFIX = "__atta:";
1052
1176
  const ANONYMOUS_FORM_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon:`;
1177
+ const ANONYMOUS_WIZARD_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon-wizard:`;
1053
1178
  const DEFAULT_MAX_RECURSION_DEPTH = 64;
1054
1179
  function normalizeNumericOption(config) {
1055
1180
  const { value, source, allowInfinity, min, defaultValue } = config;
1056
1181
  if (allowInfinity && value === Infinity) return Infinity;
1057
1182
  if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
1058
- if (plugin.__DEV__) {
1183
+ if (paths.__DEV__) {
1059
1184
  const acceptedDescription = allowInfinity ? "a non-negative integer or Infinity" : "a non-negative finite integer";
1060
1185
  console.warn(
1061
1186
  `[attaform] ${source} must be ${acceptedDescription}; got ${String(value)}. Falling back to ${String(defaultValue)}.`
@@ -1084,7 +1209,7 @@ async function getStorageAdapter(storage) {
1084
1209
  }
1085
1210
  }
1086
1211
  }
1087
- const PERSISTED_ENVELOPE_VERSION = 4;
1212
+ const PERSISTED_ENVELOPE_VERSION = 5;
1088
1213
  function readPersistedPayload(value) {
1089
1214
  if (value === null || value === void 0 || typeof value !== "object") return null;
1090
1215
  const envelope = value;
@@ -1096,7 +1221,7 @@ function readPersistedPayload(value) {
1096
1221
  if (envelope.data === void 0 || typeof envelope.data !== "object") return null;
1097
1222
  return envelope;
1098
1223
  }
1099
- const warnedVersions = plugin.__DEV__ ? /* @__PURE__ */ new Set() : null;
1224
+ const warnedVersions = paths.__DEV__ ? /* @__PURE__ */ new Set() : null;
1100
1225
  function warnVersionMismatch(observedVersion) {
1101
1226
  if (warnedVersions === null) return;
1102
1227
  if (warnedVersions.has(observedVersion)) return;
@@ -1106,7 +1231,15 @@ function warnVersionMismatch(observedVersion) {
1106
1231
  );
1107
1232
  }
1108
1233
  function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPaths) {
1109
- const transientList = blankPaths !== void 0 && blankPaths.size > 0 ? [...blankPaths] : void 0;
1234
+ let transientList;
1235
+ if (blankPaths !== void 0 && blankPaths.size > 0) {
1236
+ const dotted = [];
1237
+ for (const key of blankPaths) {
1238
+ const d = paths.pathKeyToDotted(key);
1239
+ if (d !== null) dotted.push(d);
1240
+ }
1241
+ transientList = dotted.length > 0 ? dotted : void 0;
1242
+ }
1110
1243
  if (include === "form") {
1111
1244
  if (transientList === void 0) return { v: PERSISTED_ENVELOPE_VERSION, data: { form } };
1112
1245
  return {
@@ -1274,13 +1407,40 @@ const AttaformErrorCode = {
1274
1407
  NoValueSupplied: "atta:no-value-supplied",
1275
1408
  /** The schema adapter's `validateAtPath` threw synchronously. */
1276
1409
  AdapterThrew: "atta:adapter-threw",
1410
+ /**
1411
+ * User code inside a `z.preprocess`, `.refine`, or `.transform`
1412
+ * threw (sync or async). The adapter caught the throw and surfaced
1413
+ * it as a `ValidationError` at the field path so the form's normal
1414
+ * error pipeline handles it instead of leaking as an unhandled
1415
+ * rejection or routing through `submitError`.
1416
+ */
1417
+ ValidatorThrew: "atta:validator-threw",
1418
+ /**
1419
+ * A function-form `defaultValues` factory threw or its promise
1420
+ * rejected. The runtime captures the raw error on `form.hydrateError`
1421
+ * and ALSO surfaces a form-level `ValidationError` (path `[]`) so
1422
+ * the standard error pipeline carries the signal. Critical for the
1423
+ * SSR round-trip: `hydrateError` itself does not ride the wire
1424
+ * payload, but `schemaErrors` does, so the client sees the failure
1425
+ * after rehydration without an extra channel.
1426
+ */
1427
+ HydrationFailed: "atta:hydration-failed",
1277
1428
  /** The supplied path didn't resolve to any node in the schema. */
1278
- PathNotFound: "atta:path-not-found"
1429
+ PathNotFound: "atta:path-not-found",
1430
+ /**
1431
+ * A walked form's `activate()` (async `defaultValues` factory) threw
1432
+ * during `wizard.handleSubmit`'s path walk. Surfaced as a synthetic
1433
+ * `ValidationError` at the form-level path (`[]`) so the wizard's
1434
+ * aggregate error pipeline can carry the failure alongside ordinary
1435
+ * validation errors. The raw factory error remains on
1436
+ * `form.hydrateError` for retry UX.
1437
+ */
1438
+ ActivationFailed: "atta:activation-failed"
1279
1439
  };
1280
1440
 
1281
- const warnedNoScopeStores = plugin.__DEV__ ? /* @__PURE__ */ new WeakSet() : null;
1441
+ const warnedNoScopeStores = paths.__DEV__ ? /* @__PURE__ */ new WeakSet() : null;
1282
1442
  function buildProcessForm(state, formInstanceId, options = {}) {
1283
- const invalidPolicy = options.onInvalidSubmit ?? "none";
1443
+ const invalidPolicy = options.onInvalidSubmit ?? "focus-first-error";
1284
1444
  function validate(pathInput) {
1285
1445
  const result = vue.ref({
1286
1446
  pending: true,
@@ -1330,7 +1490,7 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1330
1490
  });
1331
1491
  if (vue.getCurrentScope() !== void 0) {
1332
1492
  vue.onScopeDispose(stop);
1333
- } else if (plugin.__DEV__ && warnedNoScopeStores !== null && !warnedNoScopeStores.has(state)) {
1493
+ } else if (paths.__DEV__ && warnedNoScopeStores !== null && !warnedNoScopeStores.has(state)) {
1334
1494
  warnedNoScopeStores.add(state);
1335
1495
  console.warn(
1336
1496
  "[attaform] validate() called outside a Vue effect scope; its reactive watcher will leak until the form is garbage-collected. Fix: call validate() inside setup() / a child component, or wrap the call in `effectScope().run(...)`."
@@ -1343,7 +1503,15 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1343
1503
  const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1344
1504
  try {
1345
1505
  state.activeValidations.value += 1;
1506
+ state.cancelFieldValidation();
1346
1507
  const refinement = await runRefinementValidation(dataAtPath, segments);
1508
+ const scopePath = segments ?? [];
1509
+ const errors = refinement.success ? [] : refinement.errors;
1510
+ const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1511
+ ...err,
1512
+ path: [...segments, ...err.path]
1513
+ }));
1514
+ state.applySchemaErrorsForSubtree(scopePath, reStamped);
1347
1515
  return stripData(composeWithDerivedBlank(refinement, segments));
1348
1516
  } catch (err) {
1349
1517
  return adapterThrowResponse(err);
@@ -1431,7 +1599,7 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1431
1599
  try {
1432
1600
  await onError(merged.errors);
1433
1601
  } catch (cause) {
1434
- throw new plugin.SubmitErrorHandlerError("User-provided onError threw", { cause });
1602
+ throw new paths.SubmitErrorHandlerError("User-provided onError threw", { cause });
1435
1603
  }
1436
1604
  }
1437
1605
  return;
@@ -1440,6 +1608,9 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1440
1608
  state.clearSchemaErrors();
1441
1609
  }
1442
1610
  await onSubmit(merged.data);
1611
+ if (state.submissionGeneration.value === genAtEntry) {
1612
+ state.submitted.value = true;
1613
+ }
1443
1614
  state.emitSubmitSuccess();
1444
1615
  } catch (err) {
1445
1616
  if (state.submissionGeneration.value === genAtEntry) {
@@ -1453,7 +1624,7 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1453
1624
  state.activeSubmissions.value = Math.max(0, state.activeSubmissions.value - 1);
1454
1625
  if (state.submissionGeneration.value === genAtEntry) {
1455
1626
  state.submitting.value = state.activeSubmissions.value > 0;
1456
- state.submitCount.value += 1;
1627
+ state.submissionAttempts.value += 1;
1457
1628
  }
1458
1629
  }
1459
1630
  };
@@ -1528,7 +1699,7 @@ function extractSchemaFields(schema) {
1528
1699
  return [];
1529
1700
  }
1530
1701
 
1531
- const warnedRejections = plugin.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
1702
+ const warnedRejections = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
1532
1703
  function shouldWarnOnce$1(store, key) {
1533
1704
  if (warnedRejections === null) return false;
1534
1705
  let set = warnedRejections.get(store);
@@ -1547,6 +1718,7 @@ function slimKindOf(value) {
1547
1718
  if (value instanceof Date) return "date";
1548
1719
  if (value instanceof Map) return "map";
1549
1720
  if (value instanceof Set) return "set";
1721
+ if (typeof File !== "undefined" && value instanceof File) return "file";
1550
1722
  const t = typeof value;
1551
1723
  switch (t) {
1552
1724
  case "string":
@@ -1577,6 +1749,7 @@ function isSlimPrimitiveValid(schema, store, path, value) {
1577
1749
  return walk$1(schema, store, path, value);
1578
1750
  }
1579
1751
  function walk$1(schema, store, path, value) {
1752
+ if (schema.isPreprocessOrCoerceLeaf(path)) return true;
1580
1753
  const accepted = schema.getSlimPrimitiveTypesAtPath(path);
1581
1754
  const kind = isLeafValue(value) ? slimKindOf(value) : Array.isArray(value) ? "array" : "object";
1582
1755
  if (!accepted.has(kind)) {
@@ -1600,7 +1773,7 @@ function walk$1(schema, store, path, value) {
1600
1773
  return true;
1601
1774
  }
1602
1775
  function reportRejection(store, path, kind, accepted) {
1603
- if (!plugin.__DEV__) return;
1776
+ if (!paths.__DEV__) return;
1604
1777
  const dotted = path.map((s) => String(s)).join(".") || "(root)";
1605
1778
  const key = `${dotted}::${kind}`;
1606
1779
  if (!shouldWarnOnce$1(store, key)) return;
@@ -1674,13 +1847,13 @@ function indexRules(rules) {
1674
1847
  for (const entry of rules) {
1675
1848
  const candidate = entry;
1676
1849
  if (candidate === null || typeof candidate !== "object" || typeof candidate.transform !== "function") {
1677
- if (plugin.__DEV__) {
1850
+ if (paths.__DEV__) {
1678
1851
  console.warn("[attaform] coercion entry missing or invalid `transform` \u2014 skipped.");
1679
1852
  }
1680
1853
  continue;
1681
1854
  }
1682
1855
  const key = `${entry.input}->${entry.output}`;
1683
- if (idx.has(key) && plugin.__DEV__) {
1856
+ if (idx.has(key) && paths.__DEV__) {
1684
1857
  console.warn(`[attaform] duplicate coercion rule for '${key}' \u2014 last entry wins.`);
1685
1858
  }
1686
1859
  idx.set(key, entry);
@@ -1709,7 +1882,7 @@ function pickScalarTarget(accepted) {
1709
1882
  if (accepted.has("bigint")) return "bigint";
1710
1883
  return null;
1711
1884
  }
1712
- const warnedCoerce = plugin.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
1885
+ const warnedCoerce = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
1713
1886
  const sharedWarnStore = {};
1714
1887
  function shouldWarnOnce(key) {
1715
1888
  if (warnedCoerce === null) return false;
@@ -1734,7 +1907,7 @@ function coerceScalar(value, accepted, index) {
1734
1907
  try {
1735
1908
  result = entry.transform(value);
1736
1909
  } catch (err) {
1737
- if (plugin.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::throw`)) {
1910
+ if (paths.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::throw`)) {
1738
1911
  console.warn(
1739
1912
  `[attaform] coercion '${entry.input}->${entry.output}' threw \u2014 write passes through.`,
1740
1913
  err
@@ -1745,7 +1918,7 @@ function coerceScalar(value, accepted, index) {
1745
1918
  if (!result.coerced) return value;
1746
1919
  const returnedKind = slimKindOf(result.value);
1747
1920
  if (returnedKind !== entry.output) {
1748
- if (plugin.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::wrong-kind:${returnedKind}`)) {
1921
+ if (paths.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::wrong-kind:${returnedKind}`)) {
1749
1922
  console.warn(
1750
1923
  `[attaform] coercion '${entry.input}->${entry.output}' produced a ${returnedKind} \u2014 write passes through.`
1751
1924
  );
@@ -1753,7 +1926,7 @@ function coerceScalar(value, accepted, index) {
1753
1926
  return value;
1754
1927
  }
1755
1928
  if (entry.output === "number" && !Number.isFinite(result.value)) {
1756
- if (plugin.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::nan`)) {
1929
+ if (paths.__DEV__ && shouldWarnOnce(`${entry.input}->${entry.output}::nan`)) {
1757
1930
  console.warn(
1758
1931
  `[attaform] coercion '${entry.input}->${entry.output}' produced a non-finite number \u2014 write passes through.`
1759
1932
  );
@@ -1806,6 +1979,11 @@ function attachFocusListeners(state, segments, element, instanceMeta) {
1806
1979
  element.addEventListener("focus", handleFocus);
1807
1980
  element.addEventListener("blur", handleBlur);
1808
1981
  target[attaformListenersSymbol] = { handleFocus, handleBlur };
1982
+ const rootNode = element.getRootNode();
1983
+ const activeElement = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : null;
1984
+ if (activeElement === element) {
1985
+ state.markFocused(segments, true, focusMeta);
1986
+ }
1809
1987
  }
1810
1988
  function detachFocusListeners(element) {
1811
1989
  const target = element;
@@ -1842,6 +2020,9 @@ function buildRegister(state, formInstanceId, instanceConfig) {
1842
2020
  return String(raw);
1843
2021
  });
1844
2022
  const slimDefault = state.schema.getDefaultAtPath(segments);
2023
+ const slimTypes = state.schema.getSlimPrimitiveTypesAtPath(segments);
2024
+ const acceptsUndefined = slimTypes.has("undefined");
2025
+ const acceptsString = slimTypes.has("string");
1845
2026
  const persist = options?.persist === true;
1846
2027
  const acknowledgeSensitive = options?.acknowledgeSensitive === true;
1847
2028
  const multiTab = options?.multiTab !== false;
@@ -1863,10 +2044,10 @@ function buildRegister(state, formInstanceId, instanceConfig) {
1863
2044
  coerceIndex
1864
2045
  );
1865
2046
  if (persist && !state.ssr && !state.modules.has(PERSISTENCE_MODULE_KEY)) {
1866
- throw new plugin.AnonPersistError({
2047
+ throw new paths.AnonPersistError({
1867
2048
  cause: "register-without-config",
1868
2049
  schemaFields: extractSchemaFields(state.schema),
1869
- callSite: plugin.captureUserCallSite()
2050
+ callSite: paths.captureUserCallSite()
1870
2051
  });
1871
2052
  }
1872
2053
  const internalRv = {
@@ -1925,7 +2106,9 @@ function buildRegister(state, formInstanceId, instanceConfig) {
1925
2106
  ...unmarkNoSync !== void 0 ? { unmarkNoSync } : {},
1926
2107
  transforms,
1927
2108
  coerce,
1928
- ...coerceElement !== void 0 ? { coerceElement } : {}
2109
+ ...coerceElement !== void 0 ? { coerceElement } : {},
2110
+ acceptsUndefined,
2111
+ acceptsString
1929
2112
  };
1930
2113
  return vue.shallowReadonly(internalRv);
1931
2114
  };
@@ -1946,19 +2129,13 @@ function walkUnsetSentinels(values, schema) {
1946
2129
  const cleaned = walk(values, [], schema, paths);
1947
2130
  return { cleanedValues: cleaned, paths };
1948
2131
  }
1949
- function walk(input, segments, schema, paths$1) {
2132
+ function walk(input, segments, schema, paths) {
1950
2133
  if (isUnset(input)) {
1951
- const slim = schema.getDefaultAtPath(segments);
1952
- if (!isPrimitiveOrEmpty(slim)) {
1953
- warnNonPrimitiveLeaf(segments, slim);
1954
- return walkUnspecified(slim, segments, paths$1);
1955
- }
1956
- paths$1.push(paths.canonicalizePath(segments).key);
1957
- return slim;
2134
+ return expandUnsetAt(segments, schema, paths);
1958
2135
  }
1959
2136
  if (input === void 0) {
1960
2137
  const slim = schema.getDefaultAtPath(segments);
1961
- return walkUnspecified(slim, segments, paths$1);
2138
+ return walkUnspecified(slim, segments, paths);
1962
2139
  }
1963
2140
  if (input === null) return null;
1964
2141
  if (input instanceof Date || input instanceof RegExp || input instanceof Map || input instanceof Set || typeof input === "function") {
@@ -1968,7 +2145,7 @@ function walk(input, segments, schema, paths$1) {
1968
2145
  const out = new Array(input.length);
1969
2146
  let mutated = false;
1970
2147
  for (let i = 0; i < input.length; i++) {
1971
- const walked = walk(input[i], [...segments, i], schema, paths$1);
2148
+ const walked = walk(input[i], [...segments, i], schema, paths);
1972
2149
  out[i] = walked;
1973
2150
  if (walked !== input[i]) mutated = true;
1974
2151
  }
@@ -1977,6 +2154,7 @@ function walk(input, segments, schema, paths$1) {
1977
2154
  if (typeof input === "object") {
1978
2155
  const slim = schema.getDefaultAtPath(segments);
1979
2156
  const inputKeys = Object.keys(input);
2157
+ const inputKeysSet = new Set(inputKeys);
1980
2158
  const allKeys = new Set(inputKeys);
1981
2159
  if (slim !== null && slim !== void 0 && typeof slim === "object" && !Array.isArray(slim) && !(slim instanceof Date) && !(slim instanceof RegExp) && !(slim instanceof Map) && !(slim instanceof Set)) {
1982
2160
  for (const k of Object.keys(slim)) allKeys.add(k);
@@ -1985,7 +2163,12 @@ function walk(input, segments, schema, paths$1) {
1985
2163
  let mutated = allKeys.size !== inputKeys.length;
1986
2164
  for (const key of allKeys) {
1987
2165
  const orig = input[key];
1988
- const walked = walk(orig, [...segments, key], schema, paths$1);
2166
+ if (orig === void 0 && inputKeysSet.has(key)) {
2167
+ out[key] = void 0;
2168
+ mutated = true;
2169
+ continue;
2170
+ }
2171
+ const walked = walk(orig, [...segments, key], schema, paths);
1989
2172
  out[key] = walked;
1990
2173
  if (walked !== orig) mutated = true;
1991
2174
  }
@@ -1995,7 +2178,7 @@ function walk(input, segments, schema, paths$1) {
1995
2178
  }
1996
2179
  function walkUnspecified(slim, segments, paths$1) {
1997
2180
  if (isPrimitiveOrEmpty(slim)) {
1998
- if (isNumericPrimitive(slim)) {
2181
+ if (isSlimNumericPrimitive(slim)) {
1999
2182
  paths$1.push(paths.canonicalizePath(segments).key);
2000
2183
  }
2001
2184
  return slim;
@@ -2018,15 +2201,9 @@ function substituteUnsetSentinels(value, prefix, schema) {
2018
2201
  const cleaned = substitute(value, [...prefix], schema, paths);
2019
2202
  return { cleanedValues: cleaned, paths };
2020
2203
  }
2021
- function substitute(input, segments, schema, paths$1) {
2204
+ function substitute(input, segments, schema, paths) {
2022
2205
  if (isUnset(input)) {
2023
- const slim = schema.getDefaultAtPath(segments);
2024
- if (!isPrimitiveOrEmpty(slim)) {
2025
- warnNonPrimitiveLeaf(segments, slim);
2026
- return slim;
2027
- }
2028
- paths$1.push(paths.canonicalizePath(segments).key);
2029
- return slim;
2206
+ return expandUnsetAt(segments, schema, paths);
2030
2207
  }
2031
2208
  if (input === void 0 || input === null) return input;
2032
2209
  if (input instanceof Date || input instanceof RegExp || input instanceof Map || input instanceof Set || typeof input === "function") {
@@ -2036,7 +2213,7 @@ function substitute(input, segments, schema, paths$1) {
2036
2213
  let mutated = false;
2037
2214
  const out = new Array(input.length);
2038
2215
  for (let i = 0; i < input.length; i++) {
2039
- const walked = substitute(input[i], [...segments, i], schema, paths$1);
2216
+ const walked = substitute(input[i], [...segments, i], schema, paths);
2040
2217
  out[i] = walked;
2041
2218
  if (walked !== input[i]) mutated = true;
2042
2219
  }
@@ -2047,7 +2224,7 @@ function substitute(input, segments, schema, paths$1) {
2047
2224
  const out = {};
2048
2225
  for (const key of Object.keys(input)) {
2049
2226
  const orig = input[key];
2050
- const walked = substitute(orig, [...segments, key], schema, paths$1);
2227
+ const walked = substitute(orig, [...segments, key], schema, paths);
2051
2228
  out[key] = walked;
2052
2229
  if (walked !== orig) mutated = true;
2053
2230
  }
@@ -2060,20 +2237,40 @@ function isPrimitiveOrEmpty(value) {
2060
2237
  const t = typeof value;
2061
2238
  return t === "string" || t === "number" || t === "boolean" || t === "bigint";
2062
2239
  }
2063
- function isNumericPrimitive(value) {
2064
- const t = typeof value;
2065
- return t === "number" || t === "bigint";
2066
- }
2067
- const warnedNonPrimitivePaths = plugin.__DEV__ ? /* @__PURE__ */ new Set() : null;
2068
- function warnNonPrimitiveLeaf(segments, slim) {
2069
- if (warnedNonPrimitivePaths === null) return;
2070
- const dotted = segments.map(String).join(".");
2071
- if (warnedNonPrimitivePaths.has(dotted)) return;
2072
- warnedNonPrimitivePaths.add(dotted);
2073
- const slimType = slim === null ? "null" : slim instanceof Date ? "Date" : typeof slim;
2074
- console.warn(
2075
- `[attaform] \`unset\` at "${dotted || "<root>"}" is a no-op \u2014 unset only works at primitive leaves (string / number / boolean / bigint, plus their optional / nullable variants), got "${slimType}". The slim default was written but the path is NOT marked blank. (TypeScript catches this at compile time; this warn covers plain-JS callers.)`
2076
- );
2240
+ function isSlimNumericPrimitive(value) {
2241
+ return value === 0 || value === 0n;
2242
+ }
2243
+ function blankForKind(slimDefault) {
2244
+ if (typeof slimDefault === "string") return "";
2245
+ if (typeof slimDefault === "number") return 0;
2246
+ if (typeof slimDefault === "bigint") return 0n;
2247
+ if (typeof slimDefault === "boolean") return false;
2248
+ if (slimDefault === null) return null;
2249
+ return void 0;
2250
+ }
2251
+ function expandUnsetAt(segments, schema, paths$1) {
2252
+ const du = schema.getUnionDiscriminatorAtPath(segments);
2253
+ if (du !== void 0) {
2254
+ const discPath = [...segments, du.discriminatorKey];
2255
+ const discSlim = schema.getEmptyValueAtPath(discPath);
2256
+ paths$1.push(paths.canonicalizePath(discPath).key);
2257
+ return { [du.discriminatorKey]: blankForKind(discSlim) };
2258
+ }
2259
+ const slim = schema.getEmptyValueAtPath(segments);
2260
+ if (isPrimitiveOrEmpty(slim)) {
2261
+ paths$1.push(paths.canonicalizePath(segments).key);
2262
+ return slim;
2263
+ }
2264
+ if (slim instanceof Date || slim instanceof RegExp || slim instanceof Map || slim instanceof Set || typeof slim === "function") {
2265
+ paths$1.push(paths.canonicalizePath(segments).key);
2266
+ return slim;
2267
+ }
2268
+ if (Array.isArray(slim)) return slim;
2269
+ const result = {};
2270
+ for (const key of Object.keys(slim)) {
2271
+ result[key] = expandUnsetAt([...segments, key], schema, paths$1);
2272
+ }
2273
+ return result;
2077
2274
  }
2078
2275
 
2079
2276
  function buildValuesProxy(form) {
@@ -2124,7 +2321,7 @@ function buildValuesProxy(form) {
2124
2321
  // TypeError in strict-mode consumers, surprising users who
2125
2322
  // assigned through the proxy and expected it to be ignored.
2126
2323
  set(_, key) {
2127
- if (plugin.__DEV__) {
2324
+ if (paths.__DEV__) {
2128
2325
  console.warn(
2129
2326
  `[attaform] form.values is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
2130
2327
  );
@@ -2132,7 +2329,7 @@ function buildValuesProxy(form) {
2132
2329
  return true;
2133
2330
  },
2134
2331
  deleteProperty(_, key) {
2135
- if (plugin.__DEV__) {
2332
+ if (paths.__DEV__) {
2136
2333
  console.warn(
2137
2334
  `[attaform] form.values is read-only \u2014 delete of "${String(key)}" was ignored.`
2138
2335
  );
@@ -2143,28 +2340,6 @@ function buildValuesProxy(form) {
2143
2340
  });
2144
2341
  }
2145
2342
 
2146
- function blankForKind(slimDefault) {
2147
- if (typeof slimDefault === "string") return "";
2148
- if (typeof slimDefault === "number") return 0;
2149
- if (typeof slimDefault === "bigint") return 0n;
2150
- if (typeof slimDefault === "boolean") return false;
2151
- if (slimDefault === null) return null;
2152
- return void 0;
2153
- }
2154
- function readonlySetSnapshot(source) {
2155
- const snapshot = new Set(source);
2156
- return new Proxy(snapshot, {
2157
- get(target, prop) {
2158
- if (prop === "add" || prop === "delete" || prop === "clear") {
2159
- return () => {
2160
- throw new TypeError(`Cannot mutate readonly Set: '${String(prop)}' is not allowed.`);
2161
- };
2162
- }
2163
- const value = Reflect.get(target, prop, target);
2164
- return typeof value === "function" ? value.bind(target) : value;
2165
- }
2166
- });
2167
- }
2168
2343
  function buildFormApi(state, formInstanceId, options = {}) {
2169
2344
  const instanceMeta = (() => {
2170
2345
  const bag = {};
@@ -2187,6 +2362,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2187
2362
  Object.keys(registerConfig).length > 0 ? registerConfig : void 0
2188
2363
  );
2189
2364
  const processOptions = options.onInvalidSubmit !== void 0 ? { onInvalidSubmit: options.onInvalidSubmit } : {};
2365
+ const defaultInvalidSubmitPolicy = options.onInvalidSubmit ?? "focus-first-error";
2190
2366
  const {
2191
2367
  validate: validateBuilt,
2192
2368
  validateAsync: validateAsyncBuilt,
@@ -2207,49 +2383,53 @@ function buildFormApi(state, formInstanceId, options = {}) {
2207
2383
  next,
2208
2384
  state.schema
2209
2385
  );
2210
- const ok2 = state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2211
- if (!ok2) return false;
2212
2386
  for (const pathKey of walked2.paths) {
2213
- const segments2 = paths.segmentsForPathKey(pathKey);
2214
- if (segments2 === null) continue;
2215
- state.setValueAtPath(
2216
- segments2,
2217
- state.schema.getDefaultAtPath(segments2),
2218
- withInstanceMeta({ blank: true })
2219
- );
2387
+ state.blankPaths.add(pathKey);
2220
2388
  }
2221
- return true;
2389
+ return state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2222
2390
  }
2223
2391
  const segments = paths.canonicalizePath(pathOrValue).segments;
2224
- if (isUnset(maybeValue)) {
2392
+ const writeUnsetAt = () => {
2225
2393
  const last = segments.length > 0 ? segments[segments.length - 1] : void 0;
2226
2394
  if (typeof last === "string") {
2227
2395
  const parent = segments.slice(0, -1);
2228
2396
  const parentDU = state.schema.getUnionDiscriminatorAtPath(parent);
2229
2397
  if (parentDU?.discriminatorKey === last) {
2230
- const slimDefault = state.schema.getDefaultAtPath(segments);
2398
+ const slimDefault = state.schema.getEmptyValueAtPath(segments);
2231
2399
  const blank = blankForKind(slimDefault);
2232
2400
  return state.setValueAtPath(segments, blank, withInstanceMeta({ blank: true }));
2233
2401
  }
2234
2402
  }
2235
- return state.setValueAtPath(
2403
+ const blankPaths = [];
2404
+ const expanded = expandUnsetAt(
2236
2405
  segments,
2237
- state.schema.getDefaultAtPath(segments),
2238
- withInstanceMeta({ blank: true })
2406
+ state.schema,
2407
+ blankPaths
2239
2408
  );
2240
- }
2409
+ const segmentsKey = paths.canonicalizePath(segments).key;
2410
+ if (blankPaths.length === 1 && blankPaths[0] === segmentsKey) {
2411
+ return state.setValueAtPath(segments, expanded, withInstanceMeta({ blank: true }));
2412
+ }
2413
+ const ok2 = state.setValueAtPath(segments, expanded, withInstanceMeta());
2414
+ if (!ok2) return false;
2415
+ for (const pathKey of blankPaths) {
2416
+ const blankSegments = paths.segmentsForPathKey(pathKey);
2417
+ if (blankSegments === null) continue;
2418
+ state.setValueAtPath(
2419
+ blankSegments,
2420
+ state.getValueAtPath(blankSegments),
2421
+ withInstanceMeta({ blank: true })
2422
+ );
2423
+ }
2424
+ return true;
2425
+ };
2426
+ if (isUnset(maybeValue)) return writeUnsetAt();
2241
2427
  let resolvedValue;
2242
2428
  if (typeof maybeValue === "function") {
2243
2429
  const current = state.getValueAtPath(segments);
2244
2430
  const prev = current === void 0 ? state.schema.getDefaultAtPath(segments) : current;
2245
2431
  resolvedValue = maybeValue(prev);
2246
- if (isUnset(resolvedValue)) {
2247
- return state.setValueAtPath(
2248
- segments,
2249
- state.schema.getDefaultAtPath(segments),
2250
- withInstanceMeta({ blank: true })
2251
- );
2252
- }
2432
+ if (isUnset(resolvedValue)) return writeUnsetAt();
2253
2433
  } else {
2254
2434
  resolvedValue = maybeValue;
2255
2435
  }
@@ -2265,7 +2445,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2265
2445
  if (blankSegments === null) continue;
2266
2446
  state.setValueAtPath(
2267
2447
  blankSegments,
2268
- state.schema.getDefaultAtPath(blankSegments),
2448
+ state.getValueAtPath(blankSegments),
2269
2449
  withInstanceMeta({ blank: true })
2270
2450
  );
2271
2451
  }
@@ -2279,7 +2459,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2279
2459
  if (e.formKey === state.formKey) own.push(e);
2280
2460
  else dropped++;
2281
2461
  }
2282
- if (plugin.__DEV__ && dropped > 0) {
2462
+ if (paths.__DEV__ && dropped > 0) {
2283
2463
  console.warn(
2284
2464
  `[attaform] ${op}: dropped ${dropped} error(s) with non-matching formKey (this form's key is "${String(state.formKey)}"). Errors are scoped to the form that produced them \u2014 pass them to the matching form instance.`
2285
2465
  );
@@ -2329,8 +2509,10 @@ function buildFormApi(state, formInstanceId, options = {}) {
2329
2509
  state.userErrors.delete(paths.FORM_ERRORS_PATH_KEY);
2330
2510
  }
2331
2511
  const submitting = vue.computed(() => state.submitting.value);
2332
- const submitCount = vue.computed(() => state.submitCount.value);
2512
+ const submissionAttempts = vue.computed(() => state.submissionAttempts.value);
2513
+ const submitted = vue.computed(() => state.submitted.value);
2333
2514
  const submitError = vue.computed(() => state.submitError.value);
2515
+ const departAttempts = vue.computed(() => state.departAttempts.value);
2334
2516
  const validating = vue.computed(() => state.activeValidations.value > 0);
2335
2517
  const valid = vue.computed(
2336
2518
  () => state.firstValidationDone.value && state.schemaErrors.size === 0 && state.userErrors.size === 0 && state.derivedBlankErrors.value.size === 0 && !validating.value
@@ -2355,8 +2537,11 @@ function buildFormApi(state, formInstanceId, options = {}) {
2355
2537
  return {
2356
2538
  ...rootBase,
2357
2539
  submitting: state.submitting.value,
2358
- submitCount: state.submitCount.value,
2540
+ submissionAttempts: state.submissionAttempts.value,
2541
+ departAttempts: state.departAttempts.value,
2359
2542
  submitError: state.submitError.value,
2543
+ errorCount: rootBase.errors.length,
2544
+ submitted: state.submitted.value,
2360
2545
  instanceId: formInstanceId
2361
2546
  };
2362
2547
  };
@@ -2416,8 +2601,14 @@ function buildFormApi(state, formInstanceId, options = {}) {
2416
2601
  meta: vue.computed(() => rootFieldState.value.meta),
2417
2602
  // Lifecycle (form-level only — not on FieldState).
2418
2603
  submitting,
2419
- submitCount,
2604
+ submissionAttempts,
2605
+ departAttempts,
2420
2606
  submitError,
2607
+ // Scalar mirror over the array — meta is a single sticky surface
2608
+ // for both templates and `useWizard`'s `FormStatus`, so the
2609
+ // projection lives here.
2610
+ errorCount: vue.computed(() => metaErrors.value.length),
2611
+ submitted,
2421
2612
  // Per-`useForm()`-call identity. Stable for one mount; new on
2422
2613
  // re-mount; orthogonal to `form.key` (which is the user-supplied
2423
2614
  // shared identifier). Useful for devtools panels disambiguating
@@ -2437,13 +2628,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2437
2628
  );
2438
2629
  state.reset(walked.cleanedValues);
2439
2630
  for (const pathKey of walked.paths) {
2440
- const segments = paths.segmentsForPathKey(pathKey);
2441
- if (segments === null) continue;
2442
- state.setValueAtPath(
2443
- segments,
2444
- state.schema.getDefaultAtPath(segments),
2445
- withInstanceMeta({ blank: true })
2446
- );
2631
+ state.blankPaths.add(pathKey);
2447
2632
  state.originalBlankPaths.add(pathKey);
2448
2633
  }
2449
2634
  }
@@ -2464,7 +2649,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2464
2649
  }
2465
2650
  const persist = async (pathInput, options2) => {
2466
2651
  const segments = paths.canonicalizePath(pathInput).segments;
2467
- plugin.enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
2652
+ paths.enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
2468
2653
  if (persistence === void 0) return;
2469
2654
  await persistence.writePathImmediately(segments);
2470
2655
  };
@@ -2493,56 +2678,141 @@ function buildFormApi(state, formInstanceId, options = {}) {
2493
2678
  target.element.scrollIntoView(options2);
2494
2679
  return true;
2495
2680
  };
2681
+ const applyInvalidSubmitPolicyPublic = (policy) => {
2682
+ applyInvalidSubmitPolicy(state, formInstanceId, policy ?? defaultInvalidSubmitPolicy);
2683
+ };
2496
2684
  const fieldArrays = buildFieldArrayApi(state);
2497
2685
  const blankPathsView = vue.computed(() => {
2498
- return readonlySetSnapshot(state.blankPaths);
2686
+ const keys = /* @__PURE__ */ new Set();
2687
+ const paths$1 = [];
2688
+ for (const pk of state.blankPaths) {
2689
+ keys.add(pk);
2690
+ const segs = paths.segmentsForPathKey(pk);
2691
+ if (segs !== null) paths$1.push(segs);
2692
+ }
2693
+ Object.freeze(paths$1);
2694
+ const view = {
2695
+ get size() {
2696
+ return keys.size;
2697
+ },
2698
+ has(input) {
2699
+ const { key } = paths.canonicalizePath(input);
2700
+ return keys.has(key);
2701
+ },
2702
+ values() {
2703
+ return paths$1;
2704
+ },
2705
+ [Symbol.iterator]() {
2706
+ return paths$1[Symbol.iterator]();
2707
+ }
2708
+ };
2709
+ return Object.freeze(view);
2499
2710
  });
2500
2711
  const valuesProxy = buildValuesProxy(state.form);
2501
2712
  const fieldStateProxy = buildFieldStateProxy(state, getFormMetaBase, fieldStateAccessorOptions);
2713
+ function gated(fn) {
2714
+ return ((...args) => {
2715
+ void state.activate();
2716
+ return fn(...args);
2717
+ });
2718
+ }
2502
2719
  return {
2503
- handleSubmit,
2504
- // `values` is the callable readonly Proxy. Each `get` trap reads
2505
- // through `inner.value` (a `computed(() => readonly(form.value))`)
2506
- // so reactivity tracking propagates at the call site. Identity-
2507
- // stable across whole-form swaps (the inner readonly proxy
2508
- // re-keys; the outer callable proxy stays the same instance).
2509
- values: valuesProxy,
2510
- fields: fieldStateProxy,
2511
- setValue: setValueImpl,
2512
- validate,
2513
- validateAsync,
2514
- process,
2515
- register,
2720
+ handleSubmit: gated(handleSubmit),
2721
+ // Callable readonly Proxies (`values`, `fields`, `errors`) and the
2722
+ // reactive containers (`meta`, `history`, `blankPaths`) are exposed
2723
+ // through getters so reading them activates the form on first
2724
+ // touch. Each underlying object is identity-stable across reads.
2725
+ get values() {
2726
+ void state.activate();
2727
+ return valuesProxy;
2728
+ },
2729
+ get fields() {
2730
+ void state.activate();
2731
+ return fieldStateProxy;
2732
+ },
2733
+ setValue: gated(setValueImpl),
2734
+ validate: gated(validate),
2735
+ validateAsync: gated(validateAsync),
2736
+ process: gated(process),
2737
+ register: gated(register),
2516
2738
  key: state.formKey,
2517
- errors: errorsProxy,
2518
- toRef: pathToRef,
2519
- setFieldErrors,
2520
- addFieldErrors,
2521
- clearFieldErrors,
2522
- setFormErrors,
2523
- clearFormErrors,
2524
- meta: formMeta,
2525
- reset,
2526
- resetField,
2527
- clear,
2528
- persist,
2529
- clearPersistedDraft,
2530
- focusFirstError,
2531
- scrollToFirstError,
2532
- touch,
2533
- history: formHistory,
2534
- append: fieldArrays.append,
2535
- prepend: fieldArrays.prepend,
2536
- insert: fieldArrays.insert,
2537
- remove: fieldArrays.remove,
2538
- swap: fieldArrays.swap,
2539
- move: fieldArrays.move,
2540
- replace: fieldArrays.replace,
2541
- blankPaths: blankPathsView
2739
+ // Auto-unwrapping views over the per-store async-defaults lifecycle
2740
+ // refs (see FormStore.hydrating / hydrateError). Reading either
2741
+ // activates the form — observing factory state implies use.
2742
+ get hydrating() {
2743
+ void state.activate();
2744
+ return state.hydrating.value;
2745
+ },
2746
+ get hydrateError() {
2747
+ void state.activate();
2748
+ return state.hydrateError.value;
2749
+ },
2750
+ // Orthogonal to `hydrating` and `hydrateError`: `ready` flips true
2751
+ // once defaults are applied (sync at construction or async factory
2752
+ // resolved successfully). One-way latch — stays true through later
2753
+ // refetches even when those refetches fail, so stale-while-
2754
+ // revalidate UIs keep rendering the prior values while
2755
+ // `hydrateError` surfaces the refresh failure.
2756
+ get ready() {
2757
+ void state.activate();
2758
+ return state.defaultsResolved.value;
2759
+ },
2760
+ // `rehydrate` and `activate` are themselves activation entry points
2761
+ // — they fire the factory by design. Wrapping them with `gated`
2762
+ // would double-fire (`state.activate()` plus the underlying call),
2763
+ // so they call `state` directly.
2764
+ rehydrate: () => state.rehydrate(),
2765
+ activate: () => state.activate(),
2766
+ get errors() {
2767
+ void state.activate();
2768
+ return errorsProxy;
2769
+ },
2770
+ toRef: gated(pathToRef),
2771
+ setFieldErrors: gated(setFieldErrors),
2772
+ addFieldErrors: gated(addFieldErrors),
2773
+ clearFieldErrors: gated(clearFieldErrors),
2774
+ setFormErrors: gated(setFormErrors),
2775
+ clearFormErrors: gated(clearFormErrors),
2776
+ get meta() {
2777
+ void state.activate();
2778
+ return formMeta;
2779
+ },
2780
+ reset: gated(reset),
2781
+ resetField: gated(resetField),
2782
+ clear: gated(clear),
2783
+ persist: gated(persist),
2784
+ clearPersistedDraft: gated(clearPersistedDraft),
2785
+ focusFirstError: gated(focusFirstError),
2786
+ scrollToFirstError: gated(scrollToFirstError),
2787
+ applyInvalidSubmitPolicy: gated(applyInvalidSubmitPolicyPublic),
2788
+ touch: gated(touch),
2789
+ get history() {
2790
+ void state.activate();
2791
+ return formHistory;
2792
+ },
2793
+ append: gated(fieldArrays.append),
2794
+ prepend: gated(fieldArrays.prepend),
2795
+ insert: gated(fieldArrays.insert),
2796
+ remove: gated(fieldArrays.remove),
2797
+ swap: gated(fieldArrays.swap),
2798
+ move: gated(fieldArrays.move),
2799
+ replace: gated(fieldArrays.replace),
2800
+ get blankPaths() {
2801
+ void state.activate();
2802
+ return blankPathsView;
2803
+ }
2542
2804
  };
2543
2805
  }
2544
2806
 
2545
- const defaultShouldShowErrors = (field, formMeta) => formMeta.submitCount > 0 || field.touched === true && field.dirty;
2807
+ const defaultShouldShowErrors = (field, formMeta) => {
2808
+ const hasOwnError = field.errors.some(
2809
+ (e) => e.path.length === field.path.length && e.path.every((s, i) => s === field.path[i])
2810
+ );
2811
+ if (!hasOwnError) return false;
2812
+ if (field.validating === true) return false;
2813
+ if (formMeta.submissionAttempts > 0) return true;
2814
+ return field.touched === true && field.focused !== true;
2815
+ };
2546
2816
  const SHOW_ALWAYS = () => true;
2547
2817
  const SHOW_NEVER = () => false;
2548
2818
  function resolveShouldShowErrors(config) {
@@ -2555,7 +2825,7 @@ function resolveShouldShowErrors(config) {
2555
2825
  function isHydratedFieldRecord(value) {
2556
2826
  if (typeof value !== "object" || value === null) return false;
2557
2827
  const r = value;
2558
- return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.connected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && (typeof r.touched === "boolean" || r.touched === null);
2828
+ return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.connected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && typeof r.touched === "boolean";
2559
2829
  }
2560
2830
  function isHydratedValidationErrorArray(value) {
2561
2831
  if (!Array.isArray(value)) return false;
@@ -2570,7 +2840,7 @@ function isHydratedValidationErrorArray(value) {
2570
2840
  return true;
2571
2841
  }
2572
2842
  function warnMalformedHydration(formKey, kind, rawKey) {
2573
- if (!plugin.__DEV__) return;
2843
+ if (!paths.__DEV__) return;
2574
2844
  console.warn(
2575
2845
  `[attaform] hydration: skipping malformed ${kind} entry at key '${rawKey}' on form '${formKey}'. This usually means the SSR bundle is on a different version than the client (rolling deploy / stale cache).`
2576
2846
  );
@@ -2592,7 +2862,8 @@ function walkDuStubs(schema, value, path, warned) {
2592
2862
  if (du !== void 0) {
2593
2863
  const discValue = rec[du.discriminatorKey];
2594
2864
  if (discValue !== void 0 && !du.isVariantSelected(discValue)) {
2595
- if (warned !== void 0 && plugin.__DEV__) {
2865
+ const isKindBlank = discValue === "" || discValue === 0 || discValue === 0n || discValue === false || discValue === null;
2866
+ if (!isKindBlank && warned !== void 0 && paths.__DEV__) {
2596
2867
  const dotted = path.map((s) => String(s)).join(".") || "(root)";
2597
2868
  const key = `${dotted}::${String(discValue)}`;
2598
2869
  if (!warned.has(key)) {
@@ -2671,9 +2942,45 @@ function cloneVariantSnapshot(value) {
2671
2942
  for (const k of Object.keys(src)) out[k] = cloneVariantSnapshot(src[k]);
2672
2943
  return out;
2673
2944
  }
2945
+ function walkAuthoredFromConstraints(value, prefix, out) {
2946
+ if (prefix.length > 0) out.add(paths.canonicalizePath(prefix).key);
2947
+ if (isPlainRecord(value)) {
2948
+ for (const k of Object.keys(value)) {
2949
+ walkAuthoredFromConstraints(value[k], [...prefix, k], out);
2950
+ }
2951
+ return;
2952
+ }
2953
+ if (Array.isArray(value)) {
2954
+ for (let i = 0; i < value.length; i++) {
2955
+ walkAuthoredFromConstraints(value[i], [...prefix, i], out);
2956
+ }
2957
+ }
2958
+ }
2959
+ function walkAuthoredFromSchemaDiff(withDefaults, withoutDefaults, prefix, out) {
2960
+ if (isPlainRecord(withDefaults) && isPlainRecord(withoutDefaults)) {
2961
+ const left = withDefaults;
2962
+ const right = withoutDefaults;
2963
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
2964
+ for (const k of keys) {
2965
+ walkAuthoredFromSchemaDiff(left[k], right[k], [...prefix, k], out);
2966
+ }
2967
+ return;
2968
+ }
2969
+ if (Array.isArray(withDefaults) && Array.isArray(withoutDefaults)) {
2970
+ const len = Math.max(withDefaults.length, withoutDefaults.length);
2971
+ for (let i = 0; i < len; i++) {
2972
+ walkAuthoredFromSchemaDiff(withDefaults[i], withoutDefaults[i], [...prefix, i], out);
2973
+ }
2974
+ return;
2975
+ }
2976
+ if (!Object.is(withDefaults, withoutDefaults) && prefix.length > 0) {
2977
+ out.add(paths.canonicalizePath(prefix).key);
2978
+ }
2979
+ }
2674
2980
  function createFormStore(options) {
2675
2981
  const { formKey, schema, defaultValues, strict = true, hydration } = options;
2676
2982
  const ssr = options.ssr === true;
2983
+ const ssrPrefetch = options.ssrPrefetch;
2677
2984
  const rememberVariants = options.rememberVariants !== false;
2678
2985
  const fieldValidationMode = options.validateOn ?? "change";
2679
2986
  const fieldValidationDebounceMs = normalizeNumericOption({
@@ -2687,7 +2994,7 @@ function createFormStore(options) {
2687
2994
  const formChangeListeners = /* @__PURE__ */ new Set();
2688
2995
  const submitSuccessListeners = /* @__PURE__ */ new Set();
2689
2996
  const resetListeners = /* @__PURE__ */ new Set();
2690
- const persistOptIns = plugin.createPersistOptInRegistry();
2997
+ const persistOptIns = paths.createPersistOptInRegistry();
2691
2998
  const noSyncPaths = /* @__PURE__ */ new Set();
2692
2999
  const noSyncPathCounts = /* @__PURE__ */ new Map();
2693
3000
  function incrementNoSyncOptOut(path) {
@@ -2708,8 +3015,8 @@ function createFormStore(options) {
2708
3015
  const resolvedShouldShowErrors = resolveShouldShowErrors(
2709
3016
  options.shouldShowErrors
2710
3017
  );
2711
- const resolvedIsSensitivePath = options.isSensitivePath ?? plugin.isSensitivePath;
2712
- const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? plugin.segmentMatchesSensitive;
3018
+ const resolvedIsSensitivePath = options.isSensitivePath ?? paths.isSensitivePath;
3019
+ const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? paths.segmentMatchesSensitive;
2713
3020
  const cleanupHooks = [];
2714
3021
  const modules = /* @__PURE__ */ new Map();
2715
3022
  const completedConstraints = defaultValues === void 0 ? void 0 : mergeStructural(schema, [], defaultValues);
@@ -2719,6 +3026,29 @@ function createFormStore(options) {
2719
3026
  strict
2720
3027
  });
2721
3028
  const schemaInitialData = schemaResponse.data;
3029
+ const authoredPaths = /* @__PURE__ */ new Set();
3030
+ function rebuildAuthoredPaths(constraints, schemaWithDefaultsData) {
3031
+ authoredPaths.clear();
3032
+ if (constraints !== void 0) {
3033
+ walkAuthoredFromConstraints(constraints, [], authoredPaths);
3034
+ }
3035
+ const slimResponse = schema.getDefaultValues({
3036
+ useDefaultSchemaValues: false,
3037
+ strict
3038
+ });
3039
+ walkAuthoredFromSchemaDiff(schemaWithDefaultsData, slimResponse.data, [], authoredPaths);
3040
+ }
3041
+ rebuildAuthoredPaths(defaultValues, schemaInitialData);
3042
+ function filterAuthoredErrors(errors) {
3043
+ return errors.filter((err) => {
3044
+ const pathSegments = err.path;
3045
+ if (pathSegments.length === 0) return true;
3046
+ const value = getAtPath(form.value, pathSegments);
3047
+ if (value !== void 0) return true;
3048
+ if (authoredPaths.has(paths.canonicalizePath(pathSegments).key)) return true;
3049
+ return !schema.isPreprocessOrCoerceLeaf(pathSegments);
3050
+ });
3051
+ }
2722
3052
  const initialData = hydration !== void 0 ? hydration.form : structuralSnapshot(schemaInitialData);
2723
3053
  const stubbedInitialData = applyDuStubs(schema, initialData, {
2724
3054
  warn: true
@@ -2735,8 +3065,9 @@ function createFormStore(options) {
2735
3065
  const blankPaths = vue.reactive(/* @__PURE__ */ new Set());
2736
3066
  const originalBlankPaths = /* @__PURE__ */ new Set();
2737
3067
  for (const raw of initialTransientList) {
2738
- blankPaths.add(raw);
2739
- originalBlankPaths.add(raw);
3068
+ const key = paths.coerceToPathKey(raw);
3069
+ blankPaths.add(key);
3070
+ originalBlankPaths.add(key);
2740
3071
  }
2741
3072
  const variantMemory = /* @__PURE__ */ new Map();
2742
3073
  function clearVariantMemoryUnderPath(arrayPath) {
@@ -2804,10 +3135,18 @@ function createFormStore(options) {
2804
3135
  });
2805
3136
  const submitting = vue.ref(false);
2806
3137
  const activeSubmissions = vue.ref(0);
2807
- const submitCount = vue.ref(0);
3138
+ const submissionAttempts = vue.ref(0);
3139
+ const submitted = vue.ref(false);
2808
3140
  const submitError = vue.ref(null);
3141
+ const departAttempts = vue.ref(0);
2809
3142
  const submissionGeneration = vue.ref(0);
2810
3143
  const activeValidations = vue.ref(0);
3144
+ const hydrating = vue.ref(false);
3145
+ const hydrateError = vue.ref(null);
3146
+ const defaultValuesFactory = vue.ref(void 0);
3147
+ const defaultsResolved = vue.ref(false);
3148
+ const activated = vue.ref(false);
3149
+ const activationPromise = vue.ref(void 0);
2811
3150
  const firstValidationDone = vue.ref(!strict || schema.needsAsyncValidation?.() !== true);
2812
3151
  vue.watch(activeValidations, (now, prev) => {
2813
3152
  if (prev > 0 && now === 0) {
@@ -2872,7 +3211,7 @@ function createFormStore(options) {
2872
3211
  connected: false,
2873
3212
  focused: null,
2874
3213
  blurred: null,
2875
- touched: null
3214
+ touched: false
2876
3215
  });
2877
3216
  });
2878
3217
  if (strict && !schemaResponse.success) {
@@ -2892,9 +3231,16 @@ function createFormStore(options) {
2892
3231
  path,
2893
3232
  updatedAt: patch.updatedAt ?? current?.updatedAt ?? null,
2894
3233
  connected: patch.connected ?? current?.connected ?? false,
2895
- focused: patch.focused ?? current?.focused ?? null,
2896
- blurred: patch.blurred ?? current?.blurred ?? null,
2897
- touched: patch.touched ?? current?.touched ?? null
3234
+ // focused/blurred use an explicit-undefined guard because
3235
+ // patches legitimately carry `null` to mark a disconnect — the
3236
+ // `??` operator would short-circuit on null and fall through to
3237
+ // `current`, losing the intent. `!== undefined` honours an
3238
+ // explicit null and preserves current only on absence.
3239
+ focused: patch.focused !== void 0 ? patch.focused : current?.focused ?? null,
3240
+ blurred: patch.blurred !== void 0 ? patch.blurred : current?.blurred ?? null,
3241
+ // touched is plain `boolean`; `??` is equivalent to the explicit
3242
+ // guard here because `false` is not nullish.
3243
+ touched: patch.touched ?? current?.touched ?? false
2898
3244
  });
2899
3245
  }
2900
3246
  function applyFormReplacement(next, meta) {
@@ -2925,7 +3271,6 @@ function createFormStore(options) {
2925
3271
  }
2926
3272
  function setValueAtPath(path, value, meta) {
2927
3273
  value = stripSymbolsDeep(value);
2928
- value = schema.normalizeWriteValueAtPath(value, path);
2929
3274
  if (!isSlimPrimitiveValid(schema, form, path, value)) {
2930
3275
  return false;
2931
3276
  }
@@ -3024,9 +3369,21 @@ function createFormStore(options) {
3024
3369
  } else if (blankPaths.has(pathKey)) {
3025
3370
  blankPaths.delete(pathKey);
3026
3371
  }
3372
+ const wasAuthoredBefore = authoredPaths.has(pathKey);
3373
+ walkAuthoredFromConstraints(value, path, authoredPaths);
3374
+ const newlyAuthored = !wasAuthoredBefore && authoredPaths.has(pathKey);
3027
3375
  const completedValue = mergeStructural(schema, path, value);
3028
3376
  const currentValue = getAtPath(form.value, path);
3029
3377
  if (Object.is(currentValue, completedValue)) {
3378
+ if (newlyAuthored && schema.isPreprocessOrCoerceLeaf(path)) {
3379
+ const modeForAuthoringTransition = meta?.instance?.validateOn ?? fieldValidationMode;
3380
+ if (modeForAuthoringTransition === "change") {
3381
+ scheduleFieldValidation(path, false, {
3382
+ ...meta?.instance?.validateOn !== void 0 ? { mode: meta.instance.validateOn } : {},
3383
+ ...meta?.instance?.debounceMs !== void 0 ? { debounceMs: meta.instance.debounceMs } : {}
3384
+ });
3385
+ }
3386
+ }
3030
3387
  return true;
3031
3388
  }
3032
3389
  const nextForm = setAtPathWithSchemaFill(form.value, schema, path, completedValue);
@@ -3139,12 +3496,11 @@ function createFormStore(options) {
3139
3496
  prev.controller.abort();
3140
3497
  }
3141
3498
  const controller = new AbortController();
3142
- const fresh = { controller, timer: null };
3499
+ const fresh = { controller, timer: null, settled: false };
3143
3500
  fieldValidationState.set(key, fresh);
3144
3501
  const run = () => {
3145
3502
  fresh.timer = null;
3146
3503
  if (controller.signal.aborted) return;
3147
- const data = getAtPath(form.value, path);
3148
3504
  let activeIncremented = false;
3149
3505
  try {
3150
3506
  activeValidations.value += 1;
@@ -3156,17 +3512,16 @@ function createFormStore(options) {
3156
3512
  }
3157
3513
  throw err;
3158
3514
  }
3159
- void Promise.resolve().then(() => schema.validateAtPath(data, path)).then((response) => {
3515
+ void Promise.resolve().then(() => schema.validateAtPath(form.value, void 0)).then((response) => {
3160
3516
  if (controller.signal.aborted) return;
3161
- const reStamped = response.success ? [] : response.errors.map((err) => ({
3162
- ...err,
3163
- path: [...path, ...err.path]
3164
- }));
3165
- applySchemaErrorsForSubtree(path, reStamped);
3517
+ const errors = response.success ? [] : response.errors;
3518
+ const filtered = filterAuthoredErrors(errors);
3519
+ applySchemaErrorsForSubtree([], filtered);
3166
3520
  }).catch(() => {
3167
3521
  }).finally(() => {
3168
3522
  activeValidations.value = Math.max(0, activeValidations.value - 1);
3169
3523
  decFieldValidation(key);
3524
+ fresh.settled = true;
3170
3525
  });
3171
3526
  };
3172
3527
  if (immediate || effectiveDebounce === 0) {
@@ -3176,8 +3531,13 @@ function createFormStore(options) {
3176
3531
  }
3177
3532
  }
3178
3533
  function cancelFieldValidation() {
3179
- for (const entry of fieldValidationState.values()) {
3180
- if (entry.timer !== null) clearTimeout(entry.timer);
3534
+ for (const [pkey, entry] of fieldValidationState) {
3535
+ if (entry.timer !== null) {
3536
+ clearTimeout(entry.timer);
3537
+ } else if (!entry.settled) {
3538
+ activeValidations.value = Math.max(0, activeValidations.value - 1);
3539
+ decFieldValidation(pkey);
3540
+ }
3181
3541
  entry.controller.abort();
3182
3542
  }
3183
3543
  fieldValidationState.clear();
@@ -3346,7 +3706,12 @@ function createFormStore(options) {
3346
3706
  }
3347
3707
  elementToFormInstance.set(element, formInstanceId);
3348
3708
  sortedRegistrationsCache = null;
3349
- touchFieldRecord(key, path, { connected: true });
3709
+ const current = fields.get(key);
3710
+ touchFieldRecord(key, path, {
3711
+ connected: true,
3712
+ focused: current?.focused ?? false,
3713
+ blurred: current?.blurred ?? true
3714
+ });
3350
3715
  return true;
3351
3716
  }
3352
3717
  function deregisterElement(path, element) {
@@ -3361,7 +3726,7 @@ function createFormStore(options) {
3361
3726
  const remaining = record.elements.size;
3362
3727
  if (remaining === 0) {
3363
3728
  elements.delete(key);
3364
- touchFieldRecord(key, path, { connected: false });
3729
+ touchFieldRecord(key, path, { connected: false, focused: null, blurred: null });
3365
3730
  }
3366
3731
  return remaining;
3367
3732
  }
@@ -3370,7 +3735,11 @@ function createFormStore(options) {
3370
3735
  const { key } = paths.canonicalizePath(path);
3371
3736
  const current = fields.get(key);
3372
3737
  if (current?.connected === true) return;
3373
- touchFieldRecord(key, path, { connected: true });
3738
+ touchFieldRecord(key, path, {
3739
+ connected: true,
3740
+ focused: current?.focused ?? false,
3741
+ blurred: current?.blurred ?? true
3742
+ });
3374
3743
  }
3375
3744
  function markFocused(path, focused, meta) {
3376
3745
  const { key } = paths.canonicalizePath(path);
@@ -3379,7 +3748,7 @@ function createFormStore(options) {
3379
3748
  blurred: !focused,
3380
3749
  // `touched` flips to true on blur and stays true thereafter; while
3381
3750
  // a field is currently focused we keep whatever value it held.
3382
- touched: focused ? fields.get(key)?.touched ?? null : true
3751
+ touched: focused ? fields.get(key)?.touched ?? false : true
3383
3752
  });
3384
3753
  const focusMode = meta?.instance?.validateOn ?? fieldValidationMode;
3385
3754
  if (!focused && focusMode === "blur") {
@@ -3405,7 +3774,7 @@ function createFormStore(options) {
3405
3774
  if (current?.touched === true) continue;
3406
3775
  touchFieldRecord(leafKey, entry.segments, { touched: true });
3407
3776
  }
3408
- if (!touchedAny && plugin.__DEV__) {
3777
+ if (!touchedAny && paths.__DEV__) {
3409
3778
  console.warn(
3410
3779
  `[attaform] form.touch(): no fields resolved at path ${JSON.stringify(segments)}. Check the path matches an existing field or container.`
3411
3780
  );
@@ -3414,6 +3783,84 @@ function createFormStore(options) {
3414
3783
  function clear(path) {
3415
3784
  return setValueAtPath(path, schema.getEmptyValueAtPath(path));
3416
3785
  }
3786
+ function rehydrate() {
3787
+ const factory = defaultValuesFactory.value;
3788
+ if (factory === void 0) {
3789
+ throw new Error(
3790
+ "[attaform] form.rehydrate(): no defaultValues factory was captured. Configure useForm({ defaultValues: () => ... }) to enable rehydrate."
3791
+ );
3792
+ }
3793
+ return fireFactory(factory);
3794
+ }
3795
+ function fireFactory(factory) {
3796
+ activated.value = true;
3797
+ const promise = runFactoryAndApply(factory);
3798
+ activationPromise.value = promise;
3799
+ void promise.finally(() => {
3800
+ if (activationPromise.value === promise) activationPromise.value = void 0;
3801
+ });
3802
+ return promise;
3803
+ }
3804
+ function activate() {
3805
+ if (ssrPrefetch !== void 0) {
3806
+ ssrPrefetch.enqueue();
3807
+ if (!ssrPrefetch.shouldFire()) return Promise.resolve();
3808
+ }
3809
+ if (defaultsResolved.value === true) return Promise.resolve();
3810
+ if (activationPromise.value !== void 0) return activationPromise.value;
3811
+ if (activated.value === true) return Promise.resolve();
3812
+ const factory = defaultValuesFactory.value;
3813
+ if (factory === void 0) return Promise.resolve();
3814
+ return fireFactory(factory);
3815
+ }
3816
+ async function runFactoryAndApply(factory) {
3817
+ hydrating.value = true;
3818
+ try {
3819
+ const value = await factory();
3820
+ walkAuthoredFromConstraints(value, [], authoredPaths);
3821
+ const full = mergeSparseHydration(
3822
+ vue.toRaw(form.value),
3823
+ value,
3824
+ schema
3825
+ );
3826
+ applyFormReplacement(full, { hydration: true });
3827
+ scheduleFieldValidation(
3828
+ [],
3829
+ true
3830
+ /* immediate */
3831
+ );
3832
+ clearHydrationFailedEntry();
3833
+ hydrateError.value = null;
3834
+ defaultsResolved.value = true;
3835
+ } catch (error) {
3836
+ clearHydrationFailedEntry();
3837
+ hydrateError.value = appendHydrationFailedEntry(error);
3838
+ } finally {
3839
+ hydrating.value = false;
3840
+ }
3841
+ }
3842
+ function clearHydrationFailedEntry() {
3843
+ const existing = schemaErrors.get(paths.FORM_ERRORS_PATH_KEY);
3844
+ if (existing === void 0) return;
3845
+ const filtered = existing.filter((e) => e.code !== AttaformErrorCode.HydrationFailed);
3846
+ if (filtered.length === 0) {
3847
+ schemaErrors.delete(paths.FORM_ERRORS_PATH_KEY);
3848
+ } else {
3849
+ schemaErrors.set(paths.FORM_ERRORS_PATH_KEY, filtered);
3850
+ }
3851
+ }
3852
+ function appendHydrationFailedEntry(error) {
3853
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Hydration failed";
3854
+ const entry = {
3855
+ message,
3856
+ path: [...paths.FORM_ERRORS_PATH],
3857
+ formKey,
3858
+ code: AttaformErrorCode.HydrationFailed
3859
+ };
3860
+ const existing = schemaErrors.get(paths.FORM_ERRORS_PATH_KEY) ?? [];
3861
+ schemaErrors.set(paths.FORM_ERRORS_PATH_KEY, [...existing, entry]);
3862
+ return entry;
3863
+ }
3417
3864
  function reset(nextDefaultValues) {
3418
3865
  const resetSource = nextDefaultValues ?? defaultValues;
3419
3866
  const completedResetConstraints = resetSource === void 0 ? void 0 : mergeStructural(schema, [], resetSource);
@@ -3423,6 +3870,7 @@ function createFormStore(options) {
3423
3870
  strict
3424
3871
  });
3425
3872
  const next = resetResponse.data;
3873
+ rebuildAuthoredPaths(resetSource, next);
3426
3874
  applyFormReplacement(next);
3427
3875
  originals.clear();
3428
3876
  diffAndApply({}, next, [], (patch) => {
@@ -3465,16 +3913,18 @@ function createFormStore(options) {
3465
3913
  path: record.path,
3466
3914
  updatedAt: now,
3467
3915
  connected: record.connected,
3468
- focused: null,
3469
- blurred: null,
3470
- touched: null
3916
+ focused: record.focused,
3917
+ blurred: record.blurred,
3918
+ touched: false
3471
3919
  });
3472
3920
  }
3473
3921
  submissionGeneration.value += 1;
3474
3922
  submitting.value = false;
3475
3923
  activeSubmissions.value = 0;
3476
- submitCount.value = 0;
3924
+ submissionAttempts.value = 0;
3925
+ submitted.value = false;
3477
3926
  submitError.value = null;
3927
+ departAttempts.value = 0;
3478
3928
  cancelFieldValidation();
3479
3929
  variantMemory.clear();
3480
3930
  for (const listener of resetListeners) {
@@ -3547,9 +3997,9 @@ function createFormStore(options) {
3547
3997
  path: record.path,
3548
3998
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3549
3999
  connected: record.connected,
3550
- focused: null,
3551
- blurred: null,
3552
- touched: null
4000
+ focused: record.focused,
4001
+ blurred: record.blurred,
4002
+ touched: false
3553
4003
  });
3554
4004
  }
3555
4005
  function isPathPrefix(prefix, candidate) {
@@ -3612,8 +4062,18 @@ function createFormStore(options) {
3612
4062
  shouldShowErrors: resolvedShouldShowErrors,
3613
4063
  submitting,
3614
4064
  activeSubmissions,
3615
- submitCount,
4065
+ submissionAttempts,
4066
+ submitted,
3616
4067
  submitError,
4068
+ departAttempts,
4069
+ hydrating,
4070
+ hydrateError,
4071
+ defaultValuesFactory,
4072
+ defaultsResolved,
4073
+ activated,
4074
+ activationPromise,
4075
+ rehydrate,
4076
+ activate,
3617
4077
  submissionGeneration,
3618
4078
  activeValidations,
3619
4079
  firstValidationDone,
@@ -3628,6 +4088,7 @@ function createFormStore(options) {
3628
4088
  setSchemaErrorsForPath,
3629
4089
  setAllSchemaErrors,
3630
4090
  clearSchemaErrors,
4091
+ applySchemaErrorsForSubtree,
3631
4092
  setAllUserErrors,
3632
4093
  addUserErrors,
3633
4094
  clearUserErrors,
@@ -3873,6 +4334,11 @@ const PROTOCOL_VERSION = 1;
3873
4334
  const JOIN_COLLECTION_WINDOW_MS = 50;
3874
4335
  const SNAPSHOT_TIMEOUT_MS = 200;
3875
4336
  const MAX_LEADER_ATTEMPTS = 3;
4337
+ function isFileLikeValue(value) {
4338
+ if (typeof File !== "undefined" && value instanceof File) return true;
4339
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
4340
+ return false;
4341
+ }
3876
4342
  function isDangerousSegment(s) {
3877
4343
  return s === "__proto__" || s === "constructor" || s === "prototype";
3878
4344
  }
@@ -3882,6 +4348,32 @@ function pathContainsDangerousSegment(path) {
3882
4348
  }
3883
4349
  return false;
3884
4350
  }
4351
+ function isInboundShapeAcceptable(schema, path, value) {
4352
+ if (isFileLikeValue(value)) return false;
4353
+ let kind;
4354
+ if (Array.isArray(value)) {
4355
+ kind = "array";
4356
+ } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
4357
+ kind = "object";
4358
+ } else {
4359
+ kind = slimKindOf(value);
4360
+ }
4361
+ const accepted = schema.getSlimPrimitiveTypesAtPath(path);
4362
+ if (!accepted.has(kind)) return false;
4363
+ if (Array.isArray(value)) {
4364
+ for (let i = 0; i < value.length; i++) {
4365
+ if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
4366
+ }
4367
+ return true;
4368
+ }
4369
+ if (isPlainRecord(value)) {
4370
+ for (const key of Object.keys(value)) {
4371
+ if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
4372
+ }
4373
+ return true;
4374
+ }
4375
+ return true;
4376
+ }
3885
4377
  function diffBlankPaths(prev, curr) {
3886
4378
  const added = [];
3887
4379
  const removed = [];
@@ -3894,6 +4386,7 @@ function snapshotForm(form) {
3894
4386
  return structuralSnapshot(form);
3895
4387
  }
3896
4388
  function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
4389
+ if (isFileLikeValue(value)) return void 0;
3897
4390
  if (value === null || typeof value !== "object") return value;
3898
4391
  if (Array.isArray(value)) {
3899
4392
  return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
@@ -3998,6 +4491,7 @@ function createMultiTabSyncModule(state, channelName, options) {
3998
4491
  const safePatches = [];
3999
4492
  for (const p of rawPatches) {
4000
4493
  if (isPathLocallySuppressed(p.path)) continue;
4494
+ if ("value" in p && isFileLikeValue(p.value)) continue;
4001
4495
  safePatches.push(p);
4002
4496
  }
4003
4497
  const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
@@ -4037,6 +4531,9 @@ function createMultiTabSyncModule(state, channelName, options) {
4037
4531
  for (const p of msg.formPatches) {
4038
4532
  if (!Array.isArray(p.path)) continue;
4039
4533
  if (isPathLocallySuppressed(p.path)) continue;
4534
+ if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
4535
+ continue;
4536
+ }
4040
4537
  safePatches.push(p);
4041
4538
  }
4042
4539
  const safeBlankAdded = [];
@@ -4071,11 +4568,7 @@ function createMultiTabSyncModule(state, channelName, options) {
4071
4568
  }
4072
4569
  function handleSnapshot(msg) {
4073
4570
  if (lifecycle !== "joining") return;
4074
- try {
4075
- options.validateForm(msg.form);
4076
- } catch {
4077
- return;
4078
- }
4571
+ if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
4079
4572
  if (snapshotTimeoutTimer !== null) {
4080
4573
  clearTimeout(snapshotTimeoutTimer);
4081
4574
  snapshotTimeoutTimer = null;
@@ -4194,7 +4687,7 @@ const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
4194
4687
 
4195
4688
  const warned = /* @__PURE__ */ new Set();
4196
4689
  function warnOnceInsecureContext(feature) {
4197
- if (!plugin.__DEV__) return;
4690
+ if (!paths.__DEV__) return;
4198
4691
  if (warned.has(feature)) return;
4199
4692
  warned.add(feature);
4200
4693
  const message = featureMessage(feature);
@@ -4214,15 +4707,26 @@ function isSecureContext() {
4214
4707
  return typeof window !== "undefined" && window.isSecureContext === true;
4215
4708
  }
4216
4709
 
4217
- function useAbstractForm(configuration) {
4710
+ function resolveTrichotomy(input) {
4711
+ if (typeof input === "function") {
4712
+ return { kind: "async", factory: input };
4713
+ }
4714
+ return { kind: "sync", value: input };
4715
+ }
4716
+
4717
+ function useAbstractForm(configuration, options) {
4218
4718
  if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
4219
- throw new plugin.InvalidUseFormConfigError();
4719
+ throw new paths.InvalidUseFormConfigError();
4220
4720
  }
4221
4721
  const key = resolveFormKey(configuration.key);
4222
4722
  const instance = vue.getCurrentInstance();
4223
- if (instance !== null) plugin.ensureAttaformInstalled(instance.appContext.app);
4224
- const registry = plugin.useRegistry();
4225
- const merged = mergeWithDefaults(registry.defaults, configuration);
4723
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
4724
+ const registry = options?.registry ?? paths.useRegistry();
4725
+ const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
4726
+ const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
4727
+ const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
4728
+ const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
4729
+ const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
4226
4730
  const maxRecursionDepth = normalizeNumericOption({
4227
4731
  value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
4228
4732
  source: "useForm.maxRecursionDepth",
@@ -4232,18 +4736,38 @@ function useAbstractForm(configuration) {
4232
4736
  });
4233
4737
  const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
4234
4738
  if (configuration.persist !== void 0 && configuration.key === void 0) {
4235
- throw new plugin.AnonPersistError({
4739
+ throw new paths.AnonPersistError({
4236
4740
  cause: "no-key",
4237
4741
  schemaFields: extractSchemaFields(resolvedSchema),
4238
- callSite: plugin.captureUserCallSite()
4742
+ callSite: paths.captureUserCallSite()
4239
4743
  });
4240
4744
  }
4241
4745
  const existing = registry.forms.get(key);
4242
- if (plugin.__DEV__ && existing !== void 0) {
4746
+ if (paths.__DEV__ && existing !== void 0) {
4243
4747
  warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
4244
4748
  warnOnPersistDivergence(key, existing, configuration.persist);
4245
4749
  }
4750
+ const hadPendingHydration = registry.pendingHydration.has(key);
4246
4751
  const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
4752
+ if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
4753
+ state.defaultsResolved.value = true;
4754
+ }
4755
+ if (existing === void 0 && resolvedDefaults.kind === "async") {
4756
+ const factory = resolvedDefaults.factory;
4757
+ state.defaultValuesFactory.value = factory;
4758
+ if (hadPendingHydration) {
4759
+ state.hydrating.value = false;
4760
+ state.defaultsResolved.value = true;
4761
+ } else if (registry.ssr) {
4762
+ if (configuration.__ssrAccessed === true) {
4763
+ registry.enqueuePrefetch(key);
4764
+ }
4765
+ vue.onServerPrefetch(() => {
4766
+ if (!registry.shouldPrefetch(key)) return;
4767
+ return state.activate();
4768
+ });
4769
+ }
4770
+ }
4247
4771
  if (vue.getCurrentScope() !== void 0) {
4248
4772
  const releaseConsumer = registry.trackConsumer(key);
4249
4773
  vue.onScopeDispose(releaseConsumer);
@@ -4271,7 +4795,7 @@ function useAbstractForm(configuration) {
4271
4795
  void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
4272
4796
  }
4273
4797
  }
4274
- if (existing === void 0 && merged.multiTab !== false && configuration.key !== void 0 && !registry.ssr) {
4798
+ if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
4275
4799
  const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
4276
4800
  const secureContext = isSecureContext();
4277
4801
  if (hasBroadcastChannel && secureContext) {
@@ -4307,11 +4831,11 @@ function useAbstractForm(configuration) {
4307
4831
  }
4308
4832
  if (configuration.key === void 0) {
4309
4833
  recordAmbientProvide(registry.ssr);
4310
- vue.provide(plugin.kFormContext, state);
4834
+ vue.provide(paths.kFormContext, state);
4311
4835
  }
4312
4836
  const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
4313
4837
  if (vue.getCurrentInstance() !== null) {
4314
- vue.provide(plugin.kFormInstanceId, formInstanceId);
4838
+ vue.provide(paths.kFormInstanceId, formInstanceId);
4315
4839
  }
4316
4840
  const apiOptions = {};
4317
4841
  if (merged.onInvalidSubmit !== void 0) {
@@ -4378,10 +4902,13 @@ function buildFreshState(key, schema, configuration, registry) {
4378
4902
  configuration.defaultValues,
4379
4903
  schema
4380
4904
  );
4381
- const initialBlankPaths = pending === void 0 ? walked.paths : void 0;
4905
+ let initialBlankPaths;
4906
+ if (pending === void 0) {
4907
+ initialBlankPaths = walked.paths;
4908
+ }
4382
4909
  const resolvedSensitiveNames = configuration.sensitiveNames;
4383
- const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : plugin.createIsSensitivePath(resolvedSensitiveNames);
4384
- const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : plugin.createSegmentMatchesSensitive(resolvedSensitiveNames);
4910
+ const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
4911
+ const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : paths.createSegmentMatchesSensitive(resolvedSensitiveNames);
4385
4912
  const createOptions = {
4386
4913
  formKey: key,
4387
4914
  schema,
@@ -4391,6 +4918,21 @@ function buildFreshState(key, schema, configuration, registry) {
4391
4918
  ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
4392
4919
  ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
4393
4920
  ssr: registry.ssr,
4921
+ // Server-only: bind the SSR prefetch coordination handles. `enqueue`
4922
+ // records intent on every `state.activate()` so a wizard skip-list
4923
+ // override or a future transform mark has a consistent set to diff
4924
+ // against; `shouldFire` lets the activate path bail when the
4925
+ // wizard explicitly skipped this key — even an explicit
4926
+ // `form.activate()` defers to the wizard's render-efficiency
4927
+ // skip-list on the server.
4928
+ ...registry.ssr ? {
4929
+ ssrPrefetch: {
4930
+ enqueue: () => {
4931
+ registry.enqueuePrefetch(key);
4932
+ },
4933
+ shouldFire: () => registry.shouldPrefetch(key)
4934
+ }
4935
+ } : {},
4394
4936
  ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
4395
4937
  ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
4396
4938
  ...configuration.shouldShowErrors !== void 0 ? { shouldShowErrors: configuration.shouldShowErrors } : {},
@@ -4407,14 +4949,14 @@ function buildFreshState(key, schema, configuration, registry) {
4407
4949
  }
4408
4950
  let anonCounter = 0;
4409
4951
  let formInstanceCounter = 0;
4410
- const ambientProvideHistory = plugin.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
4952
+ const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
4411
4953
  function recordAmbientProvide(ssr) {
4412
- if (!plugin.__DEV__ || ssr || ambientProvideHistory === null) return;
4954
+ if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
4413
4955
  const instance = vue.getCurrentInstance();
4414
4956
  if (instance === null) return;
4415
4957
  const instanceKey = instance;
4416
4958
  const entry = {
4417
- source: plugin.captureUserCallSite()
4959
+ source: paths.captureUserCallSite()
4418
4960
  };
4419
4961
  const existing = ambientProvideHistory.get(instanceKey);
4420
4962
  if (existing === void 0) {
@@ -4426,7 +4968,7 @@ function recordAmbientProvide(ssr) {
4426
4968
  function resolveFormKey(key) {
4427
4969
  if (key !== void 0 && key !== null && key !== "") {
4428
4970
  if (key.startsWith(RESERVED_KEY_PREFIX)) {
4429
- throw new plugin.ReservedFormKeyError(key);
4971
+ throw new paths.ReservedFormKeyError(key);
4430
4972
  }
4431
4973
  return key;
4432
4974
  }
@@ -4571,8 +5113,9 @@ function wirePersistence(state, config) {
4571
5113
  state.originalBlankPaths.delete(k);
4572
5114
  }
4573
5115
  for (const k of payload.data.blankPaths ?? []) {
4574
- state.blankPaths.add(k);
4575
- state.originalBlankPaths.add(k);
5116
+ const key2 = paths.coerceToPathKey(k);
5117
+ state.blankPaths.add(key2);
5118
+ state.originalBlankPaths.add(key2);
4576
5119
  }
4577
5120
  if (include === "form+errors") {
4578
5121
  if (payload.data.schemaErrors !== void 0) {
@@ -4711,10 +5254,10 @@ function isEmptyContainer(value) {
4711
5254
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
4712
5255
  function enforceAnonPersistRule(formKey, ssr) {
4713
5256
  if (!formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return false;
4714
- if (plugin.__DEV__)
4715
- throw new plugin.AnonPersistError({
5257
+ if (paths.__DEV__)
5258
+ throw new paths.AnonPersistError({
4716
5259
  cause: "no-key",
4717
- callSite: plugin.captureUserCallSite()
5260
+ callSite: paths.captureUserCallSite()
4718
5261
  });
4719
5262
  if (!ssr && !warnedAnonPersistKeys.has(formKey)) {
4720
5263
  warnedAnonPersistKeys.add(formKey);
@@ -4753,22 +5296,28 @@ function isDescendantPathKey(candidate, ancestor) {
4753
5296
  }
4754
5297
 
4755
5298
  let injectedInstanceCounter = 0;
4756
- function injectForm(key) {
5299
+ function injectForm(input) {
5300
+ const key = typeof input === "string" ? input : input?.key;
5301
+ const ssrAccessed = typeof input === "object" && input !== null ? input.__ssrAccessed === true : false;
4757
5302
  const instance = vue.getCurrentInstance();
4758
- if (instance !== null) plugin.ensureAttaformInstalled(instance.appContext.app);
4759
- const registry = plugin.useRegistry();
5303
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
5304
+ const registry = paths.useRegistry();
4760
5305
  const state = resolveState(key, registry);
4761
5306
  if (state === null) return null;
4762
5307
  if (vue.getCurrentScope() !== void 0) {
4763
5308
  const releaseConsumer = registry.trackConsumer(state.formKey);
4764
5309
  vue.onScopeDispose(releaseConsumer);
4765
5310
  }
5311
+ if (registry.ssr && ssrAccessed) {
5312
+ registry.enqueuePrefetch(state.formKey);
5313
+ vue.onServerPrefetch(() => state.activate());
5314
+ }
4766
5315
  const apiOptions = {};
4767
5316
  const history = state.modules.get("history");
4768
5317
  if (history !== void 0) {
4769
5318
  apiOptions.history = history;
4770
5319
  }
4771
- const ambientInstanceId = vue.getCurrentInstance() !== null ? vue.inject(plugin.kFormInstanceId, null) : null;
5320
+ const ambientInstanceId = vue.getCurrentInstance() !== null ? vue.inject(paths.kFormInstanceId, null) : null;
4772
5321
  const formInstanceId = ambientInstanceId ?? (vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance-injected:${injectedInstanceCounter++}`);
4773
5322
  return buildFormApi(
4774
5323
  state,
@@ -4780,28 +5329,25 @@ function resolveState(key, registry) {
4780
5329
  if (key !== void 0) {
4781
5330
  const stored = registry.forms.get(key);
4782
5331
  if (stored === void 0) {
4783
- warnMiss(`no form registered for key '${key}'`, registry.ssr);
5332
+ warnMiss$1(`no form registered for key '${key}'`, registry.ssr);
4784
5333
  return null;
4785
5334
  }
4786
5335
  return stored;
4787
5336
  }
4788
- const ambient = vue.inject(plugin.kFormContext, null);
4789
- if (ambient === null) {
4790
- warnMiss("no ambient form context", registry.ssr);
4791
- return null;
4792
- }
5337
+ const ambient = vue.inject(paths.kFormContext, null);
5338
+ if (ambient === null) return null;
4793
5339
  warnIfAmbientProviderHadDuplicates();
4794
5340
  return ambient;
4795
5341
  }
4796
- function warnMiss(detail, ssr) {
4797
- if (!plugin.__DEV__ || ssr) return;
4798
- const frame = plugin.captureUserCallSite();
5342
+ function warnMiss$1(detail, ssr) {
5343
+ if (!paths.__DEV__ || ssr) return;
5344
+ const frame = paths.captureUserCallSite();
4799
5345
  console.warn(
4800
5346
  `[attaform] injectForm: ${detail}. Returning null.` + (frame !== void 0 ? ` ${frame}` : "")
4801
5347
  );
4802
5348
  }
4803
5349
  function warnIfAmbientProviderHadDuplicates() {
4804
- if (!plugin.__DEV__ || ambientProvideHistory === null) return;
5350
+ if (!paths.__DEV__ || ambientProvideHistory === null) return;
4805
5351
  let ancestor = vue.getCurrentInstance()?.parent ?? null;
4806
5352
  while (ancestor !== null) {
4807
5353
  const history = ambientProvideHistory.get(ancestor);
@@ -4818,6 +5364,1045 @@ function warnIfAmbientProviderHadDuplicates() {
4818
5364
  }
4819
5365
  }
4820
5366
 
5367
+ const LAZY_BRAND = Symbol.for("attaform/wizard-lazy");
5368
+ function lazy(resolve) {
5369
+ return { [LAZY_BRAND]: true, resolve };
5370
+ }
5371
+ function isLazyMarker(value) {
5372
+ return typeof value === "object" && value !== null && LAZY_BRAND in value;
5373
+ }
5374
+
5375
+ const NOOP_WIZARD_HISTORY = {
5376
+ push() {
5377
+ },
5378
+ replace() {
5379
+ },
5380
+ read() {
5381
+ return void 0;
5382
+ },
5383
+ subscribe() {
5384
+ },
5385
+ dispose() {
5386
+ }
5387
+ };
5388
+ function createWizardHistory(param) {
5389
+ if (typeof window === "undefined") return NOOP_WIZARD_HISTORY;
5390
+ const subscribers = [];
5391
+ let disposed = false;
5392
+ function buildUrl(key) {
5393
+ const url = new URL(window.location.href);
5394
+ url.searchParams.set(param, key);
5395
+ return url.toString();
5396
+ }
5397
+ function handlePopstate() {
5398
+ if (disposed) return;
5399
+ const url = new URL(window.location.href);
5400
+ const value = url.searchParams.get(param) ?? void 0;
5401
+ for (const subscriber of subscribers) subscriber(value);
5402
+ }
5403
+ window.addEventListener("popstate", handlePopstate);
5404
+ function safeWriteState(key, op) {
5405
+ try {
5406
+ const fn = op === "push" ? window.history.pushState : window.history.replaceState;
5407
+ fn.call(window.history, {}, "", buildUrl(key));
5408
+ } catch {
5409
+ }
5410
+ }
5411
+ return {
5412
+ push(key) {
5413
+ if (disposed) return;
5414
+ safeWriteState(key, "push");
5415
+ },
5416
+ replace(key) {
5417
+ if (disposed) return;
5418
+ safeWriteState(key, "replace");
5419
+ },
5420
+ read() {
5421
+ const url = new URL(window.location.href);
5422
+ return url.searchParams.get(param) ?? void 0;
5423
+ },
5424
+ subscribe(callback) {
5425
+ if (disposed) return;
5426
+ subscribers.push(callback);
5427
+ },
5428
+ dispose() {
5429
+ if (disposed) return;
5430
+ disposed = true;
5431
+ subscribers.length = 0;
5432
+ window.removeEventListener("popstate", handlePopstate);
5433
+ }
5434
+ };
5435
+ }
5436
+
5437
+ const NOOP_FINGERPRINT = "attaform:wizard-noop";
5438
+ const EMPTY_SLIM_KINDS = /* @__PURE__ */ new Set();
5439
+ function buildNoopWizardSchema(formKey) {
5440
+ const emptyValue = {};
5441
+ const success = {
5442
+ success: true,
5443
+ data: emptyValue,
5444
+ errors: void 0,
5445
+ formKey
5446
+ };
5447
+ const defaultsResponse = {
5448
+ success: true,
5449
+ data: emptyValue,
5450
+ errors: void 0,
5451
+ formKey
5452
+ };
5453
+ return {
5454
+ fingerprint: () => NOOP_FINGERPRINT,
5455
+ getDefaultValues: () => defaultsResponse,
5456
+ getDefaultAtPath: () => void 0,
5457
+ getEmptyValueAtPath: () => void 0,
5458
+ isPreprocessOrCoerceLeaf: () => false,
5459
+ arrayShapeAtPath: () => void 0,
5460
+ getSchemasAtPath: () => [],
5461
+ validateAtPath: () => success,
5462
+ getSlimPrimitiveTypesAtPath: () => new Set(EMPTY_SLIM_KINDS),
5463
+ isLeafAtPath: () => false,
5464
+ isRequiredAtPath: () => false,
5465
+ getUnionDiscriminatorAtPath: () => void 0
5466
+ };
5467
+ }
5468
+
5469
+ function buildWizardStatusesProxy(statuses) {
5470
+ const snapshot = vue.computed(() => {
5471
+ const result = {};
5472
+ for (const key of Object.keys(statuses)) {
5473
+ result[key] = statuses[key].value;
5474
+ }
5475
+ return result;
5476
+ });
5477
+ const target = (() => {
5478
+ });
5479
+ const proxyToString = () => JSON.stringify(snapshot.value);
5480
+ const proxyToPrimitive = (hint) => hint === "number" ? NaN : proxyToString();
5481
+ return new Proxy(target, {
5482
+ apply(_, __, args) {
5483
+ const key = args[0];
5484
+ if (key === void 0) return snapshot.value;
5485
+ const computedEntry = statuses[key];
5486
+ if (computedEntry === void 0) return void 0;
5487
+ return computedEntry.value;
5488
+ },
5489
+ get(_, key) {
5490
+ if (typeof key === "symbol") {
5491
+ if (key === Symbol.toPrimitive) return proxyToPrimitive;
5492
+ return Reflect.get(target, key);
5493
+ }
5494
+ if (key === "toJSON") return () => snapshot.value;
5495
+ if (key === "toString") return proxyToString;
5496
+ if (key === "valueOf")
5497
+ return function() {
5498
+ return this;
5499
+ };
5500
+ const computedEntry = statuses[key];
5501
+ if (computedEntry === void 0) return void 0;
5502
+ return computedEntry.value;
5503
+ },
5504
+ has(_, key) {
5505
+ if (typeof key === "symbol") return Reflect.has(target, key);
5506
+ return Object.hasOwn(statuses, key);
5507
+ },
5508
+ ownKeys() {
5509
+ return Object.keys(statuses);
5510
+ },
5511
+ getOwnPropertyDescriptor(_, key) {
5512
+ if (typeof key === "symbol") return void 0;
5513
+ const computedEntry = statuses[key];
5514
+ if (computedEntry === void 0) return void 0;
5515
+ return {
5516
+ configurable: true,
5517
+ enumerable: true,
5518
+ writable: false,
5519
+ value: computedEntry.value
5520
+ };
5521
+ },
5522
+ set(_, key) {
5523
+ if (paths.__DEV__) {
5524
+ console.warn(
5525
+ `[attaform] wizard.statuses is read-only \u2014 write to "${String(key)}" was ignored. Statuses derive from each form's meta; mutate the underlying form instead.`
5526
+ );
5527
+ }
5528
+ return true;
5529
+ },
5530
+ deleteProperty(_, key) {
5531
+ if (paths.__DEV__) {
5532
+ console.warn(
5533
+ `[attaform] wizard.statuses is read-only \u2014 delete of "${String(key)}" was ignored.`
5534
+ );
5535
+ }
5536
+ return true;
5537
+ },
5538
+ defineProperty: () => true
5539
+ });
5540
+ }
5541
+
5542
+ const DEFAULT_STEP_PARAM = "step";
5543
+ const PENDING_STATUS = {
5544
+ valid: false,
5545
+ dirty: false,
5546
+ submitted: false,
5547
+ errorCount: 0
5548
+ };
5549
+ const NOOP_VALID_STATUS = {
5550
+ valid: true,
5551
+ dirty: false,
5552
+ submitted: false,
5553
+ errorCount: 0
5554
+ };
5555
+ function useWizard(options) {
5556
+ const rawSteps = Array.isArray(options.steps) ? options.steps : [];
5557
+ if (rawSteps.length === 0 && paths.__DEV__) {
5558
+ console.error(
5559
+ "[attaform] useWizard({ steps }): expected a non-empty array of step slots. Continuing with an empty step list \u2014 wizard.currentStep reads as undefined, navigation refuses, handleSubmit no-ops."
5560
+ );
5561
+ }
5562
+ const registry = paths.useRegistry();
5563
+ const noopForms = /* @__PURE__ */ new Map();
5564
+ const lazyNoopScope = vue.effectScope(true);
5565
+ for (const slot of rawSteps) {
5566
+ if (typeof slot !== "string") continue;
5567
+ if (noopForms.has(slot)) continue;
5568
+ const noop = useAbstractForm({
5569
+ schema: buildNoopWizardSchema(slot),
5570
+ key: slot
5571
+ });
5572
+ noopForms.set(slot, noop);
5573
+ }
5574
+ function getOrBuildNoop(key) {
5575
+ const existing2 = noopForms.get(key);
5576
+ if (existing2 !== void 0) return existing2;
5577
+ const noop = lazyNoopScope.run(
5578
+ () => useAbstractForm(
5579
+ {
5580
+ schema: buildNoopWizardSchema(key),
5581
+ key
5582
+ },
5583
+ { registry }
5584
+ )
5585
+ );
5586
+ if (noop === void 0) {
5587
+ const stub = { key };
5588
+ return stub;
5589
+ }
5590
+ noopForms.set(key, noop);
5591
+ formsAccumulator.set(key, noop);
5592
+ return noop;
5593
+ }
5594
+ const trackedKeys = /* @__PURE__ */ new Set();
5595
+ function trackOnce(form) {
5596
+ if (trackedKeys.has(form.key)) return;
5597
+ trackedKeys.add(form.key);
5598
+ if (vue.getCurrentScope() !== void 0) {
5599
+ const release = registry.trackConsumer(form.key);
5600
+ vue.onScopeDispose(release);
5601
+ }
5602
+ }
5603
+ for (const slot of rawSteps) {
5604
+ if (typeof slot === "string") {
5605
+ const noop = noopForms.get(slot);
5606
+ if (noop !== void 0) trackOnce(noop);
5607
+ } else if (isAnyForm(slot)) {
5608
+ trackOnce(slot);
5609
+ }
5610
+ }
5611
+ const lazyEpoch = vue.ref(0);
5612
+ const lazyComputeds = /* @__PURE__ */ new Map();
5613
+ const activeKey = vue.ref("");
5614
+ const formsAccumulator = /* @__PURE__ */ new Map();
5615
+ for (const slot of rawSteps) {
5616
+ if (typeof slot === "string") {
5617
+ const noop = noopForms.get(slot);
5618
+ if (noop !== void 0) formsAccumulator.set(noop.key, noop);
5619
+ } else if (isAnyForm(slot)) {
5620
+ formsAccumulator.set(slot.key, slot);
5621
+ }
5622
+ }
5623
+ const slotForms = new Proxy({}, {
5624
+ get(_, key) {
5625
+ if (typeof key !== "string") return void 0;
5626
+ return formsAccumulator.get(key);
5627
+ },
5628
+ has(_, key) {
5629
+ if (typeof key !== "string") return false;
5630
+ return formsAccumulator.has(key);
5631
+ },
5632
+ ownKeys() {
5633
+ return [...formsAccumulator.keys()];
5634
+ },
5635
+ getOwnPropertyDescriptor(_, key) {
5636
+ if (typeof key !== "string") return void 0;
5637
+ const form = formsAccumulator.get(key);
5638
+ if (form === void 0) return void 0;
5639
+ return { configurable: true, enumerable: true, writable: false, value: form };
5640
+ }
5641
+ });
5642
+ const slotCtx = vue.computed(() => ({
5643
+ forms: slotForms,
5644
+ currentKey: activeKey.value === "" ? void 0 : activeKey.value
5645
+ }));
5646
+ function resolveSlot(slot, index, ctx) {
5647
+ if (typeof slot === "string") {
5648
+ return getOrBuildNoop(slot);
5649
+ }
5650
+ if (isLazyMarker(slot)) {
5651
+ const c = lazyComputeds.get(index);
5652
+ return c === void 0 ? void 0 : resolveSlotResult(c.value);
5653
+ }
5654
+ if (typeof slot === "function") {
5655
+ const result = slot(ctx);
5656
+ return resolveSlotResult(result);
5657
+ }
5658
+ if (isAnyForm(slot)) return slot;
5659
+ return void 0;
5660
+ }
5661
+ function resolveSlotResult(result) {
5662
+ if (result === void 0) return void 0;
5663
+ if (typeof result === "string") {
5664
+ return getOrBuildNoop(result);
5665
+ }
5666
+ return result;
5667
+ }
5668
+ const lazyCtx = {
5669
+ forms: slotForms,
5670
+ get currentKey() {
5671
+ return activeKey.value === "" ? void 0 : activeKey.value;
5672
+ }
5673
+ };
5674
+ for (let i = 0; i < rawSteps.length; i++) {
5675
+ const slot = rawSteps[i];
5676
+ if (isLazyMarker(slot)) {
5677
+ const idx = i;
5678
+ const marker = slot;
5679
+ lazyComputeds.set(
5680
+ idx,
5681
+ vue.computed(() => {
5682
+ void lazyEpoch.value;
5683
+ return marker.resolve(lazyCtx);
5684
+ })
5685
+ );
5686
+ }
5687
+ }
5688
+ const compiledSteps = vue.computed(() => {
5689
+ const ctx = slotCtx.value;
5690
+ const out = [];
5691
+ const seen = /* @__PURE__ */ new Set();
5692
+ for (let i = 0; i < rawSteps.length; i++) {
5693
+ const slot = rawSteps[i];
5694
+ const form = resolveSlot(slot, i, ctx);
5695
+ if (form === void 0) continue;
5696
+ if (seen.has(form.key)) {
5697
+ if (paths.__DEV__) {
5698
+ console.warn(
5699
+ `[attaform] useWizard: step "${form.key}" appears in more than one slot. The wizard treats the first occurrence as canonical and drops later duplicates.`
5700
+ );
5701
+ }
5702
+ continue;
5703
+ }
5704
+ seen.add(form.key);
5705
+ trackOnce(form);
5706
+ out.push({ key: form.key, form });
5707
+ }
5708
+ return out;
5709
+ });
5710
+ const activeIndex = vue.computed(() => {
5711
+ const key = activeKey.value;
5712
+ if (key === "") return -1;
5713
+ const list = compiledSteps.value;
5714
+ for (let i = 0; i < list.length; i++) {
5715
+ if (list[i].key === key) return i;
5716
+ }
5717
+ return -1;
5718
+ });
5719
+ const currentStep = vue.computed(() => {
5720
+ const key = activeKey.value;
5721
+ if (key !== "") return key;
5722
+ const first = compiledSteps.value[0];
5723
+ return first === void 0 ? void 0 : first.key;
5724
+ });
5725
+ const activeForm = vue.computed(() => {
5726
+ const list = compiledSteps.value;
5727
+ const idx = activeIndex.value;
5728
+ if (idx >= 0 && idx < list.length) {
5729
+ return list[idx].form;
5730
+ }
5731
+ const first = list[0];
5732
+ return first === void 0 ? void 0 : first.form;
5733
+ });
5734
+ const isFinalStep = vue.computed(() => {
5735
+ const list = compiledSteps.value;
5736
+ const idx = activeIndex.value;
5737
+ return list.length > 0 && idx === list.length - 1;
5738
+ });
5739
+ const count = vue.computed(() => compiledSteps.value.length);
5740
+ const formsRecord = vue.computed(() => {
5741
+ const out = {};
5742
+ for (const step of compiledSteps.value) out[step.key] = step.form;
5743
+ return out;
5744
+ });
5745
+ const allValues = vue.computed(() => {
5746
+ const out = {};
5747
+ for (const step of compiledSteps.value) {
5748
+ const source = step.form;
5749
+ out[step.key] = source.values;
5750
+ }
5751
+ return out;
5752
+ });
5753
+ const allErrors = vue.computed(() => {
5754
+ const out = {};
5755
+ for (const step of compiledSteps.value) {
5756
+ const source = step.form;
5757
+ const list = [];
5758
+ const store = registry.forms.get(step.key);
5759
+ const resolved = store?.defaultsResolved.value === true;
5760
+ if (resolved) {
5761
+ const errors = source.meta?.errors ?? [];
5762
+ for (const err of errors) {
5763
+ const entry = {
5764
+ formKey: step.key,
5765
+ path: err.path,
5766
+ message: err.message
5767
+ };
5768
+ if (err.code !== void 0) entry.code = err.code;
5769
+ list.push(entry);
5770
+ }
5771
+ }
5772
+ out[step.key] = list;
5773
+ }
5774
+ return out;
5775
+ });
5776
+ const seedRef = vue.ref(void 0);
5777
+ const seedInput = options.defaultStatuses;
5778
+ if (seedInput !== void 0) {
5779
+ const resolved = resolveTrichotomy(seedInput);
5780
+ if (resolved.kind === "sync") {
5781
+ seedRef.value = resolved.value;
5782
+ } else {
5783
+ const eager = resolved.factory();
5784
+ if (eager instanceof Promise) {
5785
+ void eager.then((value) => {
5786
+ seedRef.value = value;
5787
+ });
5788
+ } else {
5789
+ seedRef.value = eager;
5790
+ }
5791
+ }
5792
+ }
5793
+ const statusCache = /* @__PURE__ */ new Map();
5794
+ function statusFor(form) {
5795
+ const cached = statusCache.get(form.key);
5796
+ if (cached !== void 0) return cached;
5797
+ const source = form;
5798
+ const computedStatus = vue.computed(() => {
5799
+ const store = registry.forms.get(form.key);
5800
+ const resolved = store?.defaultsResolved.value === true;
5801
+ if (resolved) {
5802
+ const meta = source.meta;
5803
+ if (meta !== void 0 && meta !== null) {
5804
+ return {
5805
+ valid: meta.valid,
5806
+ dirty: meta.dirty,
5807
+ submitted: meta.submitted,
5808
+ errorCount: meta.errorCount
5809
+ };
5810
+ }
5811
+ }
5812
+ if (noopForms.has(form.key)) return NOOP_VALID_STATUS;
5813
+ const seedMap = seedRef.value;
5814
+ if (seedMap !== void 0 && Object.hasOwn(seedMap, form.key)) {
5815
+ return seedMap[form.key];
5816
+ }
5817
+ return PENDING_STATUS;
5818
+ });
5819
+ statusCache.set(form.key, computedStatus);
5820
+ return computedStatus;
5821
+ }
5822
+ const statusesRecord = new Proxy({}, {
5823
+ get(_, key) {
5824
+ if (typeof key !== "string") return void 0;
5825
+ const form = formsRecord.value[key];
5826
+ if (form === void 0) {
5827
+ const cached = statusCache.get(key);
5828
+ if (cached !== void 0) return cached;
5829
+ return void 0;
5830
+ }
5831
+ return statusFor(form);
5832
+ },
5833
+ ownKeys() {
5834
+ return Object.keys(formsRecord.value);
5835
+ },
5836
+ has(_, key) {
5837
+ if (typeof key !== "string") return false;
5838
+ return formsRecord.value[key] !== void 0;
5839
+ },
5840
+ getOwnPropertyDescriptor(_, key) {
5841
+ if (typeof key !== "string") return void 0;
5842
+ const form = formsRecord.value[key];
5843
+ if (form === void 0) return void 0;
5844
+ return {
5845
+ configurable: true,
5846
+ enumerable: true,
5847
+ writable: false,
5848
+ value: statusFor(form)
5849
+ };
5850
+ }
5851
+ });
5852
+ const statuses = buildWizardStatusesProxy(statusesRecord);
5853
+ if (paths.__DEV__ && seedRef.value !== void 0) {
5854
+ const seedMap = seedRef.value;
5855
+ const known = new Set(compiledSteps.value.map((s) => s.key));
5856
+ const unknown = [];
5857
+ for (const key of Object.keys(seedMap)) {
5858
+ if (!known.has(key)) unknown.push(key);
5859
+ }
5860
+ if (unknown.length > 0) {
5861
+ console.warn(
5862
+ `[attaform] useWizard.defaultStatuses: seed contains unknown key(s) ${unknown.map((k) => `"${k}"`).join(", ")}. Known step keys: ${[...known].map((k) => `"${k}"`).join(", ")}.`
5863
+ );
5864
+ }
5865
+ }
5866
+ const progressOverride = options.progress;
5867
+ const progress = vue.computed(() => {
5868
+ if (progressOverride !== void 0) {
5869
+ return progressOverride(compiledSteps.value);
5870
+ }
5871
+ const list = compiledSteps.value;
5872
+ if (list.length === 0) return 0;
5873
+ let valid = 0;
5874
+ for (const step of list) {
5875
+ const status = statusFor(step.form).value;
5876
+ if (status.valid === true) valid += 1;
5877
+ }
5878
+ return valid / list.length;
5879
+ });
5880
+ const complete = vue.computed(() => {
5881
+ if (!isFinalStep.value) return false;
5882
+ for (const step of compiledSteps.value) {
5883
+ if (statusFor(step.form).value.valid !== true) return false;
5884
+ }
5885
+ return true;
5886
+ });
5887
+ const canAdvance = vue.computed(() => activeIndex.value < count.value - 1);
5888
+ const canGoBack = vue.computed(() => activeIndex.value > 0);
5889
+ const visited = vue.ref([]);
5890
+ const wantsDefaultUrlSync = options.restore !== false || options.persist !== false;
5891
+ const historyHandle = wantsDefaultUrlSync ? createWizardHistory(DEFAULT_STEP_PARAM) : NOOP_WIZARD_HISTORY;
5892
+ const injectedResolver = vue.inject(paths.kAttaformWizardActiveStepResolver, null);
5893
+ const urlMirror = vue.ref(void 0);
5894
+ const initialUrlValue = injectedResolver !== null ? injectedResolver(DEFAULT_STEP_PARAM) : historyHandle.read();
5895
+ urlMirror.value = initialUrlValue;
5896
+ historyHandle.subscribe((value) => {
5897
+ urlMirror.value = value;
5898
+ });
5899
+ const restoreCallback = options.restore === false ? void 0 : options.restore !== void 0 ? options.restore : () => {
5900
+ const value = urlMirror.value;
5901
+ return value === void 0 ? void 0 : { step: value };
5902
+ };
5903
+ const persistCallback = options.persist === false ? void 0 : options.persist !== void 0 ? options.persist : (state) => {
5904
+ if (state.step === void 0) return;
5905
+ historyHandle.replace(state.step);
5906
+ };
5907
+ function isCompiledKey(key) {
5908
+ const list = compiledSteps.value;
5909
+ for (const step of list) if (step.key === key) return true;
5910
+ return false;
5911
+ }
5912
+ function firstKey() {
5913
+ const first = compiledSteps.value[0];
5914
+ return first === void 0 ? void 0 : first.key;
5915
+ }
5916
+ let initialKey;
5917
+ const restoredAtSetup = restoreCallback?.();
5918
+ const restoredStep = restoredAtSetup?.step;
5919
+ if (restoredStep !== void 0 && isCompiledKey(restoredStep)) {
5920
+ initialKey = restoredStep;
5921
+ } else {
5922
+ if (paths.__DEV__ && restoredStep !== void 0 && restoredStep !== "" && !isCompiledKey(restoredStep)) {
5923
+ console.warn(
5924
+ `[attaform] useWizard: restore() yielded step "${restoredStep}" which is not in the compiled step list. Falling back to the first step.`
5925
+ );
5926
+ }
5927
+ initialKey = firstKey();
5928
+ }
5929
+ if (initialKey !== void 0) {
5930
+ activeKey.value = initialKey;
5931
+ visited.value = [initialKey];
5932
+ }
5933
+ if (registry.ssr) {
5934
+ for (const step of compiledSteps.value) {
5935
+ if (step.key === initialKey) {
5936
+ registry.enqueuePrefetch(step.key);
5937
+ } else {
5938
+ registry.skipPrefetch(step.key);
5939
+ }
5940
+ }
5941
+ }
5942
+ if (!registry.ssr) {
5943
+ for (const step of compiledSteps.value) {
5944
+ const source = step.form;
5945
+ if (typeof source.activate === "function") void source.activate();
5946
+ }
5947
+ }
5948
+ let lastPersisted = initialUrlValue;
5949
+ if (restoreCallback !== void 0) {
5950
+ vue.watch(
5951
+ () => restoreCallback()?.step,
5952
+ (step) => {
5953
+ if (step === void 0) return;
5954
+ if (!isCompiledKey(step)) {
5955
+ if (paths.__DEV__) {
5956
+ console.warn(
5957
+ `[attaform] useWizard: restore() yielded step "${step}" which is not in the compiled step list. Ignoring.`
5958
+ );
5959
+ }
5960
+ return;
5961
+ }
5962
+ if (step === activeKey.value) return;
5963
+ activeKey.value = step;
5964
+ if (!visited.value.includes(step)) visited.value.push(step);
5965
+ }
5966
+ );
5967
+ }
5968
+ if (persistCallback !== void 0) {
5969
+ vue.watch(
5970
+ () => activeKey.value,
5971
+ (next2) => {
5972
+ if (next2 === lastPersisted) return;
5973
+ lastPersisted = next2;
5974
+ persistCallback({ step: next2 });
5975
+ urlMirror.value = next2;
5976
+ }
5977
+ );
5978
+ if (initialKey !== void 0 && initialKey !== initialUrlValue && initialUrlValue === void 0) {
5979
+ lastPersisted = initialKey;
5980
+ persistCallback({ step: initialKey });
5981
+ urlMirror.value = initialKey;
5982
+ }
5983
+ }
5984
+ const submitting = vue.ref(false);
5985
+ const submissionAttempts = vue.ref(0);
5986
+ const done = vue.ref(false);
5987
+ function activateForm(form) {
5988
+ const source = form;
5989
+ if (typeof source.activate === "function") {
5990
+ void source.activate();
5991
+ }
5992
+ }
5993
+ function moveTo(key, options2) {
5994
+ if (activeKey.value === key) return;
5995
+ activeKey.value = key;
5996
+ if (!visited.value.includes(key)) visited.value.push(key);
5997
+ const list = compiledSteps.value;
5998
+ for (const step of list) {
5999
+ if (step.key === key) {
6000
+ activateForm(step.form);
6001
+ return;
6002
+ }
6003
+ }
6004
+ }
6005
+ function recordDeparture(key) {
6006
+ const store = registry.forms.get(key);
6007
+ if (store !== void 0) store.departAttempts.value += 1;
6008
+ }
6009
+ async function next() {
6010
+ if (submitting.value) {
6011
+ if (paths.__DEV__) {
6012
+ console.warn(
6013
+ `[attaform] wizard.next(): blocked while a submit is in flight. Wait for handleSubmit to settle.`
6014
+ );
6015
+ }
6016
+ return;
6017
+ }
6018
+ const list = compiledSteps.value;
6019
+ if (list.length === 0) {
6020
+ if (paths.__DEV__) {
6021
+ console.warn(`[attaform] wizard.next(): wizard has no compiled steps; no-op.`);
6022
+ }
6023
+ return;
6024
+ }
6025
+ const idx = activeIndex.value;
6026
+ if (idx < 0 || idx >= list.length - 1) {
6027
+ if (paths.__DEV__) {
6028
+ console.warn(
6029
+ `[attaform] wizard.next(): already on the final step ("${activeKey.value}"). Use wizard.handleSubmit() to submit.`
6030
+ );
6031
+ }
6032
+ return;
6033
+ }
6034
+ recordDeparture(activeKey.value);
6035
+ const target = list[idx + 1];
6036
+ moveTo(target.key);
6037
+ }
6038
+ function back() {
6039
+ if (submitting.value) {
6040
+ if (paths.__DEV__) {
6041
+ console.warn(`[attaform] wizard.back(): blocked while a submit is in flight.`);
6042
+ }
6043
+ return;
6044
+ }
6045
+ if (compiledSteps.value.length === 0) {
6046
+ if (paths.__DEV__) {
6047
+ console.warn(`[attaform] wizard.back(): wizard has no compiled steps; no-op.`);
6048
+ }
6049
+ return;
6050
+ }
6051
+ const idx = activeIndex.value;
6052
+ if (idx <= 0) {
6053
+ if (paths.__DEV__) {
6054
+ console.warn(`[attaform] wizard.back(): already on the first step ("${activeKey.value}").`);
6055
+ }
6056
+ return;
6057
+ }
6058
+ recordDeparture(activeKey.value);
6059
+ const target = compiledSteps.value[idx - 1];
6060
+ moveTo(target.key);
6061
+ }
6062
+ function goTo(key) {
6063
+ if (submitting.value) {
6064
+ if (paths.__DEV__) {
6065
+ console.warn(`[attaform] wizard.goTo(): blocked while a submit is in flight.`);
6066
+ }
6067
+ return;
6068
+ }
6069
+ if (!isCompiledKey(key)) {
6070
+ if (paths.__DEV__) {
6071
+ const known = compiledSteps.value.map((s) => `"${s.key}"`).join(", ");
6072
+ console.warn(`[attaform] wizard.goTo("${key}"): unknown step key. Known keys: ${known}.`);
6073
+ }
6074
+ return;
6075
+ }
6076
+ if (key !== activeKey.value) recordDeparture(activeKey.value);
6077
+ moveTo(key);
6078
+ }
6079
+ function buildSubmitContext(valuesMap, currentKey, isFinal) {
6080
+ return {
6081
+ values: valuesMap,
6082
+ get: ((form) => valuesMap[form.key]),
6083
+ currentKey,
6084
+ isFinal
6085
+ };
6086
+ }
6087
+ async function processOne(form) {
6088
+ const full = form;
6089
+ let activationFailure;
6090
+ try {
6091
+ if (typeof full.activate === "function") await full.activate();
6092
+ } catch (err) {
6093
+ activationFailure = err?.message ?? String(err);
6094
+ }
6095
+ if (activationFailure === void 0 && full.hydrateError != null) {
6096
+ activationFailure = full.hydrateError.message;
6097
+ }
6098
+ if (activationFailure !== void 0) {
6099
+ return {
6100
+ success: false,
6101
+ data: void 0,
6102
+ errors: [
6103
+ {
6104
+ formKey: form.key,
6105
+ path: [],
6106
+ message: `Form '${form.key}' failed to activate: ${activationFailure}`,
6107
+ code: AttaformErrorCode.ActivationFailed
6108
+ }
6109
+ ],
6110
+ formKey: form.key
6111
+ };
6112
+ }
6113
+ return full.process();
6114
+ }
6115
+ function collectErrors(results) {
6116
+ const out = [];
6117
+ for (const step of compiledSteps.value) {
6118
+ const processed = results.get(step.key);
6119
+ if (processed === void 0 || processed.success === true) continue;
6120
+ for (const err of processed.errors) {
6121
+ const entry = {
6122
+ formKey: err.formKey,
6123
+ path: err.path,
6124
+ message: err.message
6125
+ };
6126
+ if (err.code !== void 0) entry.code = err.code;
6127
+ out.push(entry);
6128
+ }
6129
+ }
6130
+ return out;
6131
+ }
6132
+ function handleSubmit(onSubmit, onError) {
6133
+ return async function submitHandler(event) {
6134
+ if (event !== void 0 && typeof event.preventDefault === "function") {
6135
+ event.preventDefault();
6136
+ }
6137
+ if (compiledSteps.value.length === 0) {
6138
+ if (paths.__DEV__) {
6139
+ console.warn(`[attaform] wizard.handleSubmit: wizard has no compiled steps; no-op.`);
6140
+ }
6141
+ return;
6142
+ }
6143
+ if (submitting.value) {
6144
+ if (paths.__DEV__) {
6145
+ console.warn(
6146
+ `[attaform] wizard.handleSubmit: re-entrant submit while a prior call is still in flight; resolving no-op.`
6147
+ );
6148
+ }
6149
+ return;
6150
+ }
6151
+ submitting.value = true;
6152
+ try {
6153
+ const currentKey = activeKey.value;
6154
+ const final = isFinalStep.value;
6155
+ const list = compiledSteps.value;
6156
+ const results = /* @__PURE__ */ new Map();
6157
+ if (final) {
6158
+ await Promise.all(
6159
+ list.map(async (step) => {
6160
+ const result = await processOne(step.form);
6161
+ results.set(step.key, result);
6162
+ })
6163
+ );
6164
+ } else {
6165
+ const active = activeForm.value;
6166
+ const result = await processOne(active);
6167
+ results.set(active.key, result);
6168
+ }
6169
+ for (const key of results.keys()) {
6170
+ const store = registry.forms.get(key);
6171
+ if (store !== void 0) store.submissionAttempts.value += 1;
6172
+ }
6173
+ submissionAttempts.value += 1;
6174
+ const errors = collectErrors(results);
6175
+ if (errors.length === 0) {
6176
+ const valuesMap = {};
6177
+ for (const step of list) {
6178
+ const processed = results.get(step.key);
6179
+ if (processed !== void 0 && processed.success === true) {
6180
+ valuesMap[step.key] = processed.data;
6181
+ } else {
6182
+ const source = step.form;
6183
+ valuesMap[step.key] = source.values;
6184
+ }
6185
+ }
6186
+ const ctx = buildSubmitContext(valuesMap, currentKey, final);
6187
+ await onSubmit(ctx);
6188
+ if (final) {
6189
+ done.value = true;
6190
+ } else {
6191
+ recordDeparture(currentKey);
6192
+ const idx = activeIndex.value;
6193
+ const target = list[idx + 1];
6194
+ if (target !== void 0) moveTo(target.key);
6195
+ }
6196
+ } else {
6197
+ if (onError !== void 0) await onError(errors);
6198
+ if (options.focusFirstError !== false) {
6199
+ const firstFailedKey = errors[0]?.formKey;
6200
+ if (firstFailedKey !== void 0 && isCompiledKey(firstFailedKey)) {
6201
+ moveTo(firstFailedKey);
6202
+ await vue.nextTick();
6203
+ const failedForm = formsRecord.value[firstFailedKey];
6204
+ if (failedForm !== void 0 && typeof failedForm.applyInvalidSubmitPolicy === "function") {
6205
+ failedForm.applyInvalidSubmitPolicy();
6206
+ }
6207
+ }
6208
+ }
6209
+ }
6210
+ } finally {
6211
+ submitting.value = false;
6212
+ }
6213
+ };
6214
+ }
6215
+ function reset() {
6216
+ submissionAttempts.value = 0;
6217
+ done.value = false;
6218
+ lazyEpoch.value += 1;
6219
+ for (const step of compiledSteps.value) {
6220
+ const full = step.form;
6221
+ if (typeof full.reset === "function") full.reset();
6222
+ }
6223
+ const firstStep = compiledSteps.value[0];
6224
+ if (firstStep !== void 0) {
6225
+ activeKey.value = firstStep.key;
6226
+ visited.value = [firstStep.key];
6227
+ if (persistCallback !== void 0) {
6228
+ lastPersisted = firstStep.key;
6229
+ persistCallback({ step: firstStep.key });
6230
+ }
6231
+ }
6232
+ }
6233
+ if (vue.getCurrentScope() !== void 0) {
6234
+ vue.onScopeDispose(() => {
6235
+ historyHandle.dispose();
6236
+ lazyNoopScope.stop();
6237
+ });
6238
+ }
6239
+ const explicitKey = options.key;
6240
+ const wizardKey = resolveWizardKey(explicitKey);
6241
+ const handle = {
6242
+ key: wizardKey,
6243
+ next,
6244
+ back,
6245
+ goTo,
6246
+ handleSubmit,
6247
+ reset,
6248
+ get currentStep() {
6249
+ return currentStep.value;
6250
+ },
6251
+ get activeForm() {
6252
+ return activeForm.value;
6253
+ },
6254
+ get activeIndex() {
6255
+ return activeIndex.value;
6256
+ },
6257
+ get isFinalStep() {
6258
+ return isFinalStep.value;
6259
+ },
6260
+ get steps() {
6261
+ return compiledSteps.value;
6262
+ },
6263
+ get forms() {
6264
+ return formsRecord.value;
6265
+ },
6266
+ get count() {
6267
+ return count.value;
6268
+ },
6269
+ statuses,
6270
+ get allValues() {
6271
+ return allValues.value;
6272
+ },
6273
+ get allErrors() {
6274
+ return allErrors.value;
6275
+ },
6276
+ get progress() {
6277
+ return progress.value;
6278
+ },
6279
+ get canAdvance() {
6280
+ return canAdvance.value;
6281
+ },
6282
+ get canGoBack() {
6283
+ return canGoBack.value;
6284
+ },
6285
+ get complete() {
6286
+ return complete.value;
6287
+ },
6288
+ get done() {
6289
+ return done.value;
6290
+ },
6291
+ get submitting() {
6292
+ return submitting.value;
6293
+ },
6294
+ get submissionAttempts() {
6295
+ return submissionAttempts.value;
6296
+ },
6297
+ get visited() {
6298
+ return visited.value;
6299
+ }
6300
+ };
6301
+ const existing = registry.wizards.get(wizardKey);
6302
+ if (existing === void 0) {
6303
+ registry.wizards.set(wizardKey, handle);
6304
+ } else if (paths.__DEV__ && explicitKey !== void 0) {
6305
+ console.warn(
6306
+ `[attaform] useWizard({ key: "${wizardKey}" }): a wizard with this key is already registered. Keeping the existing handle. Pass a unique key to each useWizard call, or share the original handle via injectWizard("${wizardKey}").`
6307
+ );
6308
+ }
6309
+ if (vue.getCurrentScope() !== void 0) {
6310
+ const releaseWizard = registry.trackWizardConsumer(wizardKey);
6311
+ vue.onScopeDispose(releaseWizard);
6312
+ }
6313
+ if (vue.getCurrentInstance() !== null && explicitKey === void 0) {
6314
+ recordAmbientWizardProvide(registry.ssr);
6315
+ vue.provide(paths.kAttaformAncestorWizard, handle);
6316
+ }
6317
+ return handle;
6318
+ }
6319
+ let anonWizardCounter = 0;
6320
+ const ambientWizardProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
6321
+ function recordAmbientWizardProvide(ssr) {
6322
+ if (!paths.__DEV__ || ssr || ambientWizardProvideHistory === null) return;
6323
+ const instance = vue.getCurrentInstance();
6324
+ if (instance === null) return;
6325
+ const instanceKey = instance;
6326
+ const entry = {
6327
+ source: paths.captureUserCallSite()
6328
+ };
6329
+ const existing = ambientWizardProvideHistory.get(instanceKey);
6330
+ if (existing === void 0) {
6331
+ ambientWizardProvideHistory.set(instanceKey, [entry]);
6332
+ return;
6333
+ }
6334
+ existing.push(entry);
6335
+ }
6336
+ function resolveWizardKey(key) {
6337
+ if (key !== void 0 && key !== null && key !== "") return key;
6338
+ if (vue.getCurrentInstance() !== null) {
6339
+ return `${ANONYMOUS_WIZARD_KEY_PREFIX}${vue.useId()}`;
6340
+ }
6341
+ return `${ANONYMOUS_WIZARD_KEY_PREFIX}${anonWizardCounter++}`;
6342
+ }
6343
+ function isAnyForm(value) {
6344
+ if (value === null || typeof value !== "object") return false;
6345
+ if (typeof value.key !== "string") return false;
6346
+ return true;
6347
+ }
6348
+
6349
+ function injectWizard(input) {
6350
+ const key = typeof input === "string" ? input : input?.key;
6351
+ const instance = vue.getCurrentInstance();
6352
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
6353
+ const registry = paths.useRegistry();
6354
+ if (key !== void 0) {
6355
+ const handle = registry.wizards.get(key);
6356
+ if (handle === void 0) {
6357
+ warnMiss(
6358
+ `no wizard registered for key '${key}'`,
6359
+ registry.ssr,
6360
+ availableKeysHint(registry.wizards)
6361
+ );
6362
+ return null;
6363
+ }
6364
+ if (vue.getCurrentScope() !== void 0) {
6365
+ const release = registry.trackWizardConsumer(key);
6366
+ vue.onScopeDispose(release);
6367
+ }
6368
+ return handle;
6369
+ }
6370
+ const ambient = vue.inject(paths.kAttaformAncestorWizard, null);
6371
+ if (ambient === null) return null;
6372
+ warnIfAmbientWizardProviderHadDuplicates();
6373
+ return ambient;
6374
+ }
6375
+ function availableKeysHint(wizards) {
6376
+ if (wizards.size === 0) return void 0;
6377
+ const keys = [...wizards.keys()].map((k) => `"${k}"`).join(", ");
6378
+ return `Registered keys: ${keys}.`;
6379
+ }
6380
+ function warnMiss(detail, ssr, hint) {
6381
+ if (!paths.__DEV__ || ssr) return;
6382
+ const frame = paths.captureUserCallSite();
6383
+ const parts = [`[attaform] injectWizard: ${detail}. Returning null.`];
6384
+ if (hint !== void 0) parts.push(hint);
6385
+ if (frame !== void 0) parts.push(frame);
6386
+ console.warn(parts.join(" "));
6387
+ }
6388
+ function warnIfAmbientWizardProviderHadDuplicates() {
6389
+ if (!paths.__DEV__ || ambientWizardProvideHistory === null) return;
6390
+ let ancestor = vue.getCurrentInstance()?.parent ?? null;
6391
+ while (ancestor !== null) {
6392
+ const history = ambientWizardProvideHistory.get(ancestor);
6393
+ if (history !== void 0) {
6394
+ if (history.length > 1) {
6395
+ const lines = history.map((entry) => ` - ${entry.source ?? "<unknown location>"}`);
6396
+ console.warn(
6397
+ "[attaform] injectWizard() (no key) resolved against an ancestor with multiple anonymous useWizard() calls; descendants only see the last-provided wizard. Anonymous useWizard() calls were:\n" + lines.join("\n") + '\nFix: pass a key to each call (e.g. useWizard({ steps, key: "x" })) and reach them via injectWizard("x"), or split the wizards across separate components.'
6398
+ );
6399
+ }
6400
+ return;
6401
+ }
6402
+ ancestor = ancestor.parent;
6403
+ }
6404
+ }
6405
+
4821
6406
  exports.AttaformErrorCode = AttaformErrorCode;
4822
6407
  exports.defaultCoercionRules = defaultCoercionRules;
4823
6408
  exports.defaultShouldShowErrors = defaultShouldShowErrors;
@@ -4825,11 +6410,14 @@ exports.defineCoercion = defineCoercion;
4825
6410
  exports.getAtPath = getAtPath;
4826
6411
  exports.humanize = humanize;
4827
6412
  exports.injectForm = injectForm;
6413
+ exports.injectWizard = injectWizard;
4828
6414
  exports.isPlainRecord = isPlainRecord;
4829
6415
  exports.isUnset = isUnset;
6416
+ exports.lazy = lazy;
4830
6417
  exports.normalizeNumericOption = normalizeNumericOption;
4831
6418
  exports.setAtPath = setAtPath;
4832
6419
  exports.slimKindOf = slimKindOf;
4833
6420
  exports.unset = unset;
4834
6421
  exports.useAbstractForm = useAbstractForm;
4835
- //# sourceMappingURL=attaform.Cer8JO_P.cjs.map
6422
+ exports.useWizard = useWizard;
6423
+ //# sourceMappingURL=attaform.II89Pcf4.cjs.map