react-f0rm 1.3.0 → 1.5.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 (99) hide show
  1. package/README.md +98 -1140
  2. package/devtools.d.ts +1 -0
  3. package/devtools.js +1 -0
  4. package/dist/array-Bu7W8BSz.d.ts +54 -0
  5. package/dist/devtools/index.cjs.js +1 -1
  6. package/dist/devtools/index.cjs.js.map +1 -1
  7. package/dist/devtools/index.d.cts +24 -0
  8. package/dist/devtools/index.d.mts +24 -0
  9. package/dist/devtools/index.d.ts +8 -17
  10. package/dist/devtools/index.mjs +1 -1
  11. package/dist/devtools/index.mjs.map +1 -1
  12. package/dist/errors-DA4ReEd9.mjs +2 -0
  13. package/dist/errors-DA4ReEd9.mjs.map +1 -0
  14. package/dist/errors-TzyWwBfw.cjs.js +2 -0
  15. package/dist/errors-TzyWwBfw.cjs.js.map +1 -0
  16. package/dist/index.cjs.js +1 -1
  17. package/dist/index.cjs.js.map +1 -1
  18. package/dist/index.d.cts +1253 -0
  19. package/dist/index.d.mts +1253 -0
  20. package/dist/index.d.ts +570 -895
  21. package/dist/index.mjs +1 -1
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/index.umd.js +1256 -535
  24. package/dist/index.umd.js.map +1 -1
  25. package/dist/index.umd.min.js +2 -2
  26. package/dist/index.umd.min.js.map +1 -1
  27. package/dist/persist.cjs.js +1 -1
  28. package/dist/persist.cjs.js.map +1 -1
  29. package/dist/persist.d.cts +28 -0
  30. package/dist/persist.d.mts +28 -0
  31. package/dist/persist.d.ts +9 -30
  32. package/dist/persist.mjs +1 -1
  33. package/dist/persist.mjs.map +1 -1
  34. package/dist/resolvers/standard-schema.cjs.js +1 -1
  35. package/dist/resolvers/standard-schema.cjs.js.map +1 -1
  36. package/dist/resolvers/standard-schema.d.cts +2 -0
  37. package/dist/resolvers/standard-schema.d.mts +2 -0
  38. package/dist/resolvers/standard-schema.d.ts +1 -67
  39. package/dist/resolvers/standard-schema.mjs +1 -1
  40. package/dist/resolvers/standard-schema.mjs.map +1 -1
  41. package/dist/resolvers/yup.cjs.js +1 -1
  42. package/dist/resolvers/yup.cjs.js.map +1 -1
  43. package/dist/resolvers/yup.d.cts +6 -0
  44. package/dist/resolvers/yup.d.mts +6 -0
  45. package/dist/resolvers/yup.d.ts +0 -1
  46. package/dist/resolvers/yup.mjs +1 -1
  47. package/dist/resolvers/yup.mjs.map +1 -1
  48. package/dist/resolvers/zod.cjs.js +1 -1
  49. package/dist/resolvers/zod.cjs.js.map +1 -1
  50. package/dist/resolvers/zod.d.cts +34 -0
  51. package/dist/resolvers/zod.d.mts +34 -0
  52. package/dist/resolvers/zod.d.ts +30 -3
  53. package/dist/resolvers/zod.mjs +1 -1
  54. package/dist/resolvers/zod.mjs.map +1 -1
  55. package/dist/server/index.cjs.js +1 -1
  56. package/dist/server/index.cjs.js.map +1 -1
  57. package/dist/server/index.d.cts +38 -0
  58. package/dist/server/index.d.mts +38 -0
  59. package/dist/server/index.d.ts +26 -65
  60. package/dist/server/index.mjs +1 -1
  61. package/dist/server/index.mjs.map +1 -1
  62. package/dist/standard-schema-BAaTmAHh.d.ts +682 -0
  63. package/dist/standardSchema-5WezjHlp.mjs +2 -0
  64. package/dist/standardSchema-5WezjHlp.mjs.map +1 -0
  65. package/dist/standardSchema-DINHlsYR.cjs.js +2 -0
  66. package/dist/standardSchema-DINHlsYR.cjs.js.map +1 -0
  67. package/dist/validate-2pX-N1O6.cjs.js +2 -0
  68. package/dist/validate-2pX-N1O6.cjs.js.map +1 -0
  69. package/dist/validate-C0HOsP9v.mjs +2 -0
  70. package/dist/validate-C0HOsP9v.mjs.map +1 -0
  71. package/dist/values-BbPnLByD.cjs.js +2 -0
  72. package/dist/values-BbPnLByD.cjs.js.map +1 -0
  73. package/dist/values-CHsmcZk4.mjs +2 -0
  74. package/dist/values-CHsmcZk4.mjs.map +1 -0
  75. package/package.json +75 -28
  76. package/persist.d.ts +1 -0
  77. package/persist.js +1 -0
  78. package/resolvers/standard-schema.d.ts +1 -0
  79. package/resolvers/standard-schema.js +1 -0
  80. package/resolvers/yup.d.ts +1 -0
  81. package/resolvers/yup.js +1 -0
  82. package/resolvers/zod.d.ts +1 -0
  83. package/resolvers/zod.js +1 -0
  84. package/server.d.ts +1 -0
  85. package/server.js +1 -0
  86. package/dist/errors-BKrUdpfI.cjs.js +0 -2
  87. package/dist/errors-BKrUdpfI.cjs.js.map +0 -1
  88. package/dist/errors-CrQBddrJ.mjs +0 -2
  89. package/dist/errors-CrQBddrJ.mjs.map +0 -1
  90. package/dist/form-CeKSBs31.d.ts +0 -486
  91. package/dist/validate-CNtuUhmk.mjs +0 -2
  92. package/dist/validate-CNtuUhmk.mjs.map +0 -1
  93. package/dist/validate-Cl4ksNFu.cjs.js +0 -2
  94. package/dist/validate-Cl4ksNFu.cjs.js.map +0 -1
  95. package/dist/validate-nksgv1pR.d.ts +0 -272
  96. package/dist/values-Cu6awQOJ.cjs.js +0 -2
  97. package/dist/values-Cu6awQOJ.cjs.js.map +0 -1
  98. package/dist/values-DRY-a32G.mjs +0 -2
  99. package/dist/values-DRY-a32G.mjs.map +0 -1
