attaform 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +3 -0
  2. package/dist/chunks/devtools.cjs +1 -1
  3. package/dist/chunks/devtools.mjs +1 -1
  4. package/dist/chunks/indexeddb.cjs +1 -1
  5. package/dist/chunks/indexeddb.mjs +1 -1
  6. package/dist/chunks/local-storage.cjs +1 -1
  7. package/dist/chunks/local-storage.mjs +1 -1
  8. package/dist/chunks/session-storage.cjs +1 -1
  9. package/dist/chunks/session-storage.mjs +1 -1
  10. package/dist/index.cjs +4 -7
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +77 -110
  13. package/dist/index.d.mts +77 -110
  14. package/dist/index.d.ts +77 -110
  15. package/dist/index.mjs +5 -5
  16. package/dist/nuxt.d.cts +1 -1
  17. package/dist/nuxt.d.mts +1 -1
  18. package/dist/nuxt.d.ts +1 -1
  19. package/dist/runtime/components/AttaformDevtoolsPanel.vue +2 -2
  20. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +1 -3
  21. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +1 -3
  22. package/dist/runtime/plugins/attaform.cjs +2 -2
  23. package/dist/runtime/plugins/attaform.mjs +2 -2
  24. package/dist/shared/{attaform.CDmaxrt2.mjs → attaform.BKozEdTr.mjs} +305 -178
  25. package/dist/shared/attaform.BKozEdTr.mjs.map +1 -0
  26. package/dist/shared/{attaform.Bubm_slq.cjs → attaform.BM6YD9kZ.cjs} +212 -269
  27. package/dist/shared/attaform.BM6YD9kZ.cjs.map +1 -0
  28. package/dist/shared/{attaform.5UhpSVFI.cjs → attaform.BPxsYtTe.cjs} +2 -26
  29. package/dist/shared/attaform.BPxsYtTe.cjs.map +1 -0
  30. package/dist/shared/{attaform.BqK_L4gK.cjs → attaform.BPy-4qRx.cjs} +305 -180
  31. package/dist/shared/attaform.BPy-4qRx.cjs.map +1 -0
  32. package/dist/shared/attaform.BWgAFnsj.mjs +770 -0
  33. package/dist/shared/attaform.BWgAFnsj.mjs.map +1 -0
  34. package/dist/shared/{attaform.CGX1CNpz.d.ts → attaform.Bh3ACtts.d.ts} +152 -111
  35. package/dist/shared/{attaform.CXpzmj38.mjs → attaform.BupwXkj_.mjs} +213 -270
  36. package/dist/shared/attaform.BupwXkj_.mjs.map +1 -0
  37. package/dist/shared/{attaform.Dlk1jMuv.cjs → attaform.CIn4bMsD.cjs} +263 -799
  38. package/dist/shared/attaform.CIn4bMsD.cjs.map +1 -0
  39. package/dist/shared/{attaform.CZ-XtZt_.mjs → attaform.CKFbKFb6.mjs} +2265 -1509
  40. package/dist/shared/attaform.CKFbKFb6.mjs.map +1 -0
  41. package/dist/shared/{attaform.CuN7ZhBy.d.cts → attaform.D5-1XGQU.d.cts} +152 -111
  42. package/dist/shared/{attaform.-1GQTX2T.mjs → attaform.DEBvCjeH.mjs} +257 -793
  43. package/dist/shared/attaform.DEBvCjeH.mjs.map +1 -0
  44. package/dist/shared/{attaform.II89Pcf4.cjs → attaform.DL4CQ-oW.cjs} +2270 -1514
  45. package/dist/shared/attaform.DL4CQ-oW.cjs.map +1 -0
  46. package/dist/shared/{attaform.FnEwjhvX.d.ts → attaform.DSD85fHb.d.cts} +1 -19
  47. package/dist/shared/{attaform.CRmmNAYp.d.cts → attaform.DSD85fHb.d.mts} +1 -19
  48. package/dist/shared/{attaform.D9wuTGu9.d.mts → attaform.DSD85fHb.d.ts} +1 -19
  49. package/dist/shared/{attaform.B7rzpK1U.d.cts → attaform.DkA5J8NW.d.cts} +1 -17
  50. package/dist/shared/{attaform.B7rzpK1U.d.mts → attaform.DkA5J8NW.d.mts} +1 -17
  51. package/dist/shared/{attaform.B7rzpK1U.d.ts → attaform.DkA5J8NW.d.ts} +1 -17
  52. package/dist/shared/{attaform.B957T6NU.d.ts → attaform.Dl5kDY-A.d.ts} +1 -1
  53. package/dist/shared/attaform.Dmb6itxC.cjs +781 -0
  54. package/dist/shared/attaform.Dmb6itxC.cjs.map +1 -0
  55. package/dist/shared/{attaform.M-RanbyV.d.mts → attaform.DoKXru-a.d.mts} +1 -1
  56. package/dist/shared/attaform.DvA-CJJW.mjs +1876 -0
  57. package/dist/shared/attaform.DvA-CJJW.mjs.map +1 -0
  58. package/dist/shared/{attaform.D1gzu2GL.d.mts → attaform.EMzJcQci.d.mts} +152 -111
  59. package/dist/shared/attaform.EZG6fOFb.mjs +35 -0
  60. package/dist/shared/attaform.EZG6fOFb.mjs.map +1 -0
  61. package/dist/shared/{attaform.XDjA7sRz.d.cts → attaform.GbDo_lJi.d.cts} +1 -1
  62. package/dist/shared/{attaform.Ca5_6Ky-.d.mts → attaform.SfhU0OEY.d.cts} +499 -116
  63. package/dist/shared/{attaform.Ca5_6Ky-.d.cts → attaform.SfhU0OEY.d.mts} +499 -116
  64. package/dist/shared/{attaform.Ca5_6Ky-.d.ts → attaform.SfhU0OEY.d.ts} +499 -116
  65. package/dist/shared/attaform.jgzuNZVC.cjs +1882 -0
  66. package/dist/shared/attaform.jgzuNZVC.cjs.map +1 -0
  67. package/dist/transforms.cjs +2 -2
  68. package/dist/transforms.d.cts +22 -13
  69. package/dist/transforms.d.mts +22 -13
  70. package/dist/transforms.d.ts +22 -13
  71. package/dist/transforms.mjs +1 -1
  72. package/dist/vite.cjs +8 -7
  73. package/dist/vite.cjs.map +1 -1
  74. package/dist/vite.mjs +8 -7
  75. package/dist/vite.mjs.map +1 -1
  76. package/dist/zod-v3.cjs +3 -3
  77. package/dist/zod-v3.d.cts +32 -6
  78. package/dist/zod-v3.d.mts +32 -6
  79. package/dist/zod-v3.d.ts +32 -6
  80. package/dist/zod-v3.mjs +3 -3
  81. package/dist/zod-v4.cjs +3 -3
  82. package/dist/zod-v4.d.cts +12 -8
  83. package/dist/zod-v4.d.mts +12 -8
  84. package/dist/zod-v4.d.ts +12 -8
  85. package/dist/zod-v4.mjs +3 -3
  86. package/dist/zod.cjs +8 -8
  87. package/dist/zod.cjs.map +1 -1
  88. package/dist/zod.d.cts +6 -6
  89. package/dist/zod.d.mts +6 -6
  90. package/dist/zod.d.ts +6 -6
  91. package/dist/zod.mjs +6 -6
  92. package/package.json +2 -2
  93. package/dist/shared/attaform.-1GQTX2T.mjs.map +0 -1
  94. package/dist/shared/attaform.5UhpSVFI.cjs.map +0 -1
  95. package/dist/shared/attaform.BqK_L4gK.cjs.map +0 -1
  96. package/dist/shared/attaform.Bubm_slq.cjs.map +0 -1
  97. package/dist/shared/attaform.C8CyvYa_.cjs +0 -36
  98. package/dist/shared/attaform.C8CyvYa_.cjs.map +0 -1
  99. package/dist/shared/attaform.CDmaxrt2.mjs.map +0 -1
  100. package/dist/shared/attaform.CXpzmj38.mjs.map +0 -1
  101. package/dist/shared/attaform.CZ-XtZt_.mjs.map +0 -1
  102. package/dist/shared/attaform.D13GMFgK.mjs +0 -32
  103. package/dist/shared/attaform.D13GMFgK.mjs.map +0 -1
  104. package/dist/shared/attaform.DUHru0OF.cjs +0 -1600
  105. package/dist/shared/attaform.DUHru0OF.cjs.map +0 -1
  106. package/dist/shared/attaform.Df0tU0Ut.mjs +0 -1594
  107. package/dist/shared/attaform.Df0tU0Ut.mjs.map +0 -1
  108. package/dist/shared/attaform.Dl161U6E.mjs +0 -57
  109. package/dist/shared/attaform.Dl161U6E.mjs.map +0 -1
  110. package/dist/shared/attaform.Dlk1jMuv.cjs.map +0 -1
  111. package/dist/shared/attaform.II89Pcf4.cjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const vue = require('vue');
4
- const paths = require('./attaform.BqK_L4gK.cjs');
4
+ const paths = require('./attaform.BPy-4qRx.cjs');
5
5
 
6
6
  const NOT_FOUND = Symbol("NOT_FOUND");
7
7
  function descendStep(value, segment) {
@@ -64,7 +64,7 @@ function setAtPathOffset(root, path, value, offset) {
64
64
  arr[head] = setAtPathOffset(arr[head], path, value, nextOffset);
65
65
  return arr;
66
66
  }
67
- const rec = isPlainRecord(root) ? { ...root } : {};
67
+ const rec = isPlainRecord(root) ? Object.assign(/* @__PURE__ */ Object.create(null), root) : /* @__PURE__ */ Object.create(null);
68
68
  rec[head] = setAtPathOffset(rec[head], path, value, nextOffset);
69
69
  return rec;
70
70
  }
@@ -165,7 +165,7 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
165
165
  return consumer;
166
166
  }
167
167
  let mutated = false;
168
- const out = { ...consumer };
168
+ const out = Object.assign(/* @__PURE__ */ Object.create(null), consumer);
169
169
  for (const key of Object.keys(defaultValue)) {
170
170
  if (!(key in consumer)) {
171
171
  const defAtKey = defaultValue[key];
@@ -429,13 +429,81 @@ function structuralSnapshot(value) {
429
429
  return out2;
430
430
  }
431
431
  const src = value;
432
- const out = {};
432
+ const out = /* @__PURE__ */ Object.create(null);
433
433
  for (const k of Object.keys(src)) {
434
434
  out[k] = structuralSnapshot(src[k]);
435
435
  }
436
436
  return out;
437
437
  }
438
438
 
439
+ const defaultDisplayState = (field, formMeta) => {
440
+ const gateOpen = formMeta.submissionAttempts > 0 || field.blurredAfterInteraction === true;
441
+ if (!gateOpen) return "idle";
442
+ if (field.validating === true) return "pending";
443
+ const hasOwnError = field.errors.some(
444
+ (e) => e.path.length === field.path.length && e.path.every((s, i) => s === field.path[i])
445
+ );
446
+ if (hasOwnError) return "error";
447
+ if (field.valid === true && field.blank !== true && field.dirty === true) return "success";
448
+ return "idle";
449
+ };
450
+ function resolveGetDisplayState(config) {
451
+ return config ?? defaultDisplayState;
452
+ }
453
+
454
+ const DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS = 0;
455
+ const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 300;
456
+ const DEFAULT_HISTORY_MAX_SNAPSHOTS = 128;
457
+ const PERSISTENCE_KEY_PREFIX = "attaform:";
458
+ const RESERVED_KEY_PREFIX = "__atta:";
459
+ const ANONYMOUS_FORM_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon:`;
460
+ const ANONYMOUS_WIZARD_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon-wizard:`;
461
+ const DEFAULT_MAX_RECURSION_DEPTH = 64;
462
+ function normalizeNumericOption(config) {
463
+ const { value, source, allowInfinity, min, defaultValue } = config;
464
+ if (allowInfinity && value === Infinity) return Infinity;
465
+ if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
466
+ if (paths.__DEV__) {
467
+ const acceptedDescription = allowInfinity ? "a non-negative integer or Infinity" : "a non-negative finite integer";
468
+ console.warn(
469
+ `[attaform] ${source} must be ${acceptedDescription}; got ${String(value)}. Falling back to ${String(defaultValue)}.`
470
+ );
471
+ }
472
+ return defaultValue;
473
+ }
474
+ return Math.max(min, Math.floor(value));
475
+ }
476
+
477
+ function hashStableString(input, seed = 0) {
478
+ let h1 = 3735928559 ^ seed;
479
+ let h2 = 1103547991 ^ seed;
480
+ for (let i = 0; i < input.length; i++) {
481
+ const ch = input.charCodeAt(i);
482
+ h1 = Math.imul(h1 ^ ch, 2654435761);
483
+ h2 = Math.imul(h2 ^ ch, 1597334677);
484
+ }
485
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
486
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
487
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).padStart(11, "0");
488
+ }
489
+
490
+ const ANON_STEM = "atta";
491
+ function readableFormKeyStem(formKey) {
492
+ if (formKey === "" || formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return ANON_STEM;
493
+ const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|(?<!-)-+$/g, "");
494
+ return sanitized === "" ? ANON_STEM : sanitized;
495
+ }
496
+ function fieldIdToken(formInstanceId, pathKey) {
497
+ return hashStableString(`${formInstanceId}:${pathKey}`).slice(-7);
498
+ }
499
+ function computeFieldIdentity(formInstanceId, formKey, pathKey) {
500
+ const id = `${readableFormKeyStem(formKey)}-${fieldIdToken(formInstanceId, pathKey)}`;
501
+ return {
502
+ id,
503
+ aria: Object.freeze({ errorId: `${id}-error`, descriptionId: `${id}-description` })
504
+ };
505
+ }
506
+
439
507
  const EMPTY_RESOLVED_FIELD_META = Object.freeze({
440
508
  label: "",
441
509
  description: void 0,
@@ -456,6 +524,7 @@ function humanize(segment) {
456
524
  }).join(" ");
457
525
  }
458
526
 
