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