attaform 0.20.2 → 0.21.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 (149) hide show
  1. package/dist/chunks/dev-key-collision-warnings.cjs +58 -0
  2. package/dist/chunks/dev-key-collision-warnings.cjs.map +1 -0
  3. package/dist/chunks/dev-key-collision-warnings.mjs +55 -0
  4. package/dist/chunks/dev-key-collision-warnings.mjs.map +1 -0
  5. package/dist/chunks/devtools.cjs +1 -1
  6. package/dist/chunks/devtools.mjs +1 -1
  7. package/dist/chunks/fingerprint.cjs +186 -0
  8. package/dist/chunks/fingerprint.cjs.map +1 -0
  9. package/dist/chunks/fingerprint.mjs +184 -0
  10. package/dist/chunks/fingerprint.mjs.map +1 -0
  11. package/dist/chunks/fingerprint2.cjs +162 -0
  12. package/dist/chunks/fingerprint2.cjs.map +1 -0
  13. package/dist/chunks/fingerprint2.mjs +160 -0
  14. package/dist/chunks/fingerprint2.mjs.map +1 -0
  15. package/dist/chunks/indexeddb.cjs +1 -1
  16. package/dist/chunks/indexeddb.mjs +1 -1
  17. package/dist/chunks/local-storage.cjs +1 -1
  18. package/dist/chunks/local-storage.mjs +1 -1
  19. package/dist/chunks/multi-tab-sync.cjs +367 -0
  20. package/dist/chunks/multi-tab-sync.cjs.map +1 -0
  21. package/dist/chunks/multi-tab-sync.mjs +364 -0
  22. package/dist/chunks/multi-tab-sync.mjs.map +1 -0
  23. package/dist/chunks/session-storage.cjs +1 -1
  24. package/dist/chunks/session-storage.mjs +1 -1
  25. package/dist/chunks/wire-persistence.cjs +396 -0
  26. package/dist/chunks/wire-persistence.cjs.map +1 -0
  27. package/dist/chunks/wire-persistence.mjs +394 -0
  28. package/dist/chunks/wire-persistence.mjs.map +1 -0
  29. package/dist/esbuild.cjs +28 -0
  30. package/dist/esbuild.cjs.map +1 -0
  31. package/dist/esbuild.d.cts +56 -0
  32. package/dist/esbuild.d.mts +56 -0
  33. package/dist/esbuild.d.ts +56 -0
  34. package/dist/esbuild.mjs +26 -0
  35. package/dist/esbuild.mjs.map +1 -0
  36. package/dist/index.cjs +5 -3
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +65 -70
  39. package/dist/index.d.mts +65 -70
  40. package/dist/index.d.ts +65 -70
  41. package/dist/index.mjs +5 -5
  42. package/dist/nuxt.d.cts +1 -1
  43. package/dist/nuxt.d.mts +1 -1
  44. package/dist/nuxt.d.ts +1 -1
  45. package/dist/rollup.cjs +24 -0
  46. package/dist/rollup.cjs.map +1 -0
  47. package/dist/rollup.d.cts +35 -0
  48. package/dist/rollup.d.mts +35 -0
  49. package/dist/rollup.d.ts +35 -0
  50. package/dist/rollup.mjs +22 -0
  51. package/dist/rollup.mjs.map +1 -0
  52. package/dist/rspack.cjs +10 -0
  53. package/dist/rspack.cjs.map +1 -0
  54. package/dist/rspack.d.cts +40 -0
  55. package/dist/rspack.d.mts +40 -0
  56. package/dist/rspack.d.ts +40 -0
  57. package/dist/rspack.mjs +8 -0
  58. package/dist/rspack.mjs.map +1 -0
  59. package/dist/runtime/plugins/attaform.cjs +2 -2
  60. package/dist/runtime/plugins/attaform.mjs +2 -2
  61. package/dist/shared/{attaform.ceGEAEMk.d.ts → attaform.7lzO9pdM.d.mts} +95 -1
  62. package/dist/shared/{attaform.99cfHcIt.d.cts → attaform.B1nyO4ec.d.cts} +82 -30
  63. package/dist/shared/{attaform.99cfHcIt.d.mts → attaform.B1nyO4ec.d.mts} +82 -30
  64. package/dist/shared/{attaform.99cfHcIt.d.ts → attaform.B1nyO4ec.d.ts} +82 -30
  65. package/dist/shared/{attaform.z5j3LwJz.cjs → attaform.BA3vRDos.cjs} +3 -3
  66. package/dist/shared/attaform.BA3vRDos.cjs.map +1 -0
  67. package/dist/shared/{attaform.BXinSW2T.d.mts → attaform.BDIEq9qP.d.cts} +1 -1
  68. package/dist/shared/attaform.BJGA_UOS.mjs +37 -0
  69. package/dist/shared/attaform.BJGA_UOS.mjs.map +1 -0
  70. package/dist/shared/{attaform.DN5CvZrg.d.ts → attaform.BK1RE2ha.d.ts} +1 -1
  71. package/dist/shared/{attaform.CywE4y8x.d.cts → attaform.BQ6drorq.d.mts} +1 -1
  72. package/dist/shared/attaform.BRGIpZo4.cjs +26 -0
  73. package/dist/shared/attaform.BRGIpZo4.cjs.map +1 -0
  74. package/dist/shared/{attaform.CwLjUqmQ.cjs → attaform.BUszFoKq.cjs} +383 -911
  75. package/dist/shared/attaform.BUszFoKq.cjs.map +1 -0
  76. package/dist/shared/{attaform.C5aYC_T8.mjs → attaform.BnK_bfcb.mjs} +39 -392
  77. package/dist/shared/attaform.BnK_bfcb.mjs.map +1 -0
  78. package/dist/shared/{attaform.DAKrGhxc.cjs → attaform.BzvOdiSI.cjs} +101 -417
  79. package/dist/shared/attaform.BzvOdiSI.cjs.map +1 -0
  80. package/dist/shared/attaform.C3Doa9Pt.mjs +24 -0
  81. package/dist/shared/attaform.C3Doa9Pt.mjs.map +1 -0
  82. package/dist/shared/{attaform.D2SCCd4O.cjs → attaform.CEf6wYfD.cjs} +2 -2
  83. package/dist/shared/{attaform.D2SCCd4O.cjs.map → attaform.CEf6wYfD.cjs.map} +1 -1
  84. package/dist/shared/attaform.CQN9R62B.cjs +39 -0
  85. package/dist/shared/attaform.CQN9R62B.cjs.map +1 -0
  86. package/dist/shared/{attaform.sWm8B15V.d.mts → attaform.CRsXyy-Y.d.ts} +95 -1
  87. package/dist/shared/{attaform.Dt7dEcHk.mjs → attaform.CkjTapyq.mjs} +89 -405
  88. package/dist/shared/attaform.CkjTapyq.mjs.map +1 -0
  89. package/dist/shared/{attaform.tiWEVznj.mjs → attaform.DSqO6Db7.mjs} +372 -912
  90. package/dist/shared/attaform.DSqO6Db7.mjs.map +1 -0
  91. package/dist/shared/attaform.DuzQYscR.d.cts +41 -0
  92. package/dist/shared/attaform.DuzQYscR.d.mts +41 -0
  93. package/dist/shared/attaform.DuzQYscR.d.ts +41 -0
  94. package/dist/shared/{attaform.DbRgDFa7.d.cts → attaform.F8LMHHWV.d.cts} +95 -1
  95. package/dist/shared/attaform.LEWUFqUw.cjs +54 -0
  96. package/dist/shared/attaform.LEWUFqUw.cjs.map +1 -0
  97. package/dist/shared/{attaform.Cd4AOfwu.cjs → attaform.PnqML3xW.cjs} +68 -402
  98. package/dist/shared/attaform.PnqML3xW.cjs.map +1 -0
  99. package/dist/shared/{attaform.QG5TG8lB.mjs → attaform.Y_Mgg0Yp.mjs} +3 -3
  100. package/dist/shared/attaform.Y_Mgg0Yp.mjs.map +1 -0
  101. package/dist/shared/{attaform.B_hph5AE.cjs → attaform._rsCZy2j.cjs} +172 -20
  102. package/dist/shared/attaform._rsCZy2j.cjs.map +1 -0
  103. package/dist/shared/{attaform.CnrxbkB6.mjs → attaform.ezb5Nh2t.mjs} +2 -2
  104. package/dist/shared/{attaform.CnrxbkB6.mjs.map → attaform.ezb5Nh2t.mjs.map} +1 -1
  105. package/dist/shared/{attaform.BGk8cfw2.mjs → attaform.r3PePkDR.mjs} +172 -21
  106. package/dist/shared/attaform.r3PePkDR.mjs.map +1 -0
  107. package/dist/shared/attaform.sHkHv_98.mjs +51 -0
  108. package/dist/shared/attaform.sHkHv_98.mjs.map +1 -0
  109. package/dist/vite.cjs +9 -45
  110. package/dist/vite.cjs.map +1 -1
  111. package/dist/vite.d.cts +36 -0
  112. package/dist/vite.d.mts +36 -0
  113. package/dist/vite.d.ts +36 -0
  114. package/dist/vite.mjs +8 -44
  115. package/dist/vite.mjs.map +1 -1
  116. package/dist/webpack.cjs +10 -0
  117. package/dist/webpack.cjs.map +1 -0
  118. package/dist/webpack.d.cts +37 -0
  119. package/dist/webpack.d.mts +37 -0
  120. package/dist/webpack.d.ts +37 -0
  121. package/dist/webpack.mjs +8 -0
  122. package/dist/webpack.mjs.map +1 -0
  123. package/dist/zod-v3.cjs +3 -3
  124. package/dist/zod-v3.d.cts +3 -3
  125. package/dist/zod-v3.d.mts +3 -3
  126. package/dist/zod-v3.d.ts +3 -3
  127. package/dist/zod-v3.mjs +3 -3
  128. package/dist/zod-v4.cjs +3 -3
  129. package/dist/zod-v4.d.cts +4 -4
  130. package/dist/zod-v4.d.mts +4 -4
  131. package/dist/zod-v4.d.ts +4 -4
  132. package/dist/zod-v4.mjs +3 -3
  133. package/dist/zod.cjs +8 -8
  134. package/dist/zod.cjs.map +1 -1
  135. package/dist/zod.d.cts +5 -5
  136. package/dist/zod.d.mts +5 -5
  137. package/dist/zod.d.ts +5 -5
  138. package/dist/zod.mjs +6 -6
  139. package/package.json +19 -5
  140. package/dist/shared/attaform.BGk8cfw2.mjs.map +0 -1
  141. package/dist/shared/attaform.B_hph5AE.cjs.map +0 -1
  142. package/dist/shared/attaform.C5aYC_T8.mjs.map +0 -1
  143. package/dist/shared/attaform.Cd4AOfwu.cjs.map +0 -1
  144. package/dist/shared/attaform.CwLjUqmQ.cjs.map +0 -1
  145. package/dist/shared/attaform.DAKrGhxc.cjs.map +0 -1
  146. package/dist/shared/attaform.Dt7dEcHk.mjs.map +0 -1
  147. package/dist/shared/attaform.QG5TG8lB.mjs.map +0 -1
  148. package/dist/shared/attaform.tiWEVznj.mjs.map +0 -1
  149. package/dist/shared/attaform.z5j3LwJz.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attaform.CQN9R62B.cjs","sources":["../../src/runtime/core/canonical-stringify.ts"],"sourcesContent":["/**\n * Canonical stringify for arbitrary values, shared by the v3 and v4\n * schema fingerprint walkers. Produces a stable string surface for\n * structural equality testing: object keys are sorted, arrays walk in\n * index order, and functions / symbols / cycles collapse to opaque\n * sentinels.\n *\n * This is NOT JSON. The output is not meant to round-trip through\n * `JSON.parse`; it exists purely so two structurally-equal values\n * serialise to the same string and two structurally-different values\n * do not.\n *\n * Cycle detection uses an ancestor-stack add / delete pattern: a\n * reference is only treated as cyclic while it sits on the path from\n * the root being stringified. Without the `delete` on the way back up,\n * two sibling properties pointing at the same object would see the\n * second one falsely labelled `<cyclic>`.\n */\nexport function canonicalStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (value === null) return 'null'\n if (value === undefined) return 'undefined'\n const t = typeof value\n if (t === 'string') return JSON.stringify(value)\n if (t === 'number' || t === 'boolean') return String(value)\n if (t === 'bigint') return `${String(value)}n`\n if (t === 'function') return 'fn:*'\n if (t === 'symbol') return 'symbol:*'\n if (Array.isArray(value)) {\n if (seen.has(value)) return '<cyclic>'\n seen.add(value)\n try {\n const parts = value.map((v) => canonicalStringify(v, seen))\n return `[${parts.join(',')}]`\n } finally {\n seen.delete(value)\n }\n }\n if (t === 'object') {\n // `null` already returned above, so the remaining `object` branch is\n // non-null; narrowing against null again is redundant (eslint's\n // no-unnecessary-condition rule flags it).\n const obj = value as Record<string, unknown>\n if (seen.has(obj)) return '<cyclic>'\n seen.add(obj)\n try {\n if (value instanceof Date) return `date:${value.getTime()}`\n if (value instanceof RegExp) return `regex:${String(value)}`\n const entries = Object.entries(obj)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${canonicalStringify(v, seen)}`)\n return `{${entries.join(',')}}`\n } finally {\n seen.delete(obj)\n }\n }\n return 'unknown'\n}\n"],"names":[],"mappings":";;AAkBO,SAAS,kBAAA,CAAmB,KAAA,EAAgB,IAAA,mBAAwB,IAAI,SAAQ,EAAW;AAChG,EAAA,IAAI,KAAA,KAAU,MAAM,OAAO,MAAA;AAC3B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,WAAA;AAChC,EAAA,MAAM,IAAI,OAAO,KAAA;AACjB,EAAA,IAAI,CAAA,KAAM,QAAA,EAAU,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;AAC/C,EAAA,IAAI,MAAM,QAAA,IAAY,CAAA,KAAM,SAAA,EAAW,OAAO,OAAO,KAAK,CAAA;AAC1D,EAAA,IAAI,MAAM,QAAA,EAAU,OAAO,CAAA,EAAG,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA,CAAA;AAC3C,EAAA,IAAI,CAAA,KAAM,YAAY,OAAO,MAAA;AAC7B,EAAA,IAAI,CAAA,KAAM,UAAU,OAAO,UAAA;AAC3B,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA,EAAG,OAAO,UAAA;AAC5B,IAAA,IAAA,CAAK,IAAI,KAAK,CAAA;AACd,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,CAAC,MAAM,kBAAA,CAAmB,CAAA,EAAG,IAAI,CAAC,CAAA;AAC1D,MAAA,OAAO,CAAA,CAAA,EAAI,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AAAA,IAC5B,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,OAAO,KAAK,CAAA;AAAA,IACnB;AAAA,EACF;AACA,EAAA,IAAI,MAAM,QAAA,EAAU;AAIlB,IAAA,MAAM,GAAA,GAAM,KAAA;AACZ,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG,OAAO,UAAA;AAC1B,IAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,IAAA,IAAI;AACF,MAAA,IAAI,iBAAiB,IAAA,EAAM,OAAO,CAAA,KAAA,EAAQ,KAAA,CAAM,SAAS,CAAA,CAAA;AACzD,MAAA,IAAI,iBAAiB,MAAA,EAAQ,OAAO,CAAA,MAAA,EAAS,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAC1D,MAAA,MAAM,UAAU,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,CAC/B,KAAK,CAAC,CAAC,CAAC,CAAA,EAAG,CAAC,CAAC,CAAA,KAAO,CAAA,GAAI,CAAA,GAAI,KAAK,CAAA,GAAI,CAAA,GAAI,CAAA,GAAI,CAAE,EAC/C,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,KAAM,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,CAAC,CAAC,CAAA,CAAA,EAAI,mBAAmB,CAAA,EAAG,IAAI,CAAC,CAAA,CAAE,CAAA;AACxE,MAAA,OAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AAAA,IAC9B,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AAAA,IACjB;AAAA,EACF;AACA,EAAA,OAAO,SAAA;AACT;;;;"}
@@ -1,4 +1,4 @@
1
- import { t as FormKey, ae as SlimPrimitiveKind, C as CoercionEntry, g as CoercionRegistry, G as GenericForm, T as PathKey, S as Path, am as ValidationError, A as AbstractSchema, x as GetDisplayState, aq as WriteMeta, D as DeepPartial, ar as WriteShape, ak as ValidateOn, Y as PersistOptInRegistry, f as AttaformDefaults, aj as UseFormReturnType, a8 as RegisterValue } from './attaform.99cfHcIt.mjs';
1
+ import { v as FormKey, V as PathKey, m as DisplayCtx, z as GetDisplayState, n as DisplayMachine, ag as SlimPrimitiveKind, C as CoercionEntry, g as CoercionRegistry, G as GenericForm, U as Path, ao as ValidationError, A as AbstractSchema, as as WriteMeta, D as DeepPartial, at as WriteShape, am as ValidateOn, _ as PersistOptInRegistry, f as AttaformDefaults, al as UseFormReturnType, aa as RegisterValue } from './attaform.B1nyO4ec.js';
2
2
  import { Ref, ComputedRef, App, InjectionKey } from 'vue';
3
3
 
4
4
  /**
@@ -455,6 +455,68 @@ type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepS
455
455
  readonly reset: () => void;
456
456
  };
457
457
 
458
+ /**
459
+ * Per-form display engine: owns the clock and the timers that the pure
460
+ * `getDisplayState` reducer policy needs, so the reducer itself stays a
461
+ * deterministic `(prev, ctx) => next` function.
462
+ *
463
+ * It keeps a `Map` of the machines that are still *active* (a spinner is
464
+ * showing, a verdict is being held, or a `reviewAt` deadline is pending),
465
+ * a single reactive `tick` ref, and a single `setTimeout` aimed at the
466
+ * nearest `reviewAt` across every active field. When a field-state
467
+ * computed reads `resolve`, it subscribes to `tick`; when the timer
468
+ * fires, `tick` bumps and every dependent computed re-runs the reducer,
469
+ * so a field whose deadline elapsed transitions (verdict → spinner, or
470
+ * spinner → settled verdict) without any per-field watcher.
471
+ *
472
+ * Eviction: a machine is dropped once it reaches a terminal *idle* state
473
+ * with no pending review. Error / success / pending machines are retained
474
+ * so the reducer can hold the prior verdict under the show-delay window of
475
+ * the *next* validation streak (no success → idle → success flicker). The
476
+ * retained set is bounded by the rendered non-idle fields; none of them
477
+ * arm a timer, so retention costs memory, never CPU.
478
+ *
479
+ * Untrusted reducer: `getDisplayState` is consumer-overridable, so the
480
+ * engine treats the returned `reviewAt` as untrusted. A non-finite deadline
481
+ * (NaN / ±Infinity from a custom predicate's bad arithmetic) is ignored
482
+ * rather than handed to `setTimeout`, where it coerces to 0 and spins; an
483
+ * over-large finite deadline is clamped below the 32-bit `setTimeout`
484
+ * overflow; and the timer refuses to re-arm for the exact deadline it just
485
+ * fired, so a predicate re-emitting a fixed or past `reviewAt` can't drive an
486
+ * infinite fire loop. None of these arise from the library default, which
487
+ * always advances its deadline or drops it.
488
+ *
489
+ * Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
490
+ * so a min-visible hold can overshoot. A `visibilitychange` listener bumps
491
+ * the clock on return to the foreground, so any overdue deadline resolves at
492
+ * once instead of lingering.
493
+ *
494
+ * SSR: no clock, no timers, no listener. With `now` frozen and nothing
495
+ * validating at render, the reducer returns the plain verdict (never
496
+ * pending) and the engine stores nothing, so the server HTML and the
497
+ * client's first render agree — no hydration mismatch on the display
498
+ * projection.
499
+ */
500
+ type DisplayEngine = {
501
+ /**
502
+ * Resolve a path's next `DisplayMachine`. Subscribes the calling
503
+ * computed to the engine clock, threads the path's previous machine
504
+ * through `reducer`, persists or evicts the result, and re-arms the
505
+ * single timer to the nearest deadline.
506
+ */
507
+ resolve(key: PathKey, ctx: DisplayCtx, reducer: GetDisplayState): DisplayMachine;
508
+ /** Drop every retained machine and cancel the timer (used by `reset()`). */
509
+ clear(): void;
510
+ /** Tear down for good: `clear()` plus detaching the visibility listener. */
511
+ dispose(): void;
512
+ /** Introspection for tests: count of retained machines. */
513
+ size(): number;
514
+ /** Introspection for tests: whether a path currently has a retained machine. */
515
+ has(key: PathKey): boolean;
516
+ /** Introspection for tests: whether a deadline timer is currently armed. */
517
+ hasTimer(): boolean;
518
+ };
519
+
458
520
  /**
459
521
  * Schema-driven coercion of user-typed DOM values at the v-register
460
522
  * directive layer. When the slim schema declares a numeric or
@@ -669,6 +731,16 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
669
731
  * read.
670
732
  */
671
733
  readonly getDisplayState: GetDisplayState;
734
+ /**
735
+ * Per-form display engine: owns the clock and the single timer the timed
736
+ * `getDisplayState` reducer policy needs, keeping the reducer itself a
737
+ * pure `(prev, ctx) => next` function. The field-state computeds route
738
+ * every `displayState` read through `displayEngine.resolve(...)`, which
739
+ * threads the path's previous machine, persists or evicts the result, and
740
+ * re-arms the nearest-deadline timer. Constructed once at form
741
+ * construction; torn down via `registerCleanup` on store eviction.
742
+ */
743
+ readonly displayEngine: DisplayEngine;
672
744
  readonly submitting: Ref<boolean>;
673
745
  readonly activeSubmissions: Ref<number>;
674
746
  readonly submissionAttempts: Ref<number>;
@@ -838,6 +910,28 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
838
910
  * computed only re-runs when the count for ITS key changes.
839
911
  */
840
912
  readonly fieldValidationCounts: Map<PathKey, number>;
913
+ /**
914
+ * Per-path `Date.now()` stamp marking when the field's LATEST validation
915
+ * run started, re-anchored on every run start (every increment), deleted
916
+ * on the `→ 0` edge. The display reducer reads it as `ctx.validatingSince`
917
+ * to time the anti-flash spinner, which measures `now - validatingSince`:
918
+ * re-anchoring on each run means a burst of keystrokes (each aborting the
919
+ * prior run and starting a new one) keeps pushing the stamp forward, so the
920
+ * spinner stays suppressed until the user pauses rather than surfacing
921
+ * mid-typing. Anchoring only at the streak start would pin it to the first
922
+ * keystroke, because with `debounceMs: 0` the aborted run's decrement lands
923
+ * after the next run's increment and the count never returns to 0 between
924
+ * fast keystrokes. The field-state container walk takes the descendant-min
925
+ * so a row spinner anchors at its earliest still-active leaf. Runtime-only,
926
+ * never hydrated, like the counts. REACTIVE: the display computed reads this
927
+ * (as `ctx.validatingSince`) but not the `validating` flag, and a long
928
+ * validation that settles with an unchanged verdict (same error, still
929
+ * invalid) leaves `errors` / `valid` untouched — so a non-reactive map would
930
+ * leave a held `pending` spinner stranded after the run ends, until some
931
+ * unrelated reactive change happened to re-run the computed. Reactivity ties
932
+ * the computed to both the streak start (set) and end (delete).
933
+ */
934
+ readonly fieldValidatingSince: Map<PathKey, number>;
841
935
  /**
842
936
  * Replace the form value wholesale. Optional `meta` is forwarded to
843
937
  * every `onFormChange` listener so they can decide whether THIS write