527
+ const warnedDisplayStatePredicates = /* @__PURE__ */ new WeakSet();
459
528
  function isUnderStubAncestor(state, segments) {
460
529
  for (let i = 0; i < segments.length; i++) {
461
530
  const ancestorPath = segments.slice(0, i);
@@ -469,21 +538,21 @@ function isUnderStubAncestor(state, segments) {
469
538
  }
470
539
  return false;
471
540
  }
472
- function buildFieldStateAccessor(state, getFormMetaBase, options) {
541
+ function buildFieldStateAccessor(state, formInstanceId, getFormMetaBase, options) {
473
542
  const cache = /* @__PURE__ */ new Map();
474
- const predicate = options?.shouldShowErrors;
543
+ const predicate = options?.getDisplayState;
475
544
  return function getFieldState(pathInput) {
476
545
  const { segments, key } = paths.canonicalizePath(pathInput);
477
546
  const cached = cache.get(key);
478
547
  if (cached !== void 0) return cached;
479
548
  const c = vue.computed(
480
- () => state.schema.isLeafAtPath(segments) ? buildLeafFieldState(state, segments, key, getFormMetaBase, predicate) : buildContainerFieldState(state, segments, key, getFormMetaBase, predicate)
549
+ () => state.schema.isLeafAtPath(segments) ? buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBase, predicate) : buildContainerFieldState(state, segments, key, formInstanceId, getFormMetaBase, predicate)
481
550
  );
482
551
  cache.set(key, c);
483
552
  return c;
484
553
  };
485
554
  }
486
- function buildLeafFieldStateBase(state, segments, key) {
555
+ function buildLeafFieldStateBase(state, segments, key, formInstanceId) {
487
556
  const record = state.fields.get(key);
488
557
  const value = state.getValueAtPath(segments);
489
558
  const original = state.originals.get(key)?.value;
@@ -513,6 +582,8 @@ function buildLeafFieldStateBase(state, segments, key) {
513
582
  focused: record?.focused ?? null,
514
583
  blurred: record?.blurred ?? null,
515
584
  touched: record?.touched ?? false,
585
+ interacted: record?.interacted ?? false,
586
+ blurredAfterInteraction: record?.blurredAfterInteraction ?? false,
516
587
  connected: record?.connected ?? false,
517
588
  element: firstElement,
518
589
  elements: elementsArr,
@@ -521,6 +592,8 @@ function buildLeafFieldStateBase(state, segments, key) {
521
592
  validating,
522
593
  valid,
523
594
  path: segments,
595
+ ...computeFieldIdentity(formInstanceId, state.formKey, key),
596
+ key: state.arrayElementKey(segments),
524
597
  blank: state.blankPaths.has(key),
525
598
  label,
526
599
  description: resolved.description,
@@ -528,31 +601,32 @@ function buildLeafFieldStateBase(state, segments, key) {
528
601
  meta: resolved.meta
529
602
  };
530
603
  }
531
- function buildLeafFieldState(state, segments, key, getFormMetaBase, shouldShowErrors) {
532
- const base = buildLeafFieldStateBase(state, segments, key);
533
- return decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors);
604
+ function buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBase, getDisplayState) {
605
+ const base = buildLeafFieldStateBase(state, segments, key, formInstanceId);
606
+ return decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState);
534
607
  }
535
- function buildContainerFieldStateBase(state, segments, _key) {
608
+ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
536
609
  const formValue = state.form.value;
537
610
  const value = state.getValueAtPath(segments);
538
- const original = state.originals.get(paths.canonicalizePath(segments).key)?.value;
611
+ const original = state.originals.get(key)?.value;
539
612
  let pristine = true;
540
613
  let blank = true;
541
614
  let dirty = false;
542
615
  let focused = false;
543
616
  let blurred = false;
544
617
  let touched = false;
618
+ let interacted = false;
619
+ let blurredAfterInteraction = false;
545
620
  let connected = false;
546
621
  let validating = false;
547
622
  let updatedAt = null;
548
623
  let asyncPending = false;
549
- for (const [, entry] of state.originals) {
624
+ for (const [leafKey, entry] of state.originals) {
550
625
  if (!paths.isPathPrefix(segments, entry.segments)) continue;
551
626
  if (segments.length === entry.segments.length) continue;
552
627
  if (!hasAtPath(formValue, entry.segments)) continue;
553
- const leafKey = paths.canonicalizePath(entry.segments).key;
554
628
  const leafRecord = state.fields.get(leafKey);
555
- if (!state.isPristineAtPath(entry.segments)) {
629
+ if (!state.isPristineAtPathByKey(leafKey, entry.segments)) {
556
630
  pristine = false;
557
631
  dirty = true;
558
632
  }
@@ -560,14 +634,20 @@ function buildContainerFieldStateBase(state, segments, _key) {
560
634
  if (leafRecord?.focused === true) focused = true;
561
635
  if (leafRecord?.blurred === true) blurred = true;
562
636
  if (leafRecord?.touched === true) touched = true;
637
+ if (leafRecord?.interacted === true) interacted = true;
638
+ if (leafRecord?.blurredAfterInteraction === true) blurredAfterInteraction = true;
563
639
  if (leafRecord?.connected === true) connected = true;
564
640
  if ((state.fieldValidationCounts.get(leafKey) ?? 0) > 0) validating = true;
565
- if (state.pathHasAsyncValidation(entry.segments)) asyncPending = true;
641
+ if (state.pathHasAsyncValidationByKey(leafKey, entry.segments)) asyncPending = true;
566
642
  const ts = leafRecord?.updatedAt;
567
643
  if (ts !== void 0 && ts !== null) {
568
644
  if (updatedAt === null || ts > updatedAt) updatedAt = ts;
569
645
  }
570
646
  }
647
+ if (!dirty && state.hasStructuralChangeUnder(segments)) {
648
+ pristine = false;
649
+ dirty = true;
650
+ }
571
651
  const errors = aggregateErrorsAt(state, segments);
572
652
  if (!asyncPending && state.pathHasAsyncValidation(segments)) asyncPending = true;
573
653
  const gated = asyncPending && !state.firstValidationDone.value;
@@ -583,6 +663,8 @@ function buildContainerFieldStateBase(state, segments, _key) {
583
663
  focused,
584
664
  blurred,
585
665
  touched,
666
+ interacted,
667
+ blurredAfterInteraction,
586
668
  connected,
587
669
  element: null,
588
670
  elements: EMPTY_ELEMENTS,
@@ -591,6 +673,8 @@ function buildContainerFieldStateBase(state, segments, _key) {
591
673
  validating,
592
674
  valid,
593
675
  path: segments,
676
+ ...computeFieldIdentity(formInstanceId, state.formKey, key),
677
+ key: state.arrayElementKey(segments),
594
678
  blank,
595
679
  label,
596
680
  description: resolved.description,
@@ -598,15 +682,36 @@ function buildContainerFieldStateBase(state, segments, _key) {
598
682
  meta: resolved.meta
599
683
  };
600
684
  }
601
- function buildContainerFieldState(state, segments, key, getFormMetaBase, shouldShowErrors) {
602
- const base = buildContainerFieldStateBase(state, segments);
603
- return decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors);
685
+ function buildContainerFieldState(state, segments, key, formInstanceId, getFormMetaBase, getDisplayState) {
686
+ const base = buildContainerFieldStateBase(state, segments, key, formInstanceId);
687
+ return decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState);
604
688
  }
605
- function decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors) {
689
+ function decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState) {
606
690
  const firstError = base.errors[0];
607
- const predicate = shouldShowErrors ?? state.shouldShowErrors;
608
- const showErrors = base.errors.length > 0 && predicate(base, getFormMetaBase());
609
- return { ...base, showErrors, firstError };
691
+ const predicate = getDisplayState ?? state.getDisplayState;
692
+ const formMeta = getFormMetaBase();
693
+ let displayState;
694
+ try {
695
+ displayState = predicate(base, formMeta);
696
+ } catch (err) {
697
+ if (paths.__DEV__ && !warnedDisplayStatePredicates.has(predicate)) {
698
+ warnedDisplayStatePredicates.add(predicate);
699
+ console.warn(
700
+ "[attaform] custom getDisplayState threw \u2014 falling back to defaultDisplayState. Subsequent throws from the same predicate will not warn again.",
701
+ err
702
+ );
703
+ }
704
+ displayState = defaultDisplayState(base, formMeta);
705
+ }
706
+ return {
707
+ ...base,
708
+ displayState,
709
+ showErrors: displayState === "error",
710
+ showPending: displayState === "pending",
711
+ showSuccess: displayState === "success",
712
+ showIdle: displayState === "idle",
713
+ firstError
714
+ };
610
715
  }
611
716
  function aggregateErrorsAt(state, prefix) {
612
717
  const formValue = state.form.value;
@@ -632,6 +737,41 @@ function aggregateErrorsAt(state, prefix) {
632
737
  }
633
738
  const EMPTY_ELEMENTS = Object.freeze([]);
634
739
 
740
+ function liveKeysAtPath(state, segments) {
741
+ const value = getAtPath(state.form.value, segments);
742
+ if (value === null || value === void 0) return [];
743
+ if (Array.isArray(value)) {
744
+ const keys = new Array(value.length);
745
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
746
+ return keys;
747
+ }
748
+ if (typeof value === "object") return Object.keys(value);
749
+ return [];
750
+ }
751
+ function isArrayPath(state, segments) {
752
+ if (segments.length === 0) return false;
753
+ return Array.isArray(getAtPath(state.form.value, segments));
754
+ }
755
+
756
+ function warnReadOnly(surface, action, key) {
757
+ if (!paths.__DEV__) return;
758
+ const phrase = action === "write" ? `write to "${String(key)}"` : `${action} of "${String(key)}"`;
759
+ console.warn(
760
+ `[attaform] ${surface} is read-only \u2014 ${phrase} was ignored. Mutate the form via setValue / the directive / field-array helpers instead.`
761
+ );
762
+ }
763
+ function makeReadonlyCoercion(snapshot) {
764
+ const toString = () => JSON.stringify(snapshot());
765
+ return {
766
+ toString,
767
+ valueOf() {
768
+ return this;
769
+ },
770
+ toJSON: snapshot,
771
+ toPrimitive: (hint) => hint === "number" ? NaN : toString()
772
+ };
773
+ }
774
+
635
775
  const INTEGER_SEGMENT = /^(?:0|[1-9]\d*)$/;
636
776
  function keyToSegment(key) {
637
777
  return INTEGER_SEGMENT.test(key) ? Number(key) : key;
@@ -663,12 +803,12 @@ function buildSurfaceProxy(opts) {
663
803
  const existing = containerCache.get(cacheKey);
664
804
  if (existing !== void 0) return existing;
665
805
  const snapshotContainer = () => opts.materializeContainer === void 0 ? {} : opts.materializeContainer(segments);
666
- const containerToJSON = () => snapshotContainer();
667
- const containerToString = () => JSON.stringify(snapshotContainer());
668
- function containerValueOf() {
669
- return this;
670
- }
671
- const containerToPrimitive = (hint) => hint === "number" ? NaN : containerToString();
806
+ const {
807
+ toString: containerToString,
808
+ valueOf: containerValueOf,
809
+ toJSON: containerToJSON,
810
+ toPrimitive: containerToPrimitive
811
+ } = makeReadonlyCoercion(snapshotContainer);
672
812
  const target = isArrayLike ? [] : (() => {
673
813
  });
674
814
  const proxy = new Proxy(target, {
@@ -693,6 +833,9 @@ function buildSurfaceProxy(opts) {
693
833
  return opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
694
834
  }
695
835
  const childSegs = [...segments, keyToSegment(key)];
836
+ if ((isArrayLike || opts.isArrayContainer?.(segments) === true) && typeof keyToSegment(key) === "string" && key in Array.prototype) {
837
+ return Reflect.get(Array.prototype, key);
838
+ }
696
839
  if (key === "toString" || key === "valueOf") {
697
840
  if (!schemaHasPath(childSegs)) {
698
841
  return key === "toString" ? containerToString : containerValueOf;
@@ -737,10 +880,25 @@ function buildSurfaceProxy(opts) {
737
880
  };
738
881
  },
739
882
  // Block writes at the proxy boundary. Mutations go through
740
- // `setValue`, the directive, or the field-array helpers.
741
- set: () => false,
742
- deleteProperty: () => false,
743
- defineProperty: () => false
883
+ // `setValue`, the directive, or the field-array helpers. Each
884
+ // trap returns `true` (warn-and-noop) returning `false` from a
885
+ // `set`/`delete`/`defineProperty` trap throws `TypeError` under
886
+ // strict mode (every ESM / `<script setup>`), which would surface
887
+ // a host-level exception in consumer code that the library
888
+ // documents as "writes are ignored." Aligns with the contract on
889
+ // `form.values` / `wizard.statuses`.
890
+ set: (_, key) => {
891
+ warnReadOnly("form.fields / form.errors", "write", key);
892
+ return true;
893
+ },
894
+ deleteProperty: (_, key) => {
895
+ warnReadOnly("form.fields / form.errors", "delete", key);
896
+ return true;
897
+ },
898
+ defineProperty: (_, key) => {
899
+ warnReadOnly("form.fields / form.errors", "define", key);
900
+ return true;
901
+ }
744
902
  });
745
903
  containerCache.set(cacheKey, proxy);
746
904
  return proxy;
@@ -762,11 +920,12 @@ function buildSurfaceProxy(opts) {
762
920
  }
763
921
  return snapshot;
764
922
  };
765
- const leafToString = () => JSON.stringify(snapshotLeaf());
766
- function leafValueOf() {
767
- return this;
768
- }
769
- const leafToPrimitive = (hint) => hint === "number" ? NaN : leafToString();
923
+ const {
924
+ toString: leafToString,
925
+ valueOf: leafValueOf,
926
+ toJSON: leafToJSONHandler,
927
+ toPrimitive: leafToPrimitive
928
+ } = makeReadonlyCoercion(snapshotLeaf);
770
929
  const target = (() => {
771
930
  });
772
931
  const proxy = new Proxy(target, {
@@ -784,7 +943,7 @@ function buildSurfaceProxy(opts) {
784
943
  if (typeof key !== "string") return void 0;
785
944
  if (key === "toString") return leafToString;
786
945
  if (key === "valueOf") return leafValueOf;
787
- if (key === "toJSON") return snapshotLeaf;
946
+ if (key === "toJSON") return leafToJSONHandler;
788
947
  if (leafKeys.has(key)) {
789
948
  const leaf = opts.resolveLeaf(segments);
790
949
  return readLeafKey(leaf, key);
@@ -811,9 +970,22 @@ function buildSurfaceProxy(opts) {
811
970
  writable: false
812
971
  };
813
972
  },
814
- set: () => false,
815
- deleteProperty: () => false,
816
- defineProperty: () => false
973
+ // Same warn-and-noop contract as the container traps above.
974
+ // Returning `true` keeps strict-mode callers from throwing on
975
+ // `form.fields.email.value = …`; the actual readonly guarantee
976
+ // is the absence of any mutation, not the host-level reject.
977
+ set: (_, key) => {
978
+ warnReadOnly("form.fields.<leaf>", "write", key);
979
+ return true;
980
+ },
981
+ deleteProperty: (_, key) => {
982
+ warnReadOnly("form.fields.<leaf>", "delete", key);
983
+ return true;
984
+ },
985
+ defineProperty: (_, key) => {
986
+ warnReadOnly("form.fields.<leaf>", "define", key);
987
+ return true;
988
+ }
817
989
  });
818
990
  leafViewCache.set(cacheKey, proxy);
819
991
  return proxy;
@@ -877,33 +1049,53 @@ function buildErrorsProxy(state) {
877
1049
  // working — the call-form just extends that semantic to
878
1050
  // containers and dynamic paths.
879
1051
  resolveCallTarget: (path) => aggregateErrorsAt(state, path),
880
- // Mirror `form.fields` enumeration: `Object.keys(form.errors.items)`
881
- // and `v-for="(errs, idx) in form.errors.items"` walk the live
882
- // array indices / object keys at the path. Iteration yields the
883
- // descended sub-proxies (one per live key), so consumers can
884
- // `form.errors.items[idx]` straight from the entry.
885
- containerOwnKeys: (segments) => liveKeysAtPath$1(state, segments),
886
- isArrayContainer: (segments) => isArrayPath$1(state, segments)
1052
+ // Enumeration unions the live form-data keys at this path with the
1053
+ // first-child segments drawn from every error store. Without the
1054
+ // union, `Object.keys(form.errors)` / `{...form.errors}` /
1055
+ // `v-for="(errs, k) in form.errors"` silently dropped two
1056
+ // important error classes that the dot / call / JSON.stringify
1057
+ // surfaces already exposed:
1058
+ //
1059
+ // - **Form-level** errors at the synthetic `['']` path (set via
1060
+ // `setFormErrors` or root cross-field refines).
1061
+ // - **Server-only** errors at a key the schema doesn't know
1062
+ // about (`['ghost']`, `['address', 'ghost']`).
1063
+ //
1064
+ // The union closes that gap so `ownKeys` agrees with the rest of
1065
+ // the surface. Active-path filter mirrors `resolveLeaf`:
1066
+ // library-produced verdicts (schema + derived-blank) at unreachable
1067
+ // paths stay hidden; user-supplied errors are unconditional.
1068
+ containerOwnKeys: (segments) => errorAwareContainerKeys(state, segments),
1069
+ isArrayContainer: (segments) => isArrayPath(state, segments)
887
1070
  });
888
1071
  }
889
- function liveKeysAtPath$1(state, segments) {
890
- const value = getAtPath(state.form.value, segments);
891
- if (value === null || value === void 0) return [];
892
- if (Array.isArray(value)) {
893
- const keys = new Array(value.length);
894
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
895
- return keys;
896
- }
897
- if (typeof value === "object") return Object.keys(value);
898
- return [];
899
- }
900
- function isArrayPath$1(state, segments) {
901
- if (segments.length === 0) return false;
902
- return Array.isArray(getAtPath(state.form.value, segments));
1072
+ function errorAwareContainerKeys(state, segments) {
1073
+ const keys = new Set(liveKeysAtPath(state, segments));
1074
+ const formValue = state.form.value;
1075
+ const walk = (store, applyActivePathFilter) => {
1076
+ for (const [pathKey, errors] of store) {
1077
+ if (errors.length === 0) continue;
1078
+ if (pathKey === paths.FORM_ERRORS_PATH_KEY) {
1079
+ if (segments.length === 0) keys.add("");
1080
+ continue;
1081
+ }
1082
+ const decoded = paths.segmentsForPathKey(pathKey);
1083
+ if (decoded === null) continue;
1084
+ if (decoded.length <= segments.length) continue;
1085
+ if (!paths.isPathPrefix(segments, decoded)) continue;
1086
+ if (applyActivePathFilter && !hasAtPath(formValue, decoded)) continue;
1087
+ const nextSeg = decoded[segments.length];
1088
+ keys.add(typeof nextSeg === "number" ? String(nextSeg) : nextSeg);
1089
+ }
1090
+ };
1091
+ walk(state.schemaErrors, true);
1092
+ walk(state.derivedBlankErrors.value, true);
1093
+ walk(state.userErrors, false);
1094
+ return [...keys];
903
1095
  }
904
1096
  function materializeErrors(state, containerSegments) {
905
1097
  const liveContainer = getAtPath(state.form.value, containerSegments);
906
- const tree = Array.isArray(liveContainer) ? [] : {};
1098
+ const tree = Array.isArray(liveContainer) ? [] : /* @__PURE__ */ Object.create(null);
907
1099
  const collect = (store, applyActivePathFilter) => {
908
1100
  entries: for (const [pathKey, errors] of store) {
909
1101
  if (errors.length === 0) continue;
@@ -953,7 +1145,7 @@ function placeAt(tree, path, errors) {
953
1145
  const cursorRecord2 = cursor;
954
1146
  let child = cursorRecord2[key];
955
1147
  if (child === null || child === void 0 || typeof child !== "object") {
956
- child = typeof nextSeg === "number" ? [] : {};
1148
+ child = typeof nextSeg === "number" ? [] : /* @__PURE__ */ Object.create(null);
957
1149
  cursorRecord2[key] = child;
958
1150
  }
959
1151
  cursor = child;
@@ -988,19 +1180,20 @@ function buildFieldArrayApi(state) {
988
1180
  prepend(path, value) {
989
1181
  const next = readArray(path);
990
1182
  next.unshift(value);
991
- return writeArray(path, next, { kind: "shift-from", index: 0 });
1183
+ return writeArray(path, next, { kind: "insert", index: 0 });
992
1184
  },
993
1185
  insert(path, index, value) {
994
1186
  const next = readArray(path);
995
- next.splice(index, 0, value);
996
- const clampedIndex = Math.max(0, Math.min(index, next.length));
997
- return writeArray(path, next, { kind: "shift-from", index: clampedIndex });
1187
+ const preLen = next.length;
1188
+ const insertIndex = index < 0 ? Math.max(0, preLen + index) : Math.min(index, preLen);
1189
+ next.splice(insertIndex, 0, value);
1190
+ return writeArray(path, next, { kind: "insert", index: insertIndex });
998
1191
  },
999
1192
  remove(path, index) {
1000
1193
  const next = readArray(path);
1001
1194
  if (index < 0 || index >= next.length) return false;
1002
1195
  next.splice(index, 1);
1003
- return writeArray(path, next, { kind: "shift-from", index });
1196
+ return writeArray(path, next, { kind: "remove", index });
1004
1197
  },
1005
1198
  swap(path, a, b) {
1006
1199
  const next = readArray(path);
@@ -1018,11 +1211,7 @@ function buildFieldArrayApi(state) {
1018
1211
  const [item] = next.splice(from, 1);
1019
1212
  const clampedTo = Math.max(0, Math.min(to, next.length));
1020
1213
  next.splice(clampedTo, 0, item);
1021
- return writeArray(path, next, {
1022
- kind: "shift-range",
1023
- fromIndex: Math.min(from, clampedTo),
1024
- toIndex: Math.max(from, clampedTo)
1025
- });
1214
+ return writeArray(path, next, { kind: "move", from, to: clampedTo });
1026
1215
  },
1027
1216
  replace(path, index, value) {
1028
1217
  const next = readArray(path);
@@ -1041,6 +1230,8 @@ const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
1041
1230
  "focused",
1042
1231
  "blurred",
1043
1232
  "touched",
1233
+ "interacted",
1234
+ "blurredAfterInteraction",
1044
1235
  "connected",
1045
1236
  "element",
1046
1237
  "elements",
@@ -1048,20 +1239,28 @@ const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
1048
1239
  "errors",
1049
1240
  "validating",
1050
1241
  "valid",
1242
+ "displayState",
1051
1243
  "showErrors",
1244
+ "showPending",
1245
+ "showSuccess",
1246
+ "showIdle",
1052
1247
  "firstError",
1053
1248
  "path",
1249
+ "id",
1250
+ "aria",
1251
+ "key",
1054
1252
  "blank",
1055
1253
  "label",
1056
1254
  "description",
1057
1255
  "placeholder",
1058
1256
  "meta"
1059
1257
  ]);
1060
- function buildFieldStateProxy(state, getFormMetaBase, options) {
1258
+ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1061
1259
  const getFieldStateAt = buildFieldStateAccessor(
1062
1260
  state,
1261
+ formInstanceId,
1063
1262
  getFormMetaBase,
1064
- options?.shouldShowErrors !== void 0 ? { shouldShowErrors: options.shouldShowErrors } : void 0
1263
+ options?.getDisplayState !== void 0 ? { getDisplayState: options.getDisplayState } : void 0
1065
1264
  );
1066
1265
  const snapshotFieldStateAt = (path) => {
1067
1266
  const view = getFieldStateAt(path).value;
@@ -1110,9 +1309,34 @@ function buildFieldStateProxy(state, getFormMetaBase, options) {
1110
1309
  writable: false
1111
1310
  };
1112
1311
  },
1113
- set: () => false,
1114
- deleteProperty: () => false,
1115
- defineProperty: () => false
1312
+ // Warn-and-noop: returning `false` here throws `TypeError` under
1313
+ // strict mode (`form.fields('email').value = 1` from any ESM
1314
+ // module), which would violate the documented "writes are
1315
+ // ignored" contract. Mirrors `values-proxy` / `wizard-statuses-proxy`.
1316
+ set: (_, key) => {
1317
+ if (paths.__DEV__) {
1318
+ console.warn(
1319
+ `[attaform] form.fields(path) is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
1320
+ );
1321
+ }
1322
+ return true;
1323
+ },
1324
+ deleteProperty: (_, key) => {
1325
+ if (paths.__DEV__) {
1326
+ console.warn(
1327
+ `[attaform] form.fields(path) is read-only \u2014 delete of "${String(key)}" was ignored.`
1328
+ );
1329
+ }
1330
+ return true;
1331
+ },
1332
+ defineProperty: (_, key) => {
1333
+ if (paths.__DEV__) {
1334
+ console.warn(
1335
+ `[attaform] form.fields(path) is read-only \u2014 define of "${String(key)}" was ignored.`
1336
+ );
1337
+ }
1338
+ return true;
1339
+ }
1116
1340
  });
1117
1341
  terminalCache.set(cacheKey, proxy);
1118
1342
  return proxy;
@@ -1128,21 +1352,6 @@ function buildFieldStateProxy(state, getFormMetaBase, options) {
1128
1352
  isArrayContainer: (segments) => isArrayPath(state, segments)
1129
1353
  });
1130
1354
  }
1131
- function liveKeysAtPath(state, segments) {
1132
- const value = getAtPath(state.form.value, segments);
1133
- if (value === null || value === void 0) return [];
1134
- if (Array.isArray(value)) {
1135
- const keys = new Array(value.length);
1136
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
1137
- return keys;
1138
- }
1139
- if (typeof value === "object") return Object.keys(value);
1140
- return [];
1141
- }
1142
- function isArrayPath(state, segments) {
1143
- if (segments.length === 0) return false;
1144
- return Array.isArray(getAtPath(state.form.value, segments));
1145
- }
1146
1355
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
1147
1356
  const liveValue = getAtPath(state.form.value, containerSegments);
1148
1357
  return walk$2(liveValue, containerSegments, state.schema, snapshotFieldStateAt);
@@ -1168,29 +1377,6 @@ function walk$2(value, basePath, schema, snapshotFieldStateAt) {
1168
1377
  return result;
1169
1378
  }
1170
1379
 
1171
- const DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS = 0;
1172
- const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 300;
1173
- const DEFAULT_HISTORY_MAX_SNAPSHOTS = 128;
1174
- const PERSISTENCE_KEY_PREFIX = "attaform:";
1175
- const RESERVED_KEY_PREFIX = "__atta:";
1176
- const ANONYMOUS_FORM_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon:`;
1177
- const ANONYMOUS_WIZARD_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon-wizard:`;
1178
- const DEFAULT_MAX_RECURSION_DEPTH = 64;
1179
- function normalizeNumericOption(config) {
1180
- const { value, source, allowInfinity, min, defaultValue } = config;
1181
- if (allowInfinity && value === Infinity) return Infinity;
1182
- if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
1183
- if (paths.__DEV__) {
1184
- const acceptedDescription = allowInfinity ? "a non-negative integer or Infinity" : "a non-negative finite integer";
1185
- console.warn(
1186
- `[attaform] ${source} must be ${acceptedDescription}; got ${String(value)}. Falling back to ${String(defaultValue)}.`
1187
- );
1188
- }
1189
- return defaultValue;
1190
- }
1191
- return Math.max(min, Math.floor(value));
1192
- }
1193
-
1194
1380
  const PERSISTENCE_MODULE_KEY = "persistence";
1195
1381
  async function getStorageAdapter(storage) {
1196
1382
  if (typeof storage === "object") return storage;
@@ -1209,7 +1395,7 @@ async function getStorageAdapter(storage) {
1209
1395
  }
1210
1396
  }
1211
1397
  }
1212
- const PERSISTED_ENVELOPE_VERSION = 5;
1398
+ const PERSISTED_ENVELOPE_VERSION = 6;
1213
1399
  function readPersistedPayload(value) {
1214
1400
  if (value === null || value === void 0 || typeof value !== "object") return null;
1215
1401
  const envelope = value;
@@ -1233,12 +1419,7 @@ function warnVersionMismatch(observedVersion) {
1233
1419
  function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPaths) {
1234
1420
  let transientList;
1235
1421
  if (blankPaths !== void 0 && blankPaths.size > 0) {
1236
- const dotted = [];
1237
- for (const key of blankPaths) {
1238
- const d = paths.pathKeyToDotted(key);
1239
- if (d !== null) dotted.push(d);
1240
- }
1241
- transientList = dotted.length > 0 ? dotted : void 0;
1422
+ transientList = [...blankPaths];
1242
1423
  }
1243
1424
  if (include === "form") {
1244
1425
  if (transientList === void 0) return { v: PERSISTED_ENVELOPE_VERSION, data: { form } };
@@ -1260,30 +1441,35 @@ function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPat
1260
1441
  function createDebouncedWriter(write, debounceMs) {
1261
1442
  let timer = null;
1262
1443
  let pending = null;
1444
+ let writeGeneration = 0;
1445
+ function runWrite() {
1446
+ const gen = ++writeGeneration;
1447
+ pending = write().finally(() => {
1448
+ if (writeGeneration === gen) pending = null;
1449
+ });
1450
+ }
1263
1451
  function schedule() {
1264
1452
  if (timer !== null) clearTimeout(timer);
1265
1453
  if (debounceMs === 0) {
1266
- pending = write().finally(() => {
1267
- pending = null;
1268
- });
1454
+ runWrite();
1269
1455
  return;
1270
1456
  }
1271
1457
  timer = setTimeout(() => {
1272
1458
  timer = null;
1273
- pending = write().finally(() => {
1274
- pending = null;
1275
- });
1459
+ runWrite();
1276
1460
  }, debounceMs);
1277
1461
  }
1278
1462
  async function flush() {
1279
1463
  if (timer !== null) {
1280
1464
  clearTimeout(timer);
1281
1465
  timer = null;
1282
- pending = write().finally(() => {
1283
- pending = null;
1284
- });
1466
+ runWrite();
1467
+ }
1468
+ while (pending !== null) {
1469
+ const awaited = pending;
1470
+ await awaited;
1471
+ if (pending === awaited) break;
1285
1472
  }
1286
- if (pending !== null) await pending;
1287
1473
  }
1288
1474
  function cancel() {
1289
1475
  if (timer !== null) {
@@ -1296,7 +1482,7 @@ function createDebouncedWriter(write, debounceMs) {
1296
1482
  function resolveStorageKeyBase(config, formKey) {
1297
1483
  return config.key ?? `${PERSISTENCE_KEY_PREFIX}${formKey}`;
1298
1484
  }
1299
- async function cleanupOrphanKeys(adapter, base, currentKey) {
1485
+ async function removeMatchingKeys(adapter, base, keepKey) {
1300
1486
  let keys;
1301
1487
  try {
1302
1488
  keys = await adapter.listKeys(base);
@@ -1304,12 +1490,15 @@ async function cleanupOrphanKeys(adapter, base, currentKey) {
1304
1490
  return;
1305
1491
  }
1306
1492
  for (const key of keys) {
1307
- if (key === currentKey) continue;
1493
+ if (key === keepKey) continue;
1308
1494
  if (key === base || key.startsWith(`${base}:`)) {
1309
1495
  void adapter.removeItem(key).catch(() => void 0);
1310
1496
  }
1311
1497
  }
1312
1498
  }
1499
+ async function cleanupOrphanKeys(adapter, base, currentKey) {
1500
+ await removeMatchingKeys(adapter, base, currentKey);
1501
+ }
1313
1502
  const STANDARD_STORAGE_KINDS = ["local", "session", "indexeddb"];
1314
1503
  function normalizePersistConfig(input) {
1315
1504
  if (typeof input === "string") return { storage: input };
@@ -1320,12 +1509,7 @@ async function sweepAllOrphansAcrossStandardStores(base) {
1320
1509
  for (const kind of STANDARD_STORAGE_KINDS) {
1321
1510
  try {
1322
1511
  const adapter = await getStorageAdapter(kind);
1323
- const keys = await adapter.listKeys(base);
1324
- for (const key of keys) {
1325
- if (key === base || key.startsWith(`${base}:`)) {
1326
- void adapter.removeItem(key).catch(() => void 0);
1327
- }
1328
- }
1512
+ await removeMatchingKeys(adapter, base);
1329
1513
  } catch {
1330
1514
  }
1331
1515
  }
@@ -1336,12 +1520,7 @@ async function sweepNonConfiguredStandardStoresForOrphans(configured, base) {
1336
1520
  if (kind === configuredKind) continue;
1337
1521
  try {
1338
1522
  const adapter = await getStorageAdapter(kind);
1339
- const keys = await adapter.listKeys(base);
1340
- for (const key of keys) {
1341
- if (key === base || key.startsWith(`${base}:`)) {
1342
- void adapter.removeItem(key).catch(() => void 0);
1343
- }
1344
- }
1523
+ await removeMatchingKeys(adapter, base);
1345
1524
  } catch {
1346
1525
  }
1347
1526
  }
@@ -1357,6 +1536,29 @@ function pluckPaths(form, pathKeys) {
1357
1536
  }
1358
1537
  return sparse ?? {};
1359
1538
  }
1539
+ function stripUnacknowledgedSensitiveLeaves(form, optedInPaths, isSensitivePath) {
1540
+ const acknowledgedSensitive = [];
1541
+ for (const key of optedInPaths) {
1542
+ const segs = paths.segmentsForPathKey(key);
1543
+ if (segs !== null && isSensitivePath(segs)) acknowledgedSensitive.push(segs);
1544
+ }
1545
+ const coveredByAcknowledged = (path) => acknowledgedSensitive.some((prefix) => paths.isPathPrefix(prefix, path));
1546
+ const walk = (path, value) => {
1547
+ if (path.length > 0 && isSensitivePath(path) && !coveredByAcknowledged(path)) {
1548
+ return void 0;
1549
+ }
1550
+ if (value === null || typeof value !== "object") return value;
1551
+ if (Array.isArray(value)) return value.map((item, i) => walk([...path, i], item));
1552
+ if (!isPlainRecord(value)) return value;
1553
+ const out = /* @__PURE__ */ Object.create(null);
1554
+ for (const key of Object.keys(value)) {
1555
+ const walked = walk([...path, key], value[key]);
1556
+ if (walked !== void 0) out[key] = walked;
1557
+ }
1558
+ return out;
1559
+ };
1560
+ return walk([], form);
1561
+ }
1360
1562
  function filterErrorsByPaths(errors, pathKeys) {
1361
1563
  const out = /* @__PURE__ */ new Map();
1362
1564
  for (const [key, value] of errors) {
@@ -1383,7 +1585,7 @@ function mergeDeep(target, source, path, schema) {
1383
1585
  if (sourceDisc !== void 0) {
1384
1586
  const variantDefault = du.getVariantDefault(sourceDisc);
1385
1587
  if (isPlainRecord(variantDefault)) {
1386
- const out2 = { ...variantDefault };
1588
+ const out2 = Object.assign(/* @__PURE__ */ Object.create(null), variantDefault);
1387
1589
  for (const key of Object.keys(sourceRecord)) {
1388
1590
  if (!(key in variantDefault) && key !== du.discriminatorKey) continue;
1389
1591
  out2[key] = mergeDeep(out2[key], sourceRecord[key], [...path, key], schema);
@@ -1395,7 +1597,7 @@ function mergeDeep(target, source, path, schema) {
1395
1597
  }
1396
1598
  }
1397
1599
  const mergeTarget = target;
1398
- const out = isPlainRecord(mergeTarget) ? { ...mergeTarget } : {};
1600
+ const out = isPlainRecord(mergeTarget) ? Object.assign(/* @__PURE__ */ Object.create(null), mergeTarget) : /* @__PURE__ */ Object.create(null);
1399
1601
  for (const key of Object.keys(source)) {
1400
1602
  out[key] = mergeDeep(out[key], source[key], [...path, key], schema);
1401
1603
  }
@@ -1498,39 +1700,44 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1498
1700
  }
1499
1701
  return result;
1500
1702
  }
1501
- async function validateAsync(pathInput) {
1703
+ async function runImperativeValidation(pathInput, config) {
1502
1704
  const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1503
1705
  const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1504
1706
  try {
1505
1707
  state.activeValidations.value += 1;
1506
- state.cancelFieldValidation();
1708
+ if (config.cancelInFlight) state.cancelFieldValidation();
1507
1709
  const refinement = await runRefinementValidation(dataAtPath, segments);
1508
- const scopePath = segments ?? [];
1509
- const errors = refinement.success ? [] : refinement.errors;
1510
- const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1511
- ...err,
1512
- path: [...segments, ...err.path]
1513
- }));
1514
- state.applySchemaErrorsForSubtree(scopePath, reStamped);
1515
- return stripData(composeWithDerivedBlank(refinement, segments));
1710
+ if (config.commitToSchemaErrors) {
1711
+ const scopePath = segments ?? [];
1712
+ const errors = refinement.success ? [] : refinement.errors;
1713
+ const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1714
+ ...err,
1715
+ path: [...segments, ...err.path]
1716
+ }));
1717
+ state.applySchemaErrorsForSubtree(scopePath, reStamped);
1718
+ }
1719
+ return { ok: true, refinement, segments };
1516
1720
  } catch (err) {
1517
- return adapterThrowResponse(err);
1721
+ return { ok: false, error: adapterThrowResponse(err) };
1518
1722
  } finally {
1519
1723
  state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1520
1724
  }
1521
1725
  }
1726
+ async function validateAsync(pathInput) {
1727
+ const result = await runImperativeValidation(pathInput, {
1728
+ cancelInFlight: true,
1729
+ commitToSchemaErrors: true
1730
+ });
1731
+ if (!result.ok) return result.error;
1732
+ return stripData(composeWithDerivedBlank(result.refinement, result.segments));
1733
+ }
1522
1734
  async function process(pathInput) {
1523
- const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1524
- const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1525
- try {
1526
- state.activeValidations.value += 1;
1527
- const refinement = await runRefinementValidation(dataAtPath, segments);
1528
- return composeWithDerivedBlank(refinement, segments);
1529
- } catch (err) {
1530
- return adapterThrowResponse(err);
1531
- } finally {
1532
- state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1533
- }
1735
+ const result = await runImperativeValidation(pathInput, {
1736
+ cancelInFlight: false,
1737
+ commitToSchemaErrors: false
1738
+ });
1739
+ if (!result.ok) return result.error;
1740
+ return composeWithDerivedBlank(result.refinement, result.segments);
1534
1741
  }
1535
1742
  function adapterThrowResponse(err) {
1536
1743
  return {
@@ -1968,7 +2175,6 @@ function coerceValue(value, accepted, elementAccepted, index) {
1968
2175
  }
1969
2176
 
1970
2177
  const EMPTY_TRANSFORMS = Object.freeze([]);
1971
- const INTERACTIVE_TAG_NAMES = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
1972
2178
  const attaformListenersSymbol = Symbol.for("attaform:focus-listeners");
1973
2179
  function attachFocusListeners(state, segments, element, instanceMeta) {
1974
2180
  const target = element;
@@ -1996,6 +2202,8 @@ function detachFocusListeners(element) {
1996
2202
  function buildRegister(state, formInstanceId, instanceConfig) {
1997
2203
  const coerceIndex = instanceConfig?.coerce !== void 0 ? resolveCoercionIndex(instanceConfig.coerce) : state.coerceIndex;
1998
2204
  const instanceMeta = instanceConfig?.instanceMeta;
2205
+ const formAutoAria = instanceConfig?.autoAria ?? true;
2206
+ const getDisplayStateAt = instanceConfig?.getDisplayStateAt;
1999
2207
  const withInstanceMeta = (meta) => {
2000
2208
  if (instanceMeta === void 0) return meta;
2001
2209
  return meta === void 0 ? { instance: instanceMeta } : { ...meta, instance: instanceMeta };
@@ -2017,7 +2225,11 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2017
2225
  if (typed !== null && typeof raw === "number" && parseFloat(typed) === raw) {
2018
2226
  return typed;
2019
2227
  }
2020
- return String(raw);
2228
+ try {
2229
+ return String(raw);
2230
+ } catch {
2231
+ return Object.prototype.toString.call(raw);
2232
+ }
2021
2233
  });
2022
2234
  const slimDefault = state.schema.getDefaultAtPath(segments);
2023
2235
  const slimTypes = state.schema.getSlimPrimitiveTypesAtPath(segments);
@@ -2050,6 +2262,10 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2050
2262
  callSite: paths.captureUserCallSite()
2051
2263
  });
2052
2264
  }
2265
+ const { aria } = computeFieldIdentity(formInstanceId, state.formKey, pathKey);
2266
+ const isRequired = state.schema.isRequiredAtPath(segments);
2267
+ const ariaEnabled = options?.autoAria ?? formAutoAria;
2268
+ const ariaDisplayState = getDisplayStateAt !== void 0 ? vue.computed(() => getDisplayStateAt(segments)) : void 0;
2053
2269
  const internalRv = {
2054
2270
  innerRef,
2055
2271
  displayValue,
@@ -2064,8 +2280,11 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2064
2280
  })
2065
2281
  );
2066
2282
  },
2283
+ markInteracted: () => {
2284
+ state.markInteracted(segments);
2285
+ },
2067
2286
  registerElement: (element) => {
2068
- if (!INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2287
+ if (!paths.INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2069
2288
  const added = state.registerElement(segments, element, formInstanceId);
2070
2289
  if (added) attachFocusListeners(state, segments, element, instanceMeta);
2071
2290
  },
@@ -2108,7 +2327,12 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2108
2327
  coerce,
2109
2328
  ...coerceElement !== void 0 ? { coerceElement } : {},
2110
2329
  acceptsUndefined,
2111
- acceptsString
2330
+ acceptsString,
2331
+ // --- Aria (internal; consumed by the directive) ---
2332
+ aria,
2333
+ isRequired,
2334
+ ariaEnabled,
2335
+ ...ariaDisplayState !== void 0 ? { ariaDisplayState } : {}
2112
2336
  };
2113
2337
  return vue.shallowReadonly(internalRv);
2114
2338
  };
@@ -2159,7 +2383,7 @@ function walk(input, segments, schema, paths) {
2159
2383
  if (slim !== null && slim !== void 0 && typeof slim === "object" && !Array.isArray(slim) && !(slim instanceof Date) && !(slim instanceof RegExp) && !(slim instanceof Map) && !(slim instanceof Set)) {
2160
2384
  for (const k of Object.keys(slim)) allKeys.add(k);
2161
2385
  }
2162
- const out = {};
2386
+ const out = /* @__PURE__ */ Object.create(null);
2163
2387
  let mutated = allKeys.size !== inputKeys.length;
2164
2388
  for (const key of allKeys) {
2165
2389
  const orig = input[key];
@@ -2188,7 +2412,7 @@ function walkUnspecified(slim, segments, paths$1) {
2188
2412
  }
2189
2413
  if (Array.isArray(slim)) return slim;
2190
2414
  if (slim !== null && typeof slim === "object") {
2191
- const out = {};
2415
+ const out = /* @__PURE__ */ Object.create(null);
2192
2416
  for (const key of Object.keys(slim)) {
2193
2417
  out[key] = walkUnspecified(slim[key], [...segments, key], paths$1);
2194
2418
  }
@@ -2221,7 +2445,7 @@ function substitute(input, segments, schema, paths) {
2221
2445
  }
2222
2446
  if (typeof input === "object") {
2223
2447
  let mutated = false;
2224
- const out = {};
2448
+ const out = /* @__PURE__ */ Object.create(null);
2225
2449
  for (const key of Object.keys(input)) {
2226
2450
  const orig = input[key];
2227
2451
  const walked = substitute(orig, [...segments, key], schema, paths);
@@ -2273,70 +2497,86 @@ function expandUnsetAt(segments, schema, paths$1) {
2273
2497
  return result;
2274
2498
  }
2275
2499
 
2276
- function buildValuesProxy(form) {
2277
- const inner = vue.computed(() => vue.readonly(form.value));
2500
+ function buildCallableReadonlySnapshotProxy(opts) {
2278
2501
  const target = (() => {
2279
2502
  });
2280
- const valuesToString = () => JSON.stringify(inner.value);
2281
- const valuesToPrimitive = (hint) => hint === "number" ? NaN : valuesToString();
2503
+ const { toString, valueOf, toJSON, toPrimitive } = makeReadonlyCoercion(opts.snapshot);
2504
+ const callResolve = opts.resolveCall ?? ((arg) => opts.resolveKey(String(arg)));
2282
2505
  return new Proxy(target, {
2283
2506
  apply(_, __, args) {
2284
2507
  const arg = args[0];
2285
- if (arg === void 0) return inner.value;
2286
- const { segments } = paths.canonicalizePath(arg);
2287
- let cursor = inner.value;
2288
- for (const seg of segments) {
2289
- if (cursor === null || cursor === void 0) return void 0;
2290
- cursor = cursor[seg];
2291
- }
2292
- return cursor;
2508
+ if (arg === void 0) return opts.snapshot();
2509
+ return callResolve(arg);
2293
2510
  },
2294
2511
  get(_, key) {
2295
2512
  if (typeof key === "symbol") {
2296
- if (key === Symbol.toPrimitive) return valuesToPrimitive;
2513
+ if (key === Symbol.toPrimitive) return toPrimitive;
2297
2514
  return Reflect.get(target, key);
2298
2515
  }
2299
- if (key === "toJSON") return () => inner.value;
2300
- if (key === "toString") return valuesToString;
2301
- if (key === "valueOf")
2302
- return function() {
2303
- return this;
2304
- };
2305
- return inner.value[key];
2516
+ if (key === "toJSON") return toJSON;
2517
+ if (key === "toString") return toString;
2518
+ if (key === "valueOf") return valueOf;
2519
+ return opts.resolveKey(key);
2306
2520
  },
2307
2521
  has(_, key) {
2308
2522
  if (typeof key === "symbol") return Reflect.has(target, key);
2309
- return Reflect.has(inner.value, key);
2310
- },
2311
- ownKeys() {
2312
- return Reflect.ownKeys(inner.value);
2523
+ return opts.hasKey(key);
2313
2524
  },
2525
+ ownKeys: () => opts.ownKeys(),
2314
2526
  getOwnPropertyDescriptor(_, key) {
2315
- const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2316
- if (desc !== void 0) desc.configurable = true;
2317
- return desc;
2527
+ if (typeof key !== "string") return void 0;
2528
+ if (opts.describeKey !== void 0) return opts.describeKey(key);
2529
+ if (!opts.hasKey(key)) return void 0;
2530
+ return {
2531
+ configurable: true,
2532
+ enumerable: true,
2533
+ writable: false,
2534
+ value: opts.resolveKey(key)
2535
+ };
2318
2536
  },
2319
- // Match Vue's `readonly()` semantics: writes warn (in dev) and
2320
- // silently noop (return true). Returning false would throw
2321
- // TypeError in strict-mode consumers, surprising users who
2322
- // assigned through the proxy and expected it to be ignored.
2323
- set(_, key) {
2324
- if (paths.__DEV__) {
2325
- console.warn(
2326
- `[attaform] form.values is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
2327
- );
2328
- }
2537
+ set: (_, key) => {
2538
+ warnReadOnly(opts.surface, "write", key);
2329
2539
  return true;
2330
2540
  },
2331
- deleteProperty(_, key) {
2332
- if (paths.__DEV__) {
2333
- console.warn(
2334
- `[attaform] form.values is read-only \u2014 delete of "${String(key)}" was ignored.`
2335
- );
2336
- }
2541
+ deleteProperty: (_, key) => {
2542
+ warnReadOnly(opts.surface, "delete", key);
2337
2543
  return true;
2338
2544
  },
2339
- defineProperty: () => true
2545
+ defineProperty: (_, key) => {
2546
+ warnReadOnly(opts.surface, "define", key);
2547
+ return true;
2548
+ }
2549
+ });
2550
+ }
2551
+
2552
+ function buildValuesProxy(form) {
2553
+ const inner = vue.computed(() => vue.readonly(form.value));
2554
+ return buildCallableReadonlySnapshotProxy({
2555
+ surface: "form.values",
2556
+ snapshot: () => inner.value,
2557
+ // Read through the readonly proxy at access time so Vue's
2558
+ // dependency tracking lands inside the consumer's active effect
2559
+ // — `inner.value[key]` is what triggers per-key tracking.
2560
+ resolveKey: (key) => inner.value[key],
2561
+ // Dynamic path: walk segments through the readonly proxy. Each
2562
+ // step reads through the proxy's own get traps so dependency
2563
+ // tracking propagates at every level.
2564
+ resolveCall: (arg) => {
2565
+ const { segments } = paths.canonicalizePath(arg);
2566
+ let cursor = inner.value;
2567
+ for (const seg of segments) {
2568
+ if (cursor === null || cursor === void 0) return void 0;
2569
+ cursor = cursor[seg];
2570
+ }
2571
+ return cursor;
2572
+ },
2573
+ ownKeys: () => Reflect.ownKeys(inner.value),
2574
+ hasKey: (key) => Reflect.has(inner.value, key),
2575
+ describeKey: (key) => {
2576
+ const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2577
+ if (desc !== void 0) desc.configurable = true;
2578
+ return desc;
2579
+ }
2340
2580
  });
2341
2581
  }
2342
2582
 
@@ -2352,15 +2592,34 @@ function buildFormApi(state, formInstanceId, options = {}) {
2352
2592
  if (instanceMeta === void 0) return meta;
2353
2593
  return meta === void 0 ? { instance: instanceMeta } : { ...meta, instance: instanceMeta };
2354
2594
  };
2355
- const registerConfig = {
2356
- ...instanceMeta !== void 0 ? { instanceMeta } : {},
2357
- ...options.coerce !== void 0 ? { coerce: options.coerce } : {}
2595
+ const getFormMetaBase = () => {
2596
+ const rootBase = buildContainerFieldStateBase(state, paths.ROOT_PATH, paths.ROOT_PATH_KEY, formInstanceId);
2597
+ return {
2598
+ ...rootBase,
2599
+ submitting: state.submitting.value,
2600
+ submissionAttempts: state.submissionAttempts.value,
2601
+ departAttempts: state.departAttempts.value,
2602
+ submitError: state.submitError.value,
2603
+ errorCount: rootBase.errors.length,
2604
+ submitted: state.submitted.value,
2605
+ instanceId: formInstanceId
2606
+ };
2358
2607
  };
2359
- const register = buildRegister(
2608
+ const fieldStateAccessorOptions = options.getDisplayState !== void 0 ? { getDisplayState: options.getDisplayState } : void 0;
2609
+ const getRootFieldStateAt = buildFieldStateAccessor(
2360
2610
  state,
2361
2611
  formInstanceId,
2362
- Object.keys(registerConfig).length > 0 ? registerConfig : void 0
2612
+ getFormMetaBase,
2613
+ fieldStateAccessorOptions
2363
2614
  );
2615
+ const getDisplayStateAt = (segments) => getRootFieldStateAt(segments).value.displayState;
2616
+ const registerConfig = {
2617
+ ...instanceMeta !== void 0 ? { instanceMeta } : {},
2618
+ ...options.coerce !== void 0 ? { coerce: options.coerce } : {},
2619
+ ...options.autoAria !== void 0 ? { autoAria: options.autoAria } : {},
2620
+ getDisplayStateAt
2621
+ };
2622
+ const register = buildRegister(state, formInstanceId, registerConfig);
2364
2623
  const processOptions = options.onInvalidSubmit !== void 0 ? { onInvalidSubmit: options.onInvalidSubmit } : {};
2365
2624
  const defaultInvalidSubmitPolicy = options.onInvalidSubmit ?? "focus-first-error";
2366
2625
  const {
@@ -2383,10 +2642,18 @@ function buildFormApi(state, formInstanceId, options = {}) {
2383
2642
  next,
2384
2643
  state.schema
2385
2644
  );
2645
+ const ok2 = state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2646
+ if (!ok2) return false;
2386
2647
  for (const pathKey of walked2.paths) {
2387
- state.blankPaths.add(pathKey);
2648
+ const blankSegments = paths.segmentsForPathKey(pathKey);
2649
+ if (blankSegments === null) continue;
2650
+ state.setValueAtPath(
2651
+ blankSegments,
2652
+ state.getValueAtPath(blankSegments),
2653
+ withInstanceMeta({ blank: true })
2654
+ );
2388
2655
  }
2389
- return state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2656
+ return true;
2390
2657
  }
2391
2658
  const segments = paths.canonicalizePath(pathOrValue).segments;
2392
2659
  const writeUnsetAt = () => {
@@ -2532,43 +2799,48 @@ function buildFormApi(state, formInstanceId, options = {}) {
2532
2799
  const metaErrors = vue.computed(
2533
2800
  () => aggregateErrorsAt(state, [])
2534
2801
  );
2535
- const getFormMetaBase = () => {
2536
- const rootBase = buildContainerFieldStateBase(state, paths.ROOT_PATH);
2537
- return {
2538
- ...rootBase,
2539
- submitting: state.submitting.value,
2540
- submissionAttempts: state.submissionAttempts.value,
2541
- departAttempts: state.departAttempts.value,
2542
- submitError: state.submitError.value,
2543
- errorCount: rootBase.errors.length,
2544
- submitted: state.submitted.value,
2545
- instanceId: formInstanceId
2546
- };
2547
- };
2548
- const fieldStateAccessorOptions = options.shouldShowErrors !== void 0 ? { shouldShowErrors: options.shouldShowErrors } : void 0;
2549
- const getRootFieldStateAt = buildFieldStateAccessor(
2550
- state,
2551
- getFormMetaBase,
2552
- fieldStateAccessorOptions
2553
- );
2554
2802
  const rootFieldState = getRootFieldStateAt([]);
2555
2803
  const formMeta = vue.readonly(
2556
2804
  vue.reactive({
2557
- // FieldState fields — read through one shared root computed. Each
2558
- // property accesses `rootFieldState.value[X]`, so any descendant
2559
- // change re-evaluates the root computed once (Vue's reactive
2560
- // graph dedupes the dependent re-renders).
2561
- value: vue.computed(() => rootFieldState.value.value),
2562
- original: vue.computed(() => rootFieldState.value.original),
2563
- pristine: vue.computed(() => rootFieldState.value.pristine),
2564
- dirty: vue.computed(() => rootFieldState.value.dirty),
2565
- focused: vue.computed(() => rootFieldState.value.focused),
2566
- blurred: vue.computed(() => rootFieldState.value.blurred),
2567
- touched: vue.computed(() => rootFieldState.value.touched),
2568
- connected: vue.computed(() => rootFieldState.value.connected),
2569
- element: vue.computed(() => rootFieldState.value.element),
2570
- elements: vue.computed(() => rootFieldState.value.elements),
2571
- updatedAt: vue.computed(() => rootFieldState.value.updatedAt),
2805
+ get value() {
2806
+ return rootFieldState.value.value;
2807
+ },
2808
+ get original() {
2809
+ return rootFieldState.value.original;
2810
+ },
2811
+ get pristine() {
2812
+ return rootFieldState.value.pristine;
2813
+ },
2814
+ get dirty() {
2815
+ return rootFieldState.value.dirty;
2816
+ },
2817
+ get focused() {
2818
+ return rootFieldState.value.focused;
2819
+ },
2820
+ get blurred() {
2821
+ return rootFieldState.value.blurred;
2822
+ },
2823
+ get touched() {
2824
+ return rootFieldState.value.touched;
2825
+ },
2826
+ get interacted() {
2827
+ return rootFieldState.value.interacted;
2828
+ },
2829
+ get blurredAfterInteraction() {
2830
+ return rootFieldState.value.blurredAfterInteraction;
2831
+ },
2832
+ get connected() {
2833
+ return rootFieldState.value.connected;
2834
+ },
2835
+ get element() {
2836
+ return rootFieldState.value.element;
2837
+ },
2838
+ get elements() {
2839
+ return rootFieldState.value.elements;
2840
+ },
2841
+ get updatedAt() {
2842
+ return rootFieldState.value.updatedAt;
2843
+ },
2572
2844
  // Whole-form validating mirrors the LIFECYCLE counter
2573
2845
  // (`state.activeValidations`) ORed with any per-leaf validation
2574
2846
  // in flight (via `rootFieldState.validating`). A submit-time
@@ -2586,19 +2858,56 @@ function buildFormApi(state, formInstanceId, options = {}) {
2586
2858
  // keep the explicit form-level computation for the gate.
2587
2859
  valid,
2588
2860
  errors: metaErrors,
2589
- // `showErrors` / `firstError` flow through the same root
2590
- // field-state computed as the rest of the FieldState surface,
2591
- // so `form.meta.showErrors` matches `form.fields().showErrors`
2592
- // exactly — the predicate runs once at the root and the result
2593
- // is shared.
2594
- showErrors: vue.computed(() => rootFieldState.value.showErrors),
2595
- firstError: vue.computed(() => rootFieldState.value.firstError),
2596
- path: vue.computed(() => rootFieldState.value.path),
2597
- blank: vue.computed(() => rootFieldState.value.blank),
2598
- label: vue.computed(() => rootFieldState.value.label),
2599
- description: vue.computed(() => rootFieldState.value.description),
2600
- placeholder: vue.computed(() => rootFieldState.value.placeholder),
2601
- meta: vue.computed(() => rootFieldState.value.meta),
2861
+ // `displayState` / the `show*` booleans / `firstError` flow
2862
+ // through the same root field-state computed as the rest of the
2863
+ // FieldState surface, so `form.meta.displayState` matches
2864
+ // `form.fields().displayState` exactly — the predicate runs once
2865
+ // at the root and the result is shared.
2866
+ get displayState() {
2867
+ return rootFieldState.value.displayState;
2868
+ },
2869
+ get showErrors() {
2870
+ return rootFieldState.value.showErrors;
2871
+ },
2872
+ get showPending() {
2873
+ return rootFieldState.value.showPending;
2874
+ },
2875
+ get showSuccess() {
2876
+ return rootFieldState.value.showSuccess;
2877
+ },
2878
+ get showIdle() {
2879
+ return rootFieldState.value.showIdle;
2880
+ },
2881
+ get firstError() {
2882
+ return rootFieldState.value.firstError;
2883
+ },
2884
+ get path() {
2885
+ return rootFieldState.value.path;
2886
+ },
2887
+ get id() {
2888
+ return rootFieldState.value.id;
2889
+ },
2890
+ get aria() {
2891
+ return rootFieldState.value.aria;
2892
+ },
2893
+ get key() {
2894
+ return rootFieldState.value.key;
2895
+ },
2896
+ get blank() {
2897
+ return rootFieldState.value.blank;
2898
+ },
2899
+ get label() {
2900
+ return rootFieldState.value.label;
2901
+ },
2902
+ get description() {
2903
+ return rootFieldState.value.description;
2904
+ },
2905
+ get placeholder() {
2906
+ return rootFieldState.value.placeholder;
2907
+ },
2908
+ get meta() {
2909
+ return rootFieldState.value.meta;
2910
+ },
2602
2911
  // Lifecycle (form-level only — not on FieldState).
2603
2912
  submitting,
2604
2913
  submissionAttempts,
@@ -2607,7 +2916,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2607
2916
  // Scalar mirror over the array — meta is a single sticky surface
2608
2917
  // for both templates and `useWizard`'s `FormStatus`, so the
2609
2918
  // projection lives here.
2610
- errorCount: vue.computed(() => metaErrors.value.length),
2919
+ get errorCount() {
2920
+ return metaErrors.value.length;
2921
+ },
2611
2922
  submitted,
2612
2923
  // Per-`useForm()`-call identity. Stable for one mount; new on
2613
2924
  // re-mount; orthogonal to `form.key` (which is the user-supplied
@@ -2644,12 +2955,20 @@ function buildFormApi(state, formInstanceId, options = {}) {
2644
2955
  }
2645
2956
  };
2646
2957
  function clear(pathInput) {
2647
- const segments = pathInput === void 0 ? paths.ROOT_PATH : paths.canonicalizePath(pathInput).segments;
2648
- return state.clear(segments);
2958
+ if (pathInput === void 0) {
2959
+ return setValueImpl(unset);
2960
+ }
2961
+ return setValueImpl(pathInput, unset);
2649
2962
  }
2650
2963
  const persist = async (pathInput, options2) => {
2651
2964
  const segments = paths.canonicalizePath(pathInput).segments;
2652
- paths.enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
2965
+ if (!paths.allowSensitivePersist(
2966
+ segments,
2967
+ options2?.acknowledgeSensitive === true,
2968
+ state.isSensitivePath
2969
+ )) {
2970
+ return;
2971
+ }
2653
2972
  if (persistence === void 0) return;
2654
2973
  await persistence.writePathImmediately(segments);
2655
2974
  };
@@ -2709,13 +3028,43 @@ function buildFormApi(state, formInstanceId, options = {}) {
2709
3028
  return Object.freeze(view);
2710
3029
  });
2711
3030
  const valuesProxy = buildValuesProxy(state.form);
2712
- const fieldStateProxy = buildFieldStateProxy(state, getFormMetaBase, fieldStateAccessorOptions);
3031
+ const fieldStateProxy = buildFieldStateProxy(
3032
+ state,
3033
+ formInstanceId,
3034
+ getFormMetaBase,
3035
+ fieldStateAccessorOptions
3036
+ );
3037
+ const needsLazyGate = state.defaultValuesFactory.value !== void 0 || state.hasSsrPrefetch;
2713
3038
  function gated(fn) {
3039
+ if (!needsLazyGate) return fn;
2714
3040
  return ((...args) => {
2715
3041
  void state.activate();
2716
3042
  return fn(...args);
2717
3043
  });
2718
3044
  }
3045
+ const callTerminal = fieldStateProxy;
3046
+ const EMPTY_FIELD_LIST = Object.freeze([]);
3047
+ function list(path) {
3048
+ const { segments } = paths.canonicalizePath(path);
3049
+ const value = state.getValueAtPath(segments);
3050
+ if (!Array.isArray(value)) return EMPTY_FIELD_LIST;
3051
+ const out = new Array(value.length);
3052
+ for (let i = 0; i < value.length; i += 1) out[i] = callTerminal(`${path}.${i}`);
3053
+ return Object.freeze(out);
3054
+ }
3055
+ const EMPTY_FIELD_RECORD = Object.freeze({});
3056
+ function record(path) {
3057
+ const { segments } = paths.canonicalizePath(path);
3058
+ const value = state.getValueAtPath(segments);
3059
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3060
+ return EMPTY_FIELD_RECORD;
3061
+ }
3062
+ const out = /* @__PURE__ */ Object.create(null);
3063
+ for (const key of Object.keys(value)) {
3064
+ out[key] = callTerminal(`${path}.${key}`);
3065
+ }
3066
+ return Object.freeze(out);
3067
+ }
2719
3068
  return {
2720
3069
  handleSubmit: gated(handleSubmit),
2721
3070
  // Callable readonly Proxies (`values`, `fields`, `errors`) and the
@@ -2797,6 +3146,8 @@ function buildFormApi(state, formInstanceId, options = {}) {
2797
3146
  swap: gated(fieldArrays.swap),
2798
3147
  move: gated(fieldArrays.move),
2799
3148
  replace: gated(fieldArrays.replace),
3149
+ list: gated(list),
3150
+ record: gated(record),
2800
3151
  get blankPaths() {
2801
3152
  void state.activate();
2802
3153
  return blankPathsView;
@@ -2804,47 +3155,6 @@ function buildFormApi(state, formInstanceId, options = {}) {
2804
3155
  };
2805
3156
  }
2806
3157
 
2807
- const defaultShouldShowErrors = (field, formMeta) => {
2808
- const hasOwnError = field.errors.some(
2809
- (e) => e.path.length === field.path.length && e.path.every((s, i) => s === field.path[i])
2810
- );
2811
- if (!hasOwnError) return false;
2812
- if (field.validating === true) return false;
2813
- if (formMeta.submissionAttempts > 0) return true;
2814
- return field.touched === true && field.focused !== true;
2815
- };
2816
- const SHOW_ALWAYS = () => true;
2817
- const SHOW_NEVER = () => false;
2818
- function resolveShouldShowErrors(config) {
2819
- if (config === void 0) return defaultShouldShowErrors;
2820
- if (config === true) return SHOW_ALWAYS;
2821
- if (config === false) return SHOW_NEVER;
2822
- return config;
2823
- }
2824
-
2825
- function isHydratedFieldRecord(value) {
2826
- if (typeof value !== "object" || value === null) return false;
2827
- const r = value;
2828
- return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.connected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && typeof r.touched === "boolean";
2829
- }
2830
- function isHydratedValidationErrorArray(value) {
2831
- if (!Array.isArray(value)) return false;
2832
- for (const entry of value) {
2833
- if (typeof entry !== "object" || entry === null) return false;
2834
- const e = entry;
2835
- if (typeof e.message !== "string") return false;
2836
- if (!Array.isArray(e.path)) return false;
2837
- if (typeof e.formKey !== "string") return false;
2838
- if (typeof e.code !== "string") return false;
2839
- }
2840
- return true;
2841
- }
2842
- function warnMalformedHydration(formKey, kind, rawKey) {
2843
- if (!paths.__DEV__) return;
2844
- console.warn(
2845
- `[attaform] hydration: skipping malformed ${kind} entry at key '${rawKey}' on form '${formKey}'. This usually means the SSR bundle is on a different version than the client (rolling deploy / stale cache).`
2846
- );
2847
- }
2848
3158
  function applyDuStubs(schema, data, options = {}) {
2849
3159
  const warned = options.warn === true ? /* @__PURE__ */ new Set() : void 0;
2850
3160
  return walkDuStubs(schema, data, options.basePath ?? [], warned);
@@ -2873,49 +3183,74 @@ function walkDuStubs(schema, value, path, warned) {
2873
3183
  );
2874
3184
  }
2875
3185
  }
2876
- return { [du.discriminatorKey]: discValue };
3186
+ const stub = /* @__PURE__ */ Object.create(null);
3187
+ stub[du.discriminatorKey] = discValue;
3188
+ return stub;
2877
3189
  }
2878
3190
  }
2879
- const out = {};
3191
+ const out = /* @__PURE__ */ Object.create(null);
2880
3192
  for (const k of Object.keys(rec)) {
2881
3193
  out[k] = walkDuStubs(schema, rec[k], [...path, k], warned);
2882
3194
  }
2883
3195
  return out;
2884
3196
  }
2885
- function isPathKeyUnder(existingKey, parentPath) {
2886
- const parsed = paths.segmentsForPathKey(existingKey);
2887
- if (parsed === null) return false;
2888
- if (parsed.length <= parentPath.length) return false;
2889
- for (let i = 0; i < parentPath.length; i++) {
2890
- if (parsed[i] !== parentPath[i]) return false;
2891
- }
2892
- return true;
2893
- }
2894
- function stripSymbolsDeep(value) {
2895
- if (value === null || typeof value !== "object") return value;
2896
- if (Array.isArray(value)) {
2897
- let mutated2 = false;
2898
- const out2 = new Array(value.length);
2899
- for (let i = 0; i < value.length; i++) {
2900
- const cleaned = stripSymbolsDeep(value[i]);
2901
- out2[i] = cleaned;
2902
- if (cleaned !== value[i]) mutated2 = true;
3197
+
3198
+ function createVariantMemory() {
3199
+ const memory = /* @__PURE__ */ new Map();
3200
+ function clearAtArrayIndices(arrayPath, indexFilter) {
3201
+ for (const memKey of [...memory.keys()]) {
3202
+ const segs = paths.segmentsForPathKey(memKey);
3203
+ if (segs === null) continue;
3204
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3205
+ if (segs.length <= arrayPath.length) continue;
3206
+ const idxSeg = segs[arrayPath.length];
3207
+ if (typeof idxSeg !== "number") continue;
3208
+ if (indexFilter(idxSeg)) memory.delete(memKey);
2903
3209
  }
2904
- return mutated2 ? out2 : value;
2905
- }
2906
- const proto = Object.getPrototypeOf(value);
2907
- if (proto !== Object.prototype && proto !== null) return value;
2908
- const symKeys = Object.getOwnPropertySymbols(value);
2909
- const stringKeys = Object.keys(value);
2910
- let mutated = symKeys.length > 0;
2911
- const out = {};
2912
- const src = value;
2913
- for (const k of stringKeys) {
2914
- const cleaned = stripSymbolsDeep(src[k]);
2915
- out[k] = cleaned;
2916
- if (cleaned !== src[k]) mutated = true;
2917
3210
  }
2918
- return mutated ? out : value;
3211
+ return {
3212
+ clear() {
3213
+ memory.clear();
3214
+ },
3215
+ clearUnderPath(parentPath) {
3216
+ for (const memKey of [...memory.keys()]) {
3217
+ const segs = paths.segmentsForPathKey(memKey);
3218
+ if (segs === null) continue;
3219
+ if (paths.isPathPrefix(parentPath, segs)) memory.delete(memKey);
3220
+ }
3221
+ },
3222
+ applyArrayOp(arrayPath, op) {
3223
+ switch (op.kind) {
3224
+ case "insert":
3225
+ case "remove":
3226
+ clearAtArrayIndices(arrayPath, (i) => i >= op.index);
3227
+ return;
3228
+ case "move": {
3229
+ const lo = Math.min(op.from, op.to);
3230
+ const hi = Math.max(op.from, op.to);
3231
+ clearAtArrayIndices(arrayPath, (i) => i >= lo && i <= hi);
3232
+ return;
3233
+ }
3234
+ case "swap":
3235
+ clearAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3236
+ return;
3237
+ case "replace-at":
3238
+ clearAtArrayIndices(arrayPath, (i) => i === op.index);
3239
+ return;
3240
+ }
3241
+ },
3242
+ recordOutgoing(unionKey, discValue, snapshot) {
3243
+ let perUnion = memory.get(unionKey);
3244
+ if (perUnion === void 0) {
3245
+ perUnion = /* @__PURE__ */ new Map();
3246
+ memory.set(unionKey, perUnion);
3247
+ }
3248
+ perUnion.set(discValue, snapshot);
3249
+ },
3250
+ lookupIncoming(unionKey, discValue) {
3251
+ return memory.get(unionKey)?.get(discValue);
3252
+ }
3253
+ };
2919
3254
  }
2920
3255
  function cloneVariantSnapshot(value) {
2921
3256
  if (value === null || typeof value !== "object") return value;
@@ -2938,51 +3273,404 @@ function cloneVariantSnapshot(value) {
2938
3273
  return out2;
2939
3274
  }
2940
3275
  const src = raw;
2941
- const out = {};
3276
+ const out = /* @__PURE__ */ Object.create(null);
2942
3277
  for (const k of Object.keys(src)) out[k] = cloneVariantSnapshot(src[k]);
2943
3278
  return out;
2944
3279
  }
2945
- function walkAuthoredFromConstraints(value, prefix, out) {
2946
- if (prefix.length > 0) out.add(paths.canonicalizePath(prefix).key);
2947
- if (isPlainRecord(value)) {
2948
- for (const k of Object.keys(value)) {
2949
- walkAuthoredFromConstraints(value[k], [...prefix, k], out);
3280
+
3281
+ function createArrayIdentity(getArrayLength) {
3282
+ const tokens = /* @__PURE__ */ new Map();
3283
+ const baselines = /* @__PURE__ */ new Map();
3284
+ let counter = 0;
3285
+ const allocate = () => `k${(counter++).toString(36)}`;
3286
+ function ensure(arrayKey, expectedLen) {
3287
+ let ids = tokens.get(arrayKey);
3288
+ const firstTrack = ids === void 0;
3289
+ if (ids === void 0) {
3290
+ ids = [];
3291
+ tokens.set(arrayKey, ids);
3292
+ }
3293
+ while (ids.length < expectedLen) ids.push(allocate());
3294
+ if (ids.length > expectedLen) ids.length = expectedLen;
3295
+ if (firstTrack) baselines.set(arrayKey, [...ids]);
3296
+ return ids;
3297
+ }
3298
+ function orderPristineForKey(arrayKey) {
3299
+ const baseline = baselines.get(arrayKey);
3300
+ const current = tokens.get(arrayKey);
3301
+ if (baseline === void 0 || current === void 0) return true;
3302
+ if (baseline.length !== current.length) return false;
3303
+ for (let i = 0; i < current.length; i++) {
3304
+ if (current[i] !== baseline[i]) return false;
2950
3305
  }
2951
- return;
3306
+ return true;
2952
3307
  }
2953
- if (Array.isArray(value)) {
2954
- for (let i = 0; i < value.length; i++) {
2955
- walkAuthoredFromConstraints(value[i], [...prefix, i], out);
3308
+ return {
3309
+ tokenAt(arraySegs, index) {
3310
+ const len = getArrayLength(arraySegs);
3311
+ if (index < 0 || index >= len) return "";
3312
+ const ids = ensure(paths.canonicalizePath(arraySegs).key, len);
3313
+ return ids[index] ?? "";
3314
+ },
3315
+ applyOp(arraySegs, op) {
3316
+ const arrayKey = paths.canonicalizePath(arraySegs).key;
3317
+ const postLen = getArrayLength(arraySegs);
3318
+ const preLen = op.kind === "insert" ? postLen - 1 : op.kind === "remove" ? postLen + 1 : postLen;
3319
+ const ids = ensure(arrayKey, Math.max(0, preLen));
3320
+ switch (op.kind) {
3321
+ case "insert":
3322
+ ids.splice(op.index, 0, allocate());
3323
+ return;
3324
+ case "remove":
3325
+ ids.splice(op.index, 1);
3326
+ return;
3327
+ case "move": {
3328
+ const [moved] = ids.splice(op.from, 1);
3329
+ ids.splice(op.to, 0, moved ?? allocate());
3330
+ return;
3331
+ }
3332
+ case "swap": {
3333
+ const tmp = ids[op.a] ?? allocate();
3334
+ ids[op.a] = ids[op.b] ?? allocate();
3335
+ ids[op.b] = tmp;
3336
+ return;
3337
+ }
3338
+ case "replace-at":
3339
+ ids[op.index] = allocate();
3340
+ return;
3341
+ }
3342
+ },
3343
+ applyRemap(arrayPath, remap) {
3344
+ if (remap.moved.size === 0 && remap.vacated.size === 0 && remap.fresh.size === 0) return;
3345
+ const relocate = (store) => {
3346
+ const idxPos = arrayPath.length;
3347
+ const snapshots = [];
3348
+ for (const [key, value] of store) {
3349
+ const segments = paths.segmentsForPathKey(key);
3350
+ if (segments === null) continue;
3351
+ if (!paths.isPathPrefix(arrayPath, segments)) continue;
3352
+ if (segments.length <= idxPos) continue;
3353
+ const idxSeg = segments[idxPos];
3354
+ if (typeof idxSeg !== "number") continue;
3355
+ if (!remap.moved.has(idxSeg) && !remap.vacated.has(idxSeg) && !remap.fresh.has(idxSeg)) {
3356
+ continue;
3357
+ }
3358
+ snapshots.push({ segments: [...segments], index: idxSeg, value });
3359
+ }
3360
+ if (snapshots.length === 0) return;
3361
+ for (const snap of snapshots) store.delete(paths.canonicalizePath(snap.segments).key);
3362
+ for (const snap of snapshots) {
3363
+ const target = remap.moved.get(snap.index);
3364
+ if (target === void 0) continue;
3365
+ const relocated = snap.segments.slice();
3366
+ relocated[idxPos] = target;
3367
+ store.set(paths.canonicalizePath(relocated).key, snap.value);
3368
+ }
3369
+ };
3370
+ relocate(tokens);
3371
+ relocate(baselines);
3372
+ },
3373
+ realign(arraySegs) {
3374
+ ensure(paths.canonicalizePath(arraySegs).key, getArrayLength(arraySegs));
3375
+ },
3376
+ hasStructuralChangeUnder(prefix) {
3377
+ for (const arrayKey of tokens.keys()) {
3378
+ if (orderPristineForKey(arrayKey)) continue;
3379
+ const segs = paths.segmentsForPathKey(arrayKey);
3380
+ if (segs === null) continue;
3381
+ if (paths.isPathPrefix(prefix, segs)) return true;
3382
+ }
3383
+ return false;
3384
+ },
3385
+ rebaselineAll() {
3386
+ for (const arrayKey of [...tokens.keys()]) {
3387
+ const segs = paths.segmentsForPathKey(arrayKey);
3388
+ if (segs === null) continue;
3389
+ const ids = ensure(arrayKey, getArrayLength(segs));
3390
+ baselines.set(arrayKey, [...ids]);
3391
+ }
2956
3392
  }
2957
- }
3393
+ };
2958
3394
  }
2959
- function walkAuthoredFromSchemaDiff(withDefaults, withoutDefaults, prefix, out) {
2960
- if (isPlainRecord(withDefaults) && isPlainRecord(withoutDefaults)) {
2961
- const left = withDefaults;
2962
- const right = withoutDefaults;
2963
- const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
2964
- for (const k of keys) {
2965
- walkAuthoredFromSchemaDiff(left[k], right[k], [...prefix, k], out);
2966
- }
2967
- return;
2968
- }
2969
- if (Array.isArray(withDefaults) && Array.isArray(withoutDefaults)) {
2970
- const len = Math.max(withDefaults.length, withoutDefaults.length);
2971
- for (let i = 0; i < len; i++) {
2972
- walkAuthoredFromSchemaDiff(withDefaults[i], withoutDefaults[i], [...prefix, i], out);
2973
- }
2974
- return;
2975
- }
2976
- if (!Object.is(withDefaults, withoutDefaults) && prefix.length > 0) {
2977
- out.add(paths.canonicalizePath(prefix).key);
3395
+
3396
+ function remapForOp(op, oldLen) {
3397
+ const moved = /* @__PURE__ */ new Map();
3398
+ const vacated = /* @__PURE__ */ new Set();
3399
+ const fresh = /* @__PURE__ */ new Set();
3400
+ switch (op.kind) {
3401
+ case "insert":
3402
+ for (let i = op.index; i < oldLen; i++) moved.set(i, i + 1);
3403
+ fresh.add(op.index);
3404
+ return { moved, vacated, fresh };
3405
+ case "remove":
3406
+ vacated.add(op.index);
3407
+ for (let i = op.index + 1; i < oldLen; i++) moved.set(i, i - 1);
3408
+ return { moved, vacated, fresh };
3409
+ case "move":
3410
+ if (op.from !== op.to) {
3411
+ moved.set(op.from, op.to);
3412
+ if (op.from < op.to) {
3413
+ for (let i = op.from + 1; i <= op.to; i++) moved.set(i, i - 1);
3414
+ } else {
3415
+ for (let i = op.to; i < op.from; i++) moved.set(i, i + 1);
3416
+ }
3417
+ }
3418
+ return { moved, vacated, fresh };
3419
+ case "swap":
3420
+ if (op.a !== op.b) {
3421
+ moved.set(op.a, op.b);
3422
+ moved.set(op.b, op.a);
3423
+ }
3424
+ return { moved, vacated, fresh };
3425
+ case "replace-at":
3426
+ vacated.add(op.index);
3427
+ fresh.add(op.index);
3428
+ return { moved, vacated, fresh };
3429
+ }
3430
+ }
3431
+ function changedIndices(remap) {
3432
+ const changed = new Set(remap.vacated);
3433
+ for (const [from, to] of remap.moved) {
3434
+ changed.add(from);
3435
+ changed.add(to);
3436
+ }
3437
+ for (const index of remap.fresh) changed.add(index);
3438
+ return changed;
3439
+ }
3440
+ function elementIndexUnder(arrayPath, key, idxPos) {
3441
+ const segments = paths.segmentsForPathKey(key);
3442
+ if (segments === null) return null;
3443
+ if (!paths.isPathPrefix(arrayPath, segments)) return null;
3444
+ if (segments.length <= idxPos) return null;
3445
+ const index = segments[idxPos];
3446
+ return typeof index === "number" ? index : null;
3447
+ }
3448
+ function migrateMapSubtree(map, arrayPath, remap, rewriteValue) {
3449
+ const idxPos = arrayPath.length;
3450
+ const snapshots = [];
3451
+ for (const [key, value] of map) {
3452
+ const index = elementIndexUnder(arrayPath, key, idxPos);
3453
+ if (index === null) continue;
3454
+ if (!remap.moved.has(index) && !remap.vacated.has(index)) continue;
3455
+ snapshots.push({ segments: [...paths.segmentsForPathKey(key)], index, value });
3456
+ }
3457
+ if (snapshots.length === 0) return;
3458
+ for (const snap of snapshots) map.delete(paths.canonicalizePath(snap.segments).key);
3459
+ for (const snap of snapshots) {
3460
+ const target = remap.moved.get(snap.index);
3461
+ if (target === void 0) continue;
3462
+ const relocated = snap.segments.slice();
3463
+ relocated[idxPos] = target;
3464
+ map.set(paths.canonicalizePath(relocated).key, rewriteValue(snap.value, relocated));
3465
+ }
3466
+ }
3467
+ function migrateSetSubtree(set, arrayPath, remap) {
3468
+ const idxPos = arrayPath.length;
3469
+ const snapshots = [];
3470
+ for (const key of set) {
3471
+ const index = elementIndexUnder(arrayPath, key, idxPos);
3472
+ if (index === null) continue;
3473
+ if (!remap.moved.has(index) && !remap.vacated.has(index)) continue;
3474
+ snapshots.push({ segments: [...paths.segmentsForPathKey(key)], index });
3475
+ }
3476
+ if (snapshots.length === 0) return;
3477
+ for (const snap of snapshots) set.delete(paths.canonicalizePath(snap.segments).key);
3478
+ for (const snap of snapshots) {
3479
+ const target = remap.moved.get(snap.index);
3480
+ if (target === void 0) continue;
3481
+ const relocated = snap.segments.slice();
3482
+ relocated[idxPos] = target;
3483
+ set.add(paths.canonicalizePath(relocated).key);
2978
3484
  }
2979
3485
  }
2980
- function createFormStore(options) {
2981
- const { formKey, schema, defaultValues, strict = true, hydration } = options;
2982
- const ssr = options.ssr === true;
2983
- const ssrPrefetch = options.ssrPrefetch;
2984
- const rememberVariants = options.rememberVariants !== false;
2985
- const fieldValidationMode = options.validateOn ?? "change";
3486
+
3487
+ function createArrayBookkeeping(deps) {
3488
+ const {
3489
+ form,
3490
+ fields,
3491
+ userErrors,
3492
+ originals,
3493
+ blankPaths,
3494
+ originalBlankPaths,
3495
+ fieldValidationCounts,
3496
+ fieldValidationState,
3497
+ schemaErrors,
3498
+ activeValidations,
3499
+ arrayIdentity,
3500
+ touchFieldRecord,
3501
+ decFieldValidation
3502
+ } = deps;
3503
+ function migrateElementState(arrayPath, remap) {
3504
+ if (remap.moved.size === 0 && remap.vacated.size === 0) return;
3505
+ migrateMapSubtree(fields, arrayPath, remap, (record, segments) => ({
3506
+ ...record,
3507
+ path: segments
3508
+ }));
3509
+ migrateMapSubtree(
3510
+ userErrors,
3511
+ arrayPath,
3512
+ remap,
3513
+ (errors, segments) => errors.map((error) => ({ ...error, path: [...segments] }))
3514
+ );
3515
+ migrateMapSubtree(originals, arrayPath, remap, (record, segments) => ({
3516
+ segments,
3517
+ value: record.value
3518
+ }));
3519
+ migrateSetSubtree(blankPaths, arrayPath, remap);
3520
+ migrateSetSubtree(originalBlankPaths, arrayPath, remap);
3521
+ migrateMapSubtree(fieldValidationCounts, arrayPath, remap, (count) => count);
3522
+ arrayIdentity.applyRemap(arrayPath, remap);
3523
+ }
3524
+ function seedFreshElement(arrayPath, freshIndex) {
3525
+ const elementPath = [...arrayPath, freshIndex];
3526
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3527
+ diffAndApply(void 0, getAtPath(form.value, elementPath), elementPath, (patch) => {
3528
+ if (patch.kind !== "added") return;
3529
+ const { key } = paths.canonicalizePath(patch.path);
3530
+ if (!originals.has(key)) originals.set(key, { segments: patch.path, value: void 0 });
3531
+ touchFieldRecord(key, patch.path, { updatedAt: now });
3532
+ });
3533
+ }
3534
+ function dropSchemaErrorsAtChangedIndices(arrayPath, remap) {
3535
+ const changed = changedIndices(remap);
3536
+ if (changed.size === 0) return;
3537
+ const idxPos = arrayPath.length;
3538
+ for (const key of [...schemaErrors.keys()]) {
3539
+ const segs = paths.segmentsForPathKey(key);
3540
+ if (segs === null) continue;
3541
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3542
+ if (segs.length <= idxPos) continue;
3543
+ const idx = segs[idxPos];
3544
+ if (typeof idx === "number" && changed.has(idx)) schemaErrors.delete(key);
3545
+ }
3546
+ }
3547
+ function abortValidationAtVacatedIndices(arrayPath, remap) {
3548
+ if (remap.vacated.size === 0) return;
3549
+ const idxPos = arrayPath.length;
3550
+ for (const [key, entry] of [...fieldValidationState]) {
3551
+ const segs = paths.segmentsForPathKey(key);
3552
+ if (segs === null) continue;
3553
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3554
+ if (segs.length <= idxPos) continue;
3555
+ const idx = segs[idxPos];
3556
+ if (typeof idx !== "number" || !remap.vacated.has(idx)) continue;
3557
+ if (entry.timer !== null) {
3558
+ clearTimeout(entry.timer);
3559
+ } else if (!entry.settled) {
3560
+ activeValidations.value = Math.max(0, activeValidations.value - 1);
3561
+ decFieldValidation(key);
3562
+ }
3563
+ entry.controller.abort();
3564
+ fieldValidationState.delete(key);
3565
+ }
3566
+ }
3567
+ return {
3568
+ migrateElementState,
3569
+ seedFreshElement,
3570
+ dropSchemaErrorsAtChangedIndices,
3571
+ abortValidationAtVacatedIndices
3572
+ };
3573
+ }
3574
+
3575
+ function isHydratedFieldRecord(value) {
3576
+ if (typeof value !== "object" || value === null) return false;
3577
+ const r = value;
3578
+ 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" && typeof r.interacted === "boolean" && typeof r.blurredAfterInteraction === "boolean";
3579
+ }
3580
+ function isHydratedValidationErrorArray(value) {
3581
+ if (!Array.isArray(value)) return false;
3582
+ for (const entry of value) {
3583
+ if (typeof entry !== "object" || entry === null) return false;
3584
+ const e = entry;
3585
+ if (typeof e.message !== "string") return false;
3586
+ if (!Array.isArray(e.path)) return false;
3587
+ if (typeof e.formKey !== "string") return false;
3588
+ if (typeof e.code !== "string") return false;
3589
+ }
3590
+ return true;
3591
+ }
3592
+ function warnMalformedHydration(formKey, kind, rawKey) {
3593
+ if (!paths.__DEV__) return;
3594
+ console.warn(
3595
+ `[attaform] hydration: skipping malformed ${kind} entry at key '${rawKey}' on form '${formKey}'. This usually means the SSR bundle is on a different version than the client (rolling deploy / stale cache).`
3596
+ );
3597
+ }
3598
+ function isPathKeyUnder(existingKey, parentPath) {
3599
+ const parsed = paths.segmentsForPathKey(existingKey);
3600
+ if (parsed === null) return false;
3601
+ if (parsed.length <= parentPath.length) return false;
3602
+ for (let i = 0; i < parentPath.length; i++) {
3603
+ if (parsed[i] !== parentPath[i]) return false;
3604
+ }
3605
+ return true;
3606
+ }
3607
+ function stripSymbolsDeep(value) {
3608
+ if (value === null || typeof value !== "object") return value;
3609
+ if (Array.isArray(value)) {
3610
+ let mutated2 = false;
3611
+ const out2 = new Array(value.length);
3612
+ for (let i = 0; i < value.length; i++) {
3613
+ const cleaned = stripSymbolsDeep(value[i]);
3614
+ out2[i] = cleaned;
3615
+ if (cleaned !== value[i]) mutated2 = true;
3616
+ }
3617
+ return mutated2 ? out2 : value;
3618
+ }
3619
+ const proto = Object.getPrototypeOf(value);
3620
+ if (proto !== Object.prototype && proto !== null) return value;
3621
+ const symKeys = Object.getOwnPropertySymbols(value);
3622
+ const stringKeys = Object.keys(value);
3623
+ let mutated = symKeys.length > 0;
3624
+ const out = {};
3625
+ const src = value;
3626
+ for (const k of stringKeys) {
3627
+ const cleaned = stripSymbolsDeep(src[k]);
3628
+ out[k] = cleaned;
3629
+ if (cleaned !== src[k]) mutated = true;
3630
+ }
3631
+ return mutated ? out : value;
3632
+ }
3633
+ function walkAuthoredFromConstraints(value, prefix, out) {
3634
+ if (prefix.length > 0) out.add(paths.canonicalizePath(prefix).key);
3635
+ if (isPlainRecord(value)) {
3636
+ for (const k of Object.keys(value)) {
3637
+ walkAuthoredFromConstraints(value[k], [...prefix, k], out);
3638
+ }
3639
+ return;
3640
+ }
3641
+ if (Array.isArray(value)) {
3642
+ for (let i = 0; i < value.length; i++) {
3643
+ walkAuthoredFromConstraints(value[i], [...prefix, i], out);
3644
+ }
3645
+ }
3646
+ }
3647
+ function walkAuthoredFromSchemaDiff(withDefaults, withoutDefaults, prefix, out) {
3648
+ if (isPlainRecord(withDefaults) && isPlainRecord(withoutDefaults)) {
3649
+ const left = withDefaults;
3650
+ const right = withoutDefaults;
3651
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
3652
+ for (const k of keys) {
3653
+ walkAuthoredFromSchemaDiff(left[k], right[k], [...prefix, k], out);
3654
+ }
3655
+ return;
3656
+ }
3657
+ if (Array.isArray(withDefaults) && Array.isArray(withoutDefaults)) {
3658
+ const len = Math.max(withDefaults.length, withoutDefaults.length);
3659
+ for (let i = 0; i < len; i++) {
3660
+ walkAuthoredFromSchemaDiff(withDefaults[i], withoutDefaults[i], [...prefix, i], out);
3661
+ }
3662
+ return;
3663
+ }
3664
+ if (!Object.is(withDefaults, withoutDefaults) && prefix.length > 0) {
3665
+ out.add(paths.canonicalizePath(prefix).key);
3666
+ }
3667
+ }
3668
+ function createFormStore(options) {
3669
+ const { formKey, schema, defaultValues, strict = true, hydration } = options;
3670
+ const ssr = options.ssr === true;
3671
+ const ssrPrefetch = options.ssrPrefetch;
3672
+ const rememberVariants = options.rememberVariants !== false;
3673
+ const fieldValidationMode = options.validateOn ?? "change";
2986
3674
  const fieldValidationDebounceMs = normalizeNumericOption({
2987
3675
  value: options.debounceMs ?? DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS,
2988
3676
  source: "useForm.debounceMs",
@@ -3012,11 +3700,8 @@ function createFormStore(options) {
3012
3700
  noSyncPathCounts.set(path, current - 1);
3013
3701
  }
3014
3702
  const coerceIndex = resolveCoercionIndex(options.coerce);
3015
- const resolvedShouldShowErrors = resolveShouldShowErrors(
3016
- options.shouldShowErrors
3017
- );
3703
+ const resolvedGetDisplayState = resolveGetDisplayState(options.getDisplayState);
3018
3704
  const resolvedIsSensitivePath = options.isSensitivePath ?? paths.isSensitivePath;
3019
- const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? paths.segmentMatchesSensitive;
3020
3705
  const cleanupHooks = [];
3021
3706
  const modules = /* @__PURE__ */ new Map();
3022
3707
  const completedConstraints = defaultValues === void 0 ? void 0 : mergeStructural(schema, [], defaultValues);
@@ -3054,6 +3739,16 @@ function createFormStore(options) {
3054
3739
  warn: true
3055
3740
  });
3056
3741
  const form = vue.ref(stubbedInitialData);
3742
+ const arrayIdentity = createArrayIdentity((arraySegs) => {
3743
+ const v = getAtPath(form.value, arraySegs);
3744
+ return Array.isArray(v) ? v.length : 0;
3745
+ });
3746
+ function arrayElementKey(path) {
3747
+ if (path.length === 0) return "";
3748
+ const last = path[path.length - 1];
3749
+ if (typeof last === "number") return arrayIdentity.tokenAt(path.slice(0, -1), last);
3750
+ return "";
3751
+ }
3057
3752
  const fields = vue.reactive(/* @__PURE__ */ new Map());
3058
3753
  const elements = vue.reactive(/* @__PURE__ */ new Map());
3059
3754
  const elementToFormInstance = /* @__PURE__ */ new WeakMap();
@@ -3069,41 +3764,7 @@ function createFormStore(options) {
3069
3764
  blankPaths.add(key);
3070
3765
  originalBlankPaths.add(key);
3071
3766
  }
3072
- const variantMemory = /* @__PURE__ */ new Map();
3073
- function clearVariantMemoryUnderPath(arrayPath) {
3074
- for (const memKey of [...variantMemory.keys()]) {
3075
- const segs = paths.segmentsForPathKey(memKey);
3076
- if (segs === null) continue;
3077
- if (isPathPrefix(arrayPath, segs)) variantMemory.delete(memKey);
3078
- }
3079
- }
3080
- function clearVariantMemoryAtArrayIndices(arrayPath, indexFilter) {
3081
- for (const memKey of [...variantMemory.keys()]) {
3082
- const segs = paths.segmentsForPathKey(memKey);
3083
- if (segs === null) continue;
3084
- if (!isPathPrefix(arrayPath, segs)) continue;
3085
- if (segs.length <= arrayPath.length) continue;
3086
- const idxSeg = segs[arrayPath.length];
3087
- if (typeof idxSeg !== "number") continue;
3088
- if (indexFilter(idxSeg)) variantMemory.delete(memKey);
3089
- }
3090
- }
3091
- function applyArrayOpToMemory(arrayPath, op) {
3092
- switch (op.kind) {
3093
- case "shift-from":
3094
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= op.index);
3095
- return;
3096
- case "shift-range":
3097
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= op.fromIndex && i <= op.toIndex);
3098
- return;
3099
- case "swap":
3100
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3101
- return;
3102
- case "replace-at":
3103
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.index);
3104
- return;
3105
- }
3106
- }
3767
+ const variantMemory = createVariantMemory();
3107
3768
  const pathOrdinals = /* @__PURE__ */ new Map();
3108
3769
  let nextOrdinal = 0;
3109
3770
  function ensurePathOrdinal(key) {
@@ -3141,6 +3802,9 @@ function createFormStore(options) {
3141
3802
  const departAttempts = vue.ref(0);
3142
3803
  const submissionGeneration = vue.ref(0);
3143
3804
  const activeValidations = vue.ref(0);
3805
+ const pathSnapshots = /* @__PURE__ */ new Map();
3806
+ let scheduleEpoch = 0;
3807
+ let lastCommittedEpoch = 0;
3144
3808
  const hydrating = vue.ref(false);
3145
3809
  const hydrateError = vue.ref(null);
3146
3810
  const defaultValuesFactory = vue.ref(void 0);
@@ -3156,9 +3820,12 @@ function createFormStore(options) {
3156
3820
  const pathAsyncCache = /* @__PURE__ */ new Map();
3157
3821
  function pathHasAsyncValidation(path) {
3158
3822
  const { key } = paths.canonicalizePath(path);
3823
+ return pathHasAsyncValidationByKey(key, path);
3824
+ }
3825
+ function pathHasAsyncValidationByKey(key, segments) {
3159
3826
  const cached = pathAsyncCache.get(key);
3160
3827
  if (cached !== void 0) return cached;
3161
- const candidates = schema.getSchemasAtPath(path);
3828
+ const candidates = schema.getSchemasAtPath(segments);
3162
3829
  const hasAsync = candidates.some((sub) => sub.needsAsyncValidation?.() === true);
3163
3830
  pathAsyncCache.set(key, hasAsync);
3164
3831
  return hasAsync;
@@ -3211,7 +3878,9 @@ function createFormStore(options) {
3211
3878
  connected: false,
3212
3879
  focused: null,
3213
3880
  blurred: null,
3214
- touched: false
3881
+ touched: false,
3882
+ interacted: false,
3883
+ blurredAfterInteraction: false
3215
3884
  });
3216
3885
  });
3217
3886
  if (strict && !schemaResponse.success) {
@@ -3240,9 +3909,29 @@ function createFormStore(options) {
3240
3909
  blurred: patch.blurred !== void 0 ? patch.blurred : current?.blurred ?? null,
3241
3910
  // touched is plain `boolean`; `??` is equivalent to the explicit
3242
3911
  // guard here because `false` is not nullish.
3243
- touched: patch.touched ?? current?.touched ?? false
3912
+ touched: patch.touched ?? current?.touched ?? false,
3913
+ // interacted is sticky-true; a merge patch only ever sets it, so
3914
+ // `??` preserves the current bit. It flips back to false solely
3915
+ // through the reset paths, which reconstruct the record outright.
3916
+ interacted: patch.interacted ?? current?.interacted ?? false,
3917
+ blurredAfterInteraction: patch.blurredAfterInteraction ?? current?.blurredAfterInteraction ?? false
3244
3918
  });
3245
3919
  }
3920
+ const arrayBookkeeping = createArrayBookkeeping({
3921
+ form,
3922
+ fields,
3923
+ userErrors,
3924
+ originals,
3925
+ blankPaths,
3926
+ originalBlankPaths,
3927
+ fieldValidationCounts,
3928
+ fieldValidationState,
3929
+ schemaErrors,
3930
+ activeValidations,
3931
+ arrayIdentity,
3932
+ touchFieldRecord,
3933
+ decFieldValidation
3934
+ });
3246
3935
  function applyFormReplacement(next, meta) {
3247
3936
  const prev = form.value;
3248
3937
  if (Object.is(prev, next)) return;
@@ -3366,8 +4055,13 @@ function createFormStore(options) {
3366
4055
  const pathKey = paths.canonicalizePath(path).key;
3367
4056
  if (meta?.blank === true) {
3368
4057
  blankPaths.add(pathKey);
3369
- } else if (blankPaths.has(pathKey)) {
3370
- blankPaths.delete(pathKey);
4058
+ } else {
4059
+ if (blankPaths.has(pathKey)) blankPaths.delete(pathKey);
4060
+ if (meta?.arrayOp === void 0) {
4061
+ for (const existingKey of [...blankPaths]) {
4062
+ if (isPathKeyUnder(existingKey, path)) blankPaths.delete(existingKey);
4063
+ }
4064
+ }
3371
4065
  }
3372
4066
  const wasAuthoredBefore = authoredPaths.has(pathKey);
3373
4067
  walkAuthoredFromConstraints(value, path, authoredPaths);
@@ -3386,12 +4080,20 @@ function createFormStore(options) {
3386
4080
  }
3387
4081
  return true;
3388
4082
  }
4083
+ const oldArrayLength = Array.isArray(currentValue) ? currentValue.length : 0;
3389
4084
  const nextForm = setAtPathWithSchemaFill(form.value, schema, path, completedValue);
3390
4085
  applyFormReplacement(nextForm, meta);
3391
4086
  if (meta?.arrayOp !== void 0) {
3392
- applyArrayOpToMemory(path, meta.arrayOp);
4087
+ const remap = remapForOp(meta.arrayOp, oldArrayLength);
4088
+ arrayBookkeeping.migrateElementState(path, remap);
4089
+ for (const freshIndex of remap.fresh) arrayBookkeeping.seedFreshElement(path, freshIndex);
4090
+ arrayBookkeeping.dropSchemaErrorsAtChangedIndices(path, remap);
4091
+ arrayBookkeeping.abortValidationAtVacatedIndices(path, remap);
4092
+ variantMemory.applyArrayOp(path, meta.arrayOp);
4093
+ arrayIdentity.applyOp(path, meta.arrayOp);
3393
4094
  } else if (Array.isArray(value) && Array.isArray(currentValue)) {
3394
- clearVariantMemoryUnderPath(path);
4095
+ variantMemory.clearUnderPath(path);
4096
+ arrayIdentity.realign(path);
3395
4097
  }
3396
4098
  const effectiveModeAfterWrite = meta?.instance?.validateOn ?? fieldValidationMode;
3397
4099
  if (effectiveModeAfterWrite === "change") {
@@ -3415,18 +4117,12 @@ function createFormStore(options) {
3415
4117
  for (const k of blankPaths) {
3416
4118
  if (isPathKeyUnder(k, parentPath)) outgoingBlanks.push(k);
3417
4119
  }
3418
- let memoryForUnion2 = variantMemory.get(parentKey);
3419
- if (memoryForUnion2 === void 0) {
3420
- memoryForUnion2 = /* @__PURE__ */ new Map();
3421
- variantMemory.set(parentKey, memoryForUnion2);
3422
- }
3423
- memoryForUnion2.set(oldDiscValue, {
4120
+ variantMemory.recordOutgoing(parentKey, oldDiscValue, {
3424
4121
  value: currentValue2,
3425
4122
  blankPaths: outgoingBlanks
3426
4123
  });
3427
4124
  }
3428
- const memoryForUnion = variantMemory.get(parentKey);
3429
- const restored = memoryForUnion?.get(newDiscValue);
4125
+ const restored = variantMemory.lookupIncoming(parentKey, newDiscValue);
3430
4126
  if (restored !== void 0) {
3431
4127
  baseline = restored.value;
3432
4128
  restoredBlanks = [...restored.blankPaths];
@@ -3498,6 +4194,7 @@ function createFormStore(options) {
3498
4194
  const controller = new AbortController();
3499
4195
  const fresh = { controller, timer: null, settled: false };
3500
4196
  fieldValidationState.set(key, fresh);
4197
+ const myEpoch = ++scheduleEpoch;
3501
4198
  const run = () => {
3502
4199
  fresh.timer = null;
3503
4200
  if (controller.signal.aborted) return;
@@ -3512,11 +4209,25 @@ function createFormStore(options) {
3512
4209
  }
3513
4210
  throw err;
3514
4211
  }
3515
- void Promise.resolve().then(() => schema.validateAtPath(form.value, void 0)).then((response) => {
4212
+ const subtreeScope = path.length > 0 && schema.hasContainerOrRootRefine?.() === false;
4213
+ const scopePath = subtreeScope ? path : void 0;
4214
+ const dataAtScope = subtreeScope ? getAtPath(form.value, path) : form.value;
4215
+ const scopeKey = subtreeScope ? paths.canonicalizePath(path).key : paths.ROOT_PATH_KEY;
4216
+ void Promise.resolve().then(() => schema.validateAtPath(dataAtScope, scopePath)).then((response) => {
3516
4217
  if (controller.signal.aborted) return;
4218
+ if (myEpoch <= lastCommittedEpoch) return;
4219
+ lastCommittedEpoch = myEpoch;
4220
+ if (effectiveMode === "blur") {
4221
+ const snapshotSource = scopePath !== void 0 ? getAtPath(form.value, scopePath) : form.value;
4222
+ pathSnapshots.set(scopeKey, structuralSnapshot(snapshotSource));
4223
+ }
3517
4224
  const errors = response.success ? [] : response.errors;
3518
4225
  const filtered = filterAuthoredErrors(errors);
3519
- applySchemaErrorsForSubtree([], filtered);
4226
+ const restamped = subtreeScope ? filtered.map((err) => ({
4227
+ ...err,
4228
+ path: [...path, ...err.path]
4229
+ })) : filtered;
4230
+ applySchemaErrorsForSubtree(scopePath ?? [], restamped);
3520
4231
  }).catch(() => {
3521
4232
  }).finally(() => {
3522
4233
  activeValidations.value = Math.max(0, activeValidations.value - 1);
@@ -3743,30 +4454,61 @@ function createFormStore(options) {
3743
4454
  }
3744
4455
  function markFocused(path, focused, meta) {
3745
4456
  const { key } = paths.canonicalizePath(path);
4457
+ const current = fields.get(key);
3746
4458
  touchFieldRecord(key, path, {
3747
4459
  focused,
3748
4460
  blurred: !focused,
3749
4461
  // `touched` flips to true on blur and stays true thereafter; while
3750
4462
  // a field is currently focused we keep whatever value it held.
3751
- touched: focused ? fields.get(key)?.touched ?? false : true
4463
+ touched: focused ? current?.touched ?? false : true,
4464
+ // `blurredAfterInteraction` flips true on the first blur that lands
4465
+ // after a value edit and stays true. A tab-through blur before any
4466
+ // edit leaves it false (`interacted` is still false at that blur),
4467
+ // which is what keeps a clean tab-through from arming the gate.
4468
+ blurredAfterInteraction: !focused && current?.interacted === true ? true : current?.blurredAfterInteraction ?? false
3752
4469
  });
3753
4470
  const focusMode = meta?.instance?.validateOn ?? fieldValidationMode;
3754
4471
  if (!focused && focusMode === "blur") {
3755
- scheduleFieldValidation(path, true, {
3756
- ...meta?.instance?.validateOn !== void 0 ? { mode: meta.instance.validateOn } : {},
3757
- ...meta?.instance?.debounceMs !== void 0 ? { debounceMs: meta.instance.debounceMs } : {}
3758
- });
4472
+ const firstInteractiveBlur = current?.interacted === true && current.blurredAfterInteraction !== true;
4473
+ let snapshot = void 0;
4474
+ let snapshotScopeLength = 0;
4475
+ for (let i = path.length; i >= 0; i--) {
4476
+ const ancestorKey = paths.canonicalizePath(path.slice(0, i)).key;
4477
+ const entry = pathSnapshots.get(ancestorKey);
4478
+ if (entry !== void 0) {
4479
+ snapshot = entry;
4480
+ snapshotScopeLength = i;
4481
+ break;
4482
+ }
4483
+ }
4484
+ let changed = true;
4485
+ if (!firstInteractiveBlur && snapshot !== void 0) {
4486
+ const relPath = path.slice(snapshotScopeLength);
4487
+ const snapshotSubtree = getAtPath(snapshot, relPath);
4488
+ const liveSubtree = getAtPath(form.value, path);
4489
+ changed = false;
4490
+ diffAndApply(snapshotSubtree, liveSubtree, path, () => {
4491
+ changed = true;
4492
+ });
4493
+ }
4494
+ if (changed) {
4495
+ scheduleFieldValidation(path, true, {
4496
+ ...meta?.instance?.validateOn !== void 0 ? { mode: meta.instance.validateOn } : {},
4497
+ ...meta?.instance?.debounceMs !== void 0 ? { debounceMs: meta.instance.debounceMs } : {}
4498
+ });
4499
+ }
3759
4500
  }
3760
4501
  }
3761
- function markTouched(path) {
4502
+ function markInteracted(path) {
3762
4503
  const { key } = paths.canonicalizePath(path);
3763
- touchFieldRecord(key, path, { touched: true });
4504
+ if (fields.get(key)?.interacted === true) return;
4505
+ touchFieldRecord(key, path, { interacted: true });
3764
4506
  }
3765
4507
  function touchAtPath(segments) {
3766
4508
  const formValue = form.value;
3767
4509
  let touchedAny = false;
3768
4510
  for (const [, entry] of originals) {
3769
- if (!isPathPrefix(segments, entry.segments)) continue;
4511
+ if (!paths.isPathPrefix(segments, entry.segments)) continue;
3770
4512
  if (!hasAtPath(formValue, entry.segments)) continue;
3771
4513
  touchedAny = true;
3772
4514
  const leafKey = paths.canonicalizePath(entry.segments).key;
@@ -3780,9 +4522,6 @@ function createFormStore(options) {
3780
4522
  );
3781
4523
  }
3782
4524
  }
3783
- function clear(path) {
3784
- return setValueAtPath(path, schema.getEmptyValueAtPath(path));
3785
- }
3786
4525
  function rehydrate() {
3787
4526
  const factory = defaultValuesFactory.value;
3788
4527
  if (factory === void 0) {
@@ -3872,6 +4611,7 @@ function createFormStore(options) {
3872
4611
  const next = resetResponse.data;
3873
4612
  rebuildAuthoredPaths(resetSource, next);
3874
4613
  applyFormReplacement(next);
4614
+ arrayIdentity.rebaselineAll();
3875
4615
  originals.clear();
3876
4616
  diffAndApply({}, next, [], (patch) => {
3877
4617
  if (patch.kind !== "added") return;
@@ -3915,7 +4655,9 @@ function createFormStore(options) {
3915
4655
  connected: record.connected,
3916
4656
  focused: record.focused,
3917
4657
  blurred: record.blurred,
3918
- touched: false
4658
+ touched: false,
4659
+ interacted: false,
4660
+ blurredAfterInteraction: false
3919
4661
  });
3920
4662
  }
3921
4663
  submissionGeneration.value += 1;
@@ -3926,6 +4668,9 @@ function createFormStore(options) {
3926
4668
  submitError.value = null;
3927
4669
  departAttempts.value = 0;
3928
4670
  cancelFieldValidation();
4671
+ pathSnapshots.clear();
4672
+ scheduleEpoch = 0;
4673
+ lastCommittedEpoch = 0;
3929
4674
  variantMemory.clear();
3930
4675
  for (const listener of resetListeners) {
3931
4676
  try {
@@ -3937,13 +4682,7 @@ function createFormStore(options) {
3937
4682
  }
3938
4683
  function resetField(path) {
3939
4684
  const { key: targetKey, segments: targetSegments } = paths.canonicalizePath(path);
3940
- for (const memKey of [...variantMemory.keys()]) {
3941
- const memSegments = paths.segmentsForPathKey(memKey);
3942
- if (memSegments === null) continue;
3943
- if (isPathPrefix(targetSegments, memSegments)) {
3944
- variantMemory.delete(memKey);
3945
- }
3946
- }
4685
+ variantMemory.clearUnderPath(targetSegments);
3947
4686
  const leafEntry = originals.get(targetKey);
3948
4687
  if (leafEntry !== void 0) {
3949
4688
  const wrote = setValueAtPath(targetSegments, leafEntry.value);
@@ -3957,7 +4696,7 @@ function createFormStore(options) {
3957
4696
  let anyMatch = false;
3958
4697
  for (const [, entry] of originals) {
3959
4698
  const leafSegments = entry.segments;
3960
- if (!isPathPrefix(targetSegments, leafSegments)) continue;
4699
+ if (!paths.isPathPrefix(targetSegments, leafSegments)) continue;
3961
4700
  if (leafSegments.length === targetSegments.length) continue;
3962
4701
  anyMatch = true;
3963
4702
  const relative = leafSegments.slice(targetSegments.length);
@@ -3978,14 +4717,14 @@ function createFormStore(options) {
3978
4717
  deleteErrorsUnderPrefix(schemaErrors, targetSegments);
3979
4718
  deleteErrorsUnderPrefix(userErrors, targetSegments);
3980
4719
  for (const [fieldKey, record] of Array.from(fields.entries())) {
3981
- if (isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
4720
+ if (paths.isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
3982
4721
  }
3983
4722
  }
3984
4723
  function deleteErrorsUnderPrefix(map, prefix) {
3985
4724
  for (const [errorKey, errs] of Array.from(map.entries())) {
3986
4725
  const first = errs[0];
3987
4726
  if (first === void 0) continue;
3988
- if (isPathPrefix(prefix, first.path)) {
4727
+ if (paths.isPathPrefix(prefix, first.path)) {
3989
4728
  map.delete(errorKey);
3990
4729
  }
3991
4730
  }
@@ -3999,23 +4738,24 @@ function createFormStore(options) {
3999
4738
  connected: record.connected,
4000
4739
  focused: record.focused,
4001
4740
  blurred: record.blurred,
4002
- touched: false
4741
+ touched: false,
4742
+ interacted: false,
4743
+ blurredAfterInteraction: false
4003
4744
  });
4004
4745
  }
4005
- function isPathPrefix(prefix, candidate) {
4006
- if (prefix.length > candidate.length) return false;
4007
- for (let i = 0; i < prefix.length; i++) {
4008
- if (prefix[i] !== candidate[i]) return false;
4009
- }
4010
- return true;
4011
- }
4012
4746
  function isPristineAtPath(path) {
4013
4747
  const { key, segments } = paths.canonicalizePath(path);
4748
+ return isPristineAtPathByKey(key, segments);
4749
+ }
4750
+ function isPristineAtPathByKey(key, segments) {
4014
4751
  if (blankPaths.has(key) !== originalBlankPaths.has(key)) return false;
4015
4752
  const entry = originals.get(key);
4016
4753
  if (entry === void 0) return true;
4017
4754
  return Object.is(getAtPath(form.value, segments), entry.value);
4018
4755
  }
4756
+ function hasStructuralChangeUnder(path) {
4757
+ return arrayIdentity.hasStructuralChangeUnder(path);
4758
+ }
4019
4759
  function getFieldRecord(path) {
4020
4760
  const { key } = paths.canonicalizePath(path);
4021
4761
  return fields.get(key);
@@ -4059,7 +4799,7 @@ function createFormStore(options) {
4059
4799
  originals,
4060
4800
  schema,
4061
4801
  ssr,
4062
- shouldShowErrors: resolvedShouldShowErrors,
4802
+ getDisplayState: resolvedGetDisplayState,
4063
4803
  submitting,
4064
4804
  activeSubmissions,
4065
4805
  submissionAttempts,
@@ -4069,6 +4809,7 @@ function createFormStore(options) {
4069
4809
  hydrating,
4070
4810
  hydrateError,
4071
4811
  defaultValuesFactory,
4812
+ hasSsrPrefetch: ssrPrefetch !== void 0,
4072
4813
  defaultsResolved,
4073
4814
  activated,
4074
4815
  activationPromise,
@@ -4078,13 +4819,14 @@ function createFormStore(options) {
4078
4819
  activeValidations,
4079
4820
  firstValidationDone,
4080
4821
  pathHasAsyncValidation,
4822
+ pathHasAsyncValidationByKey,
4081
4823
  fieldValidationCounts,
4082
4824
  applyFormReplacement,
4083
4825
  setValueAtPath,
4084
4826
  getValueAtPath,
4827
+ arrayElementKey,
4085
4828
  reset,
4086
4829
  resetField,
4087
- clear,
4088
4830
  setSchemaErrorsForPath,
4089
4831
  setAllSchemaErrors,
4090
4832
  clearSchemaErrors,
@@ -4097,10 +4839,12 @@ function createFormStore(options) {
4097
4839
  registerElement,
4098
4840
  deregisterElement,
4099
4841
  markFocused,
4100
- markTouched,
4842
+ markInteracted,
4101
4843
  touchAtPath,
4102
4844
  markConnectedOptimistically,
4103
4845
  isPristineAtPath,
4846
+ isPristineAtPathByKey,
4847
+ hasStructuralChangeUnder,
4104
4848
  getFieldRecord,
4105
4849
  getOriginalAtPath,
4106
4850
  getFirstErrorElement,
@@ -4116,7 +4860,6 @@ function createFormStore(options) {
4116
4860
  modules,
4117
4861
  persistOptIns,
4118
4862
  isSensitivePath: resolvedIsSensitivePath,
4119
- segmentMatchesSensitive: resolvedSegmentMatchesSensitive,
4120
4863
  noSyncPaths,
4121
4864
  incrementNoSyncOptOut,
4122
4865
  decrementNoSyncOptOut,
@@ -4317,939 +5060,988 @@ function createHistoryModule(state, config) {
4317
5060
  };
4318
5061
  }
4319
5062
 
4320
- function hashStableString(input, seed = 0) {
4321
- let h1 = 3735928559 ^ seed;
4322
- let h2 = 1103547991 ^ seed;
4323
- for (let i = 0; i < input.length; i++) {
4324
- const ch = input.charCodeAt(i);
4325
- h1 = Math.imul(h1 ^ ch, 2654435761);
4326
- h2 = Math.imul(h2 ^ ch, 1597334677);
4327
- }
4328
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
4329
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
4330
- return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).padStart(11, "0");
4331
- }
4332
-
4333
- const PROTOCOL_VERSION = 1;
4334
- const JOIN_COLLECTION_WINDOW_MS = 50;
4335
- const SNAPSHOT_TIMEOUT_MS = 200;
4336
- const MAX_LEADER_ATTEMPTS = 3;
4337
- function isFileLikeValue(value) {
4338
- if (typeof File !== "undefined" && value instanceof File) return true;
4339
- if (typeof Blob !== "undefined" && value instanceof Blob) return true;
4340
- return false;
4341
- }
4342
- function isDangerousSegment(s) {
4343
- return s === "__proto__" || s === "constructor" || s === "prototype";
4344
- }
4345
- function pathContainsDangerousSegment(path) {
4346
- for (let i = 0; i < path.length; i++) {
4347
- if (isDangerousSegment(path[i])) return true;
4348
- }
4349
- return false;
4350
- }
4351
- function isInboundShapeAcceptable(schema, path, value) {
4352
- if (isFileLikeValue(value)) return false;
4353
- let kind;
4354
- if (Array.isArray(value)) {
4355
- kind = "array";
4356
- } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
4357
- kind = "object";
4358
- } else {
4359
- kind = slimKindOf(value);
4360
- }
4361
- const accepted = schema.getSlimPrimitiveTypesAtPath(path);
4362
- if (!accepted.has(kind)) return false;
4363
- if (Array.isArray(value)) {
4364
- for (let i = 0; i < value.length; i++) {
4365
- if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
4366
- }
4367
- return true;
4368
- }
4369
- if (isPlainRecord(value)) {
4370
- for (const key of Object.keys(value)) {
4371
- if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5063
+ function wirePersistence(state, config) {
5064
+ let fingerprint;
5065
+ try {
5066
+ fingerprint = hashStableString(state.schema.fingerprint());
5067
+ } catch (err) {
5068
+ if (paths.__DEV__) {
5069
+ console.warn(
5070
+ `[attaform] Could not fingerprint the schema for form '${state.formKey}': ${err instanceof Error ? err.message : String(err)}. Persistence falls back to a fingerprint-free key, so a schema change won't auto-invalidate a saved draft.`
5071
+ );
4372
5072
  }
4373
- return true;
5073
+ fingerprint = "unfingerprinted";
4374
5074
  }
4375
- return true;
4376
- }
4377
- function diffBlankPaths(prev, curr) {
4378
- const added = [];
4379
- const removed = [];
4380
- const prevSet = new Set(prev);
4381
- for (const k of curr) if (!prevSet.has(k)) added.push(k);
4382
- for (const k of prev) if (!curr.has(k)) removed.push(k);
4383
- return { added, removed };
4384
- }
4385
- function snapshotForm(form) {
4386
- return structuralSnapshot(form);
4387
- }
4388
- function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
4389
- if (isFileLikeValue(value)) return void 0;
4390
- if (value === null || typeof value !== "object") return value;
4391
- if (Array.isArray(value)) {
4392
- return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
4393
- }
4394
- const proto = Object.getPrototypeOf(value);
4395
- if (proto !== Object.prototype && proto !== null) return value;
4396
- const out = {};
4397
- const src = value;
4398
- for (const key of Object.keys(src)) {
4399
- const childPath = [...pathSoFar, key];
4400
- if (isSensitivePath(childPath)) {
4401
- out[key] = void 0;
4402
- continue;
4403
- }
4404
- out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
4405
- }
4406
- return out;
4407
- }
4408
- function isValidSyncMessage(data) {
4409
- if (data === null || typeof data !== "object") return false;
4410
- const m = data;
4411
- if (m["v"] !== PROTOCOL_VERSION) return false;
4412
- if (typeof m["senderId"] !== "string") return false;
4413
- if (typeof m["kind"] !== "string") return false;
4414
- switch (m["kind"]) {
4415
- case "hello":
4416
- case "announce":
4417
- return true;
4418
- case "requestSnapshot":
4419
- return typeof m["targetId"] === "string";
4420
- case "snapshot":
4421
- return Array.isArray(m["blankPaths"]) && "form" in m;
4422
- case "patches":
4423
- return Array.isArray(m["formPatches"]) && Array.isArray(m["blankPathsAdded"]) && Array.isArray(m["blankPathsRemoved"]);
4424
- default:
4425
- return false;
4426
- }
4427
- }
4428
- function generateSenderId() {
4429
- try {
4430
- return globalThis.crypto.randomUUID();
4431
- } catch {
4432
- return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
4433
- }
4434
- }
4435
- function createMultiTabSyncModule(state, channelName, options) {
4436
- if (typeof BroadcastChannel === "undefined") {
4437
- return {
4438
- dispose: () => void 0,
4439
- lifecycle: () => "established",
4440
- senderId: "",
4441
- channelName
4442
- };
4443
- }
4444
- let channel;
4445
- try {
4446
- channel = new BroadcastChannel(channelName);
4447
- } catch {
4448
- return {
4449
- dispose: () => void 0,
4450
- lifecycle: () => "established",
4451
- senderId: "",
4452
- channelName
4453
- };
4454
- }
4455
- const senderId = generateSenderId();
4456
- let lifecycle = "joining";
5075
+ const base = resolveStorageKeyBase(config, state.formKey);
5076
+ const key = `${base}:${fingerprint}`;
5077
+ const debounceMs = normalizeNumericOption({
5078
+ value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5079
+ source: "useForm.persist.debounceMs",
5080
+ allowInfinity: false,
5081
+ min: 0,
5082
+ defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5083
+ });
5084
+ const include = config.include ?? "form";
5085
+ const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5086
+ const adapterPromise = getStorageAdapter(config.storage);
4457
5087
  let disposed = false;
4458
- const peerIds = /* @__PURE__ */ new Set();
4459
- let joinCollectionTimer = null;
4460
- let snapshotTimeoutTimer = null;
4461
- let leaderAttempts = 0;
4462
- let prior = {
4463
- form: snapshotForm(state.form.value),
4464
- blankPathsSnapshot: [...state.blankPaths]
4465
- };
4466
- function safePost(msg) {
4467
- if (disposed) return;
4468
- try {
4469
- channel.postMessage(msg);
4470
- } catch {
4471
- }
4472
- }
4473
- function refreshPrior() {
4474
- prior = {
4475
- form: snapshotForm(state.form.value),
4476
- blankPathsSnapshot: [...state.blankPaths]
4477
- };
4478
- }
4479
- function isPathLocallySuppressed(path) {
4480
- if (pathContainsDangerousSegment(path)) return true;
4481
- if (options.isSensitivePath(path)) return true;
4482
- const { key } = paths.canonicalizePath([...path]);
4483
- if (options.noSyncPaths.has(key)) return true;
4484
- return false;
4485
- }
4486
- function postPatches() {
4487
- if (lifecycle !== "established") return;
4488
- const next = snapshotForm(state.form.value);
4489
- const rawPatches = [];
4490
- diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
4491
- const safePatches = [];
4492
- for (const p of rawPatches) {
4493
- if (isPathLocallySuppressed(p.path)) continue;
4494
- if ("value" in p && isFileLikeValue(p.value)) continue;
4495
- safePatches.push(p);
4496
- }
4497
- const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
4498
- if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
4499
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5088
+ const isDisposed = () => disposed;
5089
+ let inFlightFinalFlush = null;
5090
+ let pendingOptedInPaths = null;
5091
+ const writer = createDebouncedWriter(async () => {
5092
+ const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5093
+ pendingOptedInPaths = null;
5094
+ const adapter = await adapterPromise;
5095
+ if (isDisposed()) return;
5096
+ if (optedInPaths.size === 0) {
5097
+ await adapter.removeItem(key);
4500
5098
  return;
4501
5099
  }
4502
- safePost({
4503
- v: PROTOCOL_VERSION,
4504
- kind: "patches",
4505
- senderId,
4506
- formPatches: safePatches,
4507
- blankPathsAdded: added,
4508
- blankPathsRemoved: removed
4509
- });
4510
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4511
- }
5100
+ const rawForm = vue.toRaw(state.form.value);
5101
+ const filteredForm = stripUnacknowledgedSensitiveLeaves(
5102
+ pluckPaths(rawForm, optedInPaths),
5103
+ optedInPaths,
5104
+ state.isSensitivePath
5105
+ );
5106
+ const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5107
+ const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5108
+ const filteredTransientEmpty = /* @__PURE__ */ new Set();
5109
+ for (const tk of state.blankPaths) {
5110
+ if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
5111
+ }
5112
+ const payload = buildPersistedPayload(
5113
+ filteredForm,
5114
+ include,
5115
+ filteredSchemaErrors,
5116
+ filteredUserErrors,
5117
+ filteredTransientEmpty
5118
+ );
5119
+ await adapter.setItem(key, payload);
5120
+ }, debounceMs);
4512
5121
  const unsubscribeChange = state.onFormChange((_next, meta) => {
4513
- if (disposed) return;
4514
- if (lifecycle !== "established") return;
5122
+ if (isDisposed() || inFlightFinalFlush !== null) return;
4515
5123
  if (meta?.crossTab === true) return;
4516
- if (meta?.hydration === true) {
4517
- refreshPrior();
4518
- return;
4519
- }
4520
- postPatches();
5124
+ if (meta?.persist !== true) return;
5125
+ pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5126
+ writer.schedule();
4521
5127
  });
4522
- function applyIncomingForm(form, blankPaths) {
4523
- state.blankPaths.clear();
4524
- for (const k of blankPaths) state.blankPaths.add(k);
4525
- state.applyFormReplacement(form, { crossTab: true, persist: false });
4526
- refreshPrior();
5128
+ const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5129
+ if (isDisposed()) return;
5130
+ void (async () => {
5131
+ await writer.flush();
5132
+ if (isDisposed()) return;
5133
+ const adapter = await adapterPromise;
5134
+ if (isDisposed()) return;
5135
+ await adapter.removeItem(key);
5136
+ })();
5137
+ }) : () => void 0;
5138
+ const handlePageHide = () => {
5139
+ if (isDisposed()) return;
5140
+ void writer.flush();
5141
+ };
5142
+ if (typeof window !== "undefined") {
5143
+ window.addEventListener("pagehide", handlePageHide);
4527
5144
  }
4528
- function handlePatches(msg) {
4529
- if (lifecycle !== "established") return;
4530
- const safePatches = [];
4531
- for (const p of msg.formPatches) {
4532
- if (!Array.isArray(p.path)) continue;
4533
- if (isPathLocallySuppressed(p.path)) continue;
4534
- if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
4535
- continue;
4536
- }
4537
- safePatches.push(p);
4538
- }
4539
- const safeBlankAdded = [];
4540
- for (const k of msg.blankPathsAdded) {
4541
- const segs = paths.canonicalizePath(k).segments;
4542
- if (isPathLocallySuppressed(segs)) continue;
4543
- safeBlankAdded.push(k);
4544
- }
4545
- const safeBlankRemoved = [];
4546
- for (const k of msg.blankPathsRemoved) {
4547
- const segs = paths.canonicalizePath(k).segments;
4548
- if (isPathLocallySuppressed(segs)) continue;
4549
- safeBlankRemoved.push(k);
4550
- }
4551
- if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
4552
- return;
4553
- }
4554
- const candidate = applyPatchesForward(state.form.value, safePatches);
5145
+ void (async () => {
5146
+ const adapter = await adapterPromise;
5147
+ if (isDisposed()) return;
5148
+ void cleanupOrphanKeys(adapter, base, key);
4555
5149
  try {
4556
- options.validateForm(state.form.value);
4557
- try {
4558
- options.validateForm(candidate);
4559
- } catch {
5150
+ const raw = await adapter.getItem(key);
5151
+ const payload = readPersistedPayload(raw);
5152
+ if (payload === null) {
5153
+ if (raw !== null && raw !== void 0) {
5154
+ await adapter.removeItem(key);
5155
+ }
4560
5156
  return;
4561
5157
  }
5158
+ if (isDisposed()) return;
5159
+ const merged = mergeSparseHydration(
5160
+ vue.toRaw(state.form.value),
5161
+ payload.data.form,
5162
+ state.schema
5163
+ );
5164
+ state.applyFormReplacement(merged, { hydration: true });
5165
+ const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5166
+ for (const k of persistedLeafPaths) {
5167
+ state.blankPaths.delete(k);
5168
+ state.originalBlankPaths.delete(k);
5169
+ }
5170
+ for (const k of payload.data.blankPaths ?? []) {
5171
+ state.blankPaths.add(k);
5172
+ state.originalBlankPaths.add(k);
5173
+ }
5174
+ if (include === "form+errors") {
5175
+ if (payload.data.schemaErrors !== void 0) {
5176
+ const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5177
+ state.setAllSchemaErrors(flat);
5178
+ }
5179
+ if (payload.data.userErrors !== void 0) {
5180
+ const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5181
+ state.setAllUserErrors(flat);
5182
+ }
5183
+ }
5184
+ state.scheduleFieldValidation(
5185
+ [],
5186
+ true
5187
+ /* immediate */
5188
+ );
4562
5189
  } catch {
4563
5190
  }
4564
- const nextBlankPaths = new Set(state.blankPaths);
4565
- for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
4566
- for (const k of safeBlankAdded) nextBlankPaths.add(k);
4567
- applyIncomingForm(candidate, [...nextBlankPaths]);
4568
- }
4569
- function handleSnapshot(msg) {
4570
- if (lifecycle !== "joining") return;
4571
- if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
4572
- if (snapshotTimeoutTimer !== null) {
4573
- clearTimeout(snapshotTimeoutTimer);
4574
- snapshotTimeoutTimer = null;
5191
+ })();
5192
+ async function writePathImmediately(path) {
5193
+ if (isDisposed()) return;
5194
+ await writer.flush();
5195
+ if (isDisposed()) return;
5196
+ const adapter = await adapterPromise;
5197
+ if (isDisposed()) return;
5198
+ const raw = await adapter.getItem(key);
5199
+ const existing = readPersistedPayload(raw);
5200
+ const baseForm = existing?.data.form ?? /* @__PURE__ */ Object.create(null);
5201
+ const value = getAtPath(vue.toRaw(state.form.value), path);
5202
+ const nextForm = setAtPath(baseForm, path, value);
5203
+ const { key: pathKey } = paths.canonicalizePath(path);
5204
+ const transientSet = new Set(
5205
+ (existing?.data.blankPaths ?? []).filter(
5206
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5207
+ )
5208
+ );
5209
+ for (const liveKey of state.blankPaths) {
5210
+ if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5211
+ transientSet.add(liveKey);
5212
+ }
4575
5213
  }
4576
- if (joinCollectionTimer !== null) {
4577
- clearTimeout(joinCollectionTimer);
4578
- joinCollectionTimer = null;
5214
+ if (include === "form") {
5215
+ await adapter.setItem(
5216
+ key,
5217
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5218
+ );
5219
+ return;
4579
5220
  }
4580
- applyIncomingForm(msg.form, msg.blankPaths);
4581
- lifecycle = "established";
4582
- peerIds.clear();
4583
- }
4584
- function respondToHello() {
4585
- safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
4586
- }
4587
- function respondToSnapshotRequest() {
4588
- const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
4589
- safePost({
4590
- v: PROTOCOL_VERSION,
4591
- kind: "snapshot",
4592
- senderId,
4593
- form: scrubbedForm,
4594
- blankPaths: [...state.blankPaths]
4595
- });
5221
+ const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5222
+ const userMap = new Map(existing?.data.userErrors ?? []);
5223
+ const currentSchema = state.schemaErrors.get(pathKey);
5224
+ const currentUser = state.userErrors.get(pathKey);
5225
+ if (currentSchema !== void 0 && currentSchema.length > 0) {
5226
+ schemaMap.set(pathKey, [...currentSchema]);
5227
+ } else {
5228
+ schemaMap.delete(pathKey);
5229
+ }
5230
+ if (currentUser !== void 0 && currentUser.length > 0) {
5231
+ userMap.set(pathKey, [...currentUser]);
5232
+ } else {
5233
+ userMap.delete(pathKey);
5234
+ }
5235
+ await adapter.setItem(
5236
+ key,
5237
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5238
+ );
4596
5239
  }
4597
- channel.onmessage = (event) => {
4598
- if (disposed) return;
4599
- const data = event.data;
4600
- if (!isValidSyncMessage(data)) return;
4601
- const msg = data;
4602
- if (msg.senderId === senderId) return;
4603
- switch (msg.kind) {
4604
- case "hello":
4605
- if (lifecycle !== "established") return;
4606
- respondToHello();
4607
- break;
4608
- case "announce":
4609
- if (lifecycle === "joining") peerIds.add(msg.senderId);
4610
- break;
4611
- case "requestSnapshot":
4612
- if (lifecycle !== "established") return;
4613
- if (msg.targetId !== senderId) return;
4614
- respondToSnapshotRequest();
4615
- break;
4616
- case "snapshot":
4617
- handleSnapshot(msg);
4618
- break;
4619
- case "patches":
4620
- handlePatches(msg);
4621
- break;
5240
+ async function clearPersistedDraft(path) {
5241
+ if (isDisposed()) return;
5242
+ await writer.flush();
5243
+ if (isDisposed()) return;
5244
+ const adapter = await adapterPromise;
5245
+ if (isDisposed()) return;
5246
+ if (path === void 0) {
5247
+ await adapter.removeItem(key);
5248
+ return;
4622
5249
  }
4623
- };
4624
- function electLeaderAndRequest() {
4625
- if (disposed) return;
4626
- if (peerIds.size === 0) {
4627
- lifecycle = "established";
4628
- refreshPrior();
5250
+ const raw = await adapter.getItem(key);
5251
+ const existing = readPersistedPayload(raw);
5252
+ if (existing === null) return;
5253
+ const nextForm = deleteAtPath(existing.data.form, path);
5254
+ if (isEmptyContainer(nextForm)) {
5255
+ await adapter.removeItem(key);
4629
5256
  return;
4630
5257
  }
4631
- const sorted = [...peerIds].sort();
4632
- const leaderId = sorted[0];
4633
- peerIds.delete(leaderId);
4634
- leaderAttempts++;
4635
- safePost({
4636
- v: PROTOCOL_VERSION,
4637
- kind: "requestSnapshot",
4638
- senderId,
4639
- targetId: leaderId
4640
- });
4641
- snapshotTimeoutTimer = setTimeout(() => {
4642
- snapshotTimeoutTimer = null;
4643
- if (disposed) return;
4644
- if (lifecycle === "established") return;
4645
- if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
4646
- lifecycle = "established";
4647
- refreshPrior();
4648
- return;
4649
- }
4650
- electLeaderAndRequest();
4651
- }, SNAPSHOT_TIMEOUT_MS);
5258
+ const { key: pathKey } = paths.canonicalizePath(path);
5259
+ const transientSet = new Set(
5260
+ (existing.data.blankPaths ?? []).filter(
5261
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5262
+ )
5263
+ );
5264
+ if (include === "form") {
5265
+ await adapter.setItem(
5266
+ key,
5267
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5268
+ );
5269
+ return;
5270
+ }
5271
+ const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5272
+ const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5273
+ const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5274
+ const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5275
+ await adapter.setItem(
5276
+ key,
5277
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5278
+ );
4652
5279
  }
4653
- function joinFlow() {
4654
- safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
4655
- joinCollectionTimer = setTimeout(() => {
4656
- joinCollectionTimer = null;
4657
- if (disposed) return;
4658
- if (lifecycle === "established") return;
4659
- electLeaderAndRequest();
4660
- }, JOIN_COLLECTION_WINDOW_MS);
5280
+ function awaitPendingWrites() {
5281
+ if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5282
+ if (isDisposed()) return Promise.resolve();
5283
+ return writer.flush().catch(() => void 0);
4661
5284
  }
4662
- joinFlow();
4663
- return {
4664
- dispose: () => {
4665
- if (disposed) return;
5285
+ function dispose() {
5286
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5287
+ unsubscribeChange();
5288
+ unsubscribeSuccess();
5289
+ if (typeof window !== "undefined") {
5290
+ window.removeEventListener("pagehide", handlePageHide);
5291
+ }
5292
+ inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
4666
5293
  disposed = true;
4667
- if (joinCollectionTimer !== null) {
4668
- clearTimeout(joinCollectionTimer);
4669
- joinCollectionTimer = null;
4670
- }
4671
- if (snapshotTimeoutTimer !== null) {
4672
- clearTimeout(snapshotTimeoutTimer);
4673
- snapshotTimeoutTimer = null;
4674
- }
4675
- unsubscribeChange();
4676
- try {
4677
- channel.close();
4678
- } catch {
4679
- }
4680
- },
4681
- lifecycle: () => lifecycle,
4682
- senderId,
4683
- channelName
5294
+ inFlightFinalFlush = null;
5295
+ });
5296
+ }
5297
+ return {
5298
+ wiredConfig: config,
5299
+ writePathImmediately,
5300
+ clearPersistedDraft,
5301
+ awaitPendingWrites,
5302
+ dispose
4684
5303
  };
4685
5304
  }
4686
- const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
4687
-
4688
- const warned = /* @__PURE__ */ new Set();
4689
- function warnOnceInsecureContext(feature) {
4690
- if (!paths.__DEV__) return;
4691
- if (warned.has(feature)) return;
4692
- warned.add(feature);
4693
- const message = featureMessage(feature);
4694
- console.warn(`[attaform] ${message}`);
5305
+ function isEmptyContainer(value) {
5306
+ if (value === void 0 || value === null) return true;
5307
+ if (Array.isArray(value)) return value.length === 0;
5308
+ if (isPlainRecord(value)) return Object.keys(value).length === 0;
5309
+ return false;
4695
5310
  }
4696
- function featureMessage(feature) {
4697
- switch (feature) {
4698
- case "multiTab":
4699
- return "Multi-tab sync requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is interceptable by network observers, so the sync module is disabled. Serve over HTTPS in production (or develop on `localhost`) to enable cross-tab synchronisation. Use `multiTab: false` on `useForm` to silence this warning.";
4700
- case "persist:local":
4701
- return "Built-in `persist: 'local'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable localStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
4702
- case "persist:session":
4703
- return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5311
+ function collectPersistedLeafPaths(form) {
5312
+ const out = [];
5313
+ walk(form, []);
5314
+ return out;
5315
+ function walk(node, prefix) {
5316
+ if (Array.isArray(node)) {
5317
+ for (let i = 0; i < node.length; i++) {
5318
+ walk(node[i], [...prefix, i]);
5319
+ }
5320
+ return;
5321
+ }
5322
+ if (isPlainRecord(node)) {
5323
+ for (const key of Object.keys(node)) {
5324
+ walk(node[key], [...prefix, key]);
5325
+ }
5326
+ return;
5327
+ }
5328
+ if (prefix.length === 0) return;
5329
+ out.push(paths.canonicalizePath(prefix).key);
4704
5330
  }
4705
5331
  }
4706
- function isSecureContext() {
4707
- return typeof window !== "undefined" && window.isSecureContext === true;
5332
+ function isDescendantPathKey(candidate, ancestor) {
5333
+ if (candidate.length <= ancestor.length) return false;
5334
+ if (!ancestor.endsWith("]")) return false;
5335
+ const childPrefix = `${ancestor.slice(0, -1)},`;
5336
+ return candidate.startsWith(childPrefix);
4708
5337
  }
4709
5338
 
4710
- function resolveTrichotomy(input) {
4711
- if (typeof input === "function") {
4712
- return { kind: "async", factory: input };
4713
- }
4714
- return { kind: "sync", value: input };
5339
+ const PROTOCOL_VERSION = 1;
5340
+ const JOIN_COLLECTION_WINDOW_MS = 50;
5341
+ const SNAPSHOT_TIMEOUT_MS = 200;
5342
+ const MAX_LEADER_ATTEMPTS = 3;
5343
+ const SNAPSHOT_RESPONSE_MIN_INTERVAL_MS = 500;
5344
+ function isFileLikeValue(value) {
5345
+ if (typeof File !== "undefined" && value instanceof File) return true;
5346
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
5347
+ return false;
4715
5348
  }
4716
-
4717
- function useAbstractForm(configuration, options) {
4718
- if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
4719
- throw new paths.InvalidUseFormConfigError();
5349
+ function isInboundShapeAcceptable(schema, path, value) {
5350
+ if (isFileLikeValue(value)) return false;
5351
+ let kind;
5352
+ if (Array.isArray(value)) {
5353
+ kind = "array";
5354
+ } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
5355
+ kind = "object";
5356
+ } else {
5357
+ kind = slimKindOf(value);
4720
5358
  }
4721
- const key = resolveFormKey(configuration.key);
4722
- const instance = vue.getCurrentInstance();
4723
- if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
4724
- const registry = options?.registry ?? paths.useRegistry();
4725
- const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
4726
- const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
4727
- const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
4728
- const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
4729
- const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
4730
- const maxRecursionDepth = normalizeNumericOption({
4731
- value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
4732
- source: "useForm.maxRecursionDepth",
4733
- allowInfinity: true,
4734
- min: 0,
4735
- defaultValue: DEFAULT_MAX_RECURSION_DEPTH
4736
- });
4737
- const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
4738
- if (configuration.persist !== void 0 && configuration.key === void 0) {
4739
- throw new paths.AnonPersistError({
4740
- cause: "no-key",
4741
- schemaFields: extractSchemaFields(resolvedSchema),
4742
- callSite: paths.captureUserCallSite()
4743
- });
4744
- }
4745
- const existing = registry.forms.get(key);
4746
- if (paths.__DEV__ && existing !== void 0) {
4747
- warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
4748
- warnOnPersistDivergence(key, existing, configuration.persist);
4749
- }
4750
- const hadPendingHydration = registry.pendingHydration.has(key);
4751
- const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
4752
- if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
4753
- state.defaultsResolved.value = true;
4754
- }
4755
- if (existing === void 0 && resolvedDefaults.kind === "async") {
4756
- const factory = resolvedDefaults.factory;
4757
- state.defaultValuesFactory.value = factory;
4758
- if (hadPendingHydration) {
4759
- state.hydrating.value = false;
4760
- state.defaultsResolved.value = true;
4761
- } else if (registry.ssr) {
4762
- if (configuration.__ssrAccessed === true) {
4763
- registry.enqueuePrefetch(key);
4764
- }
4765
- vue.onServerPrefetch(() => {
4766
- if (!registry.shouldPrefetch(key)) return;
4767
- return state.activate();
4768
- });
4769
- }
4770
- }
4771
- if (vue.getCurrentScope() !== void 0) {
4772
- const releaseConsumer = registry.trackConsumer(key);
4773
- vue.onScopeDispose(releaseConsumer);
4774
- }
4775
- const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
4776
- if (existing === void 0 && !registry.ssr) {
4777
- if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
4778
- const resolvedPersist = normalizePersistConfig(merged.persist);
4779
- const storageKind = resolvedPersist.storage;
4780
- const isBuiltinStorage = typeof storageKind === "string";
4781
- const secureContextOk = !isBuiltinStorage || isSecureContext();
4782
- if (!secureContextOk) {
4783
- const feature = storageKind === "session" ? "persist:session" : "persist:local";
4784
- warnOnceInsecureContext(feature);
4785
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
4786
- } else {
4787
- const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
4788
- void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
4789
- const persistenceModule = wirePersistence(state, resolvedPersist);
4790
- state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
4791
- state.registerDrain(() => persistenceModule.awaitPendingWrites());
4792
- state.registerCleanup(() => persistenceModule.dispose());
4793
- }
4794
- } else {
4795
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5359
+ const accepted = schema.getSlimPrimitiveTypesAtPath(path);
5360
+ if (!accepted.has(kind)) return false;
5361
+ if (Array.isArray(value)) {
5362
+ for (let i = 0; i < value.length; i++) {
5363
+ if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
4796
5364
  }
5365
+ return true;
4797
5366
  }
4798
- if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
4799
- const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
4800
- const secureContext = isSecureContext();
4801
- if (hasBroadcastChannel && secureContext) {
4802
- let channelName;
4803
- try {
4804
- channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
4805
- } catch {
4806
- channelName = null;
4807
- }
4808
- if (channelName !== null) {
4809
- const syncModule = createMultiTabSyncModule(state, channelName, {
4810
- isSensitivePath: state.isSensitivePath,
4811
- noSyncPaths: state.noSyncPaths,
4812
- validateForm: (form) => {
4813
- const result = state.schema.validateAtPath(form, void 0, { sync: true });
4814
- if (result instanceof Promise) return;
4815
- if (!result.success) {
4816
- throw new Error("attaform multi-tab sync: post-apply schema validation failed");
4817
- }
4818
- }
4819
- });
4820
- state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
4821
- state.registerCleanup(() => syncModule.dispose());
4822
- }
4823
- } else if (hasBroadcastChannel && !secureContext) {
4824
- warnOnceInsecureContext("multiTab");
5367
+ if (isPlainRecord(value)) {
5368
+ for (const key of Object.keys(value)) {
5369
+ if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
4825
5370
  }
5371
+ return true;
4826
5372
  }
4827
- if (existing === void 0 && merged.history !== void 0) {
4828
- const historyModule = createHistoryModule(state, merged.history);
4829
- state.modules.set(HISTORY_MODULE_KEY, historyModule);
4830
- state.registerCleanup(() => historyModule.dispose());
5373
+ return true;
5374
+ }
5375
+ function diffBlankPaths(prev, curr) {
5376
+ const added = [];
5377
+ const removed = [];
5378
+ const prevSet = new Set(prev);
5379
+ for (const k of curr) if (!prevSet.has(k)) added.push(k);
5380
+ for (const k of prev) if (!curr.has(k)) removed.push(k);
5381
+ return { added, removed };
5382
+ }
5383
+ function snapshotForm(form) {
5384
+ return structuralSnapshot(form);
5385
+ }
5386
+ function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
5387
+ if (isFileLikeValue(value)) return void 0;
5388
+ if (value === null || typeof value !== "object") return value;
5389
+ if (Array.isArray(value)) {
5390
+ return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
4831
5391
  }
4832
- if (configuration.key === void 0) {
4833
- recordAmbientProvide(registry.ssr);
4834
- vue.provide(paths.kFormContext, state);
5392
+ const proto = Object.getPrototypeOf(value);
5393
+ if (proto !== Object.prototype && proto !== null) return value;
5394
+ const out = /* @__PURE__ */ Object.create(null);
5395
+ const src = value;
5396
+ for (const key of Object.keys(src)) {
5397
+ const childPath = [...pathSoFar, key];
5398
+ if (isSensitivePath(childPath)) {
5399
+ out[key] = void 0;
5400
+ continue;
5401
+ }
5402
+ out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
4835
5403
  }
4836
- const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
4837
- if (vue.getCurrentInstance() !== null) {
4838
- vue.provide(paths.kFormInstanceId, formInstanceId);
5404
+ return out;
5405
+ }
5406
+ function isStringArray(value) {
5407
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5408
+ }
5409
+ function isPatchArray(value) {
5410
+ return Array.isArray(value) && value.every(
5411
+ (p) => p !== null && typeof p === "object" && Array.isArray(p.path)
5412
+ );
5413
+ }
5414
+ function isValidSyncMessage(data) {
5415
+ if (data === null || typeof data !== "object") return false;
5416
+ const m = data;
5417
+ if (m["v"] !== PROTOCOL_VERSION) return false;
5418
+ if (typeof m["senderId"] !== "string") return false;
5419
+ if (typeof m["kind"] !== "string") return false;
5420
+ switch (m["kind"]) {
5421
+ case "hello":
5422
+ case "announce":
5423
+ return true;
5424
+ case "requestSnapshot":
5425
+ return typeof m["targetId"] === "string";
5426
+ case "snapshot":
5427
+ return isStringArray(m["blankPaths"]) && "form" in m;
5428
+ case "patches":
5429
+ return isPatchArray(m["formPatches"]) && isStringArray(m["blankPathsAdded"]) && isStringArray(m["blankPathsRemoved"]);
5430
+ default:
5431
+ return false;
4839
5432
  }
4840
- const apiOptions = {};
4841
- if (merged.onInvalidSubmit !== void 0) {
4842
- apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5433
+ }
5434
+ function generateSenderId() {
5435
+ try {
5436
+ return globalThis.crypto.randomUUID();
5437
+ } catch {
5438
+ return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
4843
5439
  }
4844
- const history = state.modules.get(HISTORY_MODULE_KEY);
4845
- if (history !== void 0) {
4846
- apiOptions.history = history;
5440
+ }
5441
+ function createMultiTabSyncModule(state, channelName, options) {
5442
+ if (typeof BroadcastChannel === "undefined") {
5443
+ return {
5444
+ dispose: () => void 0,
5445
+ lifecycle: () => "established",
5446
+ senderId: "",
5447
+ channelName
5448
+ };
4847
5449
  }
4848
- if (merged.validateOn !== void 0) {
4849
- apiOptions.validateOn = merged.validateOn;
5450
+ let channel;
5451
+ try {
5452
+ channel = new BroadcastChannel(channelName);
5453
+ } catch {
5454
+ return {
5455
+ dispose: () => void 0,
5456
+ lifecycle: () => "established",
5457
+ senderId: "",
5458
+ channelName
5459
+ };
4850
5460
  }
4851
- const mergedDebounceMs = merged.debounceMs;
4852
- if (mergedDebounceMs !== void 0) {
4853
- apiOptions.debounceMs = mergedDebounceMs;
5461
+ const senderId = generateSenderId();
5462
+ let lifecycle = "joining";
5463
+ let disposed = false;
5464
+ const peerIds = /* @__PURE__ */ new Set();
5465
+ const lastSnapshotResponseAt = /* @__PURE__ */ new Map();
5466
+ let joinCollectionTimer = null;
5467
+ let snapshotTimeoutTimer = null;
5468
+ let leaderAttempts = 0;
5469
+ let prior = {
5470
+ form: snapshotForm(state.form.value),
5471
+ blankPathsSnapshot: [...state.blankPaths]
5472
+ };
5473
+ function safePost(msg) {
5474
+ if (disposed) return;
5475
+ try {
5476
+ channel.postMessage(msg);
5477
+ } catch {
5478
+ }
4854
5479
  }
4855
- if (merged.shouldShowErrors !== void 0) {
4856
- apiOptions.shouldShowErrors = resolveShouldShowErrors(merged.shouldShowErrors);
5480
+ function refreshPrior() {
5481
+ prior = {
5482
+ form: snapshotForm(state.form.value),
5483
+ blankPathsSnapshot: [...state.blankPaths]
5484
+ };
4857
5485
  }
4858
- if (merged.coerce !== void 0) {
4859
- apiOptions.coerce = merged.coerce;
5486
+ function isPathLocallySuppressed(path) {
5487
+ if (options.isSensitivePath(path)) return true;
5488
+ const { key } = paths.canonicalizePath([...path]);
5489
+ if (options.noSyncPaths.has(key)) return true;
5490
+ return false;
4860
5491
  }
4861
- if (merged.rememberVariants !== void 0) {
4862
- apiOptions.rememberVariants = merged.rememberVariants;
5492
+ function postPatches() {
5493
+ if (lifecycle !== "established") return;
5494
+ const next = snapshotForm(state.form.value);
5495
+ const rawPatches = [];
5496
+ diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
5497
+ const safePatches = [];
5498
+ for (const p of rawPatches) {
5499
+ if (isPathLocallySuppressed(p.path)) continue;
5500
+ if ("value" in p && isFileLikeValue(p.value)) continue;
5501
+ safePatches.push(p);
5502
+ }
5503
+ const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
5504
+ if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
5505
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5506
+ return;
5507
+ }
5508
+ safePost({
5509
+ v: PROTOCOL_VERSION,
5510
+ kind: "patches",
5511
+ senderId,
5512
+ formPatches: safePatches,
5513
+ blankPathsAdded: added,
5514
+ blankPathsRemoved: removed
5515
+ });
5516
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4863
5517
  }
4864
- return buildFormApi(
4865
- state,
4866
- formInstanceId,
4867
- apiOptions
4868
- );
4869
- }
4870
- function mergeWithDefaults(defaults, configuration) {
4871
- const strict = configuration.strict ?? defaults.strict;
4872
- const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
4873
- const history = configuration.history ?? defaults.history;
4874
- const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
4875
- const coerce = configuration.coerce ?? defaults.coerce;
4876
- const validateOn = configuration.validateOn ?? defaults.validateOn;
4877
- const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
4878
- const shouldShowErrors = configuration.shouldShowErrors ?? defaults.shouldShowErrors;
4879
- const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
4880
- const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
4881
- const multiTab = configuration.multiTab ?? defaults.multiTab;
4882
- return {
4883
- ...configuration,
4884
- ...strict === void 0 ? {} : { strict },
4885
- ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
4886
- ...history === void 0 ? {} : { history },
4887
- ...rememberVariants === void 0 ? {} : { rememberVariants },
4888
- ...coerce === void 0 ? {} : { coerce },
4889
- ...validateOn === void 0 ? {} : { validateOn },
4890
- ...debounceMs === void 0 ? {} : { debounceMs },
4891
- ...shouldShowErrors === void 0 ? {} : { shouldShowErrors },
4892
- ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
4893
- ...sensitiveNames === void 0 ? {} : { sensitiveNames },
4894
- ...multiTab === void 0 ? {} : { multiTab }
4895
- };
4896
- }
4897
- const HISTORY_MODULE_KEY = "history";
4898
- function buildFreshState(key, schema, configuration, registry) {
4899
- const pending = registry.pendingHydration.get(key);
4900
- if (pending !== void 0) registry.pendingHydration.delete(key);
4901
- const walked = walkUnsetSentinels(
4902
- configuration.defaultValues,
4903
- schema
4904
- );
4905
- let initialBlankPaths;
4906
- if (pending === void 0) {
4907
- initialBlankPaths = walked.paths;
5518
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5519
+ if (disposed) return;
5520
+ if (lifecycle !== "established") return;
5521
+ if (meta?.crossTab === true) return;
5522
+ if (meta?.hydration === true) {
5523
+ refreshPrior();
5524
+ return;
5525
+ }
5526
+ postPatches();
5527
+ });
5528
+ function applyIncomingForm(form, blankPaths) {
5529
+ state.blankPaths.clear();
5530
+ for (const k of blankPaths) state.blankPaths.add(k);
5531
+ state.applyFormReplacement(form, { crossTab: true, persist: false });
5532
+ refreshPrior();
4908
5533
  }
4909
- const resolvedSensitiveNames = configuration.sensitiveNames;
4910
- const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
4911
- const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : paths.createSegmentMatchesSensitive(resolvedSensitiveNames);
4912
- const createOptions = {
4913
- formKey: key,
4914
- schema,
4915
- defaultValues: walked.cleanedValues,
4916
- ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
4917
- hydration: pending,
4918
- ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
4919
- ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
4920
- ssr: registry.ssr,
4921
- // Server-only: bind the SSR prefetch coordination handles. `enqueue`
4922
- // records intent on every `state.activate()` so a wizard skip-list
4923
- // override or a future transform mark has a consistent set to diff
4924
- // against; `shouldFire` lets the activate path bail when the
4925
- // wizard explicitly skipped this key — even an explicit
4926
- // `form.activate()` defers to the wizard's render-efficiency
4927
- // skip-list on the server.
4928
- ...registry.ssr ? {
4929
- ssrPrefetch: {
4930
- enqueue: () => {
4931
- registry.enqueuePrefetch(key);
4932
- },
4933
- shouldFire: () => registry.shouldPrefetch(key)
5534
+ function handlePatches(msg) {
5535
+ if (lifecycle !== "established") return;
5536
+ const safePatches = [];
5537
+ for (const p of msg.formPatches) {
5538
+ if (!Array.isArray(p.path)) continue;
5539
+ if (isPathLocallySuppressed(p.path)) continue;
5540
+ if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
5541
+ continue;
4934
5542
  }
4935
- } : {},
4936
- ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
4937
- ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
4938
- ...configuration.shouldShowErrors !== void 0 ? { shouldShowErrors: configuration.shouldShowErrors } : {},
4939
- ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
4940
- ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {},
4941
- ...resolvedSegmentMatchesSensitive !== void 0 ? { segmentMatchesSensitive: resolvedSegmentMatchesSensitive } : {}
4942
- };
4943
- const state = createFormStore(createOptions);
4944
- registry.forms.set(
4945
- key,
4946
- state
4947
- );
4948
- return state;
4949
- }
4950
- let anonCounter = 0;
4951
- let formInstanceCounter = 0;
4952
- const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
4953
- function recordAmbientProvide(ssr) {
4954
- if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
4955
- const instance = vue.getCurrentInstance();
4956
- if (instance === null) return;
4957
- const instanceKey = instance;
4958
- const entry = {
4959
- source: paths.captureUserCallSite()
4960
- };
4961
- const existing = ambientProvideHistory.get(instanceKey);
4962
- if (existing === void 0) {
4963
- ambientProvideHistory.set(instanceKey, [entry]);
4964
- return;
4965
- }
4966
- existing.push(entry);
4967
- }
4968
- function resolveFormKey(key) {
4969
- if (key !== void 0 && key !== null && key !== "") {
4970
- if (key.startsWith(RESERVED_KEY_PREFIX)) {
4971
- throw new paths.ReservedFormKeyError(key);
5543
+ safePatches.push(p);
4972
5544
  }
4973
- return key;
5545
+ const safeBlankAdded = [];
5546
+ for (const k of msg.blankPathsAdded) {
5547
+ const segs = paths.canonicalizePath(k).segments;
5548
+ if (isPathLocallySuppressed(segs)) continue;
5549
+ safeBlankAdded.push(k);
5550
+ }
5551
+ const safeBlankRemoved = [];
5552
+ for (const k of msg.blankPathsRemoved) {
5553
+ const segs = paths.canonicalizePath(k).segments;
5554
+ if (isPathLocallySuppressed(segs)) continue;
5555
+ safeBlankRemoved.push(k);
5556
+ }
5557
+ if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5558
+ return;
5559
+ }
5560
+ const candidate = applyPatchesForward(state.form.value, safePatches);
5561
+ try {
5562
+ options.validateForm(state.form.value);
5563
+ try {
5564
+ options.validateForm(candidate);
5565
+ } catch {
5566
+ return;
5567
+ }
5568
+ } catch {
5569
+ }
5570
+ const nextBlankPaths = new Set(state.blankPaths);
5571
+ for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
5572
+ for (const k of safeBlankAdded) nextBlankPaths.add(k);
5573
+ applyIncomingForm(candidate, [...nextBlankPaths]);
4974
5574
  }
4975
- if (vue.getCurrentInstance() !== null) {
4976
- return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5575
+ function handleSnapshot(msg) {
5576
+ if (lifecycle !== "joining") return;
5577
+ if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
5578
+ if (snapshotTimeoutTimer !== null) {
5579
+ clearTimeout(snapshotTimeoutTimer);
5580
+ snapshotTimeoutTimer = null;
5581
+ }
5582
+ if (joinCollectionTimer !== null) {
5583
+ clearTimeout(joinCollectionTimer);
5584
+ joinCollectionTimer = null;
5585
+ }
5586
+ applyIncomingForm(msg.form, msg.blankPaths);
5587
+ lifecycle = "established";
5588
+ peerIds.clear();
4977
5589
  }
4978
- return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
4979
- }
4980
- function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
4981
- let existingFp;
4982
- let incomingFp;
4983
- try {
4984
- existingFp = existing.fingerprint();
4985
- incomingFp = incoming.fingerprint();
4986
- } catch (error) {
4987
- console.error(
4988
- `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
4989
- error
4990
- );
4991
- return;
5590
+ function respondToHello() {
5591
+ safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
4992
5592
  }
4993
- if (existingFp === incomingFp) return;
4994
- console.warn(
4995
- `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
4996
- existing: ${existingFp}
4997
- incoming: ${incomingFp}`
4998
- );
4999
- }
5000
- function warnOnPersistDivergence(key, existing, incomingPersist) {
5001
- if (incomingPersist === void 0) return;
5002
- const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
5003
- const incomingNormalized = normalizePersistConfig(incomingPersist);
5004
- if (wired === void 0) {
5005
- console.warn(
5006
- `[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
5007
- );
5008
- return;
5593
+ function respondToSnapshotRequest(requesterId) {
5594
+ const now = Date.now();
5595
+ const last = lastSnapshotResponseAt.get(requesterId);
5596
+ if (last !== void 0 && now - last < SNAPSHOT_RESPONSE_MIN_INTERVAL_MS) return;
5597
+ lastSnapshotResponseAt.set(requesterId, now);
5598
+ const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
5599
+ safePost({
5600
+ v: PROTOCOL_VERSION,
5601
+ kind: "snapshot",
5602
+ senderId,
5603
+ form: scrubbedForm,
5604
+ blankPaths: [...state.blankPaths]
5605
+ });
5009
5606
  }
5010
- if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
5011
- console.warn(
5012
- `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
5013
- wired: ${describePersist(wired.wiredConfig)}
5014
- incoming: ${describePersist(incomingNormalized)}`
5015
- );
5016
- }
5017
- function persistConfigsEquivalent(a, b) {
5018
- if (a.storage !== b.storage) return false;
5019
- if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
5020
- if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
5021
- return true;
5022
- }
5023
- function describePersist(config) {
5024
- const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
5025
- const parts = [`storage=${storage}`];
5026
- if (config.key !== void 0) parts.push(`key=${config.key}`);
5027
- if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
5028
- return `{ ${parts.join(", ")} }`;
5029
- }
5030
- function wirePersistence(state, config) {
5031
- const fingerprint = hashStableString(state.schema.fingerprint());
5032
- const base = resolveStorageKeyBase(config, state.formKey);
5033
- const key = `${base}:${fingerprint}`;
5034
- const debounceMs = normalizeNumericOption({
5035
- value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5036
- source: "useForm.persist.debounceMs",
5037
- allowInfinity: false,
5038
- min: 0,
5039
- defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5040
- });
5041
- const include = config.include ?? "form";
5042
- const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5043
- const adapterPromise = getStorageAdapter(config.storage);
5044
- let disposed = false;
5045
- let inFlightFinalFlush = null;
5046
- let pendingOptedInPaths = null;
5047
- const writer = createDebouncedWriter(async () => {
5048
- const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5049
- pendingOptedInPaths = null;
5050
- const adapter = await adapterPromise;
5607
+ channel.onmessage = (event) => {
5051
5608
  if (disposed) return;
5052
- if (optedInPaths.size === 0) {
5053
- await adapter.removeItem(key);
5054
- return;
5609
+ try {
5610
+ const data = event.data;
5611
+ if (!isValidSyncMessage(data)) return;
5612
+ const msg = data;
5613
+ if (msg.senderId === senderId) return;
5614
+ switch (msg.kind) {
5615
+ case "hello":
5616
+ if (lifecycle !== "established") return;
5617
+ respondToHello();
5618
+ break;
5619
+ case "announce":
5620
+ if (lifecycle === "joining") peerIds.add(msg.senderId);
5621
+ break;
5622
+ case "requestSnapshot":
5623
+ if (lifecycle !== "established") return;
5624
+ if (msg.targetId !== senderId) return;
5625
+ respondToSnapshotRequest(msg.senderId);
5626
+ break;
5627
+ case "snapshot":
5628
+ handleSnapshot(msg);
5629
+ break;
5630
+ case "patches":
5631
+ handlePatches(msg);
5632
+ break;
5633
+ }
5634
+ } catch {
5055
5635
  }
5056
- const rawForm = vue.toRaw(state.form.value);
5057
- const filteredForm = pluckPaths(rawForm, optedInPaths);
5058
- const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5059
- const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5060
- const filteredTransientEmpty = /* @__PURE__ */ new Set();
5061
- for (const tk of state.blankPaths) {
5062
- if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
5063
- }
5064
- const payload = buildPersistedPayload(
5065
- filteredForm,
5066
- include,
5067
- filteredSchemaErrors,
5068
- filteredUserErrors,
5069
- filteredTransientEmpty
5070
- );
5071
- await adapter.setItem(key, payload);
5072
- }, debounceMs);
5073
- const unsubscribeChange = state.onFormChange((_next, meta) => {
5074
- if (disposed || inFlightFinalFlush !== null) return;
5075
- if (meta?.crossTab === true) return;
5076
- if (meta?.persist !== true) return;
5077
- pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5078
- writer.schedule();
5079
- });
5080
- const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5636
+ };
5637
+ function electLeaderAndRequest() {
5081
5638
  if (disposed) return;
5082
- void (async () => {
5083
- await writer.flush();
5084
- if (disposed) return;
5085
- const adapter = await adapterPromise;
5639
+ if (peerIds.size === 0) {
5640
+ lifecycle = "established";
5641
+ refreshPrior();
5642
+ return;
5643
+ }
5644
+ const sorted = [...peerIds].sort();
5645
+ const leaderId = sorted[0];
5646
+ peerIds.delete(leaderId);
5647
+ leaderAttempts++;
5648
+ safePost({
5649
+ v: PROTOCOL_VERSION,
5650
+ kind: "requestSnapshot",
5651
+ senderId,
5652
+ targetId: leaderId
5653
+ });
5654
+ snapshotTimeoutTimer = setTimeout(() => {
5655
+ snapshotTimeoutTimer = null;
5086
5656
  if (disposed) return;
5087
- await adapter.removeItem(key);
5088
- })();
5089
- }) : () => void 0;
5090
- void (async () => {
5091
- const adapter = await adapterPromise;
5092
- if (disposed) return;
5093
- void cleanupOrphanKeys(adapter, base, key);
5094
- try {
5095
- const raw = await adapter.getItem(key);
5096
- const payload = readPersistedPayload(raw);
5097
- if (payload === null) {
5098
- if (raw !== null && raw !== void 0) {
5099
- await adapter.removeItem(key);
5100
- }
5657
+ if (lifecycle === "established") return;
5658
+ if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
5659
+ lifecycle = "established";
5660
+ refreshPrior();
5101
5661
  return;
5102
5662
  }
5663
+ electLeaderAndRequest();
5664
+ }, SNAPSHOT_TIMEOUT_MS);
5665
+ }
5666
+ function joinFlow() {
5667
+ safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
5668
+ joinCollectionTimer = setTimeout(() => {
5669
+ joinCollectionTimer = null;
5103
5670
  if (disposed) return;
5104
- const merged = mergeSparseHydration(
5105
- vue.toRaw(state.form.value),
5106
- payload.data.form,
5107
- state.schema
5108
- );
5109
- state.applyFormReplacement(merged, { hydration: true });
5110
- const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5111
- for (const k of persistedLeafPaths) {
5112
- state.blankPaths.delete(k);
5113
- state.originalBlankPaths.delete(k);
5671
+ if (lifecycle === "established") return;
5672
+ electLeaderAndRequest();
5673
+ }, JOIN_COLLECTION_WINDOW_MS);
5674
+ }
5675
+ joinFlow();
5676
+ return {
5677
+ dispose: () => {
5678
+ if (disposed) return;
5679
+ disposed = true;
5680
+ if (joinCollectionTimer !== null) {
5681
+ clearTimeout(joinCollectionTimer);
5682
+ joinCollectionTimer = null;
5114
5683
  }
5115
- for (const k of payload.data.blankPaths ?? []) {
5116
- const key2 = paths.coerceToPathKey(k);
5117
- state.blankPaths.add(key2);
5118
- state.originalBlankPaths.add(key2);
5684
+ if (snapshotTimeoutTimer !== null) {
5685
+ clearTimeout(snapshotTimeoutTimer);
5686
+ snapshotTimeoutTimer = null;
5119
5687
  }
5120
- if (include === "form+errors") {
5121
- if (payload.data.schemaErrors !== void 0) {
5122
- const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5123
- state.setAllSchemaErrors(flat);
5124
- }
5125
- if (payload.data.userErrors !== void 0) {
5126
- const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5127
- state.setAllUserErrors(flat);
5128
- }
5688
+ unsubscribeChange();
5689
+ try {
5690
+ channel.close();
5691
+ } catch {
5129
5692
  }
5130
- state.scheduleFieldValidation(
5131
- [],
5132
- true
5133
- /* immediate */
5134
- );
5135
- } catch {
5136
- }
5137
- })();
5138
- async function writePathImmediately(path) {
5139
- if (disposed) return;
5140
- await writer.flush();
5141
- if (disposed) return;
5142
- const adapter = await adapterPromise;
5143
- if (disposed) return;
5144
- const raw = await adapter.getItem(key);
5145
- const existing = readPersistedPayload(raw);
5146
- const baseForm = existing?.data.form ?? {};
5147
- const value = getAtPath(vue.toRaw(state.form.value), path);
5148
- const nextForm = setAtPath(baseForm, path, value);
5149
- const { key: pathKey } = paths.canonicalizePath(path);
5150
- const transientSet = new Set(
5151
- (existing?.data.blankPaths ?? []).filter(
5152
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5153
- )
5154
- );
5155
- for (const liveKey of state.blankPaths) {
5156
- if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5157
- transientSet.add(liveKey);
5693
+ },
5694
+ lifecycle: () => lifecycle,
5695
+ senderId,
5696
+ channelName
5697
+ };
5698
+ }
5699
+ const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5700
+
5701
+ const warned = /* @__PURE__ */ new Set();
5702
+ function warnOnceInsecureContext(feature) {
5703
+ if (!paths.__DEV__) return;
5704
+ if (warned.has(feature)) return;
5705
+ warned.add(feature);
5706
+ const message = featureMessage(feature);
5707
+ console.warn(`[attaform] ${message}`);
5708
+ }
5709
+ function featureMessage(feature) {
5710
+ switch (feature) {
5711
+ case "multiTab":
5712
+ return "Multi-tab sync requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is interceptable by network observers, so the sync module is disabled. Serve over HTTPS in production (or develop on `localhost`) to enable cross-tab synchronisation. Use `multiTab: false` on `useForm` to silence this warning.";
5713
+ case "persist:local":
5714
+ return "Built-in `persist: 'local'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable localStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5715
+ case "persist:session":
5716
+ return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5717
+ }
5718
+ }
5719
+ function isSecureContext() {
5720
+ return typeof window !== "undefined" && window.isSecureContext === true;
5721
+ }
5722
+
5723
+ function resolveTrichotomy(input) {
5724
+ if (typeof input === "function") {
5725
+ return { kind: "async", factory: input };
5726
+ }
5727
+ return { kind: "sync", value: input };
5728
+ }
5729
+
5730
+ function useAbstractForm(configuration, options) {
5731
+ if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5732
+ throw new paths.InvalidUseFormConfigError();
5733
+ }
5734
+ const key = resolveFormKey(configuration.key);
5735
+ const instance = vue.getCurrentInstance();
5736
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
5737
+ const registry = options?.registry ?? paths.useRegistry();
5738
+ const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
5739
+ const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
5740
+ const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
5741
+ const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
5742
+ const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
5743
+ const maxRecursionDepth = normalizeNumericOption({
5744
+ value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
5745
+ source: "useForm.maxRecursionDepth",
5746
+ allowInfinity: true,
5747
+ min: 0,
5748
+ defaultValue: DEFAULT_MAX_RECURSION_DEPTH
5749
+ });
5750
+ const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
5751
+ if (configuration.persist !== void 0 && configuration.key === void 0) {
5752
+ throw new paths.AnonPersistError({
5753
+ cause: "no-key",
5754
+ schemaFields: extractSchemaFields(resolvedSchema),
5755
+ callSite: paths.captureUserCallSite()
5756
+ });
5757
+ }
5758
+ const existing = registry.forms.get(key);
5759
+ if (paths.__DEV__ && existing !== void 0) {
5760
+ warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
5761
+ warnOnPersistDivergence(key, existing, configuration.persist);
5762
+ }
5763
+ const hadPendingHydration = registry.pendingHydration.has(key);
5764
+ const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
5765
+ if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
5766
+ state.defaultsResolved.value = true;
5767
+ }
5768
+ if (existing === void 0 && resolvedDefaults.kind === "async") {
5769
+ const factory = resolvedDefaults.factory;
5770
+ state.defaultValuesFactory.value = factory;
5771
+ if (hadPendingHydration) {
5772
+ state.hydrating.value = false;
5773
+ state.defaultsResolved.value = true;
5774
+ } else if (registry.ssr) {
5775
+ if (configuration.__ssrAccessed === true) {
5776
+ registry.enqueuePrefetch(key);
5158
5777
  }
5778
+ vue.onServerPrefetch(() => {
5779
+ if (!registry.shouldPrefetch(key)) return;
5780
+ return state.activate();
5781
+ });
5159
5782
  }
5160
- if (include === "form") {
5161
- await adapter.setItem(
5162
- key,
5163
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5164
- );
5165
- return;
5166
- }
5167
- const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5168
- const userMap = new Map(existing?.data.userErrors ?? []);
5169
- const currentSchema = state.schemaErrors.get(pathKey);
5170
- const currentUser = state.userErrors.get(pathKey);
5171
- if (currentSchema !== void 0 && currentSchema.length > 0) {
5172
- schemaMap.set(pathKey, [...currentSchema]);
5173
- } else {
5174
- schemaMap.delete(pathKey);
5175
- }
5176
- if (currentUser !== void 0 && currentUser.length > 0) {
5177
- userMap.set(pathKey, [...currentUser]);
5783
+ }
5784
+ if (vue.getCurrentScope() !== void 0) {
5785
+ const releaseConsumer = registry.trackConsumer(key);
5786
+ vue.onScopeDispose(releaseConsumer);
5787
+ }
5788
+ const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
5789
+ if (existing === void 0 && !registry.ssr) {
5790
+ if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
5791
+ const resolvedPersist = normalizePersistConfig(merged.persist);
5792
+ const storageKind = resolvedPersist.storage;
5793
+ const isBuiltinStorage = typeof storageKind === "string";
5794
+ const secureContextOk = !isBuiltinStorage || isSecureContext();
5795
+ if (!secureContextOk) {
5796
+ const feature = storageKind === "session" ? "persist:session" : "persist:local";
5797
+ warnOnceInsecureContext(feature);
5798
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5799
+ } else {
5800
+ const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
5801
+ void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
5802
+ const persistenceModule = wirePersistence(state, resolvedPersist);
5803
+ state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
5804
+ state.registerDrain(() => persistenceModule.awaitPendingWrites());
5805
+ state.registerCleanup(() => persistenceModule.dispose());
5806
+ }
5178
5807
  } else {
5179
- userMap.delete(pathKey);
5808
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5180
5809
  }
5181
- await adapter.setItem(
5182
- key,
5183
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5184
- );
5185
5810
  }
5186
- async function clearPersistedDraft(path) {
5187
- if (disposed) return;
5188
- await writer.flush();
5189
- if (disposed) return;
5190
- const adapter = await adapterPromise;
5191
- if (disposed) return;
5192
- if (path === void 0) {
5193
- await adapter.removeItem(key);
5194
- return;
5195
- }
5196
- const raw = await adapter.getItem(key);
5197
- const existing = readPersistedPayload(raw);
5198
- if (existing === null) return;
5199
- const nextForm = deleteAtPath(existing.data.form, path);
5200
- if (isEmptyContainer(nextForm)) {
5201
- await adapter.removeItem(key);
5202
- return;
5811
+ if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
5812
+ const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
5813
+ const secureContext = isSecureContext();
5814
+ if (hasBroadcastChannel && secureContext) {
5815
+ let channelName;
5816
+ try {
5817
+ channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5818
+ } catch {
5819
+ channelName = null;
5820
+ }
5821
+ if (channelName !== null) {
5822
+ const syncModule = createMultiTabSyncModule(state, channelName, {
5823
+ isSensitivePath: state.isSensitivePath,
5824
+ noSyncPaths: state.noSyncPaths,
5825
+ validateForm: (form) => {
5826
+ const result = state.schema.validateAtPath(form, void 0, { sync: true });
5827
+ if (result instanceof Promise) return;
5828
+ if (!result.success) {
5829
+ throw new Error("attaform multi-tab sync: post-apply schema validation failed");
5830
+ }
5831
+ }
5832
+ });
5833
+ state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
5834
+ state.registerCleanup(() => syncModule.dispose());
5835
+ }
5836
+ } else if (hasBroadcastChannel && !secureContext) {
5837
+ warnOnceInsecureContext("multiTab");
5203
5838
  }
5204
- const { key: pathKey } = paths.canonicalizePath(path);
5205
- const transientSet = new Set(
5206
- (existing.data.blankPaths ?? []).filter(
5207
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5208
- )
5209
- );
5210
- if (include === "form") {
5211
- await adapter.setItem(
5212
- key,
5213
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5214
- );
5215
- return;
5839
+ }
5840
+ if (existing === void 0 && merged.history !== void 0) {
5841
+ const historyModule = createHistoryModule(state, merged.history);
5842
+ state.modules.set(HISTORY_MODULE_KEY, historyModule);
5843
+ state.registerCleanup(() => historyModule.dispose());
5844
+ }
5845
+ if (configuration.key === void 0) {
5846
+ recordAmbientProvide(registry.ssr);
5847
+ vue.provide(paths.kFormContext, state);
5848
+ }
5849
+ const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
5850
+ if (vue.getCurrentInstance() !== null) {
5851
+ vue.provide(paths.kFormInstanceId, formInstanceId);
5852
+ }
5853
+ const apiOptions = {};
5854
+ if (merged.onInvalidSubmit !== void 0) {
5855
+ apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5856
+ }
5857
+ const history = state.modules.get(HISTORY_MODULE_KEY);
5858
+ if (history !== void 0) {
5859
+ apiOptions.history = history;
5860
+ }
5861
+ if (merged.validateOn !== void 0) {
5862
+ apiOptions.validateOn = merged.validateOn;
5863
+ }
5864
+ const mergedDebounceMs = merged.debounceMs;
5865
+ if (mergedDebounceMs !== void 0) {
5866
+ apiOptions.debounceMs = mergedDebounceMs;
5867
+ }
5868
+ if (merged.getDisplayState !== void 0) {
5869
+ apiOptions.getDisplayState = merged.getDisplayState;
5870
+ }
5871
+ if (merged.coerce !== void 0) {
5872
+ apiOptions.coerce = merged.coerce;
5873
+ }
5874
+ if (merged.rememberVariants !== void 0) {
5875
+ apiOptions.rememberVariants = merged.rememberVariants;
5876
+ }
5877
+ if (merged.autoAria !== void 0) {
5878
+ apiOptions.autoAria = merged.autoAria;
5879
+ }
5880
+ return buildFormApi(
5881
+ state,
5882
+ formInstanceId,
5883
+ apiOptions
5884
+ );
5885
+ }
5886
+ function mergeWithDefaults(defaults, configuration) {
5887
+ const strict = configuration.strict ?? defaults.strict;
5888
+ const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
5889
+ const history = configuration.history ?? defaults.history;
5890
+ const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
5891
+ const coerce = configuration.coerce ?? defaults.coerce;
5892
+ const validateOn = configuration.validateOn ?? defaults.validateOn;
5893
+ const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
5894
+ const getDisplayState = configuration.getDisplayState ?? defaults.getDisplayState;
5895
+ const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
5896
+ const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
5897
+ const multiTab = configuration.multiTab ?? defaults.multiTab;
5898
+ const autoAria = configuration.autoAria ?? defaults.autoAria;
5899
+ return {
5900
+ ...configuration,
5901
+ ...strict === void 0 ? {} : { strict },
5902
+ ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
5903
+ ...history === void 0 ? {} : { history },
5904
+ ...rememberVariants === void 0 ? {} : { rememberVariants },
5905
+ ...coerce === void 0 ? {} : { coerce },
5906
+ ...validateOn === void 0 ? {} : { validateOn },
5907
+ ...debounceMs === void 0 ? {} : { debounceMs },
5908
+ ...getDisplayState === void 0 ? {} : { getDisplayState },
5909
+ ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
5910
+ ...sensitiveNames === void 0 ? {} : { sensitiveNames },
5911
+ ...multiTab === void 0 ? {} : { multiTab },
5912
+ ...autoAria === void 0 ? {} : { autoAria }
5913
+ };
5914
+ }
5915
+ const HISTORY_MODULE_KEY = "history";
5916
+ function buildFreshState(key, schema, configuration, registry) {
5917
+ const pending = registry.pendingHydration.get(key);
5918
+ if (pending !== void 0) registry.pendingHydration.delete(key);
5919
+ const walked = walkUnsetSentinels(
5920
+ configuration.defaultValues,
5921
+ schema
5922
+ );
5923
+ let initialBlankPaths;
5924
+ if (pending === void 0) {
5925
+ initialBlankPaths = walked.paths;
5926
+ }
5927
+ const resolvedSensitiveNames = configuration.sensitiveNames;
5928
+ const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
5929
+ const createOptions = {
5930
+ formKey: key,
5931
+ schema,
5932
+ defaultValues: walked.cleanedValues,
5933
+ ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
5934
+ hydration: pending,
5935
+ ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
5936
+ ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
5937
+ ssr: registry.ssr,
5938
+ // Server-only: bind the SSR prefetch coordination handles. `enqueue`
5939
+ // records intent on every `state.activate()` so a wizard skip-list
5940
+ // override or a future transform mark has a consistent set to diff
5941
+ // against; `shouldFire` lets the activate path bail when the
5942
+ // wizard explicitly skipped this key — even an explicit
5943
+ // `form.activate()` defers to the wizard's render-efficiency
5944
+ // skip-list on the server.
5945
+ ...registry.ssr ? {
5946
+ ssrPrefetch: {
5947
+ enqueue: () => {
5948
+ registry.enqueuePrefetch(key);
5949
+ },
5950
+ shouldFire: () => registry.shouldPrefetch(key)
5951
+ }
5952
+ } : {},
5953
+ ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
5954
+ ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
5955
+ ...configuration.getDisplayState !== void 0 ? { getDisplayState: configuration.getDisplayState } : {},
5956
+ ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
5957
+ ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {}
5958
+ };
5959
+ const state = createFormStore(createOptions);
5960
+ registry.forms.set(
5961
+ key,
5962
+ state
5963
+ );
5964
+ return state;
5965
+ }
5966
+ let anonCounter = 0;
5967
+ let formInstanceCounter = 0;
5968
+ const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
5969
+ function recordAmbientProvide(ssr) {
5970
+ if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
5971
+ const instance = vue.getCurrentInstance();
5972
+ if (instance === null) return;
5973
+ const instanceKey = instance;
5974
+ const entry = {
5975
+ source: paths.captureUserCallSite()
5976
+ };
5977
+ const existing = ambientProvideHistory.get(instanceKey);
5978
+ if (existing === void 0) {
5979
+ ambientProvideHistory.set(instanceKey, [entry]);
5980
+ return;
5981
+ }
5982
+ existing.push(entry);
5983
+ }
5984
+ function resolveFormKey(key) {
5985
+ if (key !== void 0 && key !== null && key !== "") {
5986
+ if (key.startsWith(RESERVED_KEY_PREFIX)) {
5987
+ throw new paths.ReservedFormKeyError(key);
5216
5988
  }
5217
- const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5218
- const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5219
- const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5220
- const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5221
- await adapter.setItem(
5222
- key,
5223
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5224
- );
5989
+ return key;
5225
5990
  }
5226
- function awaitPendingWrites() {
5227
- if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5228
- if (disposed) return Promise.resolve();
5229
- return writer.flush().catch(() => void 0);
5991
+ if (vue.getCurrentInstance() !== null) {
5992
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5230
5993
  }
5231
- function dispose() {
5232
- if (disposed || inFlightFinalFlush !== null) return;
5233
- unsubscribeChange();
5234
- unsubscribeSuccess();
5235
- inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
5236
- disposed = true;
5237
- inFlightFinalFlush = null;
5238
- });
5994
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
5995
+ }
5996
+ function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
5997
+ let existingFp;
5998
+ let incomingFp;
5999
+ try {
6000
+ existingFp = existing.fingerprint();
6001
+ incomingFp = incoming.fingerprint();
6002
+ } catch (error) {
6003
+ console.error(
6004
+ `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
6005
+ error
6006
+ );
6007
+ return;
5239
6008
  }
5240
- return {
5241
- wiredConfig: config,
5242
- writePathImmediately,
5243
- clearPersistedDraft,
5244
- awaitPendingWrites,
5245
- dispose
5246
- };
6009
+ if (existingFp === incomingFp) return;
6010
+ console.warn(
6011
+ `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
6012
+ existing: ${existingFp}
6013
+ incoming: ${incomingFp}`
6014
+ );
5247
6015
  }
5248
- function isEmptyContainer(value) {
5249
- if (value === void 0 || value === null) return true;
5250
- if (Array.isArray(value)) return value.length === 0;
5251
- if (isPlainRecord(value)) return Object.keys(value).length === 0;
5252
- return false;
6016
+ function warnOnPersistDivergence(key, existing, incomingPersist) {
6017
+ if (incomingPersist === void 0) return;
6018
+ const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
6019
+ const incomingNormalized = normalizePersistConfig(incomingPersist);
6020
+ if (wired === void 0) {
6021
+ console.warn(
6022
+ `[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
6023
+ );
6024
+ return;
6025
+ }
6026
+ if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
6027
+ console.warn(
6028
+ `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
6029
+ wired: ${describePersist(wired.wiredConfig)}
6030
+ incoming: ${describePersist(incomingNormalized)}`
6031
+ );
6032
+ }
6033
+ function persistConfigsEquivalent(a, b) {
6034
+ if (a.storage !== b.storage) return false;
6035
+ if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
6036
+ if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
6037
+ return true;
6038
+ }
6039
+ function describePersist(config) {
6040
+ const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
6041
+ const parts = [`storage=${storage}`];
6042
+ if (config.key !== void 0) parts.push(`key=${config.key}`);
6043
+ if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
6044
+ return `{ ${parts.join(", ")} }`;
5253
6045
  }
5254
6046
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
5255
6047
  function enforceAnonPersistRule(formKey, ssr) {
@@ -5267,33 +6059,6 @@ function enforceAnonPersistRule(formKey, ssr) {
5267
6059
  }
5268
6060
  return true;
5269
6061
  }
5270
- function collectPersistedLeafPaths(form) {
5271
- const out = [];
5272
- walk(form, []);
5273
- return out;
5274
- function walk(node, prefix) {
5275
- if (Array.isArray(node)) {
5276
- for (let i = 0; i < node.length; i++) {
5277
- walk(node[i], [...prefix, i]);
5278
- }
5279
- return;
5280
- }
5281
- if (isPlainRecord(node)) {
5282
- for (const key of Object.keys(node)) {
5283
- walk(node[key], [...prefix, key]);
5284
- }
5285
- return;
5286
- }
5287
- if (prefix.length === 0) return;
5288
- out.push(paths.canonicalizePath(prefix).key);
5289
- }
5290
- }
5291
- function isDescendantPathKey(candidate, ancestor) {
5292
- if (candidate.length <= ancestor.length) return false;
5293
- if (!ancestor.endsWith("]")) return false;
5294
- const childPrefix = `${ancestor.slice(0, -1)},`;
5295
- return candidate.startsWith(childPrefix);
5296
- }
5297
6062
 
5298
6063
  let injectedInstanceCounter = 0;
5299
6064
  function injectForm(input) {
@@ -5373,8 +6138,6 @@ function isLazyMarker(value) {
5373
6138
  }
5374
6139
 
5375
6140
  const NOOP_WIZARD_HISTORY = {
5376
- push() {
5377
- },
5378
6141
  replace() {
5379
6142
  },
5380
6143
  read() {
@@ -5401,21 +6164,16 @@ function createWizardHistory(param) {
5401
6164
  for (const subscriber of subscribers) subscriber(value);
5402
6165
  }
5403
6166
  window.addEventListener("popstate", handlePopstate);
5404
- function safeWriteState(key, op) {
6167
+ function safeReplaceState(key) {
5405
6168
  try {
5406
- const fn = op === "push" ? window.history.pushState : window.history.replaceState;
5407
- fn.call(window.history, {}, "", buildUrl(key));
6169
+ window.history.replaceState({}, "", buildUrl(key));
5408
6170
  } catch {
5409
6171
  }
5410
6172
  }
5411
6173
  return {
5412
- push(key) {
5413
- if (disposed) return;
5414
- safeWriteState(key, "push");
5415
- },
5416
6174
  replace(key) {
5417
6175
  if (disposed) return;
5418
- safeWriteState(key, "replace");
6176
+ safeReplaceState(key);
5419
6177
  },
5420
6178
  read() {
5421
6179
  const url = new URL(window.location.href);
@@ -5474,68 +6232,16 @@ function buildWizardStatusesProxy(statuses) {
5474
6232
  }
5475
6233
  return result;
5476
6234
  });
5477
- const target = (() => {
5478
- });
5479
- const proxyToString = () => JSON.stringify(snapshot.value);
5480
- const proxyToPrimitive = (hint) => hint === "number" ? NaN : proxyToString();
5481
- return new Proxy(target, {
5482
- apply(_, __, args) {
5483
- const key = args[0];
5484
- if (key === void 0) return snapshot.value;
5485
- const computedEntry = statuses[key];
5486
- if (computedEntry === void 0) return void 0;
5487
- return computedEntry.value;
5488
- },
5489
- get(_, key) {
5490
- if (typeof key === "symbol") {
5491
- if (key === Symbol.toPrimitive) return proxyToPrimitive;
5492
- return Reflect.get(target, key);
5493
- }
5494
- if (key === "toJSON") return () => snapshot.value;
5495
- if (key === "toString") return proxyToString;
5496
- if (key === "valueOf")
5497
- return function() {
5498
- return this;
5499
- };
5500
- const computedEntry = statuses[key];
5501
- if (computedEntry === void 0) return void 0;
5502
- return computedEntry.value;
5503
- },
5504
- has(_, key) {
5505
- if (typeof key === "symbol") return Reflect.has(target, key);
5506
- return Object.hasOwn(statuses, key);
5507
- },
5508
- ownKeys() {
5509
- return Object.keys(statuses);
5510
- },
5511
- getOwnPropertyDescriptor(_, key) {
5512
- if (typeof key === "symbol") return void 0;
5513
- const computedEntry = statuses[key];
5514
- if (computedEntry === void 0) return void 0;
5515
- return {
5516
- configurable: true,
5517
- enumerable: true,
5518
- writable: false,
5519
- value: computedEntry.value
5520
- };
5521
- },
5522
- set(_, key) {
5523
- if (paths.__DEV__) {
5524
- console.warn(
5525
- `[attaform] wizard.statuses is read-only \u2014 write to "${String(key)}" was ignored. Statuses derive from each form's meta; mutate the underlying form instead.`
5526
- );
5527
- }
5528
- return true;
5529
- },
5530
- deleteProperty(_, key) {
5531
- if (paths.__DEV__) {
5532
- console.warn(
5533
- `[attaform] wizard.statuses is read-only \u2014 delete of "${String(key)}" was ignored.`
5534
- );
5535
- }
5536
- return true;
5537
- },
5538
- defineProperty: () => true
6235
+ return buildCallableReadonlySnapshotProxy({
6236
+ surface: "wizard.statuses",
6237
+ snapshot: () => snapshot.value,
6238
+ resolveKey: (key) => statuses[key]?.value,
6239
+ // Single-key callable form. Strings stringify naturally; non-
6240
+ // string args coerce via `String(arg)` and miss the lookup, which
6241
+ // resolves to `undefined` (consistent with property-access).
6242
+ resolveCall: (arg) => statuses[String(arg)]?.value,
6243
+ ownKeys: () => Object.keys(statuses),
6244
+ hasKey: (key) => Object.hasOwn(statuses, key)
5539
6245
  });
5540
6246
  }
5541
6247
 
@@ -5552,6 +6258,12 @@ const NOOP_VALID_STATUS = {
5552
6258
  submitted: false,
5553
6259
  errorCount: 0
5554
6260
  };
6261
+ function asStatusSource(form) {
6262
+ return form;
6263
+ }
6264
+ function asSubmissionSource(form) {
6265
+ return form;
6266
+ }
5555
6267
  function useWizard(options) {
5556
6268
  const rawSteps = Array.isArray(options.steps) ? options.steps : [];
5557
6269
  if (rawSteps.length === 0 && paths.__DEV__) {
@@ -5639,10 +6351,12 @@ function useWizard(options) {
5639
6351
  return { configurable: true, enumerable: true, writable: false, value: form };
5640
6352
  }
5641
6353
  });
5642
- const slotCtx = vue.computed(() => ({
6354
+ const slotCtx = {
5643
6355
  forms: slotForms,
5644
- currentKey: activeKey.value === "" ? void 0 : activeKey.value
5645
- }));
6356
+ get currentKey() {
6357
+ return activeKey.value === "" ? void 0 : activeKey.value;
6358
+ }
6359
+ };
5646
6360
  function resolveSlot(slot, index, ctx) {
5647
6361
  if (typeof slot === "string") {
5648
6362
  return getOrBuildNoop(slot);
@@ -5665,12 +6379,6 @@ function useWizard(options) {
5665
6379
  }
5666
6380
  return result;
5667
6381
  }
5668
- const lazyCtx = {
5669
- forms: slotForms,
5670
- get currentKey() {
5671
- return activeKey.value === "" ? void 0 : activeKey.value;
5672
- }
5673
- };
5674
6382
  for (let i = 0; i < rawSteps.length; i++) {
5675
6383
  const slot = rawSteps[i];
5676
6384
  if (isLazyMarker(slot)) {
@@ -5680,18 +6388,17 @@ function useWizard(options) {
5680
6388
  idx,
5681
6389
  vue.computed(() => {
5682
6390
  void lazyEpoch.value;
5683
- return marker.resolve(lazyCtx);
6391
+ return marker.resolve(slotCtx);
5684
6392
  })
5685
6393
  );
5686
6394
  }
5687
6395
  }
5688
6396
  const compiledSteps = vue.computed(() => {
5689
- const ctx = slotCtx.value;
5690
6397
  const out = [];
5691
6398
  const seen = /* @__PURE__ */ new Set();
5692
6399
  for (let i = 0; i < rawSteps.length; i++) {
5693
6400
  const slot = rawSteps[i];
5694
- const form = resolveSlot(slot, i, ctx);
6401
+ const form = resolveSlot(slot, i, slotCtx);
5695
6402
  if (form === void 0) continue;
5696
6403
  if (seen.has(form.key)) {
5697
6404
  if (paths.__DEV__) {
@@ -5717,9 +6424,12 @@ function useWizard(options) {
5717
6424
  return -1;
5718
6425
  });
5719
6426
  const currentStep = vue.computed(() => {
5720
- const key = activeKey.value;
5721
- if (key !== "") return key;
5722
- const first = compiledSteps.value[0];
6427
+ const list = compiledSteps.value;
6428
+ const idx = activeIndex.value;
6429
+ if (idx >= 0 && idx < list.length) {
6430
+ return list[idx].key;
6431
+ }
6432
+ const first = list[0];
5723
6433
  return first === void 0 ? void 0 : first.key;
5724
6434
  });
5725
6435
  const activeForm = vue.computed(() => {
@@ -5742,36 +6452,94 @@ function useWizard(options) {
5742
6452
  for (const step of compiledSteps.value) out[step.key] = step.form;
5743
6453
  return out;
5744
6454
  });
5745
- const allValues = vue.computed(() => {
5746
- const out = {};
5747
- for (const step of compiledSteps.value) {
5748
- const source = step.form;
5749
- out[step.key] = source.values;
6455
+ function isFormReady(key) {
6456
+ const store = registry.forms.get(key);
6457
+ return store?.defaultsResolved.value === true;
6458
+ }
6459
+ function toWizardAggregateError(err, fallbackKey) {
6460
+ const entry = {
6461
+ formKey: err.formKey ?? fallbackKey,
6462
+ path: err.path,
6463
+ message: err.message
6464
+ };
6465
+ if (err.code !== void 0) entry.code = err.code;
6466
+ return entry;
6467
+ }
6468
+ const valuesCache = /* @__PURE__ */ new Map();
6469
+ function valuesFor(form) {
6470
+ const cached = valuesCache.get(form.key);
6471
+ if (cached !== void 0) return cached;
6472
+ const source = asStatusSource(form);
6473
+ const computedValues = vue.computed(() => source.values);
6474
+ valuesCache.set(form.key, computedValues);
6475
+ return computedValues;
6476
+ }
6477
+ const errorsCache = /* @__PURE__ */ new Map();
6478
+ function errorsFor(form) {
6479
+ const cached = errorsCache.get(form.key);
6480
+ if (cached !== void 0) return cached;
6481
+ const source = asStatusSource(form);
6482
+ const computedErrors = vue.computed(() => {
6483
+ if (!isFormReady(form.key)) return [];
6484
+ const errors = source.meta?.errors ?? [];
6485
+ const list = [];
6486
+ for (const err of errors) list.push(toWizardAggregateError(err, form.key));
6487
+ return list;
6488
+ });
6489
+ errorsCache.set(form.key, computedErrors);
6490
+ return computedErrors;
6491
+ }
6492
+ const allValues = new Proxy({}, {
6493
+ get(_, key) {
6494
+ if (typeof key !== "string") return void 0;
6495
+ const form = formsRecord.value[key];
6496
+ if (form === void 0) return void 0;
6497
+ return valuesFor(form).value;
6498
+ },
6499
+ has(_, key) {
6500
+ if (typeof key !== "string") return false;
6501
+ return formsRecord.value[key] !== void 0;
6502
+ },
6503
+ ownKeys() {
6504
+ return Object.keys(formsRecord.value);
6505
+ },
6506
+ getOwnPropertyDescriptor(_, key) {
6507
+ if (typeof key !== "string") return void 0;
6508
+ const form = formsRecord.value[key];
6509
+ if (form === void 0) return void 0;
6510
+ return {
6511
+ configurable: true,
6512
+ enumerable: true,
6513
+ writable: false,
6514
+ value: valuesFor(form).value
6515
+ };
5750
6516
  }
5751
- return out;
5752
6517
  });
5753
- const allErrors = vue.computed(() => {
5754
- const out = {};
5755
- for (const step of compiledSteps.value) {
5756
- const source = step.form;
5757
- const list = [];
5758
- const store = registry.forms.get(step.key);
5759
- const resolved = store?.defaultsResolved.value === true;
5760
- if (resolved) {
5761
- const errors = source.meta?.errors ?? [];
5762
- for (const err of errors) {
5763
- const entry = {
5764
- formKey: step.key,
5765
- path: err.path,
5766
- message: err.message
5767
- };
5768
- if (err.code !== void 0) entry.code = err.code;
5769
- list.push(entry);
5770
- }
5771
- }
5772
- out[step.key] = list;
6518
+ const allErrors = new Proxy({}, {
6519
+ get(_, key) {
6520
+ if (typeof key !== "string") return void 0;
6521
+ const form = formsRecord.value[key];
6522
+ if (form === void 0) return void 0;
6523
+ return errorsFor(form).value;
6524
+ },
6525
+ has(_, key) {
6526
+ if (typeof key !== "string") return false;
6527
+ return formsRecord.value[key] !== void 0;
6528
+ },
6529
+ ownKeys() {
6530
+ return Object.keys(formsRecord.value);
6531
+ },
6532
+ getOwnPropertyDescriptor(_, key) {
6533
+ if (typeof key !== "string") return void 0;
6534
+ const form = formsRecord.value[key];
6535
+ if (form === void 0) return void 0;
6536
+ return {
6537
+ configurable: true,
6538
+ enumerable: true,
6539
+ writable: false,
6540
+ value: errorsFor(form).value
6541
+ };
5773
6542
  }
5774
- return out;
5775
6543
  });
5776
6544
  const seedRef = vue.ref(void 0);
5777
6545
  const seedInput = options.defaultStatuses;
@@ -5794,11 +6562,9 @@ function useWizard(options) {
5794
6562
  function statusFor(form) {
5795
6563
  const cached = statusCache.get(form.key);
5796
6564
  if (cached !== void 0) return cached;
5797
- const source = form;
6565
+ const source = asStatusSource(form);
5798
6566
  const computedStatus = vue.computed(() => {
5799
- const store = registry.forms.get(form.key);
5800
- const resolved = store?.defaultsResolved.value === true;
5801
- if (resolved) {
6567
+ if (isFormReady(form.key)) {
5802
6568
  const meta = source.meta;
5803
6569
  if (meta !== void 0 && meta !== null) {
5804
6570
  return {
@@ -5941,7 +6707,7 @@ function useWizard(options) {
5941
6707
  }
5942
6708
  if (!registry.ssr) {
5943
6709
  for (const step of compiledSteps.value) {
5944
- const source = step.form;
6710
+ const source = asSubmissionSource(step.form);
5945
6711
  if (typeof source.activate === "function") void source.activate();
5946
6712
  }
5947
6713
  }
@@ -5985,7 +6751,7 @@ function useWizard(options) {
5985
6751
  const submissionAttempts = vue.ref(0);
5986
6752
  const done = vue.ref(false);
5987
6753
  function activateForm(form) {
5988
- const source = form;
6754
+ const source = asSubmissionSource(form);
5989
6755
  if (typeof source.activate === "function") {
5990
6756
  void source.activate();
5991
6757
  }
@@ -6085,7 +6851,7 @@ function useWizard(options) {
6085
6851
  };
6086
6852
  }
6087
6853
  async function processOne(form) {
6088
- const full = form;
6854
+ const full = asSubmissionSource(form);
6089
6855
  let activationFailure;
6090
6856
  try {
6091
6857
  if (typeof full.activate === "function") await full.activate();
@@ -6117,15 +6883,7 @@ function useWizard(options) {
6117
6883
  for (const step of compiledSteps.value) {
6118
6884
  const processed = results.get(step.key);
6119
6885
  if (processed === void 0 || processed.success === true) continue;
6120
- for (const err of processed.errors) {
6121
- const entry = {
6122
- formKey: err.formKey,
6123
- path: err.path,
6124
- message: err.message
6125
- };
6126
- if (err.code !== void 0) entry.code = err.code;
6127
- out.push(entry);
6128
- }
6886
+ for (const err of processed.errors) out.push(toWizardAggregateError(err, step.key));
6129
6887
  }
6130
6888
  return out;
6131
6889
  }
@@ -6179,8 +6937,7 @@ function useWizard(options) {
6179
6937
  if (processed !== void 0 && processed.success === true) {
6180
6938
  valuesMap[step.key] = processed.data;
6181
6939
  } else {
6182
- const source = step.form;
6183
- valuesMap[step.key] = source.values;
6940
+ valuesMap[step.key] = asStatusSource(step.form).values;
6184
6941
  }
6185
6942
  }
6186
6943
  const ctx = buildSubmitContext(valuesMap, currentKey, final);
@@ -6201,8 +6958,11 @@ function useWizard(options) {
6201
6958
  moveTo(firstFailedKey);
6202
6959
  await vue.nextTick();
6203
6960
  const failedForm = formsRecord.value[firstFailedKey];
6204
- if (failedForm !== void 0 && typeof failedForm.applyInvalidSubmitPolicy === "function") {
6205
- failedForm.applyInvalidSubmitPolicy();
6961
+ if (failedForm !== void 0) {
6962
+ const failedSource = asSubmissionSource(failedForm);
6963
+ if (typeof failedSource.applyInvalidSubmitPolicy === "function") {
6964
+ failedSource.applyInvalidSubmitPolicy();
6965
+ }
6206
6966
  }
6207
6967
  }
6208
6968
  }
@@ -6217,7 +6977,7 @@ function useWizard(options) {
6217
6977
  done.value = false;
6218
6978
  lazyEpoch.value += 1;
6219
6979
  for (const step of compiledSteps.value) {
6220
- const full = step.form;
6980
+ const full = asSubmissionSource(step.form);
6221
6981
  if (typeof full.reset === "function") full.reset();
6222
6982
  }
6223
6983
  const firstStep = compiledSteps.value[0];
@@ -6267,12 +7027,8 @@ function useWizard(options) {
6267
7027
  return count.value;
6268
7028
  },
6269
7029
  statuses,
6270
- get allValues() {
6271
- return allValues.value;
6272
- },
6273
- get allErrors() {
6274
- return allErrors.value;
6275
- },
7030
+ allValues,
7031
+ allErrors,
6276
7032
  get progress() {
6277
7033
  return progress.value;
6278
7034
  },
@@ -6405,7 +7161,7 @@ function warnIfAmbientWizardProviderHadDuplicates() {
6405
7161
 
6406
7162
  exports.AttaformErrorCode = AttaformErrorCode;
6407
7163
  exports.defaultCoercionRules = defaultCoercionRules;
6408
- exports.defaultShouldShowErrors = defaultShouldShowErrors;
7164
+ exports.defaultDisplayState = defaultDisplayState;
6409
7165
  exports.defineCoercion = defineCoercion;
6410
7166
  exports.getAtPath = getAtPath;
6411
7167
  exports.humanize = humanize;
@@ -6420,4 +7176,4 @@ exports.slimKindOf = slimKindOf;
6420
7176
  exports.unset = unset;
6421
7177
  exports.useAbstractForm = useAbstractForm;
6422
7178
  exports.useWizard = useWizard;
6423
- //# sourceMappingURL=attaform.II89Pcf4.cjs.map
7179
+ //# sourceMappingURL=attaform.DL4CQ-oW.cjs.map