package/devtools.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './dist/devtools/index';
package/devtools.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/devtools/index.cjs.js');
@@ -0,0 +1,54 @@
1
+ import { A as AnyPath, F as Form, e as ArrayItemOf, P as Path } from './standard-schema-BAaTmAHh.js';
2
+
3
+ /** Append one value to the array at `path`. */
4
+ declare function appendValueByPath(form: Form, path: Path, value: any): void;
5
+ /** Prepend one value to the array at `path`. */
6
+ declare function prependValueByPath(form: Form, path: Path, value: any): void;
7
+ /** Insert one value at `index` in the array at `path` (`index === length`
8
+ * appends). Returns `false` — without touching the form — when the index
9
+ * is out of range; `true` once the value landed. */
10
+ declare function insertValueByPath(form: Form, path: Path, index: number, value: any): boolean;
11
+ /** Remove one or several rows from the array at `path` in a single write.
12
+ * Indices may repeat; out-of-range entries are ignored. Returns the
13
+ * indices actually dropped, sorted descending so caller-side row-id
14
+ * bookkeeping can splice them without re-indexing drift; `[]` when nothing
15
+ * was removable. */
16
+ declare function removeValueByPath(form: Form, path: Path, indices: number | number[]): number[];
17
+ /** Move the row at `from` to `to` (rows in between shift one position).
18
+ * Returns `false` — without touching the form — when either index is out
19
+ * of range or they are equal; `true` once the row moved. */
20
+ declare function moveValueByPath(form: Form, path: Path, from: number, to: number): boolean;
21
+ /** Swap the rows at `from` and `to`. Returns `false` — without touching
22
+ * the form — when either index is out of range or they are equal; `true`
23
+ * once the rows swapped. */
24
+ declare function swapValuesByPath(form: Form, path: Path, from: number, to: number): boolean;
25
+ /** Wholesale-replace the array at `path` (length may change) in one write. */
26
+ declare function replaceValuesByPath(form: Form, path: Path, values: any[]): void;
27
+ /** Overwrite one row of the array at `path` in place (a whole-array write,
28
+ * so the row's position and every other row are untouched). Returns
29
+ * `false` when the index is out of range; `true` once the value landed. */
30
+ declare function updateValueByPath(form: Form, path: Path, index: number, value: any): boolean;
31
+ /** Append one value to the array at `name`. */
32
+ declare function appendValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, value: ArrayItemOf<T, P>): void;
33
+ /** Prepend one value to the array at `name`. See {@link appendValue}. */
34
+ declare function prependValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, value: ArrayItemOf<T, P>): void;
35
+ /** Insert one value at `index` in the array at `name`. Returns `false` on
36
+ * an out-of-range index (see {@link insertValueByPath}). */
37
+ declare function insertValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, index: number, value: ArrayItemOf<T, P>): boolean;
38
+ /** Remove one or several rows from the array at `name` in a single write.
39
+ * Returns the indices actually dropped, descending (see
40
+ * {@link removeValueByPath}). */
41
+ declare function removeValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, indices: number | number[]): number[];
42
+ /** Move the row at `from` to `to` in the array at `name`. Returns `false`
43
+ * on an out-of-range or no-op move (see {@link moveValueByPath}). */
44
+ declare function moveValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, from: number, to: number): boolean;
45
+ /** Swap the rows at `from` and `to` in the array at `name`. Returns
46
+ * `false` on an out-of-range or no-op swap (see {@link swapValuesByPath}). */
47
+ declare function swapValues<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, from: number, to: number): boolean;
48
+ /** Wholesale-replace the array at `name` (see {@link replaceValuesByPath}). */
49
+ declare function replaceValues<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, values: ArrayItemOf<T, P>[]): void;
50
+ /** Overwrite one row of the array at `name` in place. Returns `false` on
51
+ * an out-of-range index (see {@link updateValueByPath}). */
52
+ declare function updateValue<T extends Record<string, any> = any, P extends AnyPath<T> = AnyPath<T>>(form: Form<T>, name: P, index: number, value: ArrayItemOf<T, P>): boolean;
53
+
54
+ export { appendValue as a, replaceValues as b, appendValueByPath as c, insertValueByPath as d, moveValueByPath as e, prependValueByPath as f, removeValueByPath as g, replaceValuesByPath as h, insertValue as i, swapValuesByPath as j, updateValueByPath as k, moveValue as m, prependValue as p, removeValue as r, swapValues as s, updateValue as u };
@@ -1,2 +1,2 @@
1
- "use strict";var e=require("react"),t=require("@for-fun/event-emitter"),n=require("../values-Cu6awQOJ.cjs.js"),r=require("../errors-BKrUdpfI.cjs.js"),a=require("../validate-Cl4ksNFu.cjs.js");function o(e){var t=Object.create(null);return e&&Object.keys(e).forEach(function(n){if("default"!==n){var r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:function(){return e[n]}})}}),t.default=e,Object.freeze(t)}var l=o(e);function s(e){const t={};return function(e,t){for(const[n,a]of e.values){const o=JSON.parse(n);r.getDirtyBaseline(e,n,o)!==a&&t(o.join("."))}}(e,e=>{t[e]=!0}),t}function d(e){let t=r.dirtyFieldsCaches.get(e);if(t){if(t.version>0){const n=s(e);(function(e,t){const n=Object.keys(e);return n.length===Object.keys(t).length&&n.every(e=>!0===t[e])})(t.result,n)||(t.result=n),t.version=0}}else t={version:0,result:s(e)},r.dirtyFieldsCaches.set(e,t);return t.result}function i({touched:e}){return Array.from(e,e=>JSON.parse(e).join("."))}function c(n,r,a,o){const l="emitter"in n?n.emitter:n;return function(t,n,r){const a=e.useRef(null);null===a.current&&(a.current={hasValue:!1});const o=a.current,l=e.useRef(n);l.current=n;const s=e.useRef(r);s.current=r;const d=e.useCallback(()=>(o.hasValue||(o.value=l.current(),o.hasValue=!0),o.value),[o]),i=e.useCallback(e=>(o.hasValue=!1,t(()=>{const t=s.current;if(t&&o.hasValue){const n=l.current();if(t(o.value,n))return;return o.value=n,void e()}o.hasValue=!1,e()})),[t,o]);return e.useSyncExternalStore(i,d,d)}(e.useCallback(e=>t.on(l,r,e),[l,r]),a,o)}function f(e){return c(e,"touched",i.bind(null,e))}const p=e.createContext(null);p.Provider;e.createContext(null).Provider;function u({name:t,value:n,depth:r=0}){const[a,o]=e.useState(r<=1),s=void 0===t?null:l.createElement(l.Fragment,null,l.createElement("span",{className:"rf0-dt-key"},String(t)),l.createElement("span",{className:"rf0-dt-punct"},": "));if(null!==n&&"object"==typeof n){const e=Array.isArray(n),t=e?n.map((e,t)=>[t,e]):Object.entries(n),d=e?"[":"{",i=e?"]":"}",c=a?"":`${d}…${i} ${t.length}`;return l.createElement("div",{className:"rf0-dt-row",style:{paddingLeft:12*r}},l.createElement("button",{type:"button",className:"rf0-dt-node-toggle","aria-expanded":a,onClick:()=>o(!a)},l.createElement("span",{className:"rf0-dt-caret"},a?"▾":"▸"),s,l.createElement("span",{className:"rf0-dt-punct"},a?d:c)),a&&l.createElement(l.Fragment,null,t.map(([e,t])=>l.createElement(u,{key:String(e),name:e,value:t,depth:r+1})),l.createElement("span",{className:"rf0-dt-punct",style:{paddingLeft:12*r}},i)))}return l.createElement("span",{className:"rf0-dt-row",style:{paddingLeft:12*r,display:"block"}},s,l.createElement(b,{value:n}))}function b({value:e}){return void 0===e?l.createElement("span",{className:"rf0-dt-null"},"undefined"):null===e?l.createElement("span",{className:"rf0-dt-null"},"null"):"string"==typeof e?l.createElement("span",{className:"rf0-dt-string"},'"',e,'"'):"boolean"==typeof e?l.createElement("span",{className:"rf0-dt-boolean"},String(e)):l.createElement("span",{className:"rf0-dt-number"},String(e))}const m="react-f0rm-devtools-style";const g=["values","errors","touched","dirty"];function x(e){if(void 0!==e)return e?"rf0-dt-ok":"rf0-dt-err"}function h(e){if(null===e||"object"!=typeof e)return 1;let t=0;for(const n of Object.values(e))t+=h(n);return t}exports.Devtools=function({form:t,position:o="top-right"}){!function(){if("undefined"==typeof document)return;if(document.getElementById(m))return;const e=document.createElement("style");e.id=m,e.textContent="\n.rf0-dt {\n position: fixed;\n z-index: 2147483000;\n box-sizing: border-box;\n width: 308px;\n max-width: calc(100vw - 16px);\n max-height: min(70vh, 560px);\n display: flex;\n flex-direction: column;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 11px;\n line-height: 1.45;\n color: #c7d0dc;\n background: #0c1017;\n border: 1px solid #1f2735;\n border-radius: 4px;\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4);\n}\n.rf0-dt *,\n.rf0-dt-badge * {\n box-sizing: border-box;\n}\n.rf0-dt--top-right { top: 8px; right: 8px; }\n.rf0-dt--bottom-right { bottom: 8px; right: 8px; }\n.rf0-dt--top-left { top: 8px; left: 8px; }\n.rf0-dt--bottom-left { bottom: 8px; left: 8px; }\n\n/* ---- header -------------------------------------------------------- */\n.rf0-dt-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 6px 5px 9px;\n border-bottom: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-title {\n flex: 1;\n min-width: 0;\n color: #8b96a5;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.rf0-dt-title::before {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-right: 6px;\n border-radius: 50%;\n background: #e2b93b;\n vertical-align: 1px;\n}\n.rf0-dt-headerbtn {\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 1px 5px;\n color: #8b96a5;\n background: transparent;\n font: inherit;\n font-size: 10px;\n cursor: pointer;\n}\n.rf0-dt-headerbtn:hover { color: #dce3ec; border-color: #2a3547; }\n.rf0-dt-headerbtn:focus-visible,\n.rf0-dt-tab:focus-visible,\n.rf0-dt-action:focus-visible,\n.rf0-dt-badge:focus-visible,\n.rf0-dt-node-toggle:focus-visible {\n outline: 1px solid #e2b93b;\n outline-offset: 1px;\n}\n\n/* ---- tabs ---------------------------------------------------------- */\n.rf0-dt-tablist {\n display: flex;\n border-bottom: 1px solid #1f2735;\n background: #0e131b;\n}\n.rf0-dt-tab {\n flex: 1;\n padding: 4px 2px 5px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: #6b7686;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n white-space: nowrap;\n}\n.rf0-dt-tab:hover { color: #aab5c4; }\n.rf0-dt-tab[aria-selected='true'] {\n color: #e7edf4;\n border-bottom-color: #e2b93b;\n}\n.rf0-dt-tab-count {\n margin-left: 3px;\n color: inherit;\n opacity: 0.75;\n}\n.rf0-dt-tab--danger[aria-selected='true'] {\n border-bottom-color: #f0647c;\n}\n\n/* ---- panels -------------------------------------------------------- */\n.rf0-dt-panel {\n flex: 1;\n min-height: 84px;\n overflow: auto;\n padding: 6px 8px;\n scrollbar-width: thin;\n scrollbar-color: #2a3547 transparent;\n}\n.rf0-dt-empty {\n padding: 10px 2px;\n color: #4d5766;\n font-style: italic;\n}\n\n/* json tree */\n.rf0-dt-row {\n display: block;\n white-space: pre;\n tab-size: 2;\n}\n.rf0-dt-node-toggle {\n border: 0;\n padding: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n white-space: pre;\n}\n.rf0-dt-node-toggle:hover .rf0-dt-key { color: #dce3ec; }\n.rf0-dt-caret {\n display: inline-block;\n width: 1.2em;\n color: #4d5766;\n}\n.rf0-dt-key { color: #8b96a5; }\n.rf0-dt-punct { color: #4d5766; }\n.rf0-dt-string { color: #8fd68a; }\n.rf0-dt-number { color: #e2b93b; }\n.rf0-dt-boolean { color: #6fb3d9; }\n.rf0-dt-null { color: #55607080; font-style: italic; }\n\n/* errors / touched / dirty lists */\n.rf0-dt-item {\n padding: 3px 2px;\n border-bottom: 1px dotted #1a2230;\n display: flex;\n gap: 8px;\n align-items: baseline;\n}\n.rf0-dt-item:last-child { border-bottom: 0; }\n.rf0-dt-item-path {\n color: #aab5c4;\n word-break: break-all;\n}\n.rf0-dt-item-msg {\n color: #f0647c;\n word-break: break-word;\n}\n.rf0-dt-item-msg--ok { color: #8fd68a; }\n.rf0-dt-item-tag {\n flex: none;\n color: #4d5766;\n font-size: 10px;\n}\n.rf0-dt-item--touched .rf0-dt-item-path { color: #6fb3d9; }\n.rf0-dt-item--dirty .rf0-dt-item-path { color: #e2b93b; }\n\n/* ---- submit status + actions --------------------------------------- */\n.rf0-dt-status {\n display: flex;\n gap: 10px;\n padding: 4px 9px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n color: #6b7686;\n font-size: 10px;\n letter-spacing: 0.04em;\n white-space: nowrap;\n overflow: hidden;\n}\n.rf0-dt-status b { color: #aab5c4; font-weight: 400; }\n.rf0-dt-status .rf0-dt-on { color: #e2b93b; }\n.rf0-dt-status .rf0-dt-ok { color: #8fd68a; }\n.rf0-dt-status .rf0-dt-err { color: #f0647c; }\n.rf0-dt-actions {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-action {\n flex: 1;\n padding: 3px 0;\n border: 1px solid #2a3547;\n border-radius: 3px;\n background: #151b26;\n color: #c7d0dc;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.rf0-dt-action:hover { border-color: #3b4a61; background: #1a2230; color: #e7edf4; }\n.rf0-dt-action:active { transform: translateY(1px); }\n\n/* ---- collapsed badge ----------------------------------------------- */\n.rf0-dt-badge {\n position: fixed;\n z-index: 2147483000;\n width: 26px;\n height: 26px;\n border: 1px solid #2a3547;\n border-radius: 50%;\n background: #0c1017;\n color: #e2b93b;\n font: inherit;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.02em;\n cursor: pointer;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);\n}\n.rf0-dt-badge:hover { border-color: #e2b93b; }\n.rf0-dt-badge--top-right { top: 10px; right: 10px; }\n.rf0-dt-badge--bottom-right { bottom: 10px; right: 10px; }\n.rf0-dt-badge--top-left { top: 10px; left: 10px; }\n.rf0-dt-badge--bottom-left { bottom: 10px; left: 10px; }\n.rf0-dt-badge .rf0-dt-dot {\n position: absolute;\n top: 3px;\n right: 3px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: #f0647c;\n display: none;\n}\n.rf0-dt-badge--has-errors .rf0-dt-dot { display: block; }\n",document.head.appendChild(e)}();const s=e.useContext(p),i=t??s;if(!i)throw new Error("<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.");const[b,y]=e.useState(!0),[v,E]=e.useState("values"),k=e.useId().replace(/[^a-zA-Z0-9-]/g,""),N=c(i,"change",n.getValues.bind(null,i)),w=c(i,"errors",r.getErrors.bind(null,i)),C=f(i),j=function(e){return c(e,"change",d.bind(null,e))}(i),S=function(e){return c(e,"submitting",()=>e.isSubmitting)}(i),$=function(e){return c(e,"submitCount",()=>e.submitCount)}(i),O=c(i,"submitSuccessful",()=>i.isSubmitSuccessful);if(!b)return l.createElement("button",{type:"button",className:`rf0-dt-badge rf0-dt-badge--${o}${w.length>0?" rf0-dt-badge--has-errors":""}`,"aria-expanded":!1,"aria-label":`Open react-f0rm devtools (${w.length} errors)`,onClick:()=>y(!0)},"f0",l.createElement("span",{className:"rf0-dt-dot"}));const z={values:h(N),errors:w.length,touched:C.length,dirty:Object.keys(j).length};return l.createElement("section",{className:`rf0-dt rf0-dt--${o}`,"aria-label":"react-f0rm devtools"},l.createElement("header",{className:"rf0-dt-header"},l.createElement("span",{className:"rf0-dt-title"},"react-f0rm"),l.createElement("button",{type:"button",className:"rf0-dt-headerbtn","aria-label":"Collapse devtools",onClick:()=>y(!1)},"–")),l.createElement("div",{className:"rf0-dt-tablist",role:"tablist","aria-label":"Form state",tabIndex:-1,onKeyDown:e=>{const t={ArrowRight:1,ArrowLeft:-1}[e.key];if(!t)return;e.preventDefault();const n=g[(g.indexOf(v)+t+g.length)%g.length];E(n),document.getElementById(`${k}-tab-${n}`)?.focus()}},g.map(e=>l.createElement("button",{key:e,id:`${k}-tab-${e}`,type:"button",role:"tab",className:"rf0-dt-tab"+("errors"===e?" rf0-dt-tab--danger":""),"aria-selected":v===e,"aria-controls":`${k}-panel-${e}`,tabIndex:v===e?0:-1,onClick:()=>E(e)},e,l.createElement("span",{className:"rf0-dt-tab-count"},z[e])))),l.createElement("div",{id:`${k}-panel-${v}`,role:"tabpanel","aria-labelledby":`${k}-tab-${v}`,className:"rf0-dt-panel"},"values"===v&&l.createElement(u,{value:N}),"errors"===v&&(0===w.length?l.createElement("p",{className:"rf0-dt-empty"},"no errors"):w.map(({path:e,type:t,message:n},r)=>l.createElement("div",{key:`${e}:${r}`,className:"rf0-dt-item"},l.createElement("span",{className:"rf0-dt-item-path"},e),l.createElement("span",{className:"rf0-dt-item-msg"},n),l.createElement("span",{className:"rf0-dt-item-tag"},t)))),"touched"===v&&(0===C.length?l.createElement("p",{className:"rf0-dt-empty"},"no touched fields"):C.map(e=>l.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--touched"},l.createElement("span",{className:"rf0-dt-item-path"},e)))),"dirty"===v&&(0===Object.keys(j).length?l.createElement("p",{className:"rf0-dt-empty"},"no dirty fields"):Object.keys(j).map(e=>l.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--dirty"},l.createElement("span",{className:"rf0-dt-item-path"},e),l.createElement("span",{className:"rf0-dt-item-msg rf0-dt-item-msg--ok"},"changed"))))),l.createElement("p",{className:"rf0-dt-status","aria-live":"polite"},l.createElement("span",{className:S?"rf0-dt-on":void 0},"submitting ",l.createElement("b",null,String(S))),l.createElement("span",null,"submits ",l.createElement("b",null,$)),l.createElement("span",{className:x(O)},"ok"," ",l.createElement("b",null,void 0===O?"–":String(O)))),l.createElement("div",{className:"rf0-dt-actions"},l.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>n.reset(i,i.initialValues)},"Reset"),l.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>a.trigger(i)},"Validate")))};
1
+ "use strict";var e=require("react"),t=require("@for-fun/event-emitter"),n=require("../errors-TzyWwBfw.cjs.js"),r=require("../values-BbPnLByD.cjs.js"),a=require("../validate-2pX-N1O6.cjs.js");function o(e){var t=Object.create(null);return e&&Object.keys(e).forEach(function(n){if("default"!==n){var r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:function(){return e[n]}})}}),t.default=e,Object.freeze(t)}var s=o(e);function l(e){const t={};return function(e,t){for(const[r,a]of e.values){const o=n.segmentsFromKey(r);n.getDirtyBaseline(e,r,o)!==a&&t(o.join("."))}}(e,e=>{t[e]=!0}),t}function i(e){const t=n.getOrCreate(n.dirtyFieldsCaches,e,()=>({version:0,result:l(e)}));if(t.version>0){const n=l(e);(function(e,t){const n=Object.keys(e);return n.length===Object.keys(t).length&&n.every(e=>!0===t[e])})(t.result,n)||(t.result=n),t.version=0}return t.result}function c(n,r,a,o){const s="emitter"in n?n.emitter:n;return function(t,n,r){const a=e.useRef(null);null===a.current&&(a.current={hasValue:!1});const o=a.current,s=e.useRef(n);s.current=n;const l=e.useRef(r);l.current=r;const i=e.useCallback(()=>(o.hasValue||(o.value=s.current(),o.hasValue=!0),o.value),[o]),c=e.useCallback(e=>(o.hasValue=!1,t(()=>{const t=l.current;if(t&&o.hasValue){const n=s.current();if(t(o.value,n))return;return o.value=n,void e()}o.hasValue=!1,e()})),[t,o]);return e.useSyncExternalStore(c,i,i)}(e.useCallback(e=>t.on(s,r,e),[s,r]),a,o)}const d=e.createContext(null);d.Provider;e.createContext(null).Provider;function m({name:t,value:n,depth:r=0}){const[a,o]=e.useState(r<=1),l=12*r,i=void 0===t?null:s.createElement(s.Fragment,null,s.createElement("span",{className:"rf0-dt-key"},String(t)),s.createElement("span",{className:"rf0-dt-punct"},": "));if(null!==n&&"object"==typeof n){const e=Array.isArray(n),t=e?n.map((e,t)=>[t,e]):Object.entries(n),c=e?"[":"{",d=e?"]":"}",u=a?"":`${c}…${d} ${t.length}`;return s.createElement("div",{className:"rf0-dt-row",style:{paddingLeft:l}},s.createElement("button",{type:"button",className:"rf0-dt-node-toggle","aria-expanded":a,onClick:()=>o(!a)},s.createElement("span",{className:"rf0-dt-caret"},a?"▾":"▸"),i,s.createElement("span",{className:"rf0-dt-punct"},a?c:u)),a&&s.createElement(s.Fragment,null,t.map(([e,t])=>s.createElement(m,{key:String(e),name:e,value:t,depth:r+1})),s.createElement("span",{className:"rf0-dt-punct",style:{paddingLeft:l}},d)))}return s.createElement("span",{className:"rf0-dt-row",style:{paddingLeft:l,display:"block"}},i,s.createElement(u,{value:n}))}function u({value:e}){return void 0===e?s.createElement("span",{className:"rf0-dt-null"},"undefined"):null===e?s.createElement("span",{className:"rf0-dt-null"},"null"):"string"==typeof e?s.createElement("span",{className:"rf0-dt-string"},'"',e,'"'):"boolean"==typeof e?s.createElement("span",{className:"rf0-dt-boolean"},String(e)):s.createElement("span",{className:"rf0-dt-number"},String(e))}const f="react-f0rm-devtools-style";const p=["values","errors","touched","dirty","submits","subs","events"],b=["change","errors","touched","validating","submitting","submitCount","submitSuccessful","reset","disabled","status","loading","focusError"];function g(e){if(void 0!==e)return e?"rf0-dt-ok":"rf0-dt-err"}function h(e){return null===e||"object"!=typeof e?1:Object.values(e).reduce((e,t)=>e+h(t),0)}const x={ArrowRight:1,ArrowLeft:-1};function E({tab:e,idPrefix:t,counts:n,onSelect:r}){return s.createElement("div",{className:"rf0-dt-tablist",role:"tablist","aria-label":"Form state",tabIndex:-1,onKeyDown:n=>{const a=x[n.key];if(!a)return;n.preventDefault();const o=p[(p.indexOf(e)+a+p.length)%p.length];r(o),document.getElementById(`${t}-tab-${o}`)?.focus()}},p.map(a=>s.createElement("button",{key:a,id:`${t}-tab-${a}`,type:"button",role:"tab",className:"rf0-dt-tab"+("errors"===a?" rf0-dt-tab--danger":""),"aria-selected":e===a,"aria-controls":`${t}-panel-${a}`,tabIndex:e===a?0:-1,onClick:()=>r(a)},a,s.createElement("span",{className:"rf0-dt-tab-count"},n[a]))))}function v({path:e,type:t,message:n}){return s.createElement("div",{className:"rf0-dt-item"},s.createElement("span",{className:"rf0-dt-item-path"},e),s.createElement("span",{className:"rf0-dt-item-msg"},n),s.createElement("span",{className:"rf0-dt-item-tag"},t))}function y({errors:e}){return 0===e.length?s.createElement("p",{className:"rf0-dt-empty"},"no errors"):s.createElement(s.Fragment,null,e.map(({path:e,type:t,message:n},r)=>s.createElement(v,{key:`${e}:${r}`,path:e,type:t,message:n})))}function k({touched:e}){return 0===e.length?s.createElement("p",{className:"rf0-dt-empty"},"no touched fields"):s.createElement(s.Fragment,null,e.map(e=>s.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--touched"},s.createElement("span",{className:"rf0-dt-item-path"},e))))}function N({dirty:e}){const t=Object.keys(e);return 0===t.length?s.createElement("p",{className:"rf0-dt-empty"},"no dirty fields"):s.createElement(s.Fragment,null,t.map(e=>s.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--dirty"},s.createElement("span",{className:"rf0-dt-item-path"},e),s.createElement("span",{className:"rf0-dt-item-msg rf0-dt-item-msg--ok"},"changed"))))}function w({submits:e}){return 0===e.length?s.createElement("p",{className:"rf0-dt-empty"},"no submits yet"):s.createElement(s.Fragment,null,e.map(e=>s.createElement(C,{key:`${e.n}-${e.at}`,trace:e})))}function C({trace:e}){return s.createElement("details",{className:"rf0-dt-submit"},s.createElement("summary",{className:"rf0-dt-submit-summary"},s.createElement("span",{className:"rf0-dt-submit-n"},"#",e.n),s.createElement("span",{className:g(e.ok)},void 0===(t=e.ok)?"–":t?"ok":"failed"),s.createElement("span",{className:"rf0-dt-submit-time"},new Date(e.at).toLocaleTimeString())),e.errors.length>0&&s.createElement("div",{className:"rf0-dt-submit-errors"},s.createElement(y,{errors:e.errors})),s.createElement(m,{value:e.values}));var t}function S({events:e,onClear:t}){return 0===e.length?s.createElement("p",{className:"rf0-dt-empty"},"no events yet"):s.createElement(s.Fragment,null,s.createElement("div",null,s.createElement("button",{type:"button",className:"rf0-dt-action",onClick:t},"Clear")),e.map(e=>s.createElement("div",{key:`${e.n}:${e.at}`,className:"rf0-dt-item"},s.createElement("span",{className:"rf0-dt-item-tag"},e.event),s.createElement("span",{className:"rf0-dt-item-path"},e.path??"—"),s.createElement("span",{className:"rf0-dt-item-msg rf0-dt-item-msg--ok"},new Date(e.at).toLocaleTimeString()))))}function j({stats:e}){return s.createElement(s.Fragment,null,b.map(t=>{const n=e[t];return s.createElement("div",{key:t,className:"rf0-dt-item"},s.createElement("span",{className:"rf0-dt-item-tag"},t),s.createElement("span",{className:"rf0-dt-item-path"},n?.last??"—"),s.createElement("span",{className:"rf0-dt-item-msg rf0-dt-item-msg--ok"},n?.count??0))}))}function $({tab:e,values:t,errors:n,touched:r,dirty:a,submits:o,stats:l,events:i,onClearEvents:c}){return"values"===e?s.createElement(m,{value:t}):"errors"===e?s.createElement(y,{errors:n}):"touched"===e?s.createElement(k,{touched:r}):"dirty"===e?s.createElement(N,{dirty:a}):"submits"===e?s.createElement(w,{submits:o}):"subs"===e?s.createElement(j,{stats:l}):s.createElement(S,{events:i,onClear:c})}exports.Devtools=function({form:o,position:l="top-right"}){!function(){if("undefined"==typeof document)return;if(document.getElementById(f))return;const e=document.createElement("style");e.id=f,e.textContent="\n.rf0-dt {\n position: fixed;\n z-index: 2147483000;\n box-sizing: border-box;\n width: 308px;\n max-width: calc(100vw - 16px);\n max-height: min(70vh, 560px);\n display: flex;\n flex-direction: column;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 11px;\n line-height: 1.45;\n color: #c7d0dc;\n background: #0c1017;\n border: 1px solid #1f2735;\n border-radius: 4px;\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4);\n}\n.rf0-dt *,\n.rf0-dt-badge * {\n box-sizing: border-box;\n}\n.rf0-dt--top-right { top: 8px; right: 8px; }\n.rf0-dt--bottom-right { bottom: 8px; right: 8px; }\n.rf0-dt--top-left { top: 8px; left: 8px; }\n.rf0-dt--bottom-left { bottom: 8px; left: 8px; }\n\n/* ---- header -------------------------------------------------------- */\n.rf0-dt-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 6px 5px 9px;\n border-bottom: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-title {\n flex: 1;\n min-width: 0;\n color: #8b96a5;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.rf0-dt-title::before {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-right: 6px;\n border-radius: 50%;\n background: #e2b93b;\n vertical-align: 1px;\n}\n.rf0-dt-headerbtn {\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 1px 5px;\n color: #8b96a5;\n background: transparent;\n font: inherit;\n font-size: 10px;\n cursor: pointer;\n}\n.rf0-dt-headerbtn:hover { color: #dce3ec; border-color: #2a3547; }\n.rf0-dt-headerbtn:focus-visible,\n.rf0-dt-tab:focus-visible,\n.rf0-dt-action:focus-visible,\n.rf0-dt-badge:focus-visible,\n.rf0-dt-node-toggle:focus-visible {\n outline: 1px solid #e2b93b;\n outline-offset: 1px;\n}\n\n/* ---- tabs ---------------------------------------------------------- */\n.rf0-dt-tablist {\n display: flex;\n border-bottom: 1px solid #1f2735;\n background: #0e131b;\n}\n.rf0-dt-tab {\n flex: 1;\n padding: 4px 2px 5px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: #6b7686;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n white-space: nowrap;\n}\n.rf0-dt-tab:hover { color: #aab5c4; }\n.rf0-dt-tab[aria-selected='true'] {\n color: #e7edf4;\n border-bottom-color: #e2b93b;\n}\n.rf0-dt-tab-count {\n margin-left: 3px;\n color: inherit;\n opacity: 0.75;\n}\n.rf0-dt-tab--danger[aria-selected='true'] {\n border-bottom-color: #f0647c;\n}\n\n/* ---- panels -------------------------------------------------------- */\n.rf0-dt-panel {\n flex: 1;\n min-height: 84px;\n overflow: auto;\n padding: 6px 8px;\n scrollbar-width: thin;\n scrollbar-color: #2a3547 transparent;\n}\n.rf0-dt-empty {\n padding: 10px 2px;\n color: #4d5766;\n font-style: italic;\n}\n\n/* json tree */\n.rf0-dt-row {\n display: block;\n white-space: pre;\n tab-size: 2;\n}\n.rf0-dt-node-toggle {\n border: 0;\n padding: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n white-space: pre;\n}\n.rf0-dt-node-toggle:hover .rf0-dt-key { color: #dce3ec; }\n.rf0-dt-caret {\n display: inline-block;\n width: 1.2em;\n color: #4d5766;\n}\n.rf0-dt-key { color: #8b96a5; }\n.rf0-dt-punct { color: #4d5766; }\n.rf0-dt-string { color: #8fd68a; }\n.rf0-dt-number { color: #e2b93b; }\n.rf0-dt-boolean { color: #6fb3d9; }\n.rf0-dt-null { color: #55607080; font-style: italic; }\n\n/* errors / touched / dirty lists */\n.rf0-dt-item {\n padding: 3px 2px;\n border-bottom: 1px dotted #1a2230;\n display: flex;\n gap: 8px;\n align-items: baseline;\n}\n.rf0-dt-item:last-child { border-bottom: 0; }\n.rf0-dt-item-path {\n color: #aab5c4;\n word-break: break-all;\n}\n.rf0-dt-item-msg {\n color: #f0647c;\n word-break: break-word;\n}\n.rf0-dt-item-msg--ok { color: #8fd68a; }\n.rf0-dt-item-tag {\n flex: none;\n color: #4d5766;\n font-size: 10px;\n}\n.rf0-dt-item--touched .rf0-dt-item-path { color: #6fb3d9; }\n.rf0-dt-item--dirty .rf0-dt-item-path { color: #e2b93b; }\n\n/* ---- submit status + actions --------------------------------------- */\n.rf0-dt-status {\n display: flex;\n gap: 10px;\n padding: 4px 9px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n color: #6b7686;\n font-size: 10px;\n letter-spacing: 0.04em;\n white-space: nowrap;\n overflow: hidden;\n}\n.rf0-dt-status b { color: #aab5c4; font-weight: 400; }\n.rf0-dt-status .rf0-dt-on { color: #e2b93b; }\n.rf0-dt-status .rf0-dt-ok { color: #8fd68a; }\n.rf0-dt-status .rf0-dt-err { color: #f0647c; }\n.rf0-dt-actions {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-action {\n flex: 1;\n padding: 3px 0;\n border: 1px solid #2a3547;\n border-radius: 3px;\n background: #151b26;\n color: #c7d0dc;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.rf0-dt-action:hover { border-color: #3b4a61; background: #1a2230; color: #e7edf4; }\n.rf0-dt-action:active { transform: translateY(1px); }\n\n/* ---- collapsed badge ----------------------------------------------- */\n.rf0-dt-badge {\n position: fixed;\n z-index: 2147483000;\n width: 26px;\n height: 26px;\n border: 1px solid #2a3547;\n border-radius: 50%;\n background: #0c1017;\n color: #e2b93b;\n font: inherit;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.02em;\n cursor: pointer;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);\n}\n.rf0-dt-badge:hover { border-color: #e2b93b; }\n.rf0-dt-badge--top-right { top: 10px; right: 10px; }\n.rf0-dt-badge--bottom-right { bottom: 10px; right: 10px; }\n.rf0-dt-badge--top-left { top: 10px; left: 10px; }\n.rf0-dt-badge--bottom-left { bottom: 10px; left: 10px; }\n.rf0-dt-badge .rf0-dt-dot {\n position: absolute;\n top: 3px;\n right: 3px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: #f0647c;\n display: none;\n}\n.rf0-dt-badge--has-errors .rf0-dt-dot { display: block; }\n\n/* ---- submit traces -------------------------------------------------- */\n.rf0-dt-submit { border-bottom: 1px solid #1f2735; }\n.rf0-dt-submit:last-child { border-bottom: none; }\n.rf0-dt-submit-summary {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n cursor: pointer;\n list-style: none;\n color: #8b96a5;\n}\n.rf0-dt-submit-summary::-webkit-details-marker { display: none; }\n.rf0-dt-submit-summary:hover { color: #dce3ec; }\n.rf0-dt-submit-n { color: #dce3ec; font-weight: 700; }\n.rf0-dt-submit-time { margin-left: auto; color: #56616f; }\n.rf0-dt-submit[open] .rf0-dt-submit-summary { border-bottom: 1px dashed #1f2735; }\n.rf0-dt-submit-errors { padding: 2px 6px; }\n",document.head.appendChild(e)}();const m=e.useContext(d),u=o??m;if(!u)throw new Error("<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.");const[p,b]=e.useState(!0),[x,v]=e.useState("values"),y=e.useId().replace(/[^a-zA-Z0-9-]/g,""),k=c(u,"change",r.getValues.bind(null,u)),N=c(u,"errors",n.getErrors.bind(null,u)),w=function(e){return c(e,"touched",r.getTouchedFields.bind(null,e))}(u),C=function(e){return c(e,"change",i.bind(null,e))}(u),S=function(e){return c(e,"submitting",()=>e.isSubmitting)}(u),j=function(e){return c(e,"submitCount",()=>e.submitCount)}(u),O=c(u,"submitSuccessful",()=>u.isSubmitSuccessful),[F,z]=e.useState([]);e.useEffect(()=>t.on(u.emitter,"submitting",()=>{u.isSubmitting||queueMicrotask(()=>{u.isSubmitting||0===u.submitCount||z(e=>[{n:u.submitCount,ok:u.isSubmitSuccessful,at:Date.now(),values:r.getValues(u),errors:n.getErrors(u)},...e].slice(0,10))})}),[u]);const[D,V]=e.useState([]),[M,L]=e.useState({});if(e.useEffect(()=>{let e=0;const n=(t,n)=>{e+=1;const r={n:e,event:t,at:Date.now()};void 0!==n&&(r.path=n),V(e=>[r,...e].slice(0,50)),L(e=>{const r=e[t],a={count:(r?.count??0)+1};void 0!==n?a.last=n:void 0!==r?.last&&(a.last=r.last);const o={...e};return o[t]=a,o})},r=[];for(const e of["change","errors","touched","validating","disabled"])r.push(t.on(u.emitter,e,t=>n(e,t?t.value.join("."):void 0)));r.push(t.on(u.emitter,"focusError",e=>{try{n("focusError",JSON.parse(e).join("."))}catch{n("focusError",e)}}));for(const e of["submitting","submitCount","submitSuccessful","reset","status","loading"])r.push(t.on(u.emitter,e,()=>n(e)));return()=>r.forEach(e=>e())},[u]),!p)return s.createElement("button",{type:"button",className:`rf0-dt-badge rf0-dt-badge--${l}${N.length>0?" rf0-dt-badge--has-errors":""}`,"aria-expanded":!1,"aria-label":`Open react-f0rm devtools (${N.length} errors)`,onClick:()=>b(!0)},"f0",s.createElement("span",{className:"rf0-dt-dot"}));const P={values:h(k),errors:N.length,touched:w.length,dirty:Object.keys(C).length,submits:F.length,subs:Object.keys(M).length,events:D.length};return s.createElement("section",{className:`rf0-dt rf0-dt--${l}`,"aria-label":"react-f0rm devtools"},s.createElement("header",{className:"rf0-dt-header"},s.createElement("span",{className:"rf0-dt-title"},"react-f0rm"),s.createElement("button",{type:"button",className:"rf0-dt-headerbtn","aria-label":"Collapse devtools",onClick:()=>b(!1)},"–")),s.createElement(E,{tab:x,idPrefix:y,counts:P,onSelect:v}),s.createElement("div",{id:`${y}-panel-${x}`,role:"tabpanel","aria-labelledby":`${y}-tab-${x}`,className:"rf0-dt-panel"},s.createElement($,{tab:x,values:k,errors:N,touched:w,dirty:C,submits:F,stats:M,events:D,onClearEvents:()=>V([])})),s.createElement("p",{className:"rf0-dt-status","aria-live":"polite"},s.createElement("span",{className:S?"rf0-dt-on":void 0},"submitting ",s.createElement("b",null,String(S))),s.createElement("span",null,"submits ",s.createElement("b",null,j)),s.createElement("span",{className:g(O)},"ok"," ",s.createElement("b",null,void 0===O?"–":String(O)))),s.createElement("div",{className:"rf0-dt-actions"},s.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>r.reset(u,u.initialValues)},"Reset"),s.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>a.trigger(u)},"Validate")))};
2
2
  //# sourceMappingURL=index.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../../src/core/dirty.ts","../../src/core/touched.ts","../../src/hooks/form.tsx","../../src/context.ts","../../src/devtools/JsonTree.tsx","../../src/devtools/styles.ts","../../src/devtools/Devtools.tsx"],"sourcesContent":["import type {Path, PathSegments} from '../path';\nimport type {Form} from '../form';\nimport {dirtyFieldsCaches, getDirtyBaseline} from './internals';\n\n/**\n * Is one field dirty — the per-field rule behind `getFieldState`'s\n * `isDirty` and {@link useIsFieldDirty}: a live value exists at the path\n * and differs from the field's effective baseline (committed\n * `shouldDirty: false` baselines included). A leaf under a wholesale\n * ancestor write reports clean — dirtiness belongs to the branch that\n * actually diverged, the same attribution {@link getDirtyFields} applies.\n */\nexport function isFieldDirtyByPath(form: Form, path: Path): boolean {\n const live = form.values.get(path.key);\n return (\n form.values.has(path.key) &&\n getDirtyBaseline(form, path.key, path.value) !== live\n );\n}\n\n/**\n * Is dirty -- any value differs from initialValues\n * @param form\n */\nexport function isDirty(form: Form): boolean {\n let dirty = false;\n forEachDirtyField(form, () => {\n dirty = true;\n });\n return dirty;\n}\n\nfunction forEachDirtyField(form: Form, fn: (dottedKey: string) => void): void {\n for (const [key, value] of form.values) {\n const path = JSON.parse(key) as PathSegments;\n if (getDirtyBaseline(form, key, path) !== value) fn(path.join('.'));\n }\n}\n\n/** Per-path dirty-comparison baselines installed by writes with\n * `shouldDirty: false`: the written value becomes that field's baseline —\n * the write reads as a commit, not an edit. Module-private (like\n * {@link dirtyFieldsCaches}) so the Form shape is untouched for forms that\n * never opt in. */\n\nfunction computeDirtyFields(form: Form): Record<string, boolean> {\n const dirtyFields: Record<string, boolean> = {};\n forEachDirtyField(form, key => {\n dirtyFields[key] = true;\n });\n return dirtyFields;\n}\n\n/** Dirty entries only ever map to `true`, so equal key sets mean shallow\n * equal results. */\nfunction sameDirtyKeys(\n a: Record<string, boolean>,\n b: Record<string, boolean>\n): boolean {\n const aKeys = Object.keys(a);\n if (aKeys.length !== Object.keys(b).length) return false;\n return aKeys.every(key => b[key] === true);\n}\n\n/**\n * Get dirty fields -- fields whose current value differs from initialValues.\n * Keys are user-facing dotted paths ('a.b', 'a.0.c'), unlike the JSON array\n * keys stored in the values Map.\n * @param form\n * @return object mapping each dirty field's dotted path to true; the same\n * reference is returned until the dirty set actually changes\n */\nexport function getDirtyFields(form: Form): Record<string, boolean> {\n let cache = dirtyFieldsCaches.get(form);\n if (!cache) {\n cache = {version: 0, result: computeDirtyFields(form)};\n dirtyFieldsCaches.set(form, cache);\n } else if (cache.version > 0) {\n const result = computeDirtyFields(form);\n // Keep the old reference when the dirty set is unchanged (values always\n // map to true) so subscribers see identity-stable snapshots.\n if (!sameDirtyKeys(cache.result, result)) cache.result = result;\n cache.version = 0;\n }\n return cache.result;\n}\n\n/**\n * Get touched fields as user-facing dotted paths ('a.b', 'a.0.c'), unlike\n * the JSON array keys stored in the touched Set.\n * @param form\n * @return array of touched fields' dotted paths\n */\n","import {emit} from '../emitter';\nimport createPath from '../path';\nimport type {Name, Path, PathSegments} from '../path';\nimport type {FieldPath} from '../types';\nimport type {Form} from '../form';\n\n/**\n * Set field touched state\n * @param form\n * @param name\n */\nexport function setTouched(form: Form, name: Name): void {\n setTouchedByPath(form, createPath(name));\n}\n\n/**\n * Set field touched state\n * @param form\n * @param path\n */\nexport function setTouchedByPath({emitter, touched}: Form, path: Path): void {\n if (touched.has(path.key)) return;\n touched.add(path.key);\n // Path payload lets key-scoped subscribers (onKeyEvent) skip unrelated\n // fields; payload-less listeners ignore it.\n emit(emitter, 'touched', path);\n}\n\n/**\n * Check if field has been touched\n * @param form\n * @param name\n */\nexport function hasTouched<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P): boolean {\n return hasTouchedByPath(form, createPath(name));\n}\n\n/**\n * Check if field has been touched\n * @param form\n * @param path\n */\nexport function hasTouchedByPath({touched}: Form, path: Path): boolean {\n return touched.has(path.key);\n}\n\n/**\n * Is dirty -- any value differs from initialValues\n * @param form\n */\n/**\n * Get touched fields as user-facing dotted paths ('a.b', 'a.0.c'), unlike\n * the JSON array keys stored in the touched Set.\n * @param form\n * @return array of touched fields' dotted paths\n */\nexport function getTouchedFields({touched}: Form): string[] {\n return Array.from(touched, key =>\n (JSON.parse(key) as PathSegments).join('.')\n );\n}\n\n/**\n * Is touched -- any field has been touched\n * @param form\n */\nexport function isTouched({touched}: Form): boolean {\n return touched.size > 0;\n}\n\n/**\n * Remove a field: by default its live value drops out of reads and\n * `getValues()` (the path is tombstoned, so it never falls back to\n * initialValues), its dirty baseline, touched flag and errors are cleared.\n * The keep-flags preserve slices of that state instead.\n *\n * @param form\n * @param name\n */\n/**\n * Options accepted by {@link removeField}. All flags default to `false` —\n * the historical remove semantics (value dropped, path tombstoned, dirty\n * baseline/touched/errors cleared). Names mirror react-hook-form's\n * `unregister` options to ease migration; RHF's `shouldValidate` and\n * `keepDefaultValue` have no counterparts (removal never validates, and\n * the tombstone is exactly the \"do not revive from initialValues\" choice).\n */\n","import {\n useState,\n useEffect,\n useCallback,\n useRef,\n useSyncExternalStore\n} from 'react';\nimport {on} from '../emitter';\nimport type {EventEmitter} from '../emitter';\nimport {onKeyEvent, onPathEvent} from '../subscribe';\nimport type {SubscribeEvent, WatchScope} from '../subscribe';\nimport createForm, {\n FORM_ERROR,\n getErrorByPath,\n getFieldErrorsByPath,\n getValueByPath,\n getValues,\n hasTouchedByPath,\n hasErrors,\n isDirty,\n isFieldDirtyByPath,\n getDirtyFields,\n getTouchedFields,\n setInitialValues,\n runFormValidate\n} from '../form';\nimport type {FieldError, Form, FormEvents, Options} from '../form';\nimport type {FieldPath, PathValueOf} from '../types';\nimport createPath from '../path';\nimport type {PathSegments, Path} from '../path';\nimport {get, isEqual, isPromise} from '../util';\n\n/**\n * Create a form instance bound to this component.\n *\n * Beyond {@link Options}, the optional `values` object enables controlled\n * usage: when it genuinely changes it is re-synced into the form with\n * setInitialValues semantics -- uncommitted user edits are discarded\n * (master-detail semantics: selecting another record replaces the draft),\n * while touched flags and errors survive. Change detection is\n * reference-first with a structural fallback, so re-renders that pass an\n * inline literal with equal content never re-sync -- the user's\n * in-progress typing is never clobbered.\n */\nexport default function useForm<T extends Record<string, any> = any>(\n options?: Options<T> & {values?: T}\n): Form<T> {\n // Lazy initialization: createForm runs once per mount and the returned\n // instance is stable across re-renders (and StrictMode double renders),\n // without writing to refs during render. A provided `values` object is\n // seeded synchronously here (createForm does the same for initialValues)\n // so the first paint and SSR already reflect the controlled values.\n const [form] = useState(() => {\n const created = createForm<T>(options);\n if (options && options.values !== undefined) {\n setInitialValues(created, options.values);\n }\n return created;\n });\n const initialValues = options && options.initialValues;\n const values = options && options.values;\n\n // Track which initialValues source object the form was last seeded from.\n // Inline options create a fresh object every render, and re-seeding\n // clears the values Map (setInitialValues semantics), which would revert\n // every committed edit right after each re-render -- on the client and\n // after hydration alike. Memoized callers are covered by the reference\n // check; inline literals by the structural one, so only genuinely new\n // content re-seeds.\n const seededRef = useRef<{done: boolean; source: any} | null>(null);\n if (seededRef.current === null)\n seededRef.current = {done: false, source: undefined};\n\n useEffect(() => {\n // undefined = no baseline requested (createForm already defaulted to\n // {}): installing it would clear the values Map on mount for no\n // semantic gain — wiping any render-time useField initialValue seeds.\n // Async sources (Promise or thunk) are excluded too: createForm owns\n // their one-shot resolution, and a thunk's identity changes every\n // render, so re-seeding here would clobber the resolution cycle with\n // the raw function itself.\n if (initialValues === undefined) return;\n if (typeof initialValues === 'function' || isPromise(initialValues)) {\n return;\n }\n const seeded = seededRef.current!;\n if (\n seeded.done &&\n (seeded.source === initialValues || isEqual(seeded.source, initialValues))\n ) {\n return;\n }\n seeded.done = true;\n seeded.source = initialValues;\n setInitialValues(form, initialValues);\n }, [form, initialValues]);\n\n // Controlled values: re-sync only when the incoming object genuinely\n // differs from what the form was last seeded from. The reference check\n // is the fast path (memoized callers); inline literals get a fresh\n // object identity every render, so without the structural comparison\n // each re-render would clear the values Map (setInitialValues\n // semantics) and revert the user's uncommitted edits -- same hazard the\n // initialValues seed guard above protects against. Master-detail\n // semantics still apply whenever the content actually changed.\n const controlledRef = useRef<{done: boolean; source: any} | null>(null);\n if (controlledRef.current === null) {\n controlledRef.current = {done: false, source: undefined};\n }\n\n useEffect(() => {\n if (values === undefined) return;\n const seeded = controlledRef.current!;\n if (\n seeded.done &&\n (seeded.source === values || isEqual(seeded.source, values))\n ) {\n return;\n }\n seeded.done = true;\n seeded.source = values;\n setInitialValues(form, values);\n }, [form, values]);\n\n // Mount validation: the form-level `validate` runs once after mount when\n // the form opted into `validateOnMount` (field kicks are the fields' own\n // — useValidate schedules them per registration). Children's effects run\n // before the parent's, so field registrations are in place by the time\n // this fires. While an async initialValues source is pending, defer to\n // the 'loading' event — it fires after the resolved baseline has landed.\n // The run is fire-and-forget: a rejected validator is the submit path's\n // business, and a sync throw propagates exactly like trigger's.\n useEffect(() => {\n if (!form.validateOnMount || !form.validate) return;\n const run = () => {\n void runFormValidate(form).catch(() => undefined);\n };\n if (!form.isLoading) {\n run();\n return;\n }\n return on(form.emitter, 'loading', () => {\n run();\n });\n }, [form]);\n\n return form;\n}\n\n/** Per-hook snapshot cache for {@link useWatch}. */\ntype WatchCache<T> = {hasValue: boolean; value?: T};\n\n/**\n * Shared core of {@link useWatch} and the path-scoped hooks: a\n * useSyncExternalStore binding over a custom event subscription.\n * `subscribeFactory` receives the invalidate callback (drop the snapshot\n * cache, then notify React) and returns its unsubscribe function, so the\n * core stays identical whether the subscription is global or scoped to\n * one path.\n *\n * The optional `isEqual` comparator redirects `invalidate`: instead of\n * dropping the cache and waking React unconditionally, an event first\n * recomputes the getter and asks `isEqual` whether anything observable\n * changed — an equal verdict keeps the cached snapshot and skips the\n * notify entirely (no render at all), an unequal one stores the fresh\n * snapshot and notifies. Omitted, the historical drop-and-notify pipeline\n * runs byte-for-byte unchanged.\n */\nexport function useWatchCore<T>(\n subscribeFactory: (invalidate: () => void) => () => void,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): T {\n // useSyncExternalStore requires getSnapshot to return the same reference\n // until the store actually changed, otherwise React warns and loops.\n // Cache the snapshot per hook instance and recompute it only on the first\n // read and after the watched event fired.\n const cacheRef = useRef<WatchCache<T> | null>(null);\n if (cacheRef.current === null) cacheRef.current = {hasValue: false};\n const cache = cacheRef.current;\n\n // Hold the latest getter in a ref so getSnapshot keeps a stable identity\n // (callers pass a freshly bound function on every render) while still\n // recomputing with the most recent getter when the cache is invalid.\n const getterRef = useRef(getter);\n getterRef.current = getter;\n // Same freshness treatment for the comparator: invalidate is created once\n // per subscription, so it must read the latest isEqual through a ref\n // rather than capturing whichever instance the first render passed.\n const isEqualRef = useRef(isEqual);\n isEqualRef.current = isEqual;\n\n const getSnapshot = useCallback(() => {\n if (!cache.hasValue) {\n cache.value = getterRef.current();\n cache.hasValue = true;\n }\n return cache.value as T;\n }, [cache]);\n\n const subscribe = useCallback(\n (notify: () => void) => {\n // The form may have changed between render and this subscription, and\n // those events were missed: drop the cache. React's consistency check\n // right after subscribing recomputes and re-renders only when the\n // fresh value differs from the committed snapshot.\n cache.hasValue = false;\n const invalidate = () => {\n const compare = isEqualRef.current;\n if (compare && cache.hasValue) {\n // Custom comparator: decide before waking React. Equal means the\n // fresh getter result is observably the same — keep the cached\n // reference and return without notifying, so React never even\n // schedules a render. Unequal stores the fresh snapshot up front,\n // so React's own post-notify Object.is check reads it without\n // recomputing the getter.\n const next = getterRef.current();\n if (compare(cache.value as T, next)) return;\n cache.value = next;\n notify();\n return;\n }\n cache.hasValue = false;\n notify();\n };\n return subscribeFactory(invalidate);\n },\n [subscribeFactory, cache]\n );\n\n // Form state lives entirely in synchronously readable Map/Set structures\n // seeded from initialValues/values during the lazy useState initializer,\n // so the server snapshot is computed exactly like the client's first\n // render -- pass getSnapshot itself as getServerSnapshot and hydration\n // matches.\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Subscribe to a form event and keep the component's snapshot of `getter()`\n * in sync with the form state.\n *\n * Built on useSyncExternalStore, so snapshots taken while React renders are\n * guaranteed consistent (no tearing under concurrent rendering) and changes\n * emitted before the subscription effect runs are still picked up.\n *\n * The first argument is the form for the unified `fn(form, ...)` context\n * shape every hook shares; the raw emitter form remains accepted for\n * back-compat and for subscription sources that are not a full form.\n *\n * By default the re-render surface is the event's own scope: every emit\n * the subscription hears drops the snapshot cache and wakes React, which\n * then bails out when the recomputed snapshot is reference-identical\n * (Object.is) — the path/leaf scoping every built-in reader relies on.\n * The optional `isEqual` comparator exists for wide-scope getters that\n * return a fresh reference per call (a whole-values selector, say): each\n * event recomputes the getter and asks `isEqual` whether the result is\n * observably the same, and an equal verdict skips notifying React\n * altogether — no render, not even a bailed-out one. An unequal verdict\n * stores the new snapshot and re-renders. Same contract as TanStack's\n * `useSelector` compare. Omitted, behavior is unchanged.\n */\nexport function useWatch<T>(\n formOrEmitter: Form | EventEmitter<FormEvents>,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): T {\n // A form carries an `emitter` field the opaque emitter instance never\n // has, so the duck test cleanly discriminates the two accepted shapes.\n const emitter =\n 'emitter' in formOrEmitter ? formOrEmitter.emitter : formOrEmitter;\n const subscribeFactory = useCallback(\n (invalidate: () => void) => on(emitter, event, invalidate),\n [emitter, event]\n );\n return useWatchCore(subscribeFactory, getter, isEqual);\n}\n\n/** Options for {@link useValue} and {@link useValueByPath}. */\nexport type UseValueOptions<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n> = {\n /** Value to return while the field reads undefined — react-hook-form's\n * `useWatch` `defaultValue`: an untouched, never-seeded field reads\n * this instead of `undefined`. */\n defaultValue?: PathValueOf<T, P>;\n /** Watch descendants too (react-hook-form's `exact: false`): a write\n * to `a.b` invalidates a `useValue(form, 'a')` read, and the read\n * resolves the merged subtree (the copy-on-write `getValues` tree) so\n * descendant edits show up in the result. Defaults to true — the leaf\n * scope, where only the exact key and its ancestors invalidate (the\n * long-standing behavior). */\n exact?: boolean;\n};\n\n/**\n * Get field value state\n */\nexport function useValue<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P, options?: UseValueOptions<T, P>): PathValueOf<T, P> {\n return useValueByPath(form, createPath(name), options);\n}\n\n/**\n * Get field value state by path\n */\nexport function useValueByPath(\n form: Form,\n path: Path,\n options?: {defaultValue?: any; exact?: boolean}\n): any {\n const {emitter} = form;\n const {key} = path;\n const scope: WatchScope = options?.exact === false ? 'branch' : 'leaf';\n // 'leaf' scope: a leaf read depends only on its own key and its\n // ancestors' (getValueByPath fallback chain), so writes elsewhere --\n // siblings, descendants, string-prefix lookalikes ('[\"a\",\"bX\"]') -- never\n // invalidate the snapshot. 'branch' (exact: false) additionally wakes on\n // descendant writes and resolves the merged subtree through `getValues`\n // (getValueByPath only walks live ancestors, so a descendant edit would\n // otherwise read back a stale reference). Payload-less broadcasts\n // (reset, setInitialValues) still sync everything; removeField matches\n // by path.\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n onPathEvent(emitter, 'change', path, scope, invalidate),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- deps are `key` on purpose: useValue creates a fresh Path per render, so the object must stay out of the deps while the key string pins the subscription\n [emitter, key, scope]\n );\n return useWatchCore(subscribeFactory, () => {\n if (options?.exact === false) {\n return get(getValues(form), path.value);\n }\n const value = getValueByPath(form, path);\n return value === undefined ? options?.defaultValue : value;\n });\n}\n\n/**\n * Get field touched state\n */\nexport function useTouched<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P): boolean {\n return useTouchedByPath(form, createPath(name));\n}\n\n/**\n * Get field touched state by path\n */\nexport function useTouchedByPath(form: Form, path: Path): boolean {\n const {emitter} = form;\n const {key} = path;\n // Touched is stored per exact key, so only this field's own setTouched\n // (now emitted with its path) matters; payload-less broadcasts (reset,\n // removeField) still sync everything.\n const subscribeFactory = useCallback(\n (invalidate: () => void) => onKeyEvent(emitter, 'touched', key, invalidate),\n [emitter, key]\n );\n return useWatchCore(\n subscribeFactory,\n hasTouchedByPath.bind(null, form, path)\n );\n}\n\n/**\n * Get field error message state\n * @return current error's message string (display text), or undefined\n */\nexport function useError<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P): string | undefined {\n return useErrorByPath(form, createPath(name))?.message;\n}\n\n/**\n * Get field error state by path\n * @return current FieldError object ({type, message}), or undefined\n */\nexport function useErrorByPath(form: Form, path: Path): FieldError | undefined {\n const {emitter} = form;\n const {key} = path;\n // Errors are stored per exact key, so only writes to this field's error\n // (setErrorByPath now emits with its path) matter; payload-less\n // broadcasts (clearErrors, reset, removeField) still sync everything.\n const subscribeFactory = useCallback(\n (invalidate: () => void) => onKeyEvent(emitter, 'errors', key, invalidate),\n [emitter, key]\n );\n return useWatchCore(subscribeFactory, getErrorByPath.bind(null, form, path));\n}\n\n/**\n * Get all field errors\n * @return every error registered for the field (insertion order); an empty\n * array when the field has none\n */\nexport function useFieldErrors<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P): FieldError[] {\n return useFieldErrorsByPath(form, createPath(name));\n}\n\n/**\n * Get all field errors by path\n * @return every error registered for the field (insertion order); an empty\n * array when the field has none\n */\nexport function useFieldErrorsByPath(form: Form, path: Path): FieldError[] {\n const {emitter} = form;\n const {key} = path;\n // Same exact-key subscription and snapshot rules as useErrorByPath: the\n // getter returns the shared empty constant when clean and the stored\n // array by reference otherwise, so the useSyncExternalStore snapshot is\n // reference-stable between unrelated events.\n const subscribeFactory = useCallback(\n (invalidate: () => void) => onKeyEvent(emitter, 'errors', key, invalidate),\n [emitter, key]\n );\n return useWatchCore(\n subscribeFactory,\n getFieldErrorsByPath.bind(null, form, path)\n );\n}\n\nexport function useIsDirty(form: Form): boolean {\n // Dirty state is driven by value changes, not touch state: subscribe to\n // 'change' so typing flips this immediately, even before a blur.\n return useWatch(form, 'change', isDirty.bind(null, form));\n}\n\n/**\n * Get whether one field is dirty: its live value exists and differs from\n * the field's effective baseline — the same per-field rule\n * `getFieldState(form, name).isDirty` applies (committed\n * `shouldDirty: false` baselines included). Subscribes to 'change' at\n * leaf scope like {@link useValue}: own-key and ancestor writes re-check\n * the flag, payload-less broadcasts (reset, setInitialValues) always\n * sync, and writes elsewhere never re-render it.\n */\nexport function useIsFieldDirty<\n T extends Record<string, any> = any,\n P extends FieldPath<T> | PathSegments = FieldPath<T> | PathSegments\n>(form: Form<T>, name: P): boolean {\n return useIsFieldDirtyByPath(form, createPath(name));\n}\n\n/**\n * Get whether one field is dirty, by parsed path. See {@link\n * useIsFieldDirty}.\n */\nexport function useIsFieldDirtyByPath(form: Form, path: Path): boolean {\n const {emitter} = form;\n const {key} = path;\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n onPathEvent(emitter, 'change', path, 'leaf', invalidate),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- same key-pinning convention as useValueByPath\n [emitter, key]\n );\n return useWatchCore(subscribeFactory, () => isFieldDirtyByPath(form, path));\n}\n\n/**\n * Get dirty fields state -- object mapping each dirty field's user-facing\n * dotted path ('a.b', 'a.0.c') to true; recalculated after 'change' events\n */\nexport function useDirtyFields(form: Form): Record<string, boolean> {\n return useWatch(form, 'change', getDirtyFields.bind(null, form));\n}\n\n/**\n * Get touched fields state -- array of touched fields' user-facing dotted\n * paths ('a.b', 'a.0.c'); recalculated after 'touched' events\n */\nexport function useTouchedFields(form: Form): string[] {\n return useWatch(form, 'touched', getTouchedFields.bind(null, form));\n}\n\n/**\n * Aggregate snapshot of the whole form's state flags — the one-subscription\n * counterpart of react-hook-form's `formState` object (no errors object;\n * per-field error state stays with `useError`/`useFieldErrors`, and\n * `hasErrors`/`isValid` cover the whole-form questions).\n *\n * Recomputed on every state-bearing event; the field-wise comparator keeps\n * the returned reference stable while nothing observably changed, so\n * `useFormState(form).isDirty` re-renders no more often than the dedicated\n * {@link useIsDirty}. Cheaper than calling the granular hooks one by one\n * (one subscription and one snapshot instead of one per flag).\n */\nexport type FormState = {\n /** Any live value differs from its baseline (see {@link isDirty}). */\n isDirty: boolean;\n /** Dirty fields keyed by user-facing dotted path ('a.b', 'a.0.c'). */\n dirtyFields: Record<string, boolean>;\n /** At least one field is touched. */\n isTouched: boolean;\n /** Touched fields' user-facing dotted paths. */\n touchedFields: string[];\n /** Any error registered (field or form level). */\n hasErrors: boolean;\n /** No errors are registered — react-hook-form's `isValid` semantics.\n * In-flight validation is NOT factored in ({@link isValidating} is the\n * separate signal; async rounds temporarily pass this flag like RHF's). */\n isValid: boolean;\n isSubmitting: boolean;\n /**\n * Whether a submit has been attempted on this form — set on the first\n * `handleSubmit` call (validation outcome aside) and cleared by\n * `reset`, react-hook-form's `formState.isSubmitted` semantics. Read\n * after a failed submit to render a \"fix the errors below\" panel.\n */\n isSubmitted: boolean;\n /** Any validation round is running (field, form-level, or a pending\n * debounce window). */\n isValidating: boolean;\n isSubmitSuccessful: boolean | undefined;\n submitCount: number;\n /** Async initialValues still pending ({@link Form.isLoading}). */\n isLoading: boolean;\n /** The form-level disabled flag (fields OR their own `disabled`). */\n disabled: boolean;\n};\n\n/** Events any FormState field can react to: each recomputes the whole\n * snapshot — the comparator, not per-flag subscriptions, keeps the\n * re-render surface minimal. */\nconst FORM_STATE_EVENTS: readonly SubscribeEvent[] = [\n 'change',\n 'errors',\n 'touched',\n 'validating',\n 'submitting',\n 'submitCount',\n 'submitSuccessful',\n 'disabled',\n 'loading'\n];\n\nfunction getFormState(form: Form): FormState {\n return {\n isDirty: isDirty(form),\n dirtyFields: getDirtyFields(form),\n isTouched: form.touched.size > 0,\n touchedFields: getTouchedFields(form),\n hasErrors: hasErrors(form),\n isValid: !hasErrors(form),\n isSubmitting: form.isSubmitting,\n isSubmitted: form.isSubmitted,\n isValidating: form.validating.size > 0,\n isSubmitSuccessful: form.isSubmitSuccessful,\n submitCount: form.submitCount,\n isLoading: form.isLoading,\n disabled: form.disabled\n };\n}\n\n/** Field-wise equality: reference checks where the getter already memoizes\n * (dirtyFields), element-wise for the fresh array getTouchedFields builds,\n * value checks for the flags. */\nfunction isSameFormState(a: FormState, b: FormState): boolean {\n const sameTouched =\n a.touchedFields.length === b.touchedFields.length &&\n a.touchedFields.every((path, i) => path === b.touchedFields[i]);\n return (\n a.isDirty === b.isDirty &&\n a.dirtyFields === b.dirtyFields &&\n a.isTouched === b.isTouched &&\n sameTouched &&\n a.hasErrors === b.hasErrors &&\n a.isValid === b.isValid &&\n a.isSubmitting === b.isSubmitting &&\n a.isSubmitted === b.isSubmitted &&\n a.isValidating === b.isValidating &&\n a.isSubmitSuccessful === b.isSubmitSuccessful &&\n a.submitCount === b.submitCount &&\n a.isLoading === b.isLoading &&\n a.disabled === b.disabled\n );\n}\n\nexport function useFormState(form: Form): FormState {\n const getter = useCallback(() => getFormState(form), [form]);\n const subscribeFactory = useCallback(\n (invalidate: () => void) => {\n const offs = FORM_STATE_EVENTS.map(event =>\n on(form.emitter, event, invalidate)\n );\n return () => {\n for (const off of offs) off();\n };\n },\n [form.emitter]\n );\n return useWatchCore(subscribeFactory, getter, isSameFormState);\n}\n\nexport function useHasErrors(form: Form): boolean {\n return useWatch(form, 'errors', hasErrors.bind(null, form));\n}\n\n/**\n * Get whether the form currently has no errors — react-hook-form's\n * `formState.isValid` counterpart. Subscribes to the `'errors'` event only;\n * in-flight validation does not flip it (see {@link useIsValidating}).\n */\nexport function useIsValid(form: Form): boolean {\n return useWatch(form, 'errors', () => !hasErrors(form));\n}\n\nexport function useIsSubmitting(form: Form): boolean {\n return useWatch(form, 'submitting', () => form.isSubmitting);\n}\n\n/**\n * Get whether an async {@link Options.initialValues} source is still\n * pending — the flag a loading skeleton or a disabled submit button gates\n * on until the resolved baseline lands. Subscribes to the 'loading' event\n * the core emits around the resolution cycle.\n */\nexport function useIsLoading(form: Form): boolean {\n return useWatch(form, 'loading', () => form.isLoading);\n}\n\n/**\n * Get the form's user-owned metadata slot reactively (Formik's `status`\n * counterpart): any value the app stores through {@link setStatus} —\n * server session flags, wizard step state, non-field errors. Subscribes\n * to the payload-less 'status' event, so unrelated events never re-render\n * the caller, and the returned reference is stable between writes that\n * store an equal value (useSyncExternalStore's Object.is bailout).\n */\nexport function useStatus<T = any>(form: Form): T {\n return useWatch(form, 'status', () => form.status);\n}\n\n/**\n * Get whether the form accepts a submit right now:\n * `!isSubmitting && !hasErrors`. This is the single flag a submit\n * button's `disabled` prop wants — it is `false` for the whole async\n * `onSubmit` span (not just the validation pass) and whenever any field\n * holds an error (client validation or server backfill), replacing the\n * hand-rolled `useHasErrors(form) || useIsSubmitting(form)` pair.\n * Deliberately no dirty or validating semantics: an untouched-but-clean\n * form can submit.\n */\nexport function useCanSubmit(form: Form): boolean {\n const {emitter} = form;\n // canSubmit folds two events into one boolean: error writes\n // ('errors') and submit-state flips ('submitting'). useWatch subscribes\n // to a single event, so subscribe to both through useWatchCore — the\n // snapshot recomputes on either wake and re-renders only when the\n // boolean itself flips, so unrelated single-field error churn costs no\n // extra render (the same granularity useHasErrors already has).\n const subscribeFactory = useCallback(\n (invalidate: () => void) => {\n const offErrors = on(emitter, 'errors', invalidate);\n const offSubmitting = on(emitter, 'submitting', invalidate);\n return () => {\n offErrors();\n offSubmitting();\n };\n },\n [emitter]\n );\n return useWatchCore(\n subscribeFactory,\n () => !form.isSubmitting && !hasErrors(form)\n );\n}\n\nexport function useSubmitCount(form: Form): number {\n return useWatch(form, 'submitCount', () => form.submitCount);\n}\n\n/**\n * Get whether any validation round is currently running: a field\n * validator's pending `validateDebounce` window, an async field validator\n * still in flight, or the form-level validate's debounce window / in-flight\n * round — every one of them holds a key in `form.validating`, and the\n * 'validating' events they emit (field rounds with a path payload, the\n * form-level round as a payload-less broadcast) are what this subscribes\n * to. The boolean snapshot is Object.is-stable, so churn among the marks\n * (a second field opening while the first settles) costs no render while\n * the flag holds. This is the flag a submit button disables itself on, or\n * spins a spinner with, through the pre-submit validation pass — it flips\n * true the moment the first round opens and back false when the last one\n * settles.\n */\nexport function useIsValidating(form: Form): boolean {\n return useWatch(form, 'validating', () => form.validating.size > 0);\n}\n\n/**\n * Get whether the last submit succeeded: `true` once a submit's validation\n * and `onSubmit` completed without throwing, `false` after a failed submit\n * (validation rejection or a thrown callback) and before any submit ran —\n * the falsy reading of the undefined initial/reset state. Subscribes to\n * the 'submitSuccessful' event the core's setSubmitSuccessful emits, so\n * the flag flips in the same tick the outcome lands: the usual consumers\n * are a success banner and a redirect-on-success effect.\n */\nexport function useIsSubmitSuccessful(form: Form): boolean {\n return useWatch(\n form.emitter,\n 'submitSuccessful',\n () => !!form.isSubmitSuccessful\n );\n}\n\n/**\n * Get the form-level error message: the first error stored under the\n * reserved {@link FORM_ERROR} key, as display text — or undefined while\n * the slot is clean. That key is where a form-level `validate` record's\n * `_form` entry lands and where the Standard Schema adapter drops\n * path-less issues, so errors that belong to no single field still have a\n * reader. The classic usage renders it once above the submit button —\n * `useFormError(form) || null` — and the imperative twin is\n * `getError(form, FORM_ERROR)`.\n */\nexport function useFormError(form: Form): string | undefined {\n return useErrorByPath(form, createPath(FORM_ERROR))?.message;\n}\n\n/**\n * Get every form-level error: all errors stored under the reserved\n * {@link FORM_ERROR} key (insertion order), an empty array when the slot\n * is clean. The plural twin of {@link useFormError} for forms that stack\n * several form-level issues — each path-less Standard Schema issue lands\n * in this slot. The array reference is stable between unrelated events\n * (the stored array or a shared empty constant), so consumers can memo on\n * it; the imperative counterpart is `getFieldErrors(form, FORM_ERROR)`.\n */\nexport function useFormErrors(form: Form): FieldError[] {\n return useFieldErrorsByPath(form, createPath(FORM_ERROR));\n}\n","import {createContext, createElement, useContext, type ReactNode} from 'react';\nimport {\n useFieldCore,\n type UseFieldOptions,\n type UseFieldResult\n} from './hooks/field';\nimport {\n useFieldArrayCore,\n useFieldArrayItemCore,\n type UseFieldArrayOptions,\n type UseFieldArrayResult,\n type UseFieldArrayItemResult\n} from './hooks/fieldArray';\nimport type {FieldRules} from './rules';\nimport type {Form} from './form';\nimport type {Name, PathSegments} from './path';\nimport type {FieldPath} from './types';\n\nexport const FormContext = createContext<Form<any> | null>(null);\n\nexport const FormProvider = FormContext.Provider;\n\n/**\n * Read the form from the module-level {@link FormContext}. Pass the values\n * shape — `useFormContext<Values>()` — to get a fully typed `Form<Values>`\n * headless API; the `any` default keeps untyped call sites compiling.\n *\n * For multiple forms in one subtree use {@link createFormContext} instead.\n *\n * @throws when no `<FormProvider>` is mounted above the call site.\n */\nexport function useFormContext<T extends Record<string, any> = any>(): Form<T> {\n const form = useContext(FormContext);\n if (!form) throw new Error('no form provided');\n return form;\n}\n\n/**\n * Create an isolated bundle of form-context bindings: its own React context\n * plus `useField` / `useFieldArray` / `useFieldArrayItem` /\n * `useFormContext` hooks that resolve their form from it.\n *\n * Why: the module-level {@link FormContext} works fine for a single form per\n * subtree, but nesting two forms (or reusing a component inside a different\n * form) makes them fight over one context. Calling this factory once per app\n * area — `const Ctx = createFormContext<Values>()` — fixes the value shape\n * (`Ctx.useField({name: 'user.name'})` gets its `name` constrained by\n * `FieldPath<Values>` and its `value` typed accordingly), so call sites stop\n * hand-writing generics, and each instance's Provider scopes a strictly\n * separate form. The bundle also carries its raw React context\n * (`Ctx.context`) so `<Form context={Ctx.context}>` can provide into it.\n */\nexport function createFormContext<TValues extends Record<string, any> = any>() {\n const Context = createContext<Form<TValues> | null>(null);\n\n // A `form`-prop wrapper instead of exposing Context.Provider directly:\n // callers shouldn't have to know about the raw `value` prop shape.\n function FormProvider({\n form,\n children\n }: {\n form: Form<TValues>;\n children: ReactNode;\n }): ReactNode {\n return createElement(Context.Provider, {value: form}, children);\n }\n\n function useFormContext(): Form<TValues> {\n const form = useContext(Context);\n if (!form) throw new Error('no form provided');\n return form;\n }\n\n function useField<\n TPath extends FieldPath<TValues> | PathSegments =\n FieldPath<TValues> | PathSegments\n >(\n // The bare `{name: TPath}` member keeps `name` a direct inference site\n // for TPath instead of routing it through the mapped Omit type.\n // `form` is omitted on purpose — the form always comes from this\n // factory's own Context.\n options: {name: TPath} & Omit<UseFieldOptions<TValues, TPath>, 'form'>\n ): UseFieldResult<TValues, TPath> {\n return useFieldCore(options as UseFieldOptions<TValues, TPath>, Context);\n }\n\n function useFieldArray<K extends string = 'id'>(options: {\n name: FieldPath<TValues> | Name;\n keyName?: K;\n rules?: FieldRules;\n shouldUnregister?: boolean;\n }): UseFieldArrayResult<K> {\n return useFieldArrayCore(options as UseFieldArrayOptions<K>, Context);\n }\n\n function useFieldArrayItem<TValue = any>(options: {\n name: FieldPath<TValues> | Name;\n id: string;\n }): UseFieldArrayItemResult<TValue> {\n return useFieldArrayItemCore(options as {name: Name; id: string}, Context);\n }\n\n // The raw React context, for `<Form context={...}>`: the component keeps\n // its submit machinery while providing into this instance's private\n // context, so the bound hooks above resolve the form it manages.\n return {\n context: Context,\n FormProvider,\n useFormContext,\n useField,\n useFieldArray,\n useFieldArrayItem\n };\n}\n\nexport const CheckboxGroupContext = createContext<any>(null);\n\nexport const CheckboxGroupProvider = CheckboxGroupContext.Provider;\n\nexport function useCheckboxGroupContext(): any {\n const group = useContext(CheckboxGroupContext);\n if (!group) throw new Error('no group provided');\n return group;\n}\n","import * as React from 'react';\nimport {useState} from 'react';\nimport type {ReactNode} from 'react';\n\n/** Nodes deeper than this start collapsed. */\nconst DEFAULT_OPEN_DEPTH = 1;\n\n/**\n * Read-only inspection: the tree never mutates form state, so structural\n * sharing of the inspected value is safe and re-renders stay cheap.\n */\ntype JsonNodeProps = {\n /** Property name (or array index) rendering before the value. */\n name?: string | number;\n /** Value to render. */\n value: unknown;\n /** Current nesting depth (root is 0). */\n depth?: number;\n};\n\n/**\n * One line of the tree: either a collapsible container row\n * (`▸ key: {`) or a leaf (`key: value`).\n */\nfunction JsonNode({name, value, depth = 0}: JsonNodeProps) {\n const [open, setOpen] = useState(depth <= DEFAULT_OPEN_DEPTH);\n\n const label =\n name === undefined ? null : (\n <>\n <span className=\"rf0-dt-key\">{String(name)}</span>\n <span className=\"rf0-dt-punct\">: </span>\n </>\n );\n\n if (value !== null && typeof value === 'object') {\n const isArray = Array.isArray(value);\n const entries: Array<[string | number, unknown]> = isArray\n ? (value as unknown[]).map((v, i) => [i, v])\n : Object.entries(value as Record<string, unknown>);\n const openBracket = isArray ? '[' : '{';\n const closeBracket = isArray ? ']' : '}';\n const summary = open\n ? ''\n : `${openBracket}…${closeBracket} ${entries.length}`;\n\n return (\n <div className=\"rf0-dt-row\" style={{paddingLeft: depth * 12}}>\n <button\n type=\"button\"\n className=\"rf0-dt-node-toggle\"\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n <span className=\"rf0-dt-caret\">{open ? '▾' : '▸'}</span>\n {label}\n <span className=\"rf0-dt-punct\">{open ? openBracket : summary}</span>\n </button>\n {open && (\n <>\n {entries.map(([k, v]) => (\n <JsonNode key={String(k)} name={k} value={v} depth={depth + 1} />\n ))}\n <span className=\"rf0-dt-punct\" style={{paddingLeft: depth * 12}}>\n {closeBracket}\n </span>\n </>\n )}\n </div>\n );\n }\n\n return (\n <span\n className=\"rf0-dt-row\"\n style={{paddingLeft: depth * 12, display: 'block'}}\n >\n {label}\n <Primitive value={value} />\n </span>\n );\n}\n\n/** Render a primitive leaf with terminal-style type coloring. */\nfunction Primitive({value}: {value: unknown}): ReactNode {\n if (value === undefined)\n return <span className=\"rf0-dt-null\">undefined</span>;\n if (value === null) return <span className=\"rf0-dt-null\">null</span>;\n if (typeof value === 'string')\n return <span className=\"rf0-dt-string\">&quot;{value}&quot;</span>;\n if (typeof value === 'boolean')\n return <span className=\"rf0-dt-boolean\">{String(value)}</span>;\n return <span className=\"rf0-dt-number\">{String(value)}</span>;\n}\n\nexport default JsonNode;\n","/**\n * Stylesheet for the Devtools panel.\n *\n * Zero runtime dependencies by design: a single CSS string injected once\n * into <head> (idempotent across module reloads and multiple bundles).\n *\n * Aesthetic: instrument panel / terminal — near-black layers, monospace\n * stack, dense rows, hairline borders. Semantic colors only: error red,\n * success green, neutral gray, with one dim amber accent for the active\n * tab indicator and the collapsed badge.\n */\n\nconst CSS = `\n.rf0-dt {\n position: fixed;\n z-index: 2147483000;\n box-sizing: border-box;\n width: 308px;\n max-width: calc(100vw - 16px);\n max-height: min(70vh, 560px);\n display: flex;\n flex-direction: column;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 11px;\n line-height: 1.45;\n color: #c7d0dc;\n background: #0c1017;\n border: 1px solid #1f2735;\n border-radius: 4px;\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4);\n}\n.rf0-dt *,\n.rf0-dt-badge * {\n box-sizing: border-box;\n}\n.rf0-dt--top-right { top: 8px; right: 8px; }\n.rf0-dt--bottom-right { bottom: 8px; right: 8px; }\n.rf0-dt--top-left { top: 8px; left: 8px; }\n.rf0-dt--bottom-left { bottom: 8px; left: 8px; }\n\n/* ---- header -------------------------------------------------------- */\n.rf0-dt-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 6px 5px 9px;\n border-bottom: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-title {\n flex: 1;\n min-width: 0;\n color: #8b96a5;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.rf0-dt-title::before {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-right: 6px;\n border-radius: 50%;\n background: #e2b93b;\n vertical-align: 1px;\n}\n.rf0-dt-headerbtn {\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 1px 5px;\n color: #8b96a5;\n background: transparent;\n font: inherit;\n font-size: 10px;\n cursor: pointer;\n}\n.rf0-dt-headerbtn:hover { color: #dce3ec; border-color: #2a3547; }\n.rf0-dt-headerbtn:focus-visible,\n.rf0-dt-tab:focus-visible,\n.rf0-dt-action:focus-visible,\n.rf0-dt-badge:focus-visible,\n.rf0-dt-node-toggle:focus-visible {\n outline: 1px solid #e2b93b;\n outline-offset: 1px;\n}\n\n/* ---- tabs ---------------------------------------------------------- */\n.rf0-dt-tablist {\n display: flex;\n border-bottom: 1px solid #1f2735;\n background: #0e131b;\n}\n.rf0-dt-tab {\n flex: 1;\n padding: 4px 2px 5px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: #6b7686;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n white-space: nowrap;\n}\n.rf0-dt-tab:hover { color: #aab5c4; }\n.rf0-dt-tab[aria-selected='true'] {\n color: #e7edf4;\n border-bottom-color: #e2b93b;\n}\n.rf0-dt-tab-count {\n margin-left: 3px;\n color: inherit;\n opacity: 0.75;\n}\n.rf0-dt-tab--danger[aria-selected='true'] {\n border-bottom-color: #f0647c;\n}\n\n/* ---- panels -------------------------------------------------------- */\n.rf0-dt-panel {\n flex: 1;\n min-height: 84px;\n overflow: auto;\n padding: 6px 8px;\n scrollbar-width: thin;\n scrollbar-color: #2a3547 transparent;\n}\n.rf0-dt-empty {\n padding: 10px 2px;\n color: #4d5766;\n font-style: italic;\n}\n\n/* json tree */\n.rf0-dt-row {\n display: block;\n white-space: pre;\n tab-size: 2;\n}\n.rf0-dt-node-toggle {\n border: 0;\n padding: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n white-space: pre;\n}\n.rf0-dt-node-toggle:hover .rf0-dt-key { color: #dce3ec; }\n.rf0-dt-caret {\n display: inline-block;\n width: 1.2em;\n color: #4d5766;\n}\n.rf0-dt-key { color: #8b96a5; }\n.rf0-dt-punct { color: #4d5766; }\n.rf0-dt-string { color: #8fd68a; }\n.rf0-dt-number { color: #e2b93b; }\n.rf0-dt-boolean { color: #6fb3d9; }\n.rf0-dt-null { color: #55607080; font-style: italic; }\n\n/* errors / touched / dirty lists */\n.rf0-dt-item {\n padding: 3px 2px;\n border-bottom: 1px dotted #1a2230;\n display: flex;\n gap: 8px;\n align-items: baseline;\n}\n.rf0-dt-item:last-child { border-bottom: 0; }\n.rf0-dt-item-path {\n color: #aab5c4;\n word-break: break-all;\n}\n.rf0-dt-item-msg {\n color: #f0647c;\n word-break: break-word;\n}\n.rf0-dt-item-msg--ok { color: #8fd68a; }\n.rf0-dt-item-tag {\n flex: none;\n color: #4d5766;\n font-size: 10px;\n}\n.rf0-dt-item--touched .rf0-dt-item-path { color: #6fb3d9; }\n.rf0-dt-item--dirty .rf0-dt-item-path { color: #e2b93b; }\n\n/* ---- submit status + actions --------------------------------------- */\n.rf0-dt-status {\n display: flex;\n gap: 10px;\n padding: 4px 9px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n color: #6b7686;\n font-size: 10px;\n letter-spacing: 0.04em;\n white-space: nowrap;\n overflow: hidden;\n}\n.rf0-dt-status b { color: #aab5c4; font-weight: 400; }\n.rf0-dt-status .rf0-dt-on { color: #e2b93b; }\n.rf0-dt-status .rf0-dt-ok { color: #8fd68a; }\n.rf0-dt-status .rf0-dt-err { color: #f0647c; }\n.rf0-dt-actions {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-action {\n flex: 1;\n padding: 3px 0;\n border: 1px solid #2a3547;\n border-radius: 3px;\n background: #151b26;\n color: #c7d0dc;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.rf0-dt-action:hover { border-color: #3b4a61; background: #1a2230; color: #e7edf4; }\n.rf0-dt-action:active { transform: translateY(1px); }\n\n/* ---- collapsed badge ----------------------------------------------- */\n.rf0-dt-badge {\n position: fixed;\n z-index: 2147483000;\n width: 26px;\n height: 26px;\n border: 1px solid #2a3547;\n border-radius: 50%;\n background: #0c1017;\n color: #e2b93b;\n font: inherit;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.02em;\n cursor: pointer;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);\n}\n.rf0-dt-badge:hover { border-color: #e2b93b; }\n.rf0-dt-badge--top-right { top: 10px; right: 10px; }\n.rf0-dt-badge--bottom-right { bottom: 10px; right: 10px; }\n.rf0-dt-badge--top-left { top: 10px; left: 10px; }\n.rf0-dt-badge--bottom-left { bottom: 10px; left: 10px; }\n.rf0-dt-badge .rf0-dt-dot {\n position: absolute;\n top: 3px;\n right: 3px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: #f0647c;\n display: none;\n}\n.rf0-dt-badge--has-errors .rf0-dt-dot { display: block; }\n`;\n\nconst STYLE_ID = 'react-f0rm-devtools-style';\n\n/**\n * Inject the panel stylesheet into <head>. Idempotent: repeated calls\n * (module reloads, HMR, multiple Devtools mounts) never duplicate the\n * <style> element. No-ops outside a DOM environment (SSR).\n */\nexport function injectDevtoolsStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById(STYLE_ID)) return;\n const style = document.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n document.head.appendChild(style);\n}\n","import * as React from 'react';\nimport {useContext, useId, useState} from 'react';\nimport type {KeyboardEvent} from 'react';\nimport {FormContext} from '../context';\nimport {getErrors, getValues, reset, trigger} from '../form';\nimport type {FieldErrorEntry, Form} from '../form';\nimport {\n useDirtyFields,\n useIsSubmitting,\n useSubmitCount,\n useTouchedFields,\n useWatch\n} from '../hooks/form';\nimport JsonTree from './JsonTree';\nimport {injectDevtoolsStyles} from './styles';\n\n/** Corner the panel docks to. */\nexport type DevtoolsPosition =\n 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';\n\n/** Props for {@link Devtools}. */\nexport type DevtoolsProps<T extends Record<string, any> = any> = {\n /**\n * Form instance to inspect. When omitted, the panel reads the closest\n * `<Form>` / FormProvider ancestor and throws if there is none.\n */\n form?: Form<T>;\n /** Corner to dock the panel in. Defaults to `'top-right'`. */\n position?: DevtoolsPosition;\n};\n\ntype TabId = 'values' | 'errors' | 'touched' | 'dirty';\n\nconst TABS: TabId[] = ['values', 'errors', 'touched', 'dirty'];\n\n/** Status chip class for the submit-successful indicator. */\nfunction submitStatusClass(\n isSubmitSuccessful: boolean | undefined\n): string | undefined {\n if (isSubmitSuccessful === undefined) return undefined;\n return isSubmitSuccessful ? 'rf0-dt-ok' : 'rf0-dt-err';\n}\n\n/** Count primitive leaves of an inspected value tree. */\nfunction countLeaves(value: unknown): number {\n if (value === null || typeof value !== 'object') return 1;\n let count = 0;\n for (const v of Object.values(value as Record<string, unknown>)) {\n count += countLeaves(v);\n }\n return count;\n}\n\n/**\n * Live form inspector — a floating instrument panel for development.\n *\n * Renders four tabs (values / errors / touched / dirty), a submit status\n * strip (isSubmitting, submitCount, isSubmitSuccessful) and two actions:\n * Reset and Validate (full `trigger`). All state is read through the\n * library's own watch hooks, so the panel updates in real time without\n * participating in validation or submit flows. Docked at a corner,\n * collapsible to a small badge; fully keyboard operable.\n *\n * Ship it from the dedicated `react-f0rm/devtools` entry — it is never\n * re-exported by the main entry, so production bundles stay untouched.\n */\nexport default function Devtools<T extends Record<string, any> = any>({\n form,\n position = 'top-right'\n}: DevtoolsProps<T>) {\n // Idempotent + SSR-guarded; moving it off module scope keeps the\n // devtools entry free of import-time side effects (package.json\n // declares `sideEffects: false`, so bundlers may drop a bare\n // `import './styles'` in production builds).\n injectDevtoolsStyles();\n const contextForm = useContext(FormContext);\n const f: Form<any> | null = form ?? contextForm;\n if (!f) {\n throw new Error(\n '<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.'\n );\n }\n\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<TabId>('values');\n const idPrefix = useId().replace(/[^a-zA-Z0-9-]/g, '');\n\n // Live snapshots, straight through the public watch surface.\n const values = useWatch(f, 'change', getValues.bind(null, f));\n const errors = useWatch<FieldErrorEntry[]>(\n f,\n 'errors',\n getErrors.bind(null, f)\n );\n const touched = useTouchedFields(f);\n const dirty = useDirtyFields(f);\n const isSubmitting = useIsSubmitting(f);\n const submitCount = useSubmitCount(f);\n const isSubmitSuccessful = useWatch(\n f,\n 'submitSuccessful',\n () => f.isSubmitSuccessful\n );\n\n if (!open) {\n return (\n <button\n type=\"button\"\n className={`rf0-dt-badge rf0-dt-badge--${position}${\n errors.length > 0 ? ' rf0-dt-badge--has-errors' : ''\n }`}\n aria-expanded={false}\n aria-label={`Open react-f0rm devtools (${errors.length} errors)`}\n onClick={() => setOpen(true)}\n >\n f0\n <span className=\"rf0-dt-dot\" />\n </button>\n );\n }\n\n const counts: Record<TabId, number> = {\n values: countLeaves(values),\n errors: errors.length,\n touched: touched.length,\n dirty: Object.keys(dirty).length\n };\n\n /** Arrow-key tab navigation (buttons stay click/Enter/Space operable). */\n const onTabKeyDown = (e: KeyboardEvent) => {\n const deltas: Record<string, number> = {\n ArrowRight: 1,\n ArrowLeft: -1\n };\n const delta = deltas[e.key];\n if (!delta) return;\n e.preventDefault();\n const next = TABS[(TABS.indexOf(tab) + delta + TABS.length) % TABS.length];\n setTab(next);\n document.getElementById(`${idPrefix}-tab-${next}`)?.focus();\n };\n\n return (\n <section\n className={`rf0-dt rf0-dt--${position}`}\n aria-label=\"react-f0rm devtools\"\n >\n <header className=\"rf0-dt-header\">\n <span className=\"rf0-dt-title\">react-f0rm</span>\n <button\n type=\"button\"\n className=\"rf0-dt-headerbtn\"\n aria-label=\"Collapse devtools\"\n onClick={() => setOpen(false)}\n >\n –\n </button>\n </header>\n\n <div\n className=\"rf0-dt-tablist\"\n role=\"tablist\"\n aria-label=\"Form state\"\n tabIndex={-1}\n onKeyDown={onTabKeyDown}\n >\n {TABS.map(id => (\n <button\n key={id}\n id={`${idPrefix}-tab-${id}`}\n type=\"button\"\n role=\"tab\"\n className={`rf0-dt-tab${id === 'errors' ? ' rf0-dt-tab--danger' : ''}`}\n aria-selected={tab === id}\n aria-controls={`${idPrefix}-panel-${id}`}\n tabIndex={tab === id ? 0 : -1}\n onClick={() => setTab(id)}\n >\n {id}\n <span className=\"rf0-dt-tab-count\">{counts[id]}</span>\n </button>\n ))}\n </div>\n\n <div\n id={`${idPrefix}-panel-${tab}`}\n role=\"tabpanel\"\n aria-labelledby={`${idPrefix}-tab-${tab}`}\n className=\"rf0-dt-panel\"\n >\n {tab === 'values' && <JsonTree value={values} />}\n {tab === 'errors' &&\n (errors.length === 0 ? (\n <p className=\"rf0-dt-empty\">no errors</p>\n ) : (\n errors.map(({path, type, message}, index) => (\n // Same path can hold several errors now; index keeps keys\n // unique without changing what is rendered (messages may\n // legitimately repeat for one path).\n\n <div key={`${path}:${index}`} className=\"rf0-dt-item\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n <span className=\"rf0-dt-item-msg\">{message}</span>\n <span className=\"rf0-dt-item-tag\">{type}</span>\n </div>\n ))\n ))}\n {tab === 'touched' &&\n (touched.length === 0 ? (\n <p className=\"rf0-dt-empty\">no touched fields</p>\n ) : (\n touched.map(path => (\n <div key={path} className=\"rf0-dt-item rf0-dt-item--touched\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n </div>\n ))\n ))}\n {tab === 'dirty' &&\n (Object.keys(dirty).length === 0 ? (\n <p className=\"rf0-dt-empty\">no dirty fields</p>\n ) : (\n Object.keys(dirty).map(path => (\n <div key={path} className=\"rf0-dt-item rf0-dt-item--dirty\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n <span className=\"rf0-dt-item-msg rf0-dt-item-msg--ok\">\n changed\n </span>\n </div>\n ))\n ))}\n </div>\n\n <p className=\"rf0-dt-status\" aria-live=\"polite\">\n <span className={isSubmitting ? 'rf0-dt-on' : undefined}>\n submitting <b>{String(isSubmitting)}</b>\n </span>\n <span>\n submits <b>{submitCount}</b>\n </span>\n <span className={submitStatusClass(isSubmitSuccessful)}>\n ok{' '}\n <b>\n {isSubmitSuccessful === undefined\n ? '–'\n : String(isSubmitSuccessful)}\n </b>\n </span>\n </p>\n\n <div className=\"rf0-dt-actions\">\n <button\n type=\"button\"\n className=\"rf0-dt-action\"\n onClick={() => reset(f, f.initialValues)}\n >\n Reset\n </button>\n <button\n type=\"button\"\n className=\"rf0-dt-action\"\n onClick={() => trigger(f)}\n >\n Validate\n </button>\n </div>\n </section>\n );\n}\n"],"names":["computeDirtyFields","form","dirtyFields","fn","key","value","values","path","JSON","parse","getDirtyBaseline","join","forEachDirtyField","getDirtyFields","cache","dirtyFieldsCaches","get","version","result","a","b","aKeys","Object","keys","length","every","sameDirtyKeys","set","getTouchedFields","touched","Array","from","useWatch","formOrEmitter","event","getter","isEqual","emitter","subscribeFactory","cacheRef","useRef","current","hasValue","getterRef","isEqualRef","getSnapshot","useCallback","subscribe","notify","compare","next","useSyncExternalStore","useWatchCore","invalidate","on","useTouchedFields","bind","FormContext","createContext","Provider","JsonNode","name","depth","open","setOpen","useState","label","React","createElement","Fragment","className","String","isArray","entries","map","v","i","openBracket","closeBracket","summary","style","paddingLeft","type","onClick","k","display","Primitive","STYLE_ID","TABS","submitStatusClass","isSubmitSuccessful","countLeaves","count","position","document","getElementById","id","textContent","head","appendChild","injectDevtoolsStyles","contextForm","useContext","f","Error","tab","setTab","idPrefix","useId","replace","getValues","errors","getErrors","dirty","useDirtyFields","isSubmitting","useIsSubmitting","submitCount","useSubmitCount","counts","role","tabIndex","onKeyDown","e","delta","ArrowRight","ArrowLeft","preventDefault","indexOf","focus","JsonTree","message","index","reset","initialValues","trigger"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../../src/core/dirty.ts","../../src/hooks/form.tsx","../../src/context.ts","../../src/devtools/JsonTree.tsx","../../src/devtools/styles.ts","../../src/devtools/Devtools.tsx"],"sourcesContent":["import {segmentsFromKey} from '../path';\nimport type {Path} from '../path';\nimport type {Form} from '../form';\nimport {dirtyFieldsCaches, getDirtyBaseline, getOrCreate} from './internals';\n\n/** Is one field dirty — the per-field rule behind `getFieldState`'s\n * `isDirty`: a live value exists at the path and differs from the field's\n * effective baseline (committed `shouldDirty: false` baselines included).\n * A leaf under a wholesale ancestor write reports clean — dirtiness\n * belongs to the branch that actually diverged. */\nexport function isFieldDirtyByPath(form: Form, path: Path): boolean {\n const live = form.values.get(path.key);\n return (\n form.values.has(path.key) &&\n getDirtyBaseline(form, path.key, path.value) !== live\n );\n}\n\n/** Is dirty — any value differs from initialValues. */\nexport function isDirty(form: Form): boolean {\n for (const [key, value] of form.values) {\n const path = segmentsFromKey(key);\n if (getDirtyBaseline(form, key, path) !== value) return true;\n }\n return false;\n}\n\nfunction forEachDirtyField(form: Form, fn: (dottedKey: string) => void): void {\n for (const [key, value] of form.values) {\n const path = segmentsFromKey(key);\n if (getDirtyBaseline(form, key, path) !== value) fn(path.join('.'));\n }\n}\n\nfunction computeDirtyFields(form: Form): Record<string, boolean> {\n const dirtyFields: Record<string, boolean> = {};\n forEachDirtyField(form, key => {\n dirtyFields[key] = true;\n });\n return dirtyFields;\n}\n\n/** Dirty entries only ever map to `true`, so equal key sets mean shallow\n * equal results. */\nfunction sameDirtyKeys(\n a: Record<string, boolean>,\n b: Record<string, boolean>\n): boolean {\n const aKeys = Object.keys(a);\n if (aKeys.length !== Object.keys(b).length) return false;\n return aKeys.every(key => b[key] === true);\n}\n\n/** Get dirty fields — fields whose current value differs from\n * initialValues. Keys are user-facing dotted paths ('a.b', 'a.0.c'); the\n * same reference is returned until the dirty set actually changes. */\nexport function getDirtyFields(form: Form): Record<string, boolean> {\n const cache = getOrCreate(dirtyFieldsCaches, form, () => ({\n version: 0,\n result: computeDirtyFields(form)\n }));\n if (cache.version > 0) {\n const result = computeDirtyFields(form);\n // Keep the old reference when the dirty set is unchanged so subscribers\n // see identity-stable snapshots.\n if (!sameDirtyKeys(cache.result, result)) cache.result = result;\n cache.version = 0;\n }\n return cache.result;\n}\n","import {\n useState,\n useEffect,\n useCallback,\n useRef,\n useSyncExternalStore\n} from 'react';\nimport {on} from '../emitter';\nimport type {EventEmitter} from '../emitter';\nimport {onKeyEvent, onPathEvent} from '../subscribe';\nimport type {SubscribeEvent, WatchScope} from '../subscribe';\nimport createForm, {\n FORM_ERROR,\n getErrorByPath,\n getErrorsRecord,\n getErrorsTree,\n getFieldErrorsByPath,\n getValueByPath,\n getValues,\n hasTouchedByPath,\n hasErrors,\n isDirty,\n isFieldDirtyByPath,\n getDirtyFields,\n getTouchedFields,\n setInitialValues,\n runFormValidate,\n reset\n} from '../form';\nimport type {\n FieldError,\n FieldErrors,\n FieldErrorsTree,\n Form,\n FormEvents,\n Options,\n ResetOptions\n} from '../form';\nimport type {AnyPath, PathValueOf} from '../types';\nimport createPath from '../path';\nimport type {Path} from '../path';\nimport {get, isEqual, isPromise} from '../util';\nimport {pathFromKey} from './path';\n\n/** Seed bookkeeping for {@link useForm}'s baseline sync effects: what\n * source object was last seeded (`done` false = nothing seeded yet). */\ntype SeedTracker = {done: boolean; source: any};\n\n/** Seed `source` unless it is the same object — or structurally equal to —\n * the one the previous seed used: reference-first for memoized callers,\n * structural fallback for inline option literals, so only genuinely new\n * content re-seeds (re-seeding clears the values Map, reverting committed\n * edits). Shared by useForm's initialValues and controlled-values\n * effects. */\nfunction seedIfChanged(\n form: Form,\n seeded: SeedTracker,\n source: any,\n resetOptions?: ResetOptions\n): void {\n if (\n seeded.done &&\n (seeded.source === source || isEqual(seeded.source, source))\n ) {\n return;\n }\n seeded.done = true;\n seeded.source = source;\n // resetOptions reroutes the changed-content sync from setInitialValues'\n // baseline swap into full reset semantics — the keep* flags decide what\n // survives (keepDirtyValues keeps the user's uncommitted edits).\n if (resetOptions !== undefined) {\n reset(form, source, resetOptions);\n } else {\n setInitialValues(form, source);\n }\n}\n\n/**\n * Create a form instance bound to this component.\n *\n * Beyond {@link Options}, the optional `values` object enables controlled\n * usage: when it genuinely changes it is re-synced into the form with\n * setInitialValues semantics -- uncommitted user edits are discarded\n * (master-detail semantics: selecting another record replaces the draft),\n * while touched flags and errors survive. Change detection is\n * reference-first with a structural fallback, so re-renders that pass an\n * inline literal with equal content never re-sync -- the user's\n * in-progress typing is never clobbered. Passing `resetOptions` reroutes\n * those changed-content syncs through {@link reset} instead, so its keep*\n * flags govern what survives (`keepDirtyValues: true` keeps the user's\n * uncommitted edits on dirty fields); the reference/structural guard\n * applies to both routes.\n */\nexport default function useForm<T extends Record<string, any> = any>(\n options?: Options<T> & {values?: T; resetOptions?: ResetOptions}\n): Form<T> {\n // Lazy initialization: createForm runs once per mount and the returned\n // instance is stable across re-renders (and StrictMode double renders),\n // without writing to refs during render. A provided `values` object is\n // seeded synchronously here (createForm does the same for initialValues)\n // so the first paint and SSR already reflect the controlled values.\n const [form] = useState(() => {\n const created = createForm<T>(options);\n if (options && options.values !== undefined) {\n setInitialValues(created, options.values);\n }\n return created;\n });\n const initialValues = options && options.initialValues;\n const values = options && options.values;\n const resetOptions = options && options.resetOptions;\n\n // Track which initialValues source object the form was last seeded from\n // (the same-source guard lives in seedIfChanged): inline options create\n // a fresh object every render, and re-seeding clears the values Map\n // (setInitialValues semantics), which would revert every committed edit\n // right after each re-render -- on the client and after hydration alike.\n const seededRef = useRef<SeedTracker | null>(null);\n if (seededRef.current === null)\n seededRef.current = {done: false, source: undefined};\n\n useEffect(() => {\n // undefined = no baseline requested (createForm already defaulted to\n // {}): installing it would clear the values Map on mount for no\n // semantic gain — wiping any render-time useField initialValue seeds.\n // Async sources (Promise or thunk) are excluded too: createForm owns\n // their one-shot resolution, and a thunk's identity changes every\n // render, so re-seeding here would clobber the resolution cycle with\n // the raw function itself.\n if (initialValues === undefined) return;\n if (typeof initialValues === 'function' || isPromise(initialValues)) {\n return;\n }\n seedIfChanged(form, seededRef.current!, initialValues);\n }, [form, initialValues]);\n\n // Controlled values: re-sync only when the incoming object genuinely\n // differs from what the form was last seeded from (seedIfChanged's\n // reference + structural check) — otherwise each re-render's fresh\n // inline literal would clear the values Map and revert the user's\n // uncommitted edits, the same hazard the initialValues guard above\n // protects against. Master-detail semantics still apply whenever the\n // content actually changed, unless `resetOptions` reroutes the sync\n // through reset() so its keep* flags decide what survives.\n const controlledRef = useRef<SeedTracker | null>(null);\n if (controlledRef.current === null)\n controlledRef.current = {done: false, source: undefined};\n\n useEffect(() => {\n if (values === undefined) return;\n seedIfChanged(form, controlledRef.current!, values, resetOptions);\n }, [form, values, resetOptions]);\n\n // Mount validation: the form-level `validate` runs once after mount when\n // the form opted into `validateOnMount` (field kicks are the fields' own\n // — useValidate schedules them per registration). Children's effects run\n // before the parent's, so field registrations are in place by the time\n // this fires. While an async initialValues source is pending, defer to\n // the 'loading' event — it fires after the resolved baseline has landed.\n // The run is fire-and-forget: a rejected validator is the submit path's\n // business, and a sync throw propagates exactly like trigger's.\n useEffect(() => {\n if (!form.validateOnMount || !form.validate) return;\n const run = () => {\n void runFormValidate(form).catch(() => undefined);\n };\n if (!form.isLoading) {\n run();\n return;\n }\n return on(form.emitter, 'loading', () => {\n run();\n });\n }, [form]);\n\n return form;\n}\n\n/** Per-hook snapshot cache for {@link useWatch}. */\ntype WatchCache<T> = {hasValue: boolean; value?: T};\n\n/** Snapshot equality comparator — the `isEqual` contract {@link\n * useWatchCore} documents. */\ntype SnapshotComparator<T> = (prev: T, next: T) => boolean;\n\n/**\n * Shared core of {@link useWatch} and the path-scoped hooks: a\n * useSyncExternalStore binding over a custom event subscription.\n * `subscribeFactory` receives the invalidate callback (drop the snapshot\n * cache, then notify React) and returns its unsubscribe function, so the\n * core stays identical whether the subscription is global or scoped to\n * one path.\n *\n * The optional `isEqual` comparator redirects `invalidate`: instead of\n * dropping the cache and waking React unconditionally, an event first\n * recomputes the getter and asks `isEqual` whether anything observable\n * changed — an equal verdict keeps the cached snapshot and skips the\n * notify entirely (no render at all), an unequal one stores the fresh\n * snapshot and notifies. Omitted, the historical drop-and-notify pipeline\n * runs byte-for-byte unchanged.\n */\nexport function useWatchCore<T>(\n subscribeFactory: (invalidate: () => void) => () => void,\n getter: () => T,\n isEqual?: SnapshotComparator<T>\n): T {\n // useSyncExternalStore requires getSnapshot to return the same reference\n // until the store actually changed, otherwise React warns and loops.\n // Cache the snapshot per hook instance and recompute it only on the first\n // read and after the watched event fired.\n const cacheRef = useRef<WatchCache<T> | null>(null);\n if (cacheRef.current === null) cacheRef.current = {hasValue: false};\n const cache = cacheRef.current;\n\n // Hold the latest getter in a ref so getSnapshot keeps a stable identity\n // (callers pass a freshly bound function on every render) while still\n // recomputing with the most recent getter when the cache is invalid.\n const getterRef = useRef(getter);\n getterRef.current = getter;\n // Same freshness treatment for the comparator: invalidate is created once\n // per subscription, so it must read the latest isEqual through a ref\n // rather than capturing whichever instance the first render passed.\n const isEqualRef = useRef(isEqual);\n isEqualRef.current = isEqual;\n\n const getSnapshot = useCallback(() => {\n if (!cache.hasValue) {\n cache.value = getterRef.current();\n cache.hasValue = true;\n }\n return cache.value as T;\n }, [cache]);\n\n const subscribe = useCallback(\n (notify: () => void) => {\n // The form may have changed between render and this subscription, and\n // those events were missed: drop the cache. React's consistency check\n // right after subscribing recomputes and re-renders only when the\n // fresh value differs from the committed snapshot.\n cache.hasValue = false;\n const invalidate = () => {\n const compare = isEqualRef.current;\n if (compare && cache.hasValue) {\n // Custom comparator: decide before waking React. Equal means the\n // fresh getter result is observably the same — keep the cached\n // reference and return without notifying, so React never even\n // schedules a render. Unequal stores the fresh snapshot up front,\n // so React's own post-notify Object.is check reads it without\n // recomputing the getter.\n const next = getterRef.current();\n if (compare(cache.value as T, next)) return;\n cache.value = next;\n notify();\n return;\n }\n cache.hasValue = false;\n notify();\n };\n return subscribeFactory(invalidate);\n },\n [subscribeFactory, cache]\n );\n\n // Form state lives entirely in synchronously readable Map/Set structures\n // seeded from initialValues/values during the lazy useState initializer,\n // so the server snapshot is computed exactly like the client's first\n // render -- pass getSnapshot itself as getServerSnapshot and hydration\n // matches.\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Subscribe to a form event and keep the component's snapshot of `getter()`\n * in sync with the form state.\n *\n * Built on useSyncExternalStore, so snapshots taken while React renders are\n * guaranteed consistent (no tearing under concurrent rendering) and changes\n * emitted before the subscription effect runs are still picked up.\n *\n * The first argument is the form for the unified `fn(form, ...)` context\n * shape every hook shares; the raw emitter form remains accepted for\n * back-compat and for subscription sources that are not a full form.\n *\n * Framework-free counterpart: {@link watch} — the same event/getter/\n * `isEqual` contract exposed as a named export returning a\n * subscribe/getSnapshot handle any reactive runtime can bind to\n * (useWatch is its React binding; the ref-based cache here keeps inline\n * getters from re-subscribing per render).\n *\n * By default the re-render surface is the event's own scope: every emit\n * the subscription hears drops the snapshot cache and wakes React, which\n * then bails out when the recomputed snapshot is reference-identical\n * (Object.is) — the path/leaf scoping every built-in reader relies on.\n * The optional `isEqual` comparator exists for wide-scope getters that\n * return a fresh reference per call (a whole-values selector, say): each\n * event recomputes the getter and asks `isEqual` whether the result is\n * observably the same, and an equal verdict skips notifying React\n * altogether — no render, not even a bailed-out one. An unequal verdict\n * stores the new snapshot and re-renders. Same contract as TanStack's\n * `useSelector` compare. Omitted, behavior is unchanged.\n */\nexport function useWatch<T>(\n formOrEmitter: Form | EventEmitter<FormEvents>,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: SnapshotComparator<T>\n): T {\n // A form carries an `emitter` field the opaque emitter instance never\n // has, so the duck test cleanly discriminates the two accepted shapes.\n const emitter =\n 'emitter' in formOrEmitter ? formOrEmitter.emitter : formOrEmitter;\n const subscribeFactory = useCallback(\n (invalidate: () => void) => on(emitter, event, invalidate),\n [emitter, event]\n );\n return useWatchCore(subscribeFactory, getter, isEqual);\n}\n\n/** Options for {@link useValue} and {@link useValueByPath}. */\nexport type UseValueOptions<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n> = {\n /** Value to return while the field reads undefined — react-hook-form's\n * `useWatch` `defaultValue`: an untouched, never-seeded field reads\n * this instead of `undefined`. */\n defaultValue?: PathValueOf<T, P>;\n /** Watch descendants too (react-hook-form's `exact: false`): a write\n * to `a.b` invalidates a `useValue(form, 'a')` read, and the read\n * resolves the merged subtree (the copy-on-write `getValues` tree) so\n * descendant edits show up in the result. Defaults to true — the leaf\n * scope, where only the exact key and its ancestors invalidate (the\n * long-standing behavior). */\n exact?: boolean;\n};\n\n/**\n * Get field value state\n */\nexport function useValue<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n>(form: Form<T>, name: P, options?: UseValueOptions<T, P>): PathValueOf<T, P> {\n return useValueByPath(form, createPath(name), options);\n}\n\n/**\n * Get field value state by path\n */\nexport function useValueByPath(\n form: Form,\n path: Path,\n options?: {defaultValue?: any; exact?: boolean}\n): any {\n const {emitter} = form;\n const {key} = path;\n const scope: WatchScope = options?.exact === false ? 'branch' : 'leaf';\n // 'leaf' scope: a leaf read depends only on its own key and its\n // ancestors' (getValueByPath fallback chain), so writes elsewhere --\n // siblings, descendants, string-prefix lookalikes ('[\"a\",\"bX\"]') -- never\n // invalidate the snapshot. 'branch' (exact: false) additionally wakes on\n // descendant writes and resolves the merged subtree through `getValues`\n // (getValueByPath only walks live ancestors, so a descendant edit would\n // otherwise read back a stale reference). Payload-less broadcasts\n // (reset, setInitialValues) still sync everything; removeField matches\n // by path.\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n onPathEvent(emitter, 'change', pathFromKey(key), scope, invalidate),\n [emitter, key, scope]\n );\n return useWatchCore(subscribeFactory, () => {\n if (options?.exact === false) {\n return get(getValues(form), path.value);\n }\n const value = getValueByPath(form, path);\n return value === undefined ? options?.defaultValue : value;\n });\n}\n\n/**\n * Subscribe to the whole values tree: re-renders the calling component\n * whenever any 'change' event lands — react-hook-form's `watch()` with no\n * arguments. The snapshot is the memoized `getValues(form)` result, so\n * repeated reads during one render share one reference and, under\n * `__DEV__`, one frozen copy.\n *\n * Broad scope by design: read it in components that must stay cheap and\n * need the full tree; per-field readers should reach for {@link useValue}\n * instead so a keystroke re-renders exactly the affected field.\n */\nexport function useValues<T extends Record<string, any> = any>(\n form: Form<T>\n): T {\n // Arrow wrapper, not getValues.bind: bind collapses the generic, so the\n // bound getter would type as () => Record<string, any> and lose the T.\n return useWatch<T>(form, 'change', () => getValues(form));\n}\n\n/**\n * Get field touched state\n */\nexport function useTouched<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n>(form: Form<T>, name: P): boolean {\n return useTouchedByPath(form, createPath(name));\n}\n\n/** Exact-key subscription shared by the per-path readers whose state is\n * stored per key (touched, errors): the getter is passed through to\n * {@link useWatchCore} unchanged. */\nfunction useKeyWatch<T>(\n form: Form,\n path: Path,\n event: SubscribeEvent,\n getter: () => T\n): T {\n const {emitter} = form;\n const {key} = path;\n const subscribeFactory = useCallback(\n (invalidate: () => void) => onKeyEvent(emitter, event, key, invalidate),\n [emitter, event, key]\n );\n return useWatchCore(subscribeFactory, getter);\n}\n\n/**\n * Get field touched state by path\n */\nexport function useTouchedByPath(form: Form, path: Path): boolean {\n // Touched is stored per exact key, so only this field's own setTouched\n // (now emitted with its path) matters; payload-less broadcasts (reset,\n // removeField) still sync everything.\n return useKeyWatch(\n form,\n path,\n 'touched',\n hasTouchedByPath.bind(null, form, path)\n );\n}\n\n/**\n * Get field error message state\n * @return current error's message string (display text), or undefined\n */\nexport function useError<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n>(form: Form<T>, name: P): string | undefined {\n return useErrorByPath(form, createPath(name))?.message;\n}\n\n/**\n * Get field error state by path\n * @return current FieldError object ({type, message}), or undefined\n */\nexport function useErrorByPath(form: Form, path: Path): FieldError | undefined {\n // Errors are stored per exact key, so only writes to this field's error\n // (setErrorByPath now emits with its path) matter; payload-less\n // broadcasts (clearErrors, reset, removeField) still sync everything.\n return useKeyWatch(\n form,\n path,\n 'errors',\n getErrorByPath.bind(null, form, path)\n );\n}\n\n/**\n * Get all field errors\n * @return every error registered for the field (insertion order); an empty\n * array when the field has none\n */\nexport function useFieldErrors<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n>(form: Form<T>, name: P): FieldError[] {\n return useFieldErrorsByPath(form, createPath(name));\n}\n\n/**\n * Get all field errors by path\n * @return every error registered for the field (insertion order); an empty\n * array when the field has none\n */\nexport function useFieldErrorsByPath(form: Form, path: Path): FieldError[] {\n // Same exact-key subscription and snapshot rules as useErrorByPath: the\n // getter returns the shared empty constant when clean and the stored\n // array by reference otherwise, so the useSyncExternalStore snapshot is\n // reference-stable between unrelated events.\n return useKeyWatch(\n form,\n path,\n 'errors',\n getFieldErrorsByPath.bind(null, form, path)\n );\n}\n\nexport function useIsDirty(form: Form): boolean {\n // Dirty state is driven by value changes, not touch state: subscribe to\n // 'change' so typing flips this immediately, even before a blur.\n return useWatch(form, 'change', isDirty.bind(null, form));\n}\n\n/**\n * Get whether one field is dirty: its live value exists and differs from\n * the field's effective baseline — the same per-field rule\n * `getFieldState(form, name).isDirty` applies (committed\n * `shouldDirty: false` baselines included). Subscribes to 'change' at\n * leaf scope like {@link useValue}: own-key and ancestor writes re-check\n * the flag, payload-less broadcasts (reset, setInitialValues) always\n * sync, and writes elsewhere never re-render it.\n */\nexport function useIsFieldDirty<\n T extends Record<string, any> = any,\n P extends AnyPath<T> = AnyPath<T>\n>(form: Form<T>, name: P): boolean {\n return useIsFieldDirtyByPath(form, createPath(name));\n}\n\n/**\n * Get whether one field is dirty, by parsed path. See {@link\n * useIsFieldDirty}.\n */\nexport function useIsFieldDirtyByPath(form: Form, path: Path): boolean {\n const {emitter} = form;\n const {key} = path;\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n onPathEvent(emitter, 'change', pathFromKey(key), 'leaf', invalidate),\n [emitter, key]\n );\n return useWatchCore(subscribeFactory, () => isFieldDirtyByPath(form, path));\n}\n\n/**\n * Get dirty fields state -- object mapping each dirty field's user-facing\n * dotted path ('a.b', 'a.0.c') to true; recalculated after 'change' events\n */\nexport function useDirtyFields(form: Form): Record<string, boolean> {\n return useWatch(form, 'change', getDirtyFields.bind(null, form));\n}\n\n/**\n * Get touched fields state -- array of touched fields' user-facing dotted\n * paths ('a.b', 'a.0.c'); recalculated after 'touched' events\n */\nexport function useTouchedFields(form: Form): string[] {\n return useWatch(form, 'touched', getTouchedFields.bind(null, form));\n}\n\n/**\n * Aggregate snapshot of the whole form's state flags — the one-subscription\n * counterpart of react-hook-form's `formState` object (no errors object;\n * per-field error state stays with `useError`/`useFieldErrors`, and\n * `hasErrors`/`isValid` cover the whole-form questions).\n *\n * Recomputed on every state-bearing event; the field-wise comparator keeps\n * the returned reference stable while nothing observably changed, so\n * `useFormState(form).isDirty` re-renders no more often than the dedicated\n * {@link useIsDirty}. Cheaper than calling the granular hooks one by one\n * (one subscription and one snapshot instead of one per flag).\n */\nexport type FormState<T extends Record<string, any> = any> = {\n /** Any live value differs from its baseline (see {@link isDirty}). */\n isDirty: boolean;\n /** Dirty fields keyed by user-facing dotted path ('a.b', 'a.0.c'). */\n dirtyFields: Record<string, boolean>;\n /** At least one field is touched. */\n isTouched: boolean;\n /** Touched fields' user-facing dotted paths. */\n touchedFields: string[];\n /** Any error registered (field or form level). */\n hasErrors: boolean;\n /** No errors are registered — react-hook-form's `isValid` semantics.\n * In-flight validation is NOT factored in ({@link isValidating} is the\n * separate signal; async rounds temporarily pass this flag like RHF's). */\n isValid: boolean;\n /** Every error as one record keyed by user-facing dotted path\n * ('a.b', 'list.0') — react-hook-form's `formState.errors` shape,\n * typed as {@link FieldErrors} (per-key values optional). Memoized\n * (see {@link getErrorsRecord}): the reference is stable between error\n * writes, so the snapshot comparator can bail on it. */\n errors: FieldErrors<T>;\n isSubmitting: boolean;\n /**\n * Whether a submit has been attempted on this form — set on the first\n * `handleSubmit` call (validation outcome aside) and cleared by\n * `reset`, react-hook-form's `formState.isSubmitted` semantics. Read\n * after a failed submit to render a \"fix the errors below\" panel.\n */\n isSubmitted: boolean;\n /** Any validation round is running (field, form-level, or a pending\n * debounce window). */\n isValidating: boolean;\n isSubmitSuccessful: boolean | undefined;\n submitCount: number;\n /** Async initialValues still pending ({@link Form.isLoading}). */\n isLoading: boolean;\n /** The form-level disabled flag (fields OR their own `disabled`). */\n disabled: boolean;\n};\n\n/** Events any FormState field can react to: each recomputes the whole\n * snapshot — the comparator, not per-flag subscriptions, keeps the\n * re-render surface minimal. */\nconst FORM_STATE_EVENTS: readonly SubscribeEvent[] = [\n 'change',\n 'errors',\n 'touched',\n 'validating',\n 'submitting',\n 'submitCount',\n 'submitSuccessful',\n 'disabled',\n 'loading'\n];\n\n/** Subscribe to every state-bearing event at once — the wiring\n * {@link useFormState} and {@link useStore} share. */\nfunction subscribeFormStateEvents(\n emitter: EventEmitter<FormEvents>,\n invalidate: () => void\n): () => void {\n const offs = FORM_STATE_EVENTS.map(event => on(emitter, event, invalidate));\n return () => {\n for (const off of offs) off();\n };\n}\n\nfunction getFormState<T extends Record<string, any>>(\n form: Form<T>\n): FormState<T> {\n const anyErrors = hasErrors(form);\n return {\n isDirty: isDirty(form),\n dirtyFields: getDirtyFields(form),\n isTouched: form.touched.size > 0,\n touchedFields: getTouchedFields(form),\n hasErrors: anyErrors,\n isValid: !anyErrors,\n errors: getErrorsRecord(form),\n isSubmitting: form.isSubmitting,\n isSubmitted: form.isSubmitted,\n isValidating: form.validating.size > 0,\n isSubmitSuccessful: form.isSubmitSuccessful,\n submitCount: form.submitCount,\n isLoading: form.isLoading,\n disabled: form.disabled\n };\n}\n\n/** Field-wise equality: reference checks where the getter already memoizes\n * (dirtyFields), element-wise for the fresh array getTouchedFields builds,\n * value checks for the flags. */\nfunction isSameFormState<T extends Record<string, any>>(\n a: FormState<T>,\n b: FormState<T>\n): boolean {\n const sameTouched =\n a.touchedFields.length === b.touchedFields.length &&\n a.touchedFields.every((path, i) => path === b.touchedFields[i]);\n return (\n a.isDirty === b.isDirty &&\n a.dirtyFields === b.dirtyFields &&\n a.isTouched === b.isTouched &&\n sameTouched &&\n a.hasErrors === b.hasErrors &&\n a.isValid === b.isValid &&\n a.errors === b.errors &&\n a.isSubmitting === b.isSubmitting &&\n a.isSubmitted === b.isSubmitted &&\n a.isValidating === b.isValidating &&\n a.isSubmitSuccessful === b.isSubmitSuccessful &&\n a.submitCount === b.submitCount &&\n a.isLoading === b.isLoading &&\n a.disabled === b.disabled\n );\n}\n\nexport function useFormState<T extends Record<string, any> = any>(\n form: Form<T>\n): FormState<T> {\n const getter = useCallback(() => getFormState(form), [form]);\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n subscribeFormStateEvents(form.emitter, invalidate),\n [form.emitter]\n );\n return useWatchCore(subscribeFactory, getter, isSameFormState);\n}\n\n/**\n * The official selector primitive — TanStack Form's\n * `useStore(store, selector)` counterpart. Subscribes to every\n * state-bearing form event and keeps `selector()`'s result as the\n * snapshot; the selector itself is a plain closure (read the form through\n * any getter — `getValue`, `getValues`, `form.errors.size`, …).\n *\n * Without `isEqual` the subscription is object-identity based: each event\n * recomputes the selector and React bails out when the result is\n * reference-identical (the contract every `useWatch` reader relies on).\n * The optional `isEqual(prev, next)` comparator is for wide-scope\n * selectors that return a fresh reference per call (a whole-values\n * projection, say): an equal verdict skips notifying React altogether —\n * no render, not even a bailed-out one (TanStack `useSelector` compare\n * contract).\n *\n * `useFormState` is the built-in aggregate selector; {@link useWatch} is\n * the single-event version. Reach for `useStore` when the projection is\n * yours and spans state events — e.g. `form.validating.size > 0 &&\n * form.isSubmitting` as one flag.\n */\nexport function useStore<T>(\n form: Form,\n selector: () => T,\n isEqual?: SnapshotComparator<T>\n): T {\n const subscribeFactory = useCallback(\n (invalidate: () => void) =>\n subscribeFormStateEvents(form.emitter, invalidate),\n [form.emitter]\n );\n return useWatchCore(subscribeFactory, selector, isEqual);\n}\n\nexport function useHasErrors(form: Form): boolean {\n return useWatch(form, 'errors', hasErrors.bind(null, form));\n}\n\n/**\n * Get every error as one record keyed by user-facing dotted path\n * ('a.b', 'list.0') — react-hook-form's `formState.errors` shape, for\n * error-summary panels and a11y announcements. Values are the stored\n * FieldError[] arrays shared with the form (treat as read-only). The\n * record is memoized per form (see {@link getErrorsRecord}): the hook\n * re-renders only when an error write actually changed the record's\n * content.\n */\nexport function useErrors<T extends Record<string, any> = any>(\n form: Form<T>\n): FieldErrors<T> {\n return useWatch(form, 'errors', () => getErrorsRecord(form));\n}\n\n/**\n * Get every error as one nested object following the values tree\n * (`errors.items?.[0]?.name` reads) — the typed optional-chaining\n * counterpart of {@link useErrors}' flat dotted record. Leaves hold the\n * stored FieldError[] arrays shared with the form (treat as read-only).\n * Memoized alongside the record (see {@link getErrorsTree}): the hook\n * re-renders only when an error write actually changed the tree's\n * content.\n */\nexport function useErrorsTree<T extends Record<string, any> = any>(\n form: Form<T>\n): FieldErrorsTree<T> {\n return useWatch(form, 'errors', () => getErrorsTree(form));\n}\n\n/**\n * Get whether the form currently has no errors — react-hook-form's\n * `formState.isValid` counterpart. Subscribes to the `'errors'` event only;\n * in-flight validation does not flip it (see {@link useIsValidating}).\n */\nexport function useIsValid(form: Form): boolean {\n return useWatch(form, 'errors', () => !hasErrors(form));\n}\n\nexport function useIsSubmitting(form: Form): boolean {\n return useWatch(form, 'submitting', () => form.isSubmitting);\n}\n\n/**\n * Get whether an async {@link Options.initialValues} source is still\n * pending — the flag a loading skeleton or a disabled submit button gates\n * on until the resolved baseline lands. Subscribes to the 'loading' event\n * the core emits around the resolution cycle.\n */\nexport function useIsLoading(form: Form): boolean {\n return useWatch(form, 'loading', () => form.isLoading);\n}\n\n/**\n * Get the form's user-owned metadata slot reactively (Formik's `status`\n * counterpart): any value the app stores through {@link setStatus} —\n * server session flags, wizard step state, non-field errors. Subscribes\n * to the payload-less 'status' event, so unrelated events never re-render\n * the caller, and the returned reference is stable between writes that\n * store an equal value (useSyncExternalStore's Object.is bailout).\n */\nexport function useStatus<T = any>(form: Form): T {\n return useWatch(form, 'status', () => form.status);\n}\n\n/**\n * Get whether the form accepts a submit right now:\n * `!isSubmitting && !hasErrors`. This is the single flag a submit\n * button's `disabled` prop wants — it is `false` for the whole async\n * `onSubmit` span (not just the validation pass) and whenever any field\n * holds an error (client validation or server backfill), replacing the\n * hand-rolled `useHasErrors(form) || useIsSubmitting(form)` pair.\n * Deliberately no dirty or validating semantics: an untouched-but-clean\n * form can submit.\n */\nexport function useCanSubmit(form: Form): boolean {\n const {emitter} = form;\n // canSubmit folds two events into one boolean: error writes\n // ('errors') and submit-state flips ('submitting'). useWatch subscribes\n // to a single event, so subscribe to both through useWatchCore — the\n // snapshot recomputes on either wake and re-renders only when the\n // boolean itself flips, so unrelated single-field error churn costs no\n // extra render (the same granularity useHasErrors already has).\n const subscribeFactory = useCallback(\n (invalidate: () => void) => {\n const offErrors = on(emitter, 'errors', invalidate);\n const offSubmitting = on(emitter, 'submitting', invalidate);\n return () => {\n offErrors();\n offSubmitting();\n };\n },\n [emitter]\n );\n return useWatchCore(\n subscribeFactory,\n () => !form.isSubmitting && !hasErrors(form)\n );\n}\n\nexport function useSubmitCount(form: Form): number {\n return useWatch(form, 'submitCount', () => form.submitCount);\n}\n\n/**\n * Get whether any validation round is currently running: a field\n * validator's pending `validateDebounce` window, an async field validator\n * still in flight, or the form-level validate's debounce window / in-flight\n * round — every one of them holds a key in `form.validating`, and the\n * 'validating' events they emit (field rounds with a path payload, the\n * form-level round as a payload-less broadcast) are what this subscribes\n * to. The boolean snapshot is Object.is-stable, so churn among the marks\n * (a second field opening while the first settles) costs no render while\n * the flag holds. This is the flag a submit button disables itself on, or\n * spins a spinner with, through the pre-submit validation pass — it flips\n * true the moment the first round opens and back false when the last one\n * settles.\n */\nexport function useIsValidating(form: Form): boolean {\n return useWatch(form, 'validating', () => form.validating.size > 0);\n}\n\n/**\n * Get whether the last submit succeeded: `true` once a submit's validation\n * and `onSubmit` completed without throwing, `false` after a failed submit\n * (validation rejection or a thrown callback) and before any submit ran —\n * the falsy reading of the undefined initial/reset state. Subscribes to\n * the 'submitSuccessful' event the core's setSubmitSuccessful emits, so\n * the flag flips in the same tick the outcome lands: the usual consumers\n * are a success banner and a redirect-on-success effect.\n */\nexport function useIsSubmitSuccessful(form: Form): boolean {\n return useWatch(\n form.emitter,\n 'submitSuccessful',\n () => !!form.isSubmitSuccessful\n );\n}\n\n/**\n * Get the form-level error message: the first error stored under the\n * reserved {@link FORM_ERROR} key, as display text — or undefined while\n * the slot is clean. That key is where a form-level `validate` record's\n * `_form` entry lands and where the Standard Schema adapter drops\n * path-less issues, so errors that belong to no single field still have a\n * reader. The classic usage renders it once above the submit button —\n * `useFormError(form) || null` — and the imperative twin is\n * `getError(form, FORM_ERROR)`.\n */\nexport function useFormError(form: Form): string | undefined {\n return useErrorByPath(form, createPath(FORM_ERROR))?.message;\n}\n\n/**\n * Get every form-level error: all errors stored under the reserved\n * {@link FORM_ERROR} key (insertion order), an empty array when the slot\n * is clean. The plural twin of {@link useFormError} for forms that stack\n * several form-level issues — each path-less Standard Schema issue lands\n * in this slot. The array reference is stable between unrelated events\n * (the stored array or a shared empty constant), so consumers can memo on\n * it; the imperative counterpart is `getFieldErrors(form, FORM_ERROR)`.\n */\nexport function useFormErrors(form: Form): FieldError[] {\n return useFieldErrorsByPath(form, createPath(FORM_ERROR));\n}\n","import {\n createContext,\n createElement,\n useContext,\n type Context,\n type Provider,\n type ReactNode\n} from 'react';\nimport {\n useFieldCore,\n type UseFieldOptions,\n type UseFieldResult\n} from './hooks/field';\nimport {\n useFieldArrayCore,\n useFieldArrayItemCore,\n type UseFieldArrayOptions,\n type UseFieldArrayResult,\n type UseFieldArrayItemResult\n} from './hooks/fieldArray';\nimport type {FieldRules} from './rules';\nimport type {Form} from './form';\nimport type {Name} from './path';\nimport type {AnyPath, FieldPath} from './types';\n\nexport const FormContext: Context<Form<any> | null> =\n createContext<Form<any> | null>(null);\n\nexport const FormProvider: Provider<Form<any> | null> = FormContext.Provider;\n\n/**\n * Read the form from the module-level {@link FormContext}; pass the values\n * shape (`useFormContext<Values>()`) for a typed `Form<Values>`. Use\n * {@link createFormContext} for multiple forms in one subtree.\n * @throws when no `<FormProvider>` is mounted above the call site.\n */\nexport function useFormContext<T extends Record<string, any> = any>(): Form<T> {\n const form = useContext(FormContext);\n if (!form) throw new Error('no form provided');\n return form;\n}\n\n/** Options the bundle's pre-bound hooks take: the underlying hook's\n * options minus `form` — this factory's own context supplies it. For\n * `useField`, `{name: TPath}` keeps `name` a direct TPath inference site. */\ntype BoundFieldOptions<\n TValues extends Record<string, any>,\n TPath extends AnyPath<TValues>\n> = {name: TPath} & Omit<UseFieldOptions<TValues, TPath>, 'form'>;\n\ntype BoundFieldArrayOptions<\n TValues extends Record<string, any>,\n K extends string = 'id'\n> = {\n name: FieldPath<TValues> | Name;\n keyName?: K;\n rules?: FieldRules;\n shouldUnregister?: boolean;\n};\n\ntype BoundFieldArrayItemOptions<TValues extends Record<string, any>> = {\n name: FieldPath<TValues> | Name;\n id: string;\n};\n\n/** The bundle {@link createFormContext} returns: a private React context\n * plus the hooks pre-bound to it, all typed against `TValues`. */\nexport type FormContextBundle<TValues extends Record<string, any> = any> = {\n /** The raw React context, for `<Form context={...}>`: the component\n * keeps its submit machinery while providing into this instance's\n * private context. */\n context: Context<Form<TValues> | null>;\n FormProvider: (props: {\n form: Form<TValues>;\n children: ReactNode;\n }) => ReactNode;\n useFormContext: () => Form<TValues>;\n useField: <TPath extends AnyPath<TValues> = AnyPath<TValues>>(\n options: BoundFieldOptions<TValues, TPath>\n ) => UseFieldResult<TValues, TPath>;\n useFieldArray: <TItem = any, K extends string = 'id'>(\n options: BoundFieldArrayOptions<TValues, K>\n ) => UseFieldArrayResult<TItem, K>;\n useFieldArrayItem: <TValue = any>(\n options: BoundFieldArrayItemOptions<TValues>\n ) => UseFieldArrayItemResult<TValue>;\n};\n\n/**\n * Create an isolated bundle of form-context bindings: its own React context\n * plus `useField`/`useFieldArray`/`useFieldArrayItem`/`useFormContext`\n * hooks resolving from it. One factory per app area scopes a separate\n * form and fixes the value shape (`Ctx.useField` gets typed `name`/`value`);\n * `Ctx.context` lets `<Form context={Ctx.context}>` provide into it.\n */\nexport function createFormContext<\n TValues extends Record<string, any> = any\n>(): FormContextBundle<TValues> {\n const Context = createContext<Form<TValues> | null>(null);\n\n // A `form`-prop wrapper, not a raw Context.Provider: callers shouldn't\n // have to know the `value` prop shape.\n function FormProvider({\n form,\n children\n }: {\n form: Form<TValues>;\n children: ReactNode;\n }): ReactNode {\n return createElement(Context.Provider, {value: form}, children);\n }\n\n function useFormContext(): Form<TValues> {\n const form = useContext(Context);\n if (!form) throw new Error('no form provided');\n return form;\n }\n\n function useField<TPath extends AnyPath<TValues> = AnyPath<TValues>>(\n options: BoundFieldOptions<TValues, TPath>\n ): UseFieldResult<TValues, TPath> {\n return useFieldCore(options as UseFieldOptions<TValues, TPath>, Context);\n }\n\n function useFieldArray<TItem = any, K extends string = 'id'>(\n options: BoundFieldArrayOptions<TValues, K>\n ): UseFieldArrayResult<TItem, K> {\n return useFieldArrayCore<TItem, K>(\n options as UseFieldArrayOptions<K>,\n Context\n );\n }\n\n function useFieldArrayItem<TValue = any>(\n options: BoundFieldArrayItemOptions<TValues>\n ): UseFieldArrayItemResult<TValue> {\n return useFieldArrayItemCore(options as {name: Name; id: string}, Context);\n }\n\n // The raw context, for `<Form context={...}>`: the component keeps its\n // submit machinery while providing into this instance's context.\n return {\n context: Context,\n FormProvider,\n useFormContext,\n useField,\n useFieldArray,\n useFieldArrayItem\n };\n}\n\nexport const CheckboxGroupContext: Context<any> = createContext<any>(null);\n\nexport const CheckboxGroupProvider: Provider<any> =\n CheckboxGroupContext.Provider;\n\nexport function useCheckboxGroupContext(): any {\n const group = useContext(CheckboxGroupContext);\n if (!group) throw new Error('no group provided');\n return group;\n}\n","import * as React from 'react';\nimport {useState} from 'react';\nimport type {ReactNode} from 'react';\n\n/** Nodes deeper than this start collapsed. */\nconst DEFAULT_OPEN_DEPTH = 1;\n\n/** Read-only inspection: never mutates form state, so structural sharing\n * is safe and re-renders stay cheap. */\ntype JsonNodeProps = {\n /** Property name (or array index) rendering before the value. */\n name?: string | number;\n /** Value to render. */\n value: unknown;\n /** Current nesting depth (root is 0). */\n depth?: number;\n};\n\n/** One tree line: a collapsible container row (`▸ key: {`) or a leaf (`key: value`). */\nfunction JsonNode({name, value, depth = 0}: JsonNodeProps): React.JSX.Element {\n const [open, setOpen] = useState(depth <= DEFAULT_OPEN_DEPTH);\n const indent = depth * 12;\n\n const label =\n name === undefined ? null : (\n <>\n <span className=\"rf0-dt-key\">{String(name)}</span>\n <span className=\"rf0-dt-punct\">: </span>\n </>\n );\n\n if (value !== null && typeof value === 'object') {\n const isArray = Array.isArray(value);\n const entries: Array<[string | number, unknown]> = isArray\n ? (value as unknown[]).map((v, i) => [i, v])\n : Object.entries(value as Record<string, unknown>);\n const openBracket = isArray ? '[' : '{';\n const closeBracket = isArray ? ']' : '}';\n const summary = open\n ? ''\n : `${openBracket}…${closeBracket} ${entries.length}`;\n\n return (\n <div className=\"rf0-dt-row\" style={{paddingLeft: indent}}>\n <button\n type=\"button\"\n className=\"rf0-dt-node-toggle\"\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n <span className=\"rf0-dt-caret\">{open ? '▾' : '▸'}</span>\n {label}\n <span className=\"rf0-dt-punct\">{open ? openBracket : summary}</span>\n </button>\n {open && (\n <>\n {entries.map(([k, v]) => (\n <JsonNode key={String(k)} name={k} value={v} depth={depth + 1} />\n ))}\n <span className=\"rf0-dt-punct\" style={{paddingLeft: indent}}>\n {closeBracket}\n </span>\n </>\n )}\n </div>\n );\n }\n\n return (\n <span\n className=\"rf0-dt-row\"\n style={{paddingLeft: indent, display: 'block'}}\n >\n {label}\n <Primitive value={value} />\n </span>\n );\n}\n\n/** Render a primitive leaf with terminal-style type coloring. */\nfunction Primitive({value}: {value: unknown}): ReactNode {\n if (value === undefined)\n return <span className=\"rf0-dt-null\">undefined</span>;\n if (value === null) return <span className=\"rf0-dt-null\">null</span>;\n if (typeof value === 'string')\n return <span className=\"rf0-dt-string\">&quot;{value}&quot;</span>;\n if (typeof value === 'boolean')\n return <span className=\"rf0-dt-boolean\">{String(value)}</span>;\n return <span className=\"rf0-dt-number\">{String(value)}</span>;\n}\n\nexport default JsonNode;\n","/** Stylesheet for the Devtools panel — a single CSS string injected once\n * into <head>. Aesthetic: instrument panel / terminal (near-black,\n * monospace, hairline borders; semantic colors plus one amber accent). */\n\nconst CSS = `\n.rf0-dt {\n position: fixed;\n z-index: 2147483000;\n box-sizing: border-box;\n width: 308px;\n max-width: calc(100vw - 16px);\n max-height: min(70vh, 560px);\n display: flex;\n flex-direction: column;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 11px;\n line-height: 1.45;\n color: #c7d0dc;\n background: #0c1017;\n border: 1px solid #1f2735;\n border-radius: 4px;\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4);\n}\n.rf0-dt *,\n.rf0-dt-badge * {\n box-sizing: border-box;\n}\n.rf0-dt--top-right { top: 8px; right: 8px; }\n.rf0-dt--bottom-right { bottom: 8px; right: 8px; }\n.rf0-dt--top-left { top: 8px; left: 8px; }\n.rf0-dt--bottom-left { bottom: 8px; left: 8px; }\n\n/* ---- header -------------------------------------------------------- */\n.rf0-dt-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 5px 6px 5px 9px;\n border-bottom: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-title {\n flex: 1;\n min-width: 0;\n color: #8b96a5;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.rf0-dt-title::before {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-right: 6px;\n border-radius: 50%;\n background: #e2b93b;\n vertical-align: 1px;\n}\n.rf0-dt-headerbtn {\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 1px 5px;\n color: #8b96a5;\n background: transparent;\n font: inherit;\n font-size: 10px;\n cursor: pointer;\n}\n.rf0-dt-headerbtn:hover { color: #dce3ec; border-color: #2a3547; }\n.rf0-dt-headerbtn:focus-visible,\n.rf0-dt-tab:focus-visible,\n.rf0-dt-action:focus-visible,\n.rf0-dt-badge:focus-visible,\n.rf0-dt-node-toggle:focus-visible {\n outline: 1px solid #e2b93b;\n outline-offset: 1px;\n}\n\n/* ---- tabs ---------------------------------------------------------- */\n.rf0-dt-tablist {\n display: flex;\n border-bottom: 1px solid #1f2735;\n background: #0e131b;\n}\n.rf0-dt-tab {\n flex: 1;\n padding: 4px 2px 5px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: #6b7686;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n white-space: nowrap;\n}\n.rf0-dt-tab:hover { color: #aab5c4; }\n.rf0-dt-tab[aria-selected='true'] {\n color: #e7edf4;\n border-bottom-color: #e2b93b;\n}\n.rf0-dt-tab-count {\n margin-left: 3px;\n color: inherit;\n opacity: 0.75;\n}\n.rf0-dt-tab--danger[aria-selected='true'] {\n border-bottom-color: #f0647c;\n}\n\n/* ---- panels -------------------------------------------------------- */\n.rf0-dt-panel {\n flex: 1;\n min-height: 84px;\n overflow: auto;\n padding: 6px 8px;\n scrollbar-width: thin;\n scrollbar-color: #2a3547 transparent;\n}\n.rf0-dt-empty {\n padding: 10px 2px;\n color: #4d5766;\n font-style: italic;\n}\n\n/* json tree */\n.rf0-dt-row {\n display: block;\n white-space: pre;\n tab-size: 2;\n}\n.rf0-dt-node-toggle {\n border: 0;\n padding: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n white-space: pre;\n}\n.rf0-dt-node-toggle:hover .rf0-dt-key { color: #dce3ec; }\n.rf0-dt-caret {\n display: inline-block;\n width: 1.2em;\n color: #4d5766;\n}\n.rf0-dt-key { color: #8b96a5; }\n.rf0-dt-punct { color: #4d5766; }\n.rf0-dt-string { color: #8fd68a; }\n.rf0-dt-number { color: #e2b93b; }\n.rf0-dt-boolean { color: #6fb3d9; }\n.rf0-dt-null { color: #55607080; font-style: italic; }\n\n/* errors / touched / dirty lists */\n.rf0-dt-item {\n padding: 3px 2px;\n border-bottom: 1px dotted #1a2230;\n display: flex;\n gap: 8px;\n align-items: baseline;\n}\n.rf0-dt-item:last-child { border-bottom: 0; }\n.rf0-dt-item-path {\n color: #aab5c4;\n word-break: break-all;\n}\n.rf0-dt-item-msg {\n color: #f0647c;\n word-break: break-word;\n}\n.rf0-dt-item-msg--ok { color: #8fd68a; }\n.rf0-dt-item-tag {\n flex: none;\n color: #4d5766;\n font-size: 10px;\n}\n.rf0-dt-item--touched .rf0-dt-item-path { color: #6fb3d9; }\n.rf0-dt-item--dirty .rf0-dt-item-path { color: #e2b93b; }\n\n/* ---- submit status + actions --------------------------------------- */\n.rf0-dt-status {\n display: flex;\n gap: 10px;\n padding: 4px 9px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n color: #6b7686;\n font-size: 10px;\n letter-spacing: 0.04em;\n white-space: nowrap;\n overflow: hidden;\n}\n.rf0-dt-status b { color: #aab5c4; font-weight: 400; }\n.rf0-dt-status .rf0-dt-on { color: #e2b93b; }\n.rf0-dt-status .rf0-dt-ok { color: #8fd68a; }\n.rf0-dt-status .rf0-dt-err { color: #f0647c; }\n.rf0-dt-actions {\n display: flex;\n gap: 6px;\n padding: 6px 8px;\n border-top: 1px solid #1f2735;\n background: #10151e;\n}\n.rf0-dt-action {\n flex: 1;\n padding: 3px 0;\n border: 1px solid #2a3547;\n border-radius: 3px;\n background: #151b26;\n color: #c7d0dc;\n font: inherit;\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.rf0-dt-action:hover { border-color: #3b4a61; background: #1a2230; color: #e7edf4; }\n.rf0-dt-action:active { transform: translateY(1px); }\n\n/* ---- collapsed badge ----------------------------------------------- */\n.rf0-dt-badge {\n position: fixed;\n z-index: 2147483000;\n width: 26px;\n height: 26px;\n border: 1px solid #2a3547;\n border-radius: 50%;\n background: #0c1017;\n color: #e2b93b;\n font: inherit;\n font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono',\n Menlo, Consolas, 'Liberation Mono', monospace;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.02em;\n cursor: pointer;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);\n}\n.rf0-dt-badge:hover { border-color: #e2b93b; }\n.rf0-dt-badge--top-right { top: 10px; right: 10px; }\n.rf0-dt-badge--bottom-right { bottom: 10px; right: 10px; }\n.rf0-dt-badge--top-left { top: 10px; left: 10px; }\n.rf0-dt-badge--bottom-left { bottom: 10px; left: 10px; }\n.rf0-dt-badge .rf0-dt-dot {\n position: absolute;\n top: 3px;\n right: 3px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: #f0647c;\n display: none;\n}\n.rf0-dt-badge--has-errors .rf0-dt-dot { display: block; }\n\n/* ---- submit traces -------------------------------------------------- */\n.rf0-dt-submit { border-bottom: 1px solid #1f2735; }\n.rf0-dt-submit:last-child { border-bottom: none; }\n.rf0-dt-submit-summary {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n cursor: pointer;\n list-style: none;\n color: #8b96a5;\n}\n.rf0-dt-submit-summary::-webkit-details-marker { display: none; }\n.rf0-dt-submit-summary:hover { color: #dce3ec; }\n.rf0-dt-submit-n { color: #dce3ec; font-weight: 700; }\n.rf0-dt-submit-time { margin-left: auto; color: #56616f; }\n.rf0-dt-submit[open] .rf0-dt-submit-summary { border-bottom: 1px dashed #1f2735; }\n.rf0-dt-submit-errors { padding: 2px 6px; }\n`;\n\nconst STYLE_ID = 'react-f0rm-devtools-style';\n\n/** Inject the panel stylesheet into <head> — idempotent; no-ops outside a\n * DOM environment (SSR). */\nexport function injectDevtoolsStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById(STYLE_ID)) return;\n const style = document.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n document.head.appendChild(style);\n}\n","import * as React from 'react';\nimport {useContext, useEffect, useId, useState} from 'react';\nimport type {KeyboardEvent} from 'react';\nimport {on} from '../emitter';\nimport {FormContext} from '../context';\nimport {getErrors, getValues, reset, trigger} from '../form';\nimport type {FieldErrorEntry, Form, FormEvents} from '../form';\nimport type {Path} from '../path';\nimport {\n useDirtyFields,\n useIsSubmitting,\n useSubmitCount,\n useTouchedFields,\n useWatch\n} from '../hooks/form';\nimport JsonTree from './JsonTree';\nimport {injectDevtoolsStyles} from './styles';\n\n/** Corner the panel docks to. */\nexport type DevtoolsPosition =\n 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';\n\n/** Props for {@link Devtools}. */\nexport type DevtoolsProps<T extends Record<string, any> = any> = {\n /** Form to inspect; omitted, reads the closest <Form>/FormProvider and throws if none. */\n form?: Form<T>;\n /** Corner to dock the panel in. Defaults to `'top-right'`. */\n position?: DevtoolsPosition;\n};\n\nconst TABS = [\n 'values',\n 'errors',\n 'touched',\n 'dirty',\n 'submits',\n 'subs',\n 'events'\n] as const;\n\ntype TabId = (typeof TABS)[number];\n\ntype EventNameOf<E> = E extends readonly [infer N, unknown] ? N : never;\n\n/** Union of every event name in the FormEvents table. */\ntype FormEventName = EventNameOf<FormEvents>;\n\n/** Audited event types in FormEvents declaration order — the subs tab\n * renders one row per entry, so row order stays stable. */\nconst SUBS_EVENTS = [\n 'change',\n 'errors',\n 'touched',\n 'validating',\n 'submitting',\n 'submitCount',\n 'submitSuccessful',\n 'reset',\n 'disabled',\n 'status',\n 'loading',\n 'focusError'\n] as const satisfies readonly FormEventName[];\n\n/** One event type's row on the subs tab: lifetime trigger count plus the\n * dotted path of its latest payload. */\ntype EventStat = {\n count: number;\n last?: string;\n};\n\n/** One emitted form event, newest first in the event timeline tab. */\ntype EventTrace = {\n n: number;\n event: string;\n at: number;\n path?: string;\n};\n\n/** One completed submit attempt, snapshotted when `isSubmitting` flips\n * false — outcome/values/errors are settled by then. */\ntype SubmitTrace = {\n n: number;\n ok: boolean | undefined;\n at: number;\n values: unknown;\n errors: FieldErrorEntry[];\n};\n\n/** Status chip class for the submit-successful indicator. */\nfunction submitStatusClass(\n isSubmitSuccessful: boolean | undefined\n): string | undefined {\n if (isSubmitSuccessful === undefined) return undefined;\n return isSubmitSuccessful ? 'rf0-dt-ok' : 'rf0-dt-err';\n}\n\n/** Count primitive leaves of an inspected value tree. */\nfunction countLeaves(value: unknown): number {\n if (value === null || typeof value !== 'object') return 1;\n return Object.values(value as Record<string, unknown>).reduce<number>(\n (sum, child) => sum + countLeaves(child),\n 0\n );\n}\n\n/** Status label for the submit-successful indicator. */\nfunction submitStatusLabel(ok: boolean | undefined): string {\n if (ok === undefined) return '–';\n return ok ? 'ok' : 'failed';\n}\n\n// Arrow-key tab navigation — buttons stay click/Enter/Space operable.\nconst ARROW_DELTAS: Record<string, number> = {ArrowRight: 1, ArrowLeft: -1};\n\ntype TabListProps = {\n tab: TabId;\n idPrefix: string;\n counts: Record<TabId, number>;\n onSelect: (tab: TabId) => void;\n};\n\n/** Tab strip; buttons stay click/Enter/Space operable, arrows move focus. */\nfunction TabList({\n tab,\n idPrefix,\n counts,\n onSelect\n}: TabListProps): React.JSX.Element {\n const onKeyDown = (e: KeyboardEvent) => {\n const delta = ARROW_DELTAS[e.key];\n if (!delta) return;\n e.preventDefault();\n const next = TABS[(TABS.indexOf(tab) + delta + TABS.length) % TABS.length];\n onSelect(next);\n document.getElementById(`${idPrefix}-tab-${next}`)?.focus();\n };\n\n return (\n <div\n className=\"rf0-dt-tablist\"\n role=\"tablist\"\n aria-label=\"Form state\"\n tabIndex={-1}\n onKeyDown={onKeyDown}\n >\n {TABS.map(id => (\n <button\n key={id}\n id={`${idPrefix}-tab-${id}`}\n type=\"button\"\n role=\"tab\"\n className={`rf0-dt-tab${id === 'errors' ? ' rf0-dt-tab--danger' : ''}`}\n aria-selected={tab === id}\n aria-controls={`${idPrefix}-panel-${id}`}\n tabIndex={tab === id ? 0 : -1}\n onClick={() => onSelect(id)}\n >\n {id}\n <span className=\"rf0-dt-tab-count\">{counts[id]}</span>\n </button>\n ))}\n </div>\n );\n}\n\ntype ErrorItemProps = {path: string; type: string; message: string};\n\n/** One flattened error row (shared by the errors tab and submit traces). */\nfunction ErrorItem({path, type, message}: ErrorItemProps): React.JSX.Element {\n return (\n <div className=\"rf0-dt-item\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n <span className=\"rf0-dt-item-msg\">{message}</span>\n <span className=\"rf0-dt-item-tag\">{type}</span>\n </div>\n );\n}\n\nfunction ErrorList({errors}: {errors: FieldErrorEntry[]}): React.JSX.Element {\n if (errors.length === 0) return <p className=\"rf0-dt-empty\">no errors</p>;\n return (\n <>\n {/* Same path can hold several errors; index keeps keys unique. */}\n {errors.map(({path, type, message}, index) => (\n <ErrorItem\n key={`${path}:${index}`}\n path={path}\n type={type}\n message={message}\n />\n ))}\n </>\n );\n}\n\nfunction TouchedList({touched}: {touched: string[]}): React.JSX.Element {\n if (touched.length === 0)\n return <p className=\"rf0-dt-empty\">no touched fields</p>;\n return (\n <>\n {touched.map(path => (\n <div key={path} className=\"rf0-dt-item rf0-dt-item--touched\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n </div>\n ))}\n </>\n );\n}\n\nfunction DirtyList({\n dirty\n}: {\n dirty: Record<string, boolean>;\n}): React.JSX.Element {\n const paths = Object.keys(dirty);\n if (paths.length === 0)\n return <p className=\"rf0-dt-empty\">no dirty fields</p>;\n return (\n <>\n {paths.map(path => (\n <div key={path} className=\"rf0-dt-item rf0-dt-item--dirty\">\n <span className=\"rf0-dt-item-path\">{path}</span>\n <span className=\"rf0-dt-item-msg rf0-dt-item-msg--ok\">changed</span>\n </div>\n ))}\n </>\n );\n}\n\nfunction SubmitTraceList({\n submits\n}: {\n submits: SubmitTrace[];\n}): React.JSX.Element {\n if (submits.length === 0)\n return <p className=\"rf0-dt-empty\">no submits yet</p>;\n return (\n <>\n {submits.map(trace => (\n <SubmitTraceItem key={`${trace.n}-${trace.at}`} trace={trace} />\n ))}\n </>\n );\n}\n\nfunction SubmitTraceItem({trace}: {trace: SubmitTrace}): React.JSX.Element {\n return (\n <details className=\"rf0-dt-submit\">\n <summary className=\"rf0-dt-submit-summary\">\n <span className=\"rf0-dt-submit-n\">#{trace.n}</span>\n <span className={submitStatusClass(trace.ok)}>\n {submitStatusLabel(trace.ok)}\n </span>\n <span className=\"rf0-dt-submit-time\">\n {new Date(trace.at).toLocaleTimeString()}\n </span>\n </summary>\n {trace.errors.length > 0 && (\n <div className=\"rf0-dt-submit-errors\">\n <ErrorList errors={trace.errors} />\n </div>\n )}\n <JsonTree value={trace.values} />\n </details>\n );\n}\n\nfunction EventStream({\n events,\n onClear\n}: {\n events: EventTrace[];\n onClear: () => void;\n}): React.JSX.Element {\n if (events.length === 0) return <p className=\"rf0-dt-empty\">no events yet</p>;\n return (\n <>\n <div>\n <button type=\"button\" className=\"rf0-dt-action\" onClick={onClear}>\n Clear\n </button>\n </div>\n {events.map(trace => (\n <div key={`${trace.n}:${trace.at}`} className=\"rf0-dt-item\">\n <span className=\"rf0-dt-item-tag\">{trace.event}</span>\n <span className=\"rf0-dt-item-path\">{trace.path ?? '—'}</span>\n <span className=\"rf0-dt-item-msg rf0-dt-item-msg--ok\">\n {new Date(trace.at).toLocaleTimeString()}\n </span>\n </div>\n ))}\n </>\n );\n}\n\n/** Subscription audit — one row per event type in {@link SUBS_EVENTS}\n * order: lifetime trigger count plus the latest payload's dotted path.\n * The emitter API exposes no listenerCount/eventNames, so live listener\n * totals are deliberately absent (never monkey-patch emit). */\nfunction SubsList({\n stats\n}: {\n stats: Partial<Record<FormEventName, EventStat>>;\n}): React.JSX.Element {\n return (\n <>\n {SUBS_EVENTS.map(event => {\n const stat = stats[event];\n return (\n <div key={event} className=\"rf0-dt-item\">\n <span className=\"rf0-dt-item-tag\">{event}</span>\n <span className=\"rf0-dt-item-path\">{stat?.last ?? '—'}</span>\n <span className=\"rf0-dt-item-msg rf0-dt-item-msg--ok\">\n {stat?.count ?? 0}\n </span>\n </div>\n );\n })}\n </>\n );\n}\n\ntype PanelProps = {\n tab: TabId;\n values: unknown;\n errors: FieldErrorEntry[];\n touched: string[];\n dirty: Record<string, boolean>;\n submits: SubmitTrace[];\n stats: Partial<Record<FormEventName, EventStat>>;\n events: EventTrace[];\n onClearEvents: () => void;\n};\n\n/** Content for the active tab. */\nfunction Panel({\n tab,\n values,\n errors,\n touched,\n dirty,\n submits,\n stats,\n events,\n onClearEvents\n}: PanelProps): React.JSX.Element {\n if (tab === 'values') return <JsonTree value={values} />;\n if (tab === 'errors') return <ErrorList errors={errors} />;\n if (tab === 'touched') return <TouchedList touched={touched} />;\n if (tab === 'dirty') return <DirtyList dirty={dirty} />;\n if (tab === 'submits') return <SubmitTraceList submits={submits} />;\n if (tab === 'subs') return <SubsList stats={stats} />;\n return <EventStream events={events} onClear={onClearEvents} />;\n}\n\n/** Live form inspector — a floating dev panel. Tabs for values / errors /\n * touched / dirty / submits (per-attempt traces) / events / subs\n * (per-event-type audit) plus a status strip and Reset/Validate actions.\n * Reads state through the library's watch hooks, so it updates live\n * without participating in validation/submit. Docked\n * at a corner, collapsible; keyboard operable. Not re-exported by the\n * main entry. */\nexport default function Devtools<T extends Record<string, any> = any>({\n form,\n position = 'top-right'\n}: DevtoolsProps<T>): React.JSX.Element {\n // Module-scope call keeps the entry free of import-time side effects\n // (sideEffects: false lets bundlers drop bare imports).\n injectDevtoolsStyles();\n const contextForm = useContext(FormContext);\n const f: Form<any> | null = form ?? contextForm;\n if (!f) {\n throw new Error(\n '<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.'\n );\n }\n\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<TabId>('values');\n const idPrefix = useId().replace(/[^a-zA-Z0-9-]/g, '');\n\n const values = useWatch(f, 'change', getValues.bind(null, f));\n const errors = useWatch<FieldErrorEntry[]>(\n f,\n 'errors',\n getErrors.bind(null, f)\n );\n const touched = useTouchedFields(f);\n const dirty = useDirtyFields(f);\n const isSubmitting = useIsSubmitting(f);\n const submitCount = useSubmitCount(f);\n const isSubmitSuccessful = useWatch(\n f,\n 'submitSuccessful',\n () => f.isSubmitSuccessful\n );\n\n // Submit traces: snapshot each completed attempt ('submitting' emit with\n // isSubmitting=false), capped at 10 newest-first. The push is deferred\n // one microtask — the failed path flips isSubmitting before\n // isSubmitSuccessful — so by microtask time the attempt is final.\n // setState from an event callback, never the effect body or render.\n const [submits, setSubmits] = useState<SubmitTrace[]>([]);\n useEffect(() => {\n return on(f.emitter, 'submitting', () => {\n if (f.isSubmitting) return;\n queueMicrotask(() => {\n if (f.isSubmitting || f.submitCount === 0) return;\n setSubmits(prev =>\n [\n {\n n: f.submitCount,\n ok: f.isSubmitSuccessful,\n at: Date.now(),\n values: getValues(f),\n errors: getErrors(f)\n },\n ...prev\n ].slice(0, 10)\n );\n });\n });\n }, [f]);\n\n // Event timeline: every event lands here, newest first, capped at 50.\n // State writes ride the event callback (never the effect body/render).\n const [events, setEvents] = useState<EventTrace[]>([]);\n // Per-event audit for the subs tab — accumulated by the same on()\n // callbacks below (no second subscription). Counts need no deferred\n // read, unlike submit traces: nothing settles after the emit.\n const [stats, setStats] = useState<Partial<Record<FormEventName, EventStat>>>(\n {}\n );\n useEffect(() => {\n let seq = 0;\n const push = (event: FormEventName, path?: string) => {\n seq += 1;\n const trace: EventTrace = {n: seq, event, at: Date.now()};\n if (path !== undefined) trace.path = path;\n setEvents(prev => [trace, ...prev].slice(0, 50));\n setStats(prev => {\n const prior = prev[event];\n const stat: EventStat = {count: (prior?.count ?? 0) + 1};\n if (path !== undefined) stat.last = path;\n else if (prior?.last !== undefined) stat.last = prior.last;\n const next = {...prev};\n next[event] = stat;\n return next;\n });\n };\n const disposers: Array<() => void> = [];\n // 'disabled' carries an optional path like the field events.\n for (const event of [\n 'change',\n 'errors',\n 'touched',\n 'validating',\n 'disabled'\n ] as const) {\n disposers.push(\n on(f.emitter, event, (path?: Path) =>\n push(event, path ? path.value.join('.') : undefined)\n )\n );\n }\n disposers.push(\n on(f.emitter, 'focusError', (key: string) => {\n // The key is JSON-serialized segments — pretty-print it; malformed\n // input falls back to the raw key.\n try {\n push('focusError', (JSON.parse(key) as string[]).join('.'));\n } catch {\n push('focusError', key);\n }\n })\n );\n for (const event of [\n 'submitting',\n 'submitCount',\n 'submitSuccessful',\n 'reset',\n 'status',\n 'loading'\n ] as const) {\n disposers.push(on(f.emitter, event, () => push(event)));\n }\n return () => disposers.forEach(dispose => dispose());\n }, [f]);\n\n if (!open) {\n return (\n <button\n type=\"button\"\n className={`rf0-dt-badge rf0-dt-badge--${position}${\n errors.length > 0 ? ' rf0-dt-badge--has-errors' : ''\n }`}\n aria-expanded={false}\n aria-label={`Open react-f0rm devtools (${errors.length} errors)`}\n onClick={() => setOpen(true)}\n >\n f0\n <span className=\"rf0-dt-dot\" />\n </button>\n );\n }\n\n const counts: Record<TabId, number> = {\n values: countLeaves(values),\n errors: errors.length,\n touched: touched.length,\n dirty: Object.keys(dirty).length,\n submits: submits.length,\n subs: Object.keys(stats).length,\n events: events.length\n };\n\n return (\n <section\n className={`rf0-dt rf0-dt--${position}`}\n aria-label=\"react-f0rm devtools\"\n >\n <header className=\"rf0-dt-header\">\n <span className=\"rf0-dt-title\">react-f0rm</span>\n <button\n type=\"button\"\n className=\"rf0-dt-headerbtn\"\n aria-label=\"Collapse devtools\"\n onClick={() => setOpen(false)}\n >\n –\n </button>\n </header>\n\n <TabList\n tab={tab}\n idPrefix={idPrefix}\n counts={counts}\n onSelect={setTab}\n />\n\n <div\n id={`${idPrefix}-panel-${tab}`}\n role=\"tabpanel\"\n aria-labelledby={`${idPrefix}-tab-${tab}`}\n className=\"rf0-dt-panel\"\n >\n <Panel\n tab={tab}\n values={values}\n errors={errors}\n touched={touched}\n dirty={dirty}\n submits={submits}\n stats={stats}\n events={events}\n onClearEvents={() => setEvents([])}\n />\n </div>\n\n <p className=\"rf0-dt-status\" aria-live=\"polite\">\n <span className={isSubmitting ? 'rf0-dt-on' : undefined}>\n submitting <b>{String(isSubmitting)}</b>\n </span>\n <span>\n submits <b>{submitCount}</b>\n </span>\n <span className={submitStatusClass(isSubmitSuccessful)}>\n ok{' '}\n <b>\n {isSubmitSuccessful === undefined\n ? '–'\n : String(isSubmitSuccessful)}\n </b>\n </span>\n </p>\n\n <div className=\"rf0-dt-actions\">\n <button\n type=\"button\"\n className=\"rf0-dt-action\"\n onClick={() => reset(f, f.initialValues)}\n >\n Reset\n </button>\n <button\n type=\"button\"\n className=\"rf0-dt-action\"\n onClick={() => trigger(f)}\n >\n Validate\n </button>\n </div>\n </section>\n );\n}\n"],"names":["computeDirtyFields","form","dirtyFields","fn","key","value","values","path","segmentsFromKey","getDirtyBaseline","join","forEachDirtyField","getDirtyFields","cache","getOrCreate","dirtyFieldsCaches","version","result","a","b","aKeys","Object","keys","length","every","sameDirtyKeys","useWatch","formOrEmitter","event","getter","isEqual","emitter","subscribeFactory","cacheRef","useRef","current","hasValue","getterRef","isEqualRef","getSnapshot","useCallback","subscribe","notify","compare","next","useSyncExternalStore","useWatchCore","invalidate","on","FormContext","createContext","Provider","JsonNode","name","depth","open","setOpen","useState","indent","label","React","createElement","Fragment","className","String","isArray","Array","entries","map","v","i","openBracket","closeBracket","summary","style","paddingLeft","type","onClick","k","display","Primitive","STYLE_ID","TABS","SUBS_EVENTS","submitStatusClass","isSubmitSuccessful","countLeaves","reduce","sum","child","ARROW_DELTAS","ArrowRight","ArrowLeft","TabList","tab","idPrefix","counts","onSelect","role","tabIndex","onKeyDown","e","delta","preventDefault","indexOf","document","getElementById","focus","id","ErrorItem","message","ErrorList","errors","index","TouchedList","touched","DirtyList","dirty","paths","SubmitTraceList","submits","trace","SubmitTraceItem","n","at","ok","Date","toLocaleTimeString","JsonTree","EventStream","events","onClear","SubsList","stats","stat","last","count","Panel","onClearEvents","position","textContent","head","appendChild","injectDevtoolsStyles","contextForm","useContext","f","Error","setTab","useId","replace","getValues","bind","getErrors","getTouchedFields","useTouchedFields","useDirtyFields","isSubmitting","useIsSubmitting","submitCount","useSubmitCount","setSubmits","useEffect","queueMicrotask","prev","now","slice","setEvents","setStats","seq","push","prior","disposers","JSON","parse","forEach","dispose","subs","reset","initialValues","trigger"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import { FormInstance as Form } from '../index.js';
3
+ import '@for-fun/event-emitter';
4
+
5
+ /** Corner the panel docks to. */
6
+ type DevtoolsPosition = 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';
7
+ /** Props for {@link Devtools}. */
8
+ type DevtoolsProps<T extends Record<string, any> = any> = {
9
+ /** Form to inspect; omitted, reads the closest <Form>/FormProvider and throws if none. */
10
+ form?: Form<T>;
11
+ /** Corner to dock the panel in. Defaults to `'top-right'`. */
12
+ position?: DevtoolsPosition;
13
+ };
14
+ /** Live form inspector — a floating dev panel. Tabs for values / errors /
15
+ * touched / dirty / submits (per-attempt traces) / events / subs
16
+ * (per-event-type audit) plus a status strip and Reset/Validate actions.
17
+ * Reads state through the library's watch hooks, so it updates live
18
+ * without participating in validation/submit. Docked
19
+ * at a corner, collapsible; keyboard operable. Not re-exported by the
20
+ * main entry. */
21
+ declare function Devtools<T extends Record<string, any> = any>({ form, position }: DevtoolsProps<T>): React.JSX.Element;
22
+
23
+ export { Devtools };
24
+ export type { DevtoolsPosition, DevtoolsProps };