attaform 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/chunks/devtools.cjs +3 -3
  2. package/dist/chunks/devtools.cjs.map +1 -1
  3. package/dist/chunks/devtools.mjs +3 -3
  4. package/dist/chunks/devtools.mjs.map +1 -1
  5. package/dist/chunks/indexeddb.cjs +1 -1
  6. package/dist/chunks/indexeddb.mjs +1 -1
  7. package/dist/chunks/local-storage.cjs +1 -1
  8. package/dist/chunks/local-storage.mjs +1 -1
  9. package/dist/chunks/session-storage.cjs +1 -1
  10. package/dist/chunks/session-storage.mjs +1 -1
  11. package/dist/index.cjs +5 -4
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +4 -4
  14. package/dist/index.d.mts +4 -4
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.mjs +6 -6
  17. package/dist/nuxt.d.cts +1 -1
  18. package/dist/nuxt.d.mts +1 -1
  19. package/dist/nuxt.d.ts +1 -1
  20. package/dist/runtime/plugins/attaform.cjs +1 -1
  21. package/dist/runtime/plugins/attaform.mjs +1 -1
  22. package/dist/shared/{attaform.DDXrY-1Q.d.mts → attaform.0Gxd_OOx.d.cts} +558 -174
  23. package/dist/shared/{attaform.DDXrY-1Q.d.ts → attaform.0Gxd_OOx.d.mts} +558 -174
  24. package/dist/shared/{attaform.DDXrY-1Q.d.cts → attaform.0Gxd_OOx.d.ts} +558 -174
  25. package/dist/shared/{attaform.xKWYHMdq.cjs → attaform.BOi138GE.cjs} +10 -2
  26. package/dist/shared/{attaform.xKWYHMdq.cjs.map → attaform.BOi138GE.cjs.map} +1 -1
  27. package/dist/shared/{attaform.CRgix6_n.cjs → attaform.BgYBU8gV.cjs} +18 -17
  28. package/dist/shared/attaform.BgYBU8gV.cjs.map +1 -0
  29. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  30. package/dist/shared/{attaform.CNJO3mME.cjs → attaform.CDJVeoJU.cjs} +633 -236
  31. package/dist/shared/attaform.CDJVeoJU.cjs.map +1 -0
  32. package/dist/shared/{attaform.DlgKK10S.mjs → attaform.CRk8NhlD.mjs} +18 -17
  33. package/dist/shared/attaform.CRk8NhlD.mjs.map +1 -0
  34. package/dist/shared/{attaform.CXZgUECn.d.cts → attaform.CVv9Oh0a.d.mts} +41 -9
  35. package/dist/shared/{attaform.BYc9kugA.d.ts → attaform.CWCx2r0x.d.ts} +41 -9
  36. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  37. package/dist/shared/{attaform.Cc93zNzD.mjs → attaform.DXye3JKf.mjs} +10 -3
  38. package/dist/shared/{attaform.Cc93zNzD.mjs.map → attaform.DXye3JKf.mjs.map} +1 -1
  39. package/dist/shared/{attaform.DOKOyb3Y.d.mts → attaform.Dq5BabH1.d.cts} +41 -9
  40. package/dist/shared/{attaform.B5GWYl76.cjs → attaform.RypIkgVy.cjs} +38 -7
  41. package/dist/shared/attaform.RypIkgVy.cjs.map +1 -0
  42. package/dist/shared/{attaform.al_rpt7_.mjs → attaform.a99dQV7Q.mjs} +39 -8
  43. package/dist/shared/attaform.a99dQV7Q.mjs.map +1 -0
  44. package/dist/shared/{attaform.BRTxpA3q.mjs → attaform.qxyip_aN.mjs} +634 -238
  45. package/dist/shared/attaform.qxyip_aN.mjs.map +1 -0
  46. package/dist/transforms.d.cts +2 -2
  47. package/dist/transforms.d.mts +2 -2
  48. package/dist/transforms.d.ts +2 -2
  49. package/dist/zod-v3.cjs +55 -3
  50. package/dist/zod-v3.cjs.map +1 -1
  51. package/dist/zod-v3.d.cts +77 -4
  52. package/dist/zod-v3.d.mts +77 -4
  53. package/dist/zod-v3.d.ts +77 -4
  54. package/dist/zod-v3.mjs +56 -6
  55. package/dist/zod-v3.mjs.map +1 -1
  56. package/dist/zod.cjs +372 -5
  57. package/dist/zod.cjs.map +1 -1
  58. package/dist/zod.d.cts +120 -4
  59. package/dist/zod.d.mts +120 -4
  60. package/dist/zod.d.ts +120 -4
  61. package/dist/zod.mjs +371 -8
  62. package/dist/zod.mjs.map +1 -1
  63. package/package.json +3 -1
  64. package/dist/shared/attaform.B5GWYl76.cjs.map +0 -1
  65. package/dist/shared/attaform.BRTxpA3q.mjs.map +0 -1
  66. package/dist/shared/attaform.CNJO3mME.cjs.map +0 -1
  67. package/dist/shared/attaform.CRgix6_n.cjs.map +0 -1
  68. package/dist/shared/attaform.DlgKK10S.mjs.map +0 -1
  69. package/dist/shared/attaform.al_rpt7_.mjs.map +0 -1
@@ -1,6 +1,173 @@
1
- import { computed, ref, watchEffect, getCurrentScope, onScopeDispose, readonly, reactive, shallowRef, provide, getCurrentInstance, useId, toRaw, inject } from 'vue';
2
- import { c as canonicalizePath, s as segmentsForPathKey } from './attaform.Cc93zNzD.mjs';
3
- import { _ as __DEV__, c as SubmitErrorHandlerError, A as AnonPersistError, m as captureUserCallSite, j as enforceSensitiveCheck, n as createPersistOptInRegistry, e as useRegistry, o as kFormContext, p as kFormInstanceId, b as ReservedFormKeyError } from './attaform.al_rpt7_.mjs';
1
+ import { computed, ref, watchEffect, getCurrentScope, onScopeDispose, shallowReadonly, readonly, reactive, watch, markRaw, shallowRef, provide, getCurrentInstance, useId, toRaw, inject } from 'vue';
2
+ import { s as segmentsForPathKey, i as isPathPrefix, c as canonicalizePath, a as ROOT_PATH_KEY } from './attaform.DXye3JKf.mjs';
3
+ import { _ as __DEV__, c as SubmitErrorHandlerError, A as AnonPersistError, m as captureUserCallSite, j as enforceSensitiveCheck, n as createPersistOptInRegistry, e as useRegistry, o as kFormContext, p as kFormInstanceId, b as ReservedFormKeyError } from './attaform.a99dQV7Q.mjs';
4
+
5
+ function isDescendable(value) {
6
+ if (value === null || typeof value !== "object") return false;
7
+ if (Array.isArray(value)) return true;
8
+ const proto = Object.getPrototypeOf(value);
9
+ return proto === null || proto === Object.prototype;
10
+ }
11
+ function appendSegment(prefix, segment) {
12
+ const next = new Array(prefix.length + 1);
13
+ for (let i = 0; i < prefix.length; i++) {
14
+ const s = prefix[i];
15
+ next[i] = s;
16
+ }
17
+ next[prefix.length] = segment;
18
+ return next;
19
+ }
20
+ function diffAndApply(oldValue, newValue, prefix, visit) {
21
+ if (Object.is(oldValue, newValue)) return;
22
+ const oldIsDescendable = isDescendable(oldValue);
23
+ const newIsDescendable = isDescendable(newValue);
24
+ if (oldValue === void 0 && newIsDescendable) {
25
+ if (Array.isArray(newValue)) {
26
+ for (let i = 0; i < newValue.length; i++) {
27
+ diffAndApply(void 0, newValue[i], appendSegment(prefix, i), visit);
28
+ }
29
+ } else {
30
+ const rec = newValue;
31
+ for (const k of Object.keys(rec)) {
32
+ diffAndApply(void 0, rec[k], appendSegment(prefix, k), visit);
33
+ }
34
+ }
35
+ return;
36
+ }
37
+ if (oldIsDescendable && newValue === void 0) {
38
+ if (Array.isArray(oldValue)) {
39
+ for (let i = 0; i < oldValue.length; i++) {
40
+ diffAndApply(oldValue[i], void 0, appendSegment(prefix, i), visit);
41
+ }
42
+ } else {
43
+ const rec = oldValue;
44
+ for (const k of Object.keys(rec)) {
45
+ diffAndApply(rec[k], void 0, appendSegment(prefix, k), visit);
46
+ }
47
+ }
48
+ return;
49
+ }
50
+ if (oldIsDescendable && newIsDescendable) {
51
+ const oldIsArray = Array.isArray(oldValue);
52
+ const newIsArray = Array.isArray(newValue);
53
+ if (oldIsArray && newIsArray) {
54
+ const oldArr = oldValue;
55
+ const newArr = newValue;
56
+ const max = Math.max(oldArr.length, newArr.length);
57
+ for (let i = 0; i < max; i++) {
58
+ diffAndApply(oldArr[i], newArr[i], appendSegment(prefix, i), visit);
59
+ }
60
+ return;
61
+ }
62
+ if (!oldIsArray && !newIsArray) {
63
+ const oldRec = oldValue;
64
+ const newRec = newValue;
65
+ const seen = /* @__PURE__ */ new Set();
66
+ for (const k of Object.keys(oldRec)) {
67
+ seen.add(k);
68
+ diffAndApply(oldRec[k], newRec[k], appendSegment(prefix, k), visit);
69
+ }
70
+ for (const k of Object.keys(newRec)) {
71
+ if (seen.has(k)) continue;
72
+ diffAndApply(oldRec[k], newRec[k], appendSegment(prefix, k), visit);
73
+ }
74
+ return;
75
+ }
76
+ visit({ kind: "changed", path: prefix, oldValue, newValue });
77
+ return;
78
+ }
79
+ if (oldIsDescendable && !newIsDescendable) {
80
+ visit({ kind: "changed", path: prefix, oldValue, newValue });
81
+ return;
82
+ }
83
+ if (!oldIsDescendable && newIsDescendable) {
84
+ visit({ kind: "changed", path: prefix, oldValue, newValue });
85
+ return;
86
+ }
87
+ if (oldValue === void 0) {
88
+ visit({ kind: "added", path: prefix, newValue });
89
+ return;
90
+ }
91
+ if (newValue === void 0) {
92
+ visit({ kind: "removed", path: prefix, oldValue });
93
+ return;
94
+ }
95
+ visit({ kind: "changed", path: prefix, oldValue, newValue });
96
+ }
97
+ function applyChangedKeys(target, source) {
98
+ if (!isDescendable(target) || !isDescendable(source)) return false;
99
+ const targetIsArray = Array.isArray(target);
100
+ const sourceIsArray = Array.isArray(source);
101
+ if (targetIsArray !== sourceIsArray) return false;
102
+ const ROOT_SENTINEL = Symbol.for("attaform.applyChangedKeys.rootMismatch");
103
+ const changedFirstSegments = /* @__PURE__ */ new Set();
104
+ diffAndApply(target, source, [], (patch) => {
105
+ if (patch.path.length === 0) {
106
+ changedFirstSegments.add(ROOT_SENTINEL);
107
+ return;
108
+ }
109
+ changedFirstSegments.add(patch.path[0]);
110
+ });
111
+ if (changedFirstSegments.has(ROOT_SENTINEL)) return false;
112
+ if (targetIsArray) {
113
+ const t = target;
114
+ const s = source;
115
+ if (t.length > s.length) t.length = s.length;
116
+ for (const idx of changedFirstSegments) {
117
+ if (typeof idx === "symbol") continue;
118
+ const i = typeof idx === "number" ? idx : Number(idx);
119
+ t[i] = s[i];
120
+ }
121
+ } else {
122
+ const t = target;
123
+ const s = source;
124
+ const sourceKeys = new Set(Object.keys(s));
125
+ for (const k of Object.keys(t)) {
126
+ if (!sourceKeys.has(k)) delete t[k];
127
+ }
128
+ for (const k of changedFirstSegments) {
129
+ if (typeof k === "symbol") continue;
130
+ t[String(k)] = s[String(k)];
131
+ }
132
+ }
133
+ return true;
134
+ }
135
+ function structuralSnapshot(value) {
136
+ if (!isDescendable(value)) return value;
137
+ if (Array.isArray(value)) {
138
+ const out2 = new Array(value.length);
139
+ for (let i = 0; i < value.length; i++) {
140
+ out2[i] = structuralSnapshot(value[i]);
141
+ }
142
+ return out2;
143
+ }
144
+ const src = value;
145
+ const out = {};
146
+ for (const k of Object.keys(src)) {
147
+ out[k] = structuralSnapshot(src[k]);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ const EMPTY_RESOLVED_FIELD_META = Object.freeze({
153
+ label: "",
154
+ description: void 0,
155
+ placeholder: void 0,
156
+ meta: Object.freeze({})
157
+ });
158
+
159
+ function humanize(segment) {
160
+ if (typeof segment === "number") return "";
161
+ const str = String(segment);
162
+ if (str.length === 0) return "";
163
+ if (/^\d+$/.test(str)) return "";
164
+ const tokens = str.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().split(" ").filter((part) => part.length > 0);
165
+ if (tokens.length === 0) return "";
166
+ return tokens.map((part) => {
167
+ const head = part[0];
168
+ return head === void 0 ? part : head.toUpperCase() + part.slice(1).toLowerCase();
169
+ }).join(" ");
170
+ }
4
171
 
5
172
  const NOT_FOUND = Symbol("NOT_FOUND");
6
173
  function descendStep(value, segment) {
@@ -246,6 +413,153 @@ function setAtPathWithSchemaFillImpl(root, schema, fullPath, value, startIdx) {
246
413
  return rec;
247
414
  }
248
415
 
416
+ function buildFieldStateAccessor(state) {
417
+ const cache = /* @__PURE__ */ new Map();
418
+ return function getFieldState(pathInput) {
419
+ const { segments, key } = canonicalizePath(pathInput);
420
+ const cached = cache.get(key);
421
+ if (cached !== void 0) return cached;
422
+ const c = computed(
423
+ () => state.schema.isLeafAtPath(segments) ? buildLeafFieldState(state, segments, key) : buildContainerFieldState(state, segments)
424
+ );
425
+ cache.set(key, c);
426
+ return c;
427
+ };
428
+ }
429
+ function buildLeafFieldState(state, segments, key) {
430
+ const record = state.fields.get(key);
431
+ const value = state.getValueAtPath(segments);
432
+ const original = state.originals.get(key)?.value;
433
+ const pristine = state.isPristineAtPath(segments);
434
+ const schemaForKey = state.schemaErrors.get(key);
435
+ const blankForKey = state.derivedBlankErrors.value.get(key);
436
+ const userForKey = state.userErrors.get(key);
437
+ const errors = [];
438
+ if (schemaForKey !== void 0) errors.push(...schemaForKey);
439
+ if (blankForKey !== void 0) errors.push(...blankForKey);
440
+ if (userForKey !== void 0) errors.push(...userForKey);
441
+ const validating = (state.fieldValidationCounts.get(key) ?? 0) > 0;
442
+ const gated = state.pathHasAsyncValidation(segments) && !state.firstValidationDone.value;
443
+ const valid = !gated && errors.length === 0 && !validating;
444
+ const elementRecord = state.elements.get(key);
445
+ const elementsArr = elementRecord ? Object.freeze([...elementRecord.elements]) : EMPTY_ELEMENTS;
446
+ const firstElement = elementsArr[0] ?? null;
447
+ const resolved = state.schema.getFieldMetaAtPath ? state.schema.getFieldMetaAtPath(segments) : EMPTY_RESOLVED_FIELD_META;
448
+ const lastSegment = segments.length === 0 ? "" : segments[segments.length - 1] ?? "";
449
+ const label = resolved.label || humanize(lastSegment);
450
+ return {
451
+ value,
452
+ original,
453
+ pristine,
454
+ dirty: !pristine,
455
+ focused: record?.focused ?? null,
456
+ blurred: record?.blurred ?? null,
457
+ touched: record?.touched ?? null,
458
+ connected: record?.connected ?? false,
459
+ element: firstElement,
460
+ elements: elementsArr,
461
+ updatedAt: record?.updatedAt ?? null,
462
+ errors,
463
+ validating,
464
+ valid,
465
+ path: segments,
466
+ blank: state.blankPaths.has(key),
467
+ label,
468
+ description: resolved.description,
469
+ placeholder: resolved.placeholder,
470
+ meta: resolved.meta
471
+ };
472
+ }
473
+ function buildContainerFieldState(state, segments, _key) {
474
+ const formValue = state.form.value;
475
+ const value = state.getValueAtPath(segments);
476
+ const original = state.originals.get(canonicalizePath(segments).key)?.value;
477
+ let pristine = true;
478
+ let blank = true;
479
+ let dirty = false;
480
+ let focused = false;
481
+ let blurred = false;
482
+ let touched = false;
483
+ let connected = false;
484
+ let validating = false;
485
+ let updatedAt = null;
486
+ let asyncPending = false;
487
+ for (const [, entry] of state.originals) {
488
+ if (!isPathPrefix(segments, entry.segments)) continue;
489
+ if (segments.length === entry.segments.length) continue;
490
+ if (!hasAtPath(formValue, entry.segments)) continue;
491
+ const leafKey = canonicalizePath(entry.segments).key;
492
+ const leafRecord = state.fields.get(leafKey);
493
+ if (!state.isPristineAtPath(entry.segments)) {
494
+ pristine = false;
495
+ dirty = true;
496
+ }
497
+ if (!state.blankPaths.has(leafKey)) blank = false;
498
+ if (leafRecord?.focused === true) focused = true;
499
+ if (leafRecord?.blurred === true) blurred = true;
500
+ if (leafRecord?.touched === true) touched = true;
501
+ if (leafRecord?.connected === true) connected = true;
502
+ if ((state.fieldValidationCounts.get(leafKey) ?? 0) > 0) validating = true;
503
+ if (state.pathHasAsyncValidation(entry.segments)) asyncPending = true;
504
+ const ts = leafRecord?.updatedAt;
505
+ if (ts !== void 0 && ts !== null) {
506
+ if (updatedAt === null || ts > updatedAt) updatedAt = ts;
507
+ }
508
+ }
509
+ const errors = aggregateErrorsAt(state, segments);
510
+ if (!asyncPending && state.pathHasAsyncValidation(segments)) asyncPending = true;
511
+ const gated = asyncPending && !state.firstValidationDone.value;
512
+ const valid = !gated && errors.length === 0 && !validating;
513
+ const resolved = state.schema.getFieldMetaAtPath ? state.schema.getFieldMetaAtPath(segments) : EMPTY_RESOLVED_FIELD_META;
514
+ const lastSegment = segments.length === 0 ? "" : segments[segments.length - 1] ?? "";
515
+ const label = resolved.label || humanize(lastSegment);
516
+ return {
517
+ value,
518
+ original,
519
+ pristine,
520
+ dirty,
521
+ focused,
522
+ blurred,
523
+ touched,
524
+ connected,
525
+ element: null,
526
+ elements: EMPTY_ELEMENTS,
527
+ updatedAt,
528
+ errors,
529
+ validating,
530
+ valid,
531
+ path: segments,
532
+ blank,
533
+ label,
534
+ description: resolved.description,
535
+ placeholder: resolved.placeholder,
536
+ meta: resolved.meta
537
+ };
538
+ }
539
+ function aggregateErrorsAt(state, prefix) {
540
+ const formValue = state.form.value;
541
+ const buckets = /* @__PURE__ */ new Map();
542
+ const collect = (errs) => {
543
+ for (const [pathKey, list] of errs) {
544
+ if (list.length === 0) continue;
545
+ const segs = segmentsForPathKey(pathKey);
546
+ if (segs === null) continue;
547
+ if (!isPathPrefix(prefix, segs)) continue;
548
+ if (segs.length > 0 && !hasAtPath(formValue, segs)) continue;
549
+ const ordinal = state.ensurePathOrdinal(pathKey);
550
+ const existing = buckets.get(ordinal);
551
+ if (existing === void 0) buckets.set(ordinal, [...list]);
552
+ else existing.push(...list);
553
+ }
554
+ };
555
+ collect(state.schemaErrors);
556
+ collect(state.derivedBlankErrors.value);
557
+ collect(state.userErrors);
558
+ if (buckets.size === 0) return [];
559
+ return [...buckets.entries()].sort(([a], [b]) => a - b).flatMap(([, errs]) => errs);
560
+ }
561
+ const EMPTY_ELEMENTS = Object.freeze([]);
562
+
249
563
  const INTEGER_SEGMENT = /^(?:0|[1-9]\d*)$/;
250
564
  function keyToSegment(key) {
251
565
  return INTEGER_SEGMENT.test(key) ? Number(key) : key;
@@ -270,11 +584,6 @@ function buildSurfaceProxy(opts) {
270
584
  }
271
585
  return containerProxyAt(segs);
272
586
  }
273
- function navigateTo(input) {
274
- if (input === void 0) return rootProxy;
275
- const { segments } = canonicalizePath(input);
276
- return descendOrTerminate(segments);
277
- }
278
587
  function containerProxyAt(segments) {
279
588
  const cacheKey = JSON.stringify(segments);
280
589
  const existing = containerCache.get(cacheKey);
@@ -291,8 +600,9 @@ function buildSurfaceProxy(opts) {
291
600
  const proxy = new Proxy(target, {
292
601
  apply(_, __, args) {
293
602
  const arg = args[0];
294
- if (arg === void 0) return proxy;
295
- return navigateTo(arg);
603
+ if (arg === void 0) return opts.resolveCallTarget(segments);
604
+ const { segments: argSegs } = canonicalizePath(arg);
605
+ return opts.resolveCallTarget(argSegs);
296
606
  },
297
607
  get(_, key) {
298
608
  if (typeof key === "symbol") {
@@ -354,8 +664,9 @@ function buildSurfaceProxy(opts) {
354
664
  const proxy = new Proxy(target, {
355
665
  apply(_, __, args) {
356
666
  const arg = args[0];
357
- if (arg === void 0) return opts.resolveLeaf(segments);
358
- return navigateTo(arg);
667
+ if (arg === void 0) return opts.resolveCallTarget(segments);
668
+ const { segments: argSegs } = canonicalizePath(arg);
669
+ return opts.resolveCallTarget(argSegs);
359
670
  },
360
671
  get(_, key) {
361
672
  if (typeof key === "symbol") {
@@ -378,9 +689,8 @@ function buildSurfaceProxy(opts) {
378
689
  return true;
379
690
  },
380
691
  // Iteration: leaf-views expose the leaf-key set so
381
- // `JSON.stringify(form.fields.email)` produces the expected
382
- // FieldStateView snapshot (matching the legacy
383
- // `JSON.stringify(form.getFieldState('email').value)` shape).
692
+ // `JSON.stringify(form.fields.email)` produces a FieldState
693
+ // snapshot rather than the function-target placeholder.
384
694
  ownKeys: () => Array.from(leafKeys),
385
695
  getOwnPropertyDescriptor(_, key) {
386
696
  if (typeof key !== "string") return void 0;
@@ -421,7 +731,20 @@ function buildErrorsProxy(state) {
421
731
  },
422
732
  // No leafKeys — at a leaf, the resolved value (the merged array or
423
733
  // undefined) IS the terminal.
424
- materializeContainer: (segments) => materializeErrors(state, segments)
734
+ materializeContainer: (segments) => materializeErrors(state, segments),
735
+ // Call-form aggregates: `form.errors(path)` returns a single
736
+ // `ValidationError[]` for any depth (leaf or container) — same
737
+ // shared `aggregateErrorsAt` helper that `form.meta.errors` and
738
+ // `form.fields(path).errors` use, so the three surfaces never
739
+ // drift. Empty results return `undefined`, matching the leaf
740
+ // proxy's pre-existing semantic (`form.errors.email === undefined`
741
+ // when valid) so consumer code that branches on truthiness keeps
742
+ // working — the call-form just extends that semantic to
743
+ // containers and dynamic paths.
744
+ resolveCallTarget: (path) => {
745
+ const errs = aggregateErrorsAt(state, path);
746
+ return errs.length === 0 ? void 0 : errs;
747
+ }
425
748
  });
426
749
  }
427
750
  function materializeErrors(state, containerSegments) {
@@ -528,39 +851,6 @@ function buildFieldArrayApi(state) {
528
851
  };
529
852
  }
530
853
 
531
- function buildFieldStateAccessor(state) {
532
- return function getFieldState(pathInput) {
533
- const { segments, key } = canonicalizePath(pathInput);
534
- return computed(() => {
535
- const record = state.fields.get(key);
536
- const value = state.getValueAtPath(segments);
537
- const original = state.originals.get(key)?.value;
538
- const pristine = state.isPristineAtPath(segments);
539
- const schemaForKey = state.schemaErrors.get(key);
540
- const blankForKey = state.derivedBlankErrors.value.get(key);
541
- const userForKey = state.userErrors.get(key);
542
- const errors = [];
543
- if (schemaForKey !== void 0) errors.push(...schemaForKey);
544
- if (blankForKey !== void 0) errors.push(...blankForKey);
545
- if (userForKey !== void 0) errors.push(...userForKey);
546
- return {
547
- value,
548
- original,
549
- pristine,
550
- dirty: !pristine,
551
- focused: record?.focused ?? null,
552
- blurred: record?.blurred ?? null,
553
- touched: record?.touched ?? null,
554
- isConnected: record?.isConnected ?? false,
555
- updatedAt: record?.updatedAt ?? null,
556
- errors,
557
- path: segments,
558
- blank: state.blankPaths.has(key)
559
- };
560
- });
561
- };
562
- }
563
-
564
854
  const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
565
855
  "value",
566
856
  "original",
@@ -569,11 +859,19 @@ const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
569
859
  "focused",
570
860
  "blurred",
571
861
  "touched",
572
- "isConnected",
862
+ "connected",
863
+ "element",
864
+ "elements",
573
865
  "updatedAt",
574
866
  "errors",
867
+ "validating",
868
+ "valid",
575
869
  "path",
576
- "blank"
870
+ "blank",
871
+ "label",
872
+ "description",
873
+ "placeholder",
874
+ "meta"
577
875
  ]);
578
876
  function buildFieldStateProxy(state) {
579
877
  const getFieldStateAt = buildFieldStateAccessor(state);
@@ -583,12 +881,61 @@ function buildFieldStateProxy(state) {
583
881
  for (const k of FIELD_STATE_KEYS) snapshot[k] = view[k];
584
882
  return snapshot;
585
883
  };
884
+ const terminalCache = /* @__PURE__ */ new Map();
885
+ function fieldStateTerminalAt(segments) {
886
+ const cacheKey = JSON.stringify(segments);
887
+ const existing = terminalCache.get(cacheKey);
888
+ if (existing !== void 0) return existing;
889
+ const target = (() => {
890
+ });
891
+ const proxy = new Proxy(target, {
892
+ get(_, key) {
893
+ if (typeof key === "symbol") {
894
+ if (key === Symbol.toPrimitive) {
895
+ return (hint) => hint === "number" ? NaN : JSON.stringify(snapshotFieldStateAt(segments));
896
+ }
897
+ return Reflect.get(target, key);
898
+ }
899
+ if (typeof key !== "string") return void 0;
900
+ if (key === "toJSON") return () => snapshotFieldStateAt(segments);
901
+ if (key === "toString") return () => JSON.stringify(snapshotFieldStateAt(segments));
902
+ if (key === "valueOf")
903
+ return function() {
904
+ return this;
905
+ };
906
+ if (FIELD_STATE_KEYS.has(key)) {
907
+ const computed = getFieldStateAt(segments);
908
+ return computed.value[key];
909
+ }
910
+ return void 0;
911
+ },
912
+ has: (_, key) => typeof key === "string" && FIELD_STATE_KEYS.has(key),
913
+ ownKeys: () => Array.from(FIELD_STATE_KEYS),
914
+ getOwnPropertyDescriptor(_, key) {
915
+ if (typeof key !== "string") return void 0;
916
+ if (!FIELD_STATE_KEYS.has(key)) return void 0;
917
+ const computed = getFieldStateAt(segments);
918
+ return {
919
+ configurable: true,
920
+ enumerable: true,
921
+ value: computed.value[key],
922
+ writable: false
923
+ };
924
+ },
925
+ set: () => false,
926
+ deleteProperty: () => false,
927
+ defineProperty: () => false
928
+ });
929
+ terminalCache.set(cacheKey, proxy);
930
+ return proxy;
931
+ }
586
932
  return buildSurfaceProxy({
587
933
  schema: state.schema,
588
934
  resolveLeaf: (path) => getFieldStateAt(path),
589
935
  leafKeys: FIELD_STATE_KEYS,
590
936
  readLeafKey: (computed, key) => computed.value[key],
591
- materializeContainer: (segments) => materializeFields(state, segments, snapshotFieldStateAt)
937
+ materializeContainer: (segments) => materializeFields(state, segments, snapshotFieldStateAt),
938
+ resolveCallTarget: (path) => fieldStateTerminalAt(path)
592
939
  });
593
940
  }
594
941
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
@@ -920,7 +1267,7 @@ function buildProcessForm(state, formInstanceId, options = {}) {
920
1267
  }
921
1268
  const genAtEntry = state.submissionGeneration.value;
922
1269
  state.activeSubmissions.value += 1;
923
- state.isSubmitting.value = true;
1270
+ state.submitting.value = true;
924
1271
  state.submitError.value = null;
925
1272
  state.cancelFieldValidation();
926
1273
  state.activeValidations.value += 1;
@@ -967,7 +1314,7 @@ function buildProcessForm(state, formInstanceId, options = {}) {
967
1314
  }
968
1315
  state.activeSubmissions.value = Math.max(0, state.activeSubmissions.value - 1);
969
1316
  if (state.submissionGeneration.value === genAtEntry) {
970
- state.isSubmitting.value = state.activeSubmissions.value > 0;
1317
+ state.submitting.value = state.activeSubmissions.value > 0;
971
1318
  state.submitCount.value += 1;
972
1319
  }
973
1320
  }
@@ -1363,14 +1710,14 @@ function buildRegister(state, formInstanceId) {
1363
1710
  segments,
1364
1711
  state.coerceIndex
1365
1712
  );
1366
- if (persist && !state.isSSR && !state.modules.has(PERSISTENCE_MODULE_KEY)) {
1713
+ if (persist && !state.ssr && !state.modules.has(PERSISTENCE_MODULE_KEY)) {
1367
1714
  throw new AnonPersistError({
1368
1715
  cause: "register-without-config",
1369
1716
  schemaFields: extractSchemaFields(state.schema),
1370
1717
  callSite: captureUserCallSite()
1371
1718
  });
1372
1719
  }
1373
- return {
1720
+ const internalRv = {
1374
1721
  innerRef,
1375
1722
  displayValue,
1376
1723
  lastTypedForm,
@@ -1394,18 +1741,25 @@ function buildRegister(state, formInstanceId) {
1394
1741
  },
1395
1742
  // Called by the `vRegisterHint` compile-time transform's wrapping
1396
1743
  // IIFE on every server-side render of `<element v-register="…">`.
1397
- // Without it, every SSR'd FieldState serialises `isConnected: false`
1744
+ // Without it, every SSR'd FieldState serialises `connected: false`
1398
1745
  // (because Vue skips directive lifecycle during SSR) and the client
1399
1746
  // briefly shows that stale flag until hydration runs the directive's
1400
- // `created` hook. The mark only takes effect when `state.isSSR` is
1747
+ // `created` hook. The mark only takes effect when `state.ssr` is
1401
1748
  // true; on the client this is a no-op so the directive lifecycle
1402
1749
  // remains the source of truth.
1403
1750
  markConnectedOptimistically: () => {
1404
1751
  state.markConnectedOptimistically(segments);
1405
1752
  },
1753
+ path: pathKey,
1754
+ // Frozen so a wrapper component can pass `rv.segments` directly
1755
+ // to `form.fields(...)` without defensive copying — and so test
1756
+ // fixtures or downstream code can't mutate the canonical
1757
+ // segment list out from under the directive.
1758
+ segments: Object.freeze(segments.slice()),
1759
+ formKey: state.formKey,
1760
+ formInstanceId,
1406
1761
  // --- Persistence opt-in (internal; the directive is the only
1407
1762
  // legitimate consumer) ---
1408
- path: pathKey,
1409
1763
  persist,
1410
1764
  acknowledgeSensitive,
1411
1765
  persistOptIns: state.persistOptIns,
@@ -1413,6 +1767,7 @@ function buildRegister(state, formInstanceId) {
1413
1767
  coerce,
1414
1768
  ...coerceElement !== void 0 ? { coerceElement } : {}
1415
1769
  };
1770
+ return shallowReadonly(internalRv);
1416
1771
  };
1417
1772
  }
1418
1773
 
@@ -1451,22 +1806,30 @@ function walk(input, segments, schema, paths) {
1451
1806
  }
1452
1807
  if (Array.isArray(input)) {
1453
1808
  const out = new Array(input.length);
1809
+ let mutated = false;
1454
1810
  for (let i = 0; i < input.length; i++) {
1455
- out[i] = walk(input[i], [...segments, i], schema, paths);
1811
+ const walked = walk(input[i], [...segments, i], schema, paths);
1812
+ out[i] = walked;
1813
+ if (walked !== input[i]) mutated = true;
1456
1814
  }
1457
- return out;
1815
+ return mutated ? out : input;
1458
1816
  }
1459
1817
  if (typeof input === "object") {
1460
1818
  const slim = schema.getDefaultAtPath(segments);
1461
- const allKeys = new Set(Object.keys(input));
1819
+ const inputKeys = Object.keys(input);
1820
+ const allKeys = new Set(inputKeys);
1462
1821
  if (slim !== null && slim !== void 0 && typeof slim === "object" && !Array.isArray(slim) && !(slim instanceof Date) && !(slim instanceof RegExp) && !(slim instanceof Map) && !(slim instanceof Set)) {
1463
1822
  for (const k of Object.keys(slim)) allKeys.add(k);
1464
1823
  }
1465
1824
  const out = {};
1825
+ let mutated = allKeys.size !== inputKeys.length;
1466
1826
  for (const key of allKeys) {
1467
- out[key] = walk(input[key], [...segments, key], schema, paths);
1827
+ const orig = input[key];
1828
+ const walked = walk(orig, [...segments, key], schema, paths);
1829
+ out[key] = walked;
1830
+ if (walked !== orig) mutated = true;
1468
1831
  }
1469
- return out;
1832
+ return mutated ? out : input;
1470
1833
  }
1471
1834
  return input;
1472
1835
  }
@@ -1490,6 +1853,48 @@ function walkUnspecified(slim, segments, paths) {
1490
1853
  }
1491
1854
  return slim;
1492
1855
  }
1856
+ function substituteUnsetSentinels(value, prefix, schema) {
1857
+ const paths = [];
1858
+ const cleaned = substitute(value, [...prefix], schema, paths);
1859
+ return { cleanedValues: cleaned, paths };
1860
+ }
1861
+ function substitute(input, segments, schema, paths) {
1862
+ if (isUnset(input)) {
1863
+ const slim = schema.getDefaultAtPath(segments);
1864
+ if (!isPrimitiveOrEmpty(slim)) {
1865
+ warnNonPrimitiveLeaf(segments, slim);
1866
+ return slim;
1867
+ }
1868
+ paths.push(canonicalizePath(segments).key);
1869
+ return slim;
1870
+ }
1871
+ if (input === void 0 || input === null) return input;
1872
+ if (input instanceof Date || input instanceof RegExp || input instanceof Map || input instanceof Set || typeof input === "function") {
1873
+ return input;
1874
+ }
1875
+ if (Array.isArray(input)) {
1876
+ let mutated = false;
1877
+ const out = new Array(input.length);
1878
+ for (let i = 0; i < input.length; i++) {
1879
+ const walked = substitute(input[i], [...segments, i], schema, paths);
1880
+ out[i] = walked;
1881
+ if (walked !== input[i]) mutated = true;
1882
+ }
1883
+ return mutated ? out : input;
1884
+ }
1885
+ if (typeof input === "object") {
1886
+ let mutated = false;
1887
+ const out = {};
1888
+ for (const key of Object.keys(input)) {
1889
+ const orig = input[key];
1890
+ const walked = substitute(orig, [...segments, key], schema, paths);
1891
+ out[key] = walked;
1892
+ if (walked !== orig) mutated = true;
1893
+ }
1894
+ return mutated ? out : input;
1895
+ }
1896
+ return input;
1897
+ }
1493
1898
  function isPrimitiveOrEmpty(value) {
1494
1899
  if (value === null || value === void 0) return true;
1495
1900
  const t = typeof value;
@@ -1598,14 +2003,14 @@ function buildFormApi(state, formInstanceId, options = {}) {
1598
2003
  }
1599
2004
  function setValueImpl(pathOrValue, maybeValue) {
1600
2005
  if (arguments.length === 1) {
1601
- const next = typeof pathOrValue === "function" ? pathOrValue(state.form.value) : pathOrValue;
1602
- const walked = walkUnsetSentinels(
2006
+ const next = typeof pathOrValue === "function" ? pathOrValue(structuralSnapshot(state.form.value)) : pathOrValue;
2007
+ const walked2 = walkUnsetSentinels(
1603
2008
  next,
1604
2009
  state.schema
1605
2010
  );
1606
- const ok = state.setValueAtPath([], walked.cleanedValues);
1607
- if (!ok) return false;
1608
- for (const pathKey of walked.paths) {
2011
+ const ok2 = state.setValueAtPath([], walked2.cleanedValues);
2012
+ if (!ok2) return false;
2013
+ for (const pathKey of walked2.paths) {
1609
2014
  const segments2 = segmentsForPathKey(pathKey);
1610
2015
  if (segments2 === null) continue;
1611
2016
  state.setValueAtPath(segments2, state.schema.getDefaultAtPath(segments2), {
@@ -1633,7 +2038,21 @@ function buildFormApi(state, formInstanceId, options = {}) {
1633
2038
  } else {
1634
2039
  resolvedValue = maybeValue;
1635
2040
  }
1636
- return state.setValueAtPath(segments, resolvedValue);
2041
+ const walked = substituteUnsetSentinels(
2042
+ resolvedValue,
2043
+ segments,
2044
+ state.schema
2045
+ );
2046
+ const ok = state.setValueAtPath(segments, walked.cleanedValues);
2047
+ if (!ok) return false;
2048
+ for (const pathKey of walked.paths) {
2049
+ const blankSegments = segmentsForPathKey(pathKey);
2050
+ if (blankSegments === null) continue;
2051
+ state.setValueAtPath(blankSegments, state.schema.getDefaultAtPath(blankSegments), {
2052
+ blank: true
2053
+ });
2054
+ }
2055
+ return true;
1637
2056
  }
1638
2057
  const errorsProxy = buildErrorsProxy(state);
1639
2058
  function setFieldErrors(errors) {
@@ -1652,58 +2071,89 @@ function buildFormApi(state, formInstanceId, options = {}) {
1652
2071
  state.clearSchemaErrors(segments);
1653
2072
  state.clearUserErrors(segments);
1654
2073
  }
1655
- const isDirty = computed(() => {
1656
- for (const [, { segments, value: original }] of state.originals) {
1657
- if (!Object.is(getAtPath(state.form.value, segments), original)) return true;
1658
- }
1659
- if (state.blankPaths.size !== state.originalBlankPaths.size) return true;
1660
- for (const key of state.blankPaths) {
1661
- if (!state.originalBlankPaths.has(key)) return true;
2074
+ function setFormErrors(errors) {
2075
+ if (errors.length === 0) {
2076
+ state.userErrors.delete(ROOT_PATH_KEY);
2077
+ return;
1662
2078
  }
1663
- return false;
1664
- });
1665
- const isValid = computed(
1666
- () => state.schemaErrors.size === 0 && state.userErrors.size === 0 && state.derivedBlankErrors.value.size === 0
1667
- );
1668
- const isSubmitting = computed(() => state.isSubmitting.value);
2079
+ state.userErrors.set(
2080
+ ROOT_PATH_KEY,
2081
+ errors.map((e) => ({
2082
+ path: [],
2083
+ message: e.message,
2084
+ formKey: state.formKey,
2085
+ code: e.code ?? "atta:form-error"
2086
+ }))
2087
+ );
2088
+ }
2089
+ function clearFormErrors() {
2090
+ state.userErrors.delete(ROOT_PATH_KEY);
2091
+ }
2092
+ const submitting = computed(() => state.submitting.value);
1669
2093
  const submitCount = computed(() => state.submitCount.value);
1670
2094
  const submitError = computed(() => state.submitError.value);
1671
- const isValidating = computed(() => state.activeValidations.value > 0);
2095
+ const validating = computed(() => state.activeValidations.value > 0);
2096
+ const valid = computed(
2097
+ () => state.firstValidationDone.value && state.schemaErrors.size === 0 && state.userErrors.size === 0 && state.derivedBlankErrors.value.size === 0 && !validating.value
2098
+ );
1672
2099
  const history = options.history;
1673
2100
  const undo = history?.undo ?? (() => false);
1674
2101
  const redo = history?.redo ?? (() => false);
1675
2102
  const canUndo = history?.canUndo ?? computed(() => false);
1676
2103
  const canRedo = history?.canRedo ?? computed(() => false);
1677
2104
  const historySize = history?.historySize ?? computed(() => 0);
1678
- const metaErrors = computed(() => {
1679
- const buckets = /* @__PURE__ */ new Map();
1680
- const collect = (errs) => {
1681
- for (const [pathKey, list] of errs) {
1682
- if (list.length === 0) continue;
1683
- const ordinal = state.ensurePathOrdinal(pathKey);
1684
- const existing = buckets.get(ordinal);
1685
- if (existing === void 0) buckets.set(ordinal, [...list]);
1686
- else existing.push(...list);
1687
- }
1688
- };
1689
- collect(state.schemaErrors);
1690
- collect(state.derivedBlankErrors.value);
1691
- collect(state.userErrors);
1692
- if (buckets.size === 0) return [];
1693
- return [...buckets.entries()].sort(([a], [b]) => a - b).flatMap(([, errs]) => errs);
1694
- });
2105
+ const metaErrors = computed(
2106
+ () => aggregateErrorsAt(state, [])
2107
+ );
2108
+ const getRootFieldStateAt = buildFieldStateAccessor(state);
2109
+ const rootFieldState = getRootFieldStateAt([]);
1695
2110
  const formMeta = readonly(
1696
2111
  reactive({
1697
- isDirty,
1698
- isValid,
1699
- isSubmitting,
1700
- isValidating,
2112
+ // FieldState fields — read through one shared root computed. Each
2113
+ // property accesses `rootFieldState.value[X]`, so any descendant
2114
+ // change re-evaluates the root computed once (Vue's reactive
2115
+ // graph dedupes the dependent re-renders).
2116
+ value: computed(() => rootFieldState.value.value),
2117
+ original: computed(() => rootFieldState.value.original),
2118
+ pristine: computed(() => rootFieldState.value.pristine),
2119
+ dirty: computed(() => rootFieldState.value.dirty),
2120
+ focused: computed(() => rootFieldState.value.focused),
2121
+ blurred: computed(() => rootFieldState.value.blurred),
2122
+ touched: computed(() => rootFieldState.value.touched),
2123
+ connected: computed(() => rootFieldState.value.connected),
2124
+ element: computed(() => rootFieldState.value.element),
2125
+ elements: computed(() => rootFieldState.value.elements),
2126
+ updatedAt: computed(() => rootFieldState.value.updatedAt),
2127
+ // Whole-form validating mirrors the LIFECYCLE counter
2128
+ // (`state.activeValidations`) ORed with any per-leaf validation
2129
+ // in flight (via `rootFieldState.validating`). A submit-time
2130
+ // validate run shows up as activeValidations; per-field
2131
+ // debounced validators show up as fieldValidationCounts. Either
2132
+ // flips the flag.
2133
+ validating: computed(
2134
+ () => state.activeValidations.value > 0 || rootFieldState.value.validating
2135
+ ),
2136
+ // Whole-form valid keeps the original `firstValidationDone`
2137
+ // mount gate so the surface doesn't lie about a yet-to-arrive
2138
+ // verdict at construction time. The shared `aggregateErrorsAt`
2139
+ // ensures `form.meta.errors` and `rootFieldState.errors` match,
2140
+ // so `errors.length === 0` here would agree with `valid` —
2141
+ // keep the explicit form-level computation for the gate.
2142
+ valid,
2143
+ errors: metaErrors,
2144
+ path: computed(() => rootFieldState.value.path),
2145
+ blank: computed(() => rootFieldState.value.blank),
2146
+ label: computed(() => rootFieldState.value.label),
2147
+ description: computed(() => rootFieldState.value.description),
2148
+ placeholder: computed(() => rootFieldState.value.placeholder),
2149
+ meta: computed(() => rootFieldState.value.meta),
2150
+ // Lifecycle (form-level only — not on FieldState).
2151
+ submitting,
1701
2152
  submitCount,
1702
2153
  submitError,
1703
2154
  canUndo,
1704
2155
  canRedo,
1705
2156
  historySize,
1706
- errors: metaErrors,
1707
2157
  // Per-`useForm()`-call identity. Stable for one mount; new on
1708
2158
  // re-mount; orthogonal to `form.key` (which is the user-supplied
1709
2159
  // shared identifier). Useful for devtools panels disambiguating
@@ -1794,6 +2244,8 @@ function buildFormApi(state, formInstanceId, options = {}) {
1794
2244
  setFieldErrors,
1795
2245
  addFieldErrors,
1796
2246
  clearFieldErrors,
2247
+ setFormErrors,
2248
+ clearFormErrors,
1797
2249
  meta: formMeta,
1798
2250
  reset,
1799
2251
  resetField,
@@ -1814,103 +2266,10 @@ function buildFormApi(state, formInstanceId, options = {}) {
1814
2266
  };
1815
2267
  }
1816
2268
 
1817
- function isDescendable(value) {
1818
- if (value === null || typeof value !== "object") return false;
1819
- if (Array.isArray(value)) return true;
1820
- const proto = Object.getPrototypeOf(value);
1821
- return proto === null || proto === Object.prototype;
1822
- }
1823
- function appendSegment(prefix, segment) {
1824
- const next = new Array(prefix.length + 1);
1825
- for (let i = 0; i < prefix.length; i++) {
1826
- const s = prefix[i];
1827
- next[i] = s;
1828
- }
1829
- next[prefix.length] = segment;
1830
- return next;
1831
- }
1832
- function diffAndApply(oldValue, newValue, prefix, visit) {
1833
- if (Object.is(oldValue, newValue)) return;
1834
- const oldIsDescendable = isDescendable(oldValue);
1835
- const newIsDescendable = isDescendable(newValue);
1836
- if (oldValue === void 0 && newIsDescendable) {
1837
- if (Array.isArray(newValue)) {
1838
- for (let i = 0; i < newValue.length; i++) {
1839
- diffAndApply(void 0, newValue[i], appendSegment(prefix, i), visit);
1840
- }
1841
- } else {
1842
- const rec = newValue;
1843
- for (const k of Object.keys(rec)) {
1844
- diffAndApply(void 0, rec[k], appendSegment(prefix, k), visit);
1845
- }
1846
- }
1847
- return;
1848
- }
1849
- if (oldIsDescendable && newValue === void 0) {
1850
- if (Array.isArray(oldValue)) {
1851
- for (let i = 0; i < oldValue.length; i++) {
1852
- diffAndApply(oldValue[i], void 0, appendSegment(prefix, i), visit);
1853
- }
1854
- } else {
1855
- const rec = oldValue;
1856
- for (const k of Object.keys(rec)) {
1857
- diffAndApply(rec[k], void 0, appendSegment(prefix, k), visit);
1858
- }
1859
- }
1860
- return;
1861
- }
1862
- if (oldIsDescendable && newIsDescendable) {
1863
- const oldIsArray = Array.isArray(oldValue);
1864
- const newIsArray = Array.isArray(newValue);
1865
- if (oldIsArray && newIsArray) {
1866
- const oldArr = oldValue;
1867
- const newArr = newValue;
1868
- const max = Math.max(oldArr.length, newArr.length);
1869
- for (let i = 0; i < max; i++) {
1870
- diffAndApply(oldArr[i], newArr[i], appendSegment(prefix, i), visit);
1871
- }
1872
- return;
1873
- }
1874
- if (!oldIsArray && !newIsArray) {
1875
- const oldRec = oldValue;
1876
- const newRec = newValue;
1877
- const seen = /* @__PURE__ */ new Set();
1878
- for (const k of Object.keys(oldRec)) {
1879
- seen.add(k);
1880
- diffAndApply(oldRec[k], newRec[k], appendSegment(prefix, k), visit);
1881
- }
1882
- for (const k of Object.keys(newRec)) {
1883
- if (seen.has(k)) continue;
1884
- diffAndApply(oldRec[k], newRec[k], appendSegment(prefix, k), visit);
1885
- }
1886
- return;
1887
- }
1888
- visit({ kind: "changed", path: prefix, oldValue, newValue });
1889
- return;
1890
- }
1891
- if (oldIsDescendable && !newIsDescendable) {
1892
- visit({ kind: "changed", path: prefix, oldValue, newValue });
1893
- return;
1894
- }
1895
- if (!oldIsDescendable && newIsDescendable) {
1896
- visit({ kind: "changed", path: prefix, oldValue, newValue });
1897
- return;
1898
- }
1899
- if (oldValue === void 0) {
1900
- visit({ kind: "added", path: prefix, newValue });
1901
- return;
1902
- }
1903
- if (newValue === void 0) {
1904
- visit({ kind: "removed", path: prefix, oldValue });
1905
- return;
1906
- }
1907
- visit({ kind: "changed", path: prefix, oldValue, newValue });
1908
- }
1909
-
1910
2269
  function isHydratedFieldRecord(value) {
1911
2270
  if (typeof value !== "object" || value === null) return false;
1912
2271
  const r = value;
1913
- return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.isConnected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && (typeof r.touched === "boolean" || r.touched === null);
2272
+ 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);
1914
2273
  }
1915
2274
  function isHydratedValidationErrorArray(value) {
1916
2275
  if (!Array.isArray(value)) return false;
@@ -1941,7 +2300,7 @@ function isPathKeyUnder(existingKey, parentPath) {
1941
2300
  }
1942
2301
  function createFormStore(options) {
1943
2302
  const { formKey, schema, defaultValues, strict = true, hydration } = options;
1944
- const isSSR = options.isSSR === true;
2303
+ const ssr = options.ssr === true;
1945
2304
  const rememberVariants = options.rememberVariants !== false;
1946
2305
  const fieldValidationMode = options.validateOn ?? "change";
1947
2306
  const fieldValidationDebounceMs = options.debounceMs ?? DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS;
@@ -1960,7 +2319,7 @@ function createFormStore(options) {
1960
2319
  strict
1961
2320
  });
1962
2321
  const schemaInitialData = schemaResponse.data;
1963
- const initialData = hydration !== void 0 ? hydration.form : schemaInitialData;
2322
+ const initialData = hydration !== void 0 ? hydration.form : structuralSnapshot(schemaInitialData);
1964
2323
  const form = ref(initialData);
1965
2324
  const fields = reactive(/* @__PURE__ */ new Map());
1966
2325
  const elements = reactive(/* @__PURE__ */ new Map());
@@ -1968,7 +2327,7 @@ function createFormStore(options) {
1968
2327
  let sortedRegistrationsCache = null;
1969
2328
  const schemaErrors = reactive(/* @__PURE__ */ new Map());
1970
2329
  const userErrors = reactive(/* @__PURE__ */ new Map());
1971
- const originals = /* @__PURE__ */ new Map();
2330
+ const originals = reactive(/* @__PURE__ */ new Map());
1972
2331
  const initialTransientList = hydration?.blankPaths ?? options.initialBlankPaths ?? [];
1973
2332
  const blankPaths = reactive(/* @__PURE__ */ new Set());
1974
2333
  const originalBlankPaths = /* @__PURE__ */ new Set();
@@ -2006,12 +2365,37 @@ function createFormStore(options) {
2006
2365
  }
2007
2366
  return result;
2008
2367
  });
2009
- const isSubmitting = ref(false);
2368
+ const submitting = ref(false);
2010
2369
  const activeSubmissions = ref(0);
2011
2370
  const submitCount = ref(0);
2012
2371
  const submitError = ref(null);
2013
2372
  const submissionGeneration = ref(0);
2014
2373
  const activeValidations = ref(0);
2374
+ const firstValidationDone = ref(!strict || schema.needsAsyncValidation?.() !== true);
2375
+ watch(activeValidations, (now, prev) => {
2376
+ if (prev > 0 && now === 0) {
2377
+ firstValidationDone.value = true;
2378
+ }
2379
+ });
2380
+ const pathAsyncCache = /* @__PURE__ */ new Map();
2381
+ function pathHasAsyncValidation(path) {
2382
+ const { key } = canonicalizePath(path);
2383
+ const cached = pathAsyncCache.get(key);
2384
+ if (cached !== void 0) return cached;
2385
+ const candidates = schema.getSchemasAtPath(path);
2386
+ const hasAsync = candidates.some((sub) => sub.needsAsyncValidation?.() === true);
2387
+ pathAsyncCache.set(key, hasAsync);
2388
+ return hasAsync;
2389
+ }
2390
+ const fieldValidationCounts = reactive(/* @__PURE__ */ new Map());
2391
+ function incFieldValidation(key) {
2392
+ fieldValidationCounts.set(key, (fieldValidationCounts.get(key) ?? 0) + 1);
2393
+ }
2394
+ function decFieldValidation(key) {
2395
+ const next = (fieldValidationCounts.get(key) ?? 0) - 1;
2396
+ if (next <= 0) fieldValidationCounts.delete(key);
2397
+ else fieldValidationCounts.set(key, next);
2398
+ }
2015
2399
  const initStamp = (/* @__PURE__ */ new Date()).toISOString();
2016
2400
  diffAndApply({}, schemaInitialData, [], (patch) => {
2017
2401
  if (patch.kind !== "added") return;
@@ -2048,7 +2432,7 @@ function createFormStore(options) {
2048
2432
  fields.set(key, {
2049
2433
  path: patch.path,
2050
2434
  updatedAt: initStamp,
2051
- isConnected: false,
2435
+ connected: false,
2052
2436
  focused: null,
2053
2437
  blurred: null,
2054
2438
  touched: null
@@ -2058,7 +2442,7 @@ function createFormStore(options) {
2058
2442
  setAllSchemaErrors(schemaResponse.errors);
2059
2443
  }
2060
2444
  }
2061
- if (!isSSR && strict && schema.needsAsyncValidation?.() === true) {
2445
+ if (!ssr && strict && schema.needsAsyncValidation?.() === true) {
2062
2446
  queueMicrotask(() => scheduleFieldValidation(
2063
2447
  [],
2064
2448
  true
@@ -2070,7 +2454,7 @@ function createFormStore(options) {
2070
2454
  fields.set(pathKey, {
2071
2455
  path,
2072
2456
  updatedAt: patch.updatedAt ?? current?.updatedAt ?? null,
2073
- isConnected: patch.isConnected ?? current?.isConnected ?? false,
2457
+ connected: patch.connected ?? current?.connected ?? false,
2074
2458
  focused: patch.focused ?? current?.focused ?? null,
2075
2459
  blurred: patch.blurred ?? current?.blurred ?? null,
2076
2460
  touched: patch.touched ?? current?.touched ?? null
@@ -2079,18 +2463,24 @@ function createFormStore(options) {
2079
2463
  function applyFormReplacement(next, meta) {
2080
2464
  const prev = form.value;
2081
2465
  if (Object.is(prev, next)) return;
2082
- form.value = next;
2083
2466
  const now = (/* @__PURE__ */ new Date()).toISOString();
2467
+ const patches = [];
2084
2468
  diffAndApply(prev, next, [], (patch) => {
2469
+ patches.push(patch);
2470
+ });
2471
+ if (!applyChangedKeys(prev, next)) {
2472
+ form.value = next;
2473
+ }
2474
+ for (const patch of patches) {
2085
2475
  const { key } = canonicalizePath(patch.path);
2086
2476
  if (patch.kind === "added" && !originals.has(key)) {
2087
2477
  originals.set(key, { segments: patch.path, value: void 0 });
2088
2478
  }
2089
2479
  touchFieldRecord(key, patch.path, { updatedAt: now });
2090
- });
2480
+ }
2091
2481
  for (const listener of formChangeListeners) {
2092
2482
  try {
2093
- listener(next, meta);
2483
+ listener(form.value, meta);
2094
2484
  } catch (err) {
2095
2485
  console.error("[attaform] onFormChange threw:", err);
2096
2486
  }
@@ -2264,6 +2654,7 @@ function createFormStore(options) {
2264
2654
  if (controller.signal.aborted) return;
2265
2655
  const data = getAtPath(form.value, path);
2266
2656
  activeValidations.value += 1;
2657
+ incFieldValidation(key);
2267
2658
  void Promise.resolve().then(() => schema.validateAtPath(data, path)).then((response) => {
2268
2659
  if (controller.signal.aborted) return;
2269
2660
  const reStamped = response.success ? [] : response.errors.map((err) => ({
@@ -2274,6 +2665,7 @@ function createFormStore(options) {
2274
2665
  }).catch(() => {
2275
2666
  }).finally(() => {
2276
2667
  activeValidations.value = Math.max(0, activeValidations.value - 1);
2668
+ decFieldValidation(key);
2277
2669
  });
2278
2670
  };
2279
2671
  if (immediate || fieldValidationDebounceMs === 0) {
@@ -2429,15 +2821,16 @@ function createFormStore(options) {
2429
2821
  function registerElement(path, element, formInstanceId) {
2430
2822
  const { key } = canonicalizePath(path);
2431
2823
  const record = elements.get(key);
2824
+ const raw = markRaw(element);
2432
2825
  if (record === void 0) {
2433
- elements.set(key, { path, elements: /* @__PURE__ */ new Set([element]) });
2826
+ elements.set(key, { path, elements: reactive(/* @__PURE__ */ new Set([raw])) });
2434
2827
  } else {
2435
- if (record.elements.has(element)) return false;
2436
- record.elements.add(element);
2828
+ if (record.elements.has(raw)) return false;
2829
+ record.elements.add(raw);
2437
2830
  }
2438
2831
  elementToFormInstance.set(element, formInstanceId);
2439
2832
  sortedRegistrationsCache = null;
2440
- touchFieldRecord(key, path, { isConnected: true });
2833
+ touchFieldRecord(key, path, { connected: true });
2441
2834
  return true;
2442
2835
  }
2443
2836
  function deregisterElement(path, element) {
@@ -2452,16 +2845,16 @@ function createFormStore(options) {
2452
2845
  const remaining = record.elements.size;
2453
2846
  if (remaining === 0) {
2454
2847
  elements.delete(key);
2455
- touchFieldRecord(key, path, { isConnected: false });
2848
+ touchFieldRecord(key, path, { connected: false });
2456
2849
  }
2457
2850
  return remaining;
2458
2851
  }
2459
2852
  function markConnectedOptimistically(path) {
2460
- if (!isSSR) return;
2853
+ if (!ssr) return;
2461
2854
  const { key } = canonicalizePath(path);
2462
2855
  const current = fields.get(key);
2463
- if (current?.isConnected === true) return;
2464
- touchFieldRecord(key, path, { isConnected: true });
2856
+ if (current?.connected === true) return;
2857
+ touchFieldRecord(key, path, { connected: true });
2465
2858
  }
2466
2859
  function markFocused(path, focused) {
2467
2860
  const { key } = canonicalizePath(path);
@@ -2513,14 +2906,14 @@ function createFormStore(options) {
2513
2906
  fields.set(pathKey, {
2514
2907
  path: record.path,
2515
2908
  updatedAt: now,
2516
- isConnected: record.isConnected,
2909
+ connected: record.connected,
2517
2910
  focused: null,
2518
2911
  blurred: null,
2519
2912
  touched: null
2520
2913
  });
2521
2914
  }
2522
2915
  submissionGeneration.value += 1;
2523
- isSubmitting.value = false;
2916
+ submitting.value = false;
2524
2917
  activeSubmissions.value = 0;
2525
2918
  submitCount.value = 0;
2526
2919
  submitError.value = null;
@@ -2597,7 +2990,7 @@ function createFormStore(options) {
2597
2990
  fields.set(pathKey, {
2598
2991
  path: record.path,
2599
2992
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2600
- isConnected: record.isConnected,
2993
+ connected: record.connected,
2601
2994
  focused: null,
2602
2995
  blurred: null,
2603
2996
  touched: null
@@ -2659,13 +3052,16 @@ function createFormStore(options) {
2659
3052
  derivedBlankErrors,
2660
3053
  originals,
2661
3054
  schema,
2662
- isSSR,
2663
- isSubmitting,
3055
+ ssr,
3056
+ submitting,
2664
3057
  activeSubmissions,
2665
3058
  submitCount,
2666
3059
  submitError,
2667
3060
  submissionGeneration,
2668
3061
  activeValidations,
3062
+ firstValidationDone,
3063
+ pathHasAsyncValidation,
3064
+ fieldValidationCounts,
2669
3065
  applyFormReplacement,
2670
3066
  setValueAtPath,
2671
3067
  getValueAtPath,
@@ -2718,7 +3114,7 @@ function createHistoryModule(state, config) {
2718
3114
  let suppressNext = false;
2719
3115
  function captureSnapshot() {
2720
3116
  return {
2721
- form: state.form.value,
3117
+ form: structuralSnapshot(state.form.value),
2722
3118
  blankPaths: [...state.blankPaths],
2723
3119
  schemaErrors: [...state.schemaErrors.entries()].map(([k, v]) => [k, [...v]]),
2724
3120
  userErrors: [...state.userErrors.entries()].map(([k, v]) => [k, [...v]])
@@ -2825,8 +3221,8 @@ function useAbstractForm(configuration) {
2825
3221
  const releaseConsumer = registry.trackConsumer(key);
2826
3222
  onScopeDispose(releaseConsumer);
2827
3223
  }
2828
- const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.isSSR);
2829
- if (existing === void 0 && !registry.isSSR) {
3224
+ const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
3225
+ if (existing === void 0 && !registry.ssr) {
2830
3226
  if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
2831
3227
  const resolvedPersist = normalizePersistConfig(merged.persist);
2832
3228
  const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
@@ -2845,7 +3241,7 @@ function useAbstractForm(configuration) {
2845
3241
  state.registerCleanup(() => historyModule.dispose());
2846
3242
  }
2847
3243
  if (configuration.key === void 0) {
2848
- recordAmbientProvide(registry.isSSR);
3244
+ recordAmbientProvide(registry.ssr);
2849
3245
  provide(kFormContext, state);
2850
3246
  }
2851
3247
  const formInstanceId = getCurrentInstance() !== null ? useId() : `atta:form-instance:${formInstanceCounter++}`;
@@ -2898,7 +3294,7 @@ function buildFreshState(key, schema, configuration, registry) {
2898
3294
  hydration: pending,
2899
3295
  ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
2900
3296
  ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
2901
- isSSR: registry.isSSR,
3297
+ ssr: registry.ssr,
2902
3298
  ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
2903
3299
  ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
2904
3300
  ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {}
@@ -2913,8 +3309,8 @@ function buildFreshState(key, schema, configuration, registry) {
2913
3309
  let anonCounter = 0;
2914
3310
  let formInstanceCounter = 0;
2915
3311
  const ambientProvideHistory = __DEV__ ? /* @__PURE__ */ new WeakMap() : null;
2916
- function recordAmbientProvide(isSSR) {
2917
- if (!__DEV__ || isSSR || ambientProvideHistory === null) return;
3312
+ function recordAmbientProvide(ssr) {
3313
+ if (!__DEV__ || ssr || ambientProvideHistory === null) return;
2918
3314
  const instance = getCurrentInstance();
2919
3315
  if (instance === null) return;
2920
3316
  const instanceKey = instance;
@@ -3176,14 +3572,14 @@ function isEmptyContainer(value) {
3176
3572
  return false;
3177
3573
  }
3178
3574
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
3179
- function enforceAnonPersistRule(formKey, isSSR) {
3575
+ function enforceAnonPersistRule(formKey, ssr) {
3180
3576
  if (!formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return false;
3181
3577
  if (__DEV__)
3182
3578
  throw new AnonPersistError({
3183
3579
  cause: "no-key",
3184
3580
  callSite: captureUserCallSite()
3185
3581
  });
3186
- if (!isSSR && !warnedAnonPersistKeys.has(formKey)) {
3582
+ if (!ssr && !warnedAnonPersistKeys.has(formKey)) {
3187
3583
  warnedAnonPersistKeys.add(formKey);
3188
3584
  console.warn(
3189
3585
  "[attaform] persist: ignored \u2014 anonymous useForm() can't safely persist (key drift + cross-form collision risk).\n Persistence is disabled for this form; the app keeps working.\n Fix: useForm({ schema, key: 'login', persist: '...' })"
@@ -3241,21 +3637,21 @@ function resolveState(key, registry) {
3241
3637
  if (key !== void 0) {
3242
3638
  const stored = registry.forms.get(key);
3243
3639
  if (stored === void 0) {
3244
- warnMiss(`no form registered for key '${key}'`, registry.isSSR);
3640
+ warnMiss(`no form registered for key '${key}'`, registry.ssr);
3245
3641
  return null;
3246
3642
  }
3247
3643
  return stored;
3248
3644
  }
3249
3645
  const ambient = inject(kFormContext, null);
3250
3646
  if (ambient === null) {
3251
- warnMiss("no ambient form context", registry.isSSR);
3647
+ warnMiss("no ambient form context", registry.ssr);
3252
3648
  return null;
3253
3649
  }
3254
3650
  warnIfAmbientProviderHadDuplicates();
3255
3651
  return ambient;
3256
3652
  }
3257
- function warnMiss(detail, isSSR) {
3258
- if (!__DEV__ || isSSR) return;
3653
+ function warnMiss(detail, ssr) {
3654
+ if (!__DEV__ || ssr) return;
3259
3655
  const frame = captureUserCallSite();
3260
3656
  console.warn(
3261
3657
  `[attaform] injectForm: ${detail}. Returning null.` + (frame !== void 0 ? ` ${frame}` : "")
@@ -3279,5 +3675,5 @@ function warnIfAmbientProviderHadDuplicates() {
3279
3675
  }
3280
3676
  }
3281
3677
 
3282
- export { AttaformErrorCode as A, defineCoercion as a, isUnset as b, useAbstractForm as c, defaultCoercionRules as d, setAtPath as e, isPlainRecord as f, getAtPath as g, injectForm as i, slimKindOf as s, unset as u };
3283
- //# sourceMappingURL=attaform.BRTxpA3q.mjs.map
3678
+ export { AttaformErrorCode as A, defineCoercion as a, isUnset as b, useAbstractForm as c, defaultCoercionRules as d, setAtPath as e, isPlainRecord as f, getAtPath as g, humanize as h, injectForm as i, slimKindOf as s, unset as u };
3679
+ //# sourceMappingURL=attaform.qxyip_aN.mjs.map