react-f0rm 1.1.0 → 1.2.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 (61) hide show
  1. package/README.md +152 -32
  2. package/dist/devtools/index.cjs.js +1 -1
  3. package/dist/devtools/index.cjs.js.map +1 -1
  4. package/dist/devtools/index.d.ts +2 -2
  5. package/dist/devtools/index.mjs +1 -1
  6. package/dist/devtools/index.mjs.map +1 -1
  7. package/dist/errors-CxSjrWJO.cjs.js +2 -0
  8. package/dist/errors-CxSjrWJO.cjs.js.map +1 -0
  9. package/dist/errors-CzWtwjO0.mjs +2 -0
  10. package/dist/errors-CzWtwjO0.mjs.map +1 -0
  11. package/dist/form-CvmWHUrd.d.ts +423 -0
  12. package/dist/index.cjs.js +1 -1
  13. package/dist/index.cjs.js.map +1 -1
  14. package/dist/index.d.ts +786 -102
  15. package/dist/index.mjs +1 -1
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/index.umd.js +801 -354
  18. package/dist/index.umd.js.map +1 -1
  19. package/dist/index.umd.min.js +2 -2
  20. package/dist/index.umd.min.js.map +1 -1
  21. package/dist/persist.cjs.js +2 -0
  22. package/dist/persist.cjs.js.map +1 -0
  23. package/dist/persist.d.ts +49 -0
  24. package/dist/persist.mjs +2 -0
  25. package/dist/persist.mjs.map +1 -0
  26. package/dist/resolvers/standard-schema.cjs.js +1 -1
  27. package/dist/resolvers/standard-schema.cjs.js.map +1 -1
  28. package/dist/resolvers/standard-schema.d.ts +7 -5
  29. package/dist/resolvers/standard-schema.mjs +1 -1
  30. package/dist/resolvers/standard-schema.mjs.map +1 -1
  31. package/dist/resolvers/yup.cjs.js +1 -1
  32. package/dist/resolvers/yup.cjs.js.map +1 -1
  33. package/dist/resolvers/yup.d.ts +1 -1
  34. package/dist/resolvers/yup.mjs +1 -1
  35. package/dist/resolvers/yup.mjs.map +1 -1
  36. package/dist/resolvers/zod.cjs.js +1 -1
  37. package/dist/resolvers/zod.cjs.js.map +1 -1
  38. package/dist/resolvers/zod.d.ts +1 -1
  39. package/dist/resolvers/zod.mjs +1 -1
  40. package/dist/resolvers/zod.mjs.map +1 -1
  41. package/dist/server/index.cjs.js +2 -0
  42. package/dist/server/index.cjs.js.map +1 -0
  43. package/dist/server/index.d.ts +77 -0
  44. package/dist/server/index.mjs +2 -0
  45. package/dist/server/index.mjs.map +1 -0
  46. package/dist/validate-B1Gdjeaq.mjs +2 -0
  47. package/dist/validate-B1Gdjeaq.mjs.map +1 -0
  48. package/dist/validate-CUmNZqg6.d.ts +238 -0
  49. package/dist/validate-DAfz8Nbb.cjs.js +2 -0
  50. package/dist/validate-DAfz8Nbb.cjs.js.map +1 -0
  51. package/dist/values-B1IV-6V4.mjs +2 -0
  52. package/dist/values-B1IV-6V4.mjs.map +1 -0
  53. package/dist/values-CDNAYEOB.cjs.js +2 -0
  54. package/dist/values-CDNAYEOB.cjs.js.map +1 -0
  55. package/package.json +71 -24
  56. package/dist/form-BGWPwts2.mjs +0 -2
  57. package/dist/form-BGWPwts2.mjs.map +0 -1
  58. package/dist/form-BiDaJLjD.d.ts +0 -826
  59. package/dist/form-DwuY91QB.cjs.js +0 -2
  60. package/dist/form-DwuY91QB.cjs.js.map +0 -1
  61. package/dist/validate-2XUilILy.d.ts +0 -22
package/README.md CHANGED
@@ -12,11 +12,12 @@ Coming from TanStack Form? [Migrating from TanStack Form](./docs/from-tanstack-f
12
12
  ## Features
13
13
 
14
14
  - **Field-level subscriptions.** Editing one field re-renders exactly that field's component, not the whole form. State is read through `useSyncExternalStore`, so snapshots stay consistent under concurrent rendering (no tearing).
15
- - **Truly type-safe paths.** `FieldPath<T>` enumerates every valid field name for your values shape and `PathValue<T, P>` resolves the value type at that path — typos in field names fail at compile time, values are inferred.
15
+ - **Truly type-safe paths.** `FieldPath<T>` enumerates every valid field name for your values shape and `PathValue<T, P>` resolves the value type at that path — typos in field names fail at compile time on the generic APIs (`useField`, `setValue`, `getValue`, `useValue`, …), values are inferred.
16
16
  - **One schema adapter for every library.** The Standard Schema resolver covers zod (v3.24+/v4), valibot v1, arktype and any other Standard Schema v1 implementation through a single tree-shakeable entry point.
17
17
  - **Headless, with accessibility hooks.** You own the markup. When you opt into error rendering via `renderError`, `aria-invalid` and `aria-describedby` are wired up automatically.
18
- - **Tombstone unregister.** Unmounted fields drop out of `getValues()` instead of silently reviving their initial values on the next read.
19
- - **Copy-on-write `getValues()`.** An ownership-tracked merge allocates each container once per read instead of re-copying whole branches for every key.
18
+ - **Tombstone unregister.** Unmounted fields drop out of `getValues()` instead of silently reviving their initial values on the next read; `createForm({shouldUnregister: false})` flips the form-wide default to RHF's keep-the-value semantics.
19
+ - **Async initial values.** `initialValues` accepts a Promise or a thunk returning one the form starts empty with `isLoading: true` (`useIsLoading`, `useFormState().isLoading`) and lands the resolved values as the baseline, react-hook-form's async `defaultValues` shape.
20
+ - **Copy-on-write `getValues()`.** An ownership-tracked merge allocates each container once per read instead of re-copying whole branches for every key. The result shares references across reads, so treat it as read-only — `structuredClone` the tree when you need a mutable copy. In development the snapshot is deep-frozen: mutating it throws at the offending line instead of silently corrupting the shared cache (production builds share baseline references unchanged).
20
21
  - **Multiple errors per field.** Each field stores an ordered `FieldError[]` — `getFieldErrors`/`useFieldErrors` read them all, and schema resolvers forward every issue instead of stopping at the first.
21
22
  - **Async validation with cancellation.** `validateDebounce` per field — and on the form-level `validate` — plus an `AbortSignal` handed to every validator: a superseded round aborts its in-flight fetch, and pending debounce windows count as validating so submit waits them out.
22
23
  - **Precise lifecycle control.** `reset(form, values, {keepDirtyValues, …})` covers refetch-without-clobbering-dirty-drafts, `setFocus(form, name)` focuses programmatically, and `trigger(form, name?)` resolves `Promise<boolean>` once validation settles.
@@ -36,21 +37,22 @@ yarn add react-f0rm
36
37
 
37
38
  ## Benchmarks
38
39
 
39
- tinybench; relative margin of error 0.9% for the first three scenarios, 2.5% for the scale scenarios.
40
+ tinybench, run on a desktop-class machine (AMD Ryzen 7 8745HS). Measured rme varies by run 17–249% depending on scenario and load — so treat the µs means as one significant figure.
40
41
 
41
42
  | Scenario | react-f0rm | Baseline | Speedup |
42
43
  |---|---|---|---|
43
- | Change one of 100 controlled fields | 113µs/change (~8,880 ops/s) | RHF `Controller`: 200µs (~5,000 ops/s) | ~1.8× |
44
+ | Change one of 100 controlled fields | 111µs/change (~9,300 ops/s) | RHF `Controller`: 131µs (~7,800 ops/s) | ~1.2× |
44
45
  | Components re-rendered per change | 1 of 100 `Field`s | — | — |
45
- | `getValues()`, 100 fields × depth 3 | 42.5µs (ownership merge) | legacy chained `set`: 93.0µs | 2.19× |
46
- | Change one of 1000 controlled fields | 0.418ms/change (~2,390 ops/s, rme ±1.1%) | RHF `Controller`: 1.662ms (~602 ops/s) | 4.0× |
47
- | Async validation storm — burst of 3 changes × 50 debounced async validators, settled via `trigger` | 20.5ms/burst (~49 ops/s, rme ±2.4%) | | — |
48
- | `await trigger(form)`100 mixed validators (50 sync + 50 async) settle | 1.48ms (~676 ops/s, rme ±0.9%) | — | — |
46
+ | Change one of 100 uncontrolled fields | 12.6µs/change (~80,500 ops/s) | RHF `register`: 11.9µs (~85,200 ops/s) | parity (~6% apart) |
47
+ | `getValues()`, 100 fields × depth 3 (cold compute; DEV snapshot guard on both paths) | 55.5µs (ownership merge) | legacy chained `set`: 104µs | 1.9× |
48
+ | Change one of 1000 controlled fields | 0.611ms/change (~1,660 ops/s) | RHF `Controller`: 1.07ms (~944 ops/s) | ~1.8× |
49
+ | Async validation storm burst of 3 changes × 50 debounced async validators, settled via `trigger` | 27.9ms/burst (~36 ops/s) | — | — |
50
+ | `await trigger(form)` — 100 mixed validators (50 sync + 50 async) settle | 1.23ms (~846 ops/s) | — | — |
49
51
 
50
52
  Notes:
51
53
 
52
- - For reference, RHF's uncontrolled `register` which has no per-field re-render at allfloors at 21µs/change; the controlled comparison above uses `Controller`, the fair apples-to-apples baseline.
53
- - In the `getValues()` benchmark, ownership merging also cut container allocations from 300 to 111.
54
+ - The uncontrolled row is the apples-to-apples `register` comparison: react-f0rm's `uncontrolled: true` (no value subscription) runs at RHF-`register` parity 12.6µs vs 11.9µs per change while keeping errors/touched/disabled/validating reactive, which raw `register` does not. The controlled comparison above uses `Controller`, RHF's per-field-subscribed controlled counterpart.
55
+ - Both `getValues()` paths pay the DEV snapshot guard (`freezeValues`: clone + freeze), so the comparison isolates the merge strategy; in production neither side pays it. Ownership merging also cut container allocations from 300 to 111.
54
56
 
55
57
  Reproduce with:
56
58
 
@@ -65,25 +67,26 @@ react-f0rm vs the established options. react-f0rm figures come from this repo (s
65
67
 
66
68
  | | react-f0rm | React Hook Form | TanStack Form | Formik |
67
69
  |---|---|---|---|---|
68
- | Rendering model | Controlled fields with field-level subscriptions (`useSyncExternalStore`): editing one of 100 fields re-renders exactly 1 component | Uncontrolled `register` by default (no React re-render while typing); `Controller` opts into per-field re-renders | Field-level subscriptions (`form.Field` / `useField`), each field re-renders itself | Form-wide context: any state change re-renders all subscribed components |
69
- | Unregister on unmount | Unregisters by default — an unmounted field drops out of `getValues()` (tombstone) instead of silently reviving its initial value; `shouldUnregister: false` keeps it | Value kept by default (`shouldUnregister` defaults to `false`); opt in per field or form to unregister on unmount | Values live in the form store; unmounting a field's UI keeps its value and state | No unregister concept — values persist until `reset` |
70
+ | Rendering model | Controlled fields with field-level subscriptions (`useSyncExternalStore`): editing one of 100 fields re-renders exactly 1 component; `uncontrolled: true` drops the value subscription and runs at RHF-`register` parity (12.6µs vs 11.9µs bench) while errors/touched/disabled stay reactive | Uncontrolled `register` by default (no React re-render while typing); `Controller` opts into per-field re-renders | Field-level subscriptions (`form.Field` / `useField`), each field re-renders itself | Form-wide context: any state change re-renders all subscribed components |
71
+ | Unregister on unmount | Unregisters by default — an unmounted field drops out of `getValues()` (tombstone) instead of silently reviving its initial value; `shouldUnregister: false` per field or per form (`createForm({shouldUnregister: false})`) keeps it | Value kept by default (`shouldUnregister` defaults to `false`); opt in per field or form to unregister on unmount | Values live in the form store; unmounting a field's UI keeps its value and state | No unregister concept — values persist until `reset` |
70
72
  | Schema adapters | One Standard Schema entry point (`react-f0rm/resolvers/standard-schema`) covers zod, valibot, arktype, …; legacy zod/yup resolvers also shipped | `@hookform/resolvers` — one adapter module per validation library | Built-in `standardSchemaValidators` (Standard Schema v1), plus per-library adapter packages | Yup built in via `validationSchema`; other libraries hand-wired in `validate` |
71
- | Path type safety | `FieldPath<T>` / `PathValue<T, P>`: every valid path enumerated, value type resolved, typos fail at compile time | `Path<T>` / `FieldPath` type-level path checking | Deep inference, including validator argument types — the strongest of the four | Top-level `keyof` only; nested paths are untyped strings |
73
+ | Path type safety | `FieldPath<T>` / `PathValue<T, P>`: every valid path enumerated, value type resolved, typos fail at compile time on the generic APIs (`useField`, `setValue`, `getValue`, …); segment arrays and runtime-dynamic entry points (`useFieldArray`, `removeField`, …) stay untyped | `Path<T>` / `FieldPath` type-level path checking | Deep inference, including validator argument types — the strongest of the four | Top-level `keyof` only; nested paths are untyped strings |
74
+ | Async initial values | `initialValues: T \| Promise<T> \| () => T \| Promise<T>`: async sources start the form empty with `isLoading: true` (`useIsLoading` / `useFormState().isLoading`) and land the resolved values as the baseline | Async `defaultValues` supported (`formState.isLoading`) | `defaultValues: () => Promise<T>` supported | Not built in — resolve before rendering, or re-render after fetch |
72
75
  | Async validation | `validateDebounce` per field + `meta.signal` (`AbortSignal`) handed to every validator — superseded rounds cancel their in-flight work; pending debounce counts as validating so submit waits | Async validators supported, but no built-in debounce and no cancellation signal — both are hand-rolled per project | Built in: `asyncDebounceMs` debounces and the validator meta carries an `AbortSignal` | Async `validate` supported; no debounce, no signal |
73
76
  | Multiple errors per field | Native: every field holds `FieldError[]`; `getFieldErrors`/`useFieldErrors` read them; resolvers forward every schema issue | `criteriaMode: 'all'` collects all failing rules per field | Errors are arrays of messages per field | — |
74
- | SSR / hydration | `renderToString` renders initial values out of the box; server snapshot matches the client's first render | SSR-safe | SSR-safe | SSR-safe |
75
- | React 19 / Server Actions | Bridge pattern: dispatch the action from `onValidSubmit` via `startTransition`/`useActionState`, passing the values object rather than FormData (see the stance above and the React 19 Server Actions guide); no submit before JS loads — first-class `action` prop support is not on the 0.x roadmap | `<Form>` accepts a function `action` prop (server-action-style submit) since v7.84, and ships a `react-server` export | Documented server action integration (`createServerValidate` for server-side validation, Next.js examples) | — |
76
- | Bundle size | 11.18 KB gzip (7.1 KB brotli, minified), full core | ~11 KB gzip | ~17.5 KB gzip | ~12.8 KB gzip |
77
+ | SSR / hydration | `renderToString` renders initial values out of the box; server snapshot matches the client's first render (async initialValues render empty + `isLoading` on both sides) | SSR-safe | SSR-safe | SSR-safe |
78
+ | React 19 / Server Actions | Bridge pattern: dispatch the action from `onValidSubmit` via `startTransition`/`useActionState`, passing the values object rather than FormData (see the stance above and the React 19 Server Actions guide); the `react-f0rm/server` entry's `validateValues` re-validates payloads server-side without wrapping them in an action; no submit before JS loads — first-class `action` prop support is not on the 0.x roadmap | `<Form>` accepts a function `action` prop (server-action-style submit) since v7.84, and ships a `react-server` export | Documented server action integration (`createServerValidate` for server-side validation, Next.js examples) | — |
79
+ | Bundle size | 11.83 KB gzip minified (10.73 KB brotli), full core | ~11 KB gzip | ~17.5 KB gzip | ~12.8 KB gzip |
77
80
  | Devtools | `<Devtools />` from `react-f0rm/devtools` — separate entry point, tree-shakeable, never lands in the main bundle | `@hookform/devtools` (separate package) | Built-in devtools panel | None (official) |
78
81
  | Ecosystem maturity | New, 0.x — small audience, few integrations so far | Most mature: massive adoption, resolvers, UI-kit integrations, abundant examples and answers | Backed by the TanStack family, actively growing | Maintenance mode; the author recommends considering RHF or Final Form for new projects |
79
82
 
80
- Bundle-size basis: every column is gzip. react-f0rm is measured on the local build — gzip of the shipped, unminified `dist/index.mjs` after `npm run build` (minified, the same file gzips to ~7.88 KB; 7.1 KB brotli via size-limit, which minifies and tree-shakes). Competitor figures are Bundlephobia observations of minified+gzip bundles — so ours is the conservative number, not the flattering one.
83
+ Bundle-size basis: every column is gzip. react-f0rm is measured on the local build — gzip of the shipped, unminified `dist/index.mjs` after `npm run build` is 11.58 KB (minified, the same file gzips to 11.83 KB; 10.73 KB brotli via size-limit, which minifies and tree-shakes). Competitor figures are Bundlephobia observations of minified+gzip bundles — so ours is the conservative number, not the flattering one.
81
84
 
82
85
  ### Which one should you use?
83
86
 
84
- **Pick react-f0rm** when you want controlled components with true per-field subscriptions (design systems, editor-like forms), one Standard Schema adapter instead of a package per validator, compile-time-checked paths, and a small core (11.18 KB gzip / 7.1 KB brotli) — and you are comfortable with a young 0.x library.
87
+ **Pick react-f0rm** when you want controlled components with true per-field subscriptions (design systems, editor-like forms), one Standard Schema adapter instead of a package per validator, compile-time-checked paths, and a small core (11.83 KB minified gzip / 10.73 KB brotli) — and you are comfortable with a young library.
85
88
 
86
- **Pick React Hook Form** when uncontrolled inputs are an option: its raw `register` performs no per-field re-render at all and floors at 21µs/change vs our 113µs (see [Benchmarks](#benchmarks)) uncontrolled is simply a cheaper rendering model. RHF is also the right call when you need its mature ecosystem of resolvers, UI-library integrations and community answers today. TanStack Form sits in between: choose it when the deepest possible type inference (including validator signatures) matters more to you than bundle size.
89
+ **Pick React Hook Form** when you want the mature ecosystem resolvers, UI-library integrations and community answers — today. Its performance edge is gone at the rendering level: raw `register` benches at 11.9µs/change and react-f0rm's `uncontrolled: true` at 12.6µs (parity, see [Benchmarks](#benchmarks)), while our controlled model beats `Controller` 1.2–1.8×. TanStack Form sits in between: choose it when the deepest possible type inference (including validator signatures) matters more to you than bundle size.
87
90
 
88
91
  **Server Actions: bridge, not first-class.** RHF-style `action` prop support, a `react-server` entry point, or a TanStack-style `createServerValidate` helper are **not on the 0.x roadmap** — a deliberate stance, not a gap. react-f0rm's source of truth is the values store, not the DOM: an `action`-prop submit would ship FormData keyed by JSON-stringified path keys, drop every store-only value, and skip the validation gate entirely (the [React 19 Server Actions guide](docs-site/docs/guides/react19-server-actions.md) unpacks all four failure modes). The recommended shape is the bridge — dispatch from `onValidSubmit` via `startTransition`/`useActionState`, passing the values object rather than FormData — which keeps validation gating the action and types/nesting intact. If submitting without JavaScript loaded is a hard requirement, RHF's `action` prop support is the better fit today.
89
92
 
@@ -134,6 +137,17 @@ const form = useForm({initialValues: {email: ''}});
134
137
  const {value, onChange} = useField({form, name: 'email'});
135
138
  ```
136
139
 
140
+ The result also carries `focusRef` — a stable callback ref for the input element. `setFocus` and a failed submit's first-error auto-focus ([Focusing the first error](#focusing-the-first-error)) ride the `'focusError'` event, which reaches your element through this ref; leave it off and focus requests aimed at the field are silent no-ops. It only matters for headless callers building their own input — `<Field>` wires it internally, so its users never see it:
141
+
142
+ ```jsx
143
+ const {value, onChange, focusRef} = useField({name: 'email'});
144
+ <input ref={focusRef} value={value} onChange={e => onChange(e.target.value)} />
145
+ ```
146
+
147
+ `uncontrolled: true` pins the value at mount and skips the value subscription — typing re-renders nothing (the store still carries every write; errors/touched/disabled/validating still re-render the field), the react-hook-form `register` model at `register` parity (12.6µs vs 11.9µs bench). Bind the element with `defaultValue` instead of `value`, exactly like `<Field uncontrolled />`.
148
+
149
+ On unmount the field unregisters by default: its live value drops out of reads and `getValues()` (tombstone — no silent revival from `initialValues`). `shouldUnregister: false` keeps the value per field; `createForm({shouldUnregister: false})` or `<Form shouldUnregister={false}>` flips the form-wide default to react-hook-form's keep-the-value semantics, and a field-level option overrides the form-level flag in either direction.
150
+
137
151
  ### `subscribe`
138
152
 
139
153
  Linked fields and other non-render side effects — province changed → clear city, autosave, analytics — should not require a mounted watching component. `subscribe` exposes the event core imperatively:
@@ -155,13 +169,13 @@ const unsubscribe = subscribe(form, {
155
169
  | Option | Type | Default |
156
170
  |---|---|---|
157
171
  | `name` | field path, or an array of them | omitted — every emission of `event`, payload-less broadcasts (reset, …) included |
158
- | `event` | `'change'` \| `'errors'` \| `'touched'` \| `'submitting'` \| `'submitCount'` | `'change'` |
172
+ | `event` | `'change'` \| `'errors'` \| `'touched'` \| `'validating'` \| `'submitting'` \| `'submitCount'` \| `'submitSuccessful'` \| `'disabled'` | `'change'` |
159
173
  | `scope` | `'leaf'` \| `'branch'` | `'branch'` |
160
174
  | `callback` | `() => void`, fired with no arguments | required |
161
175
 
162
- Matching follows the event's shape. `'change'` walks the path tree: the default `'branch'` scope wakes a `'tags'` subscriber when any `tags.*` descendant is written, while `'leaf'` matches only the exact key and its ancestors. `'errors'` and `'touched'` always match the exact key — another field's error never wakes this subscriber. `'submitting'`/`'submitCount'` are payload-less, so a `name` narrows nothing. An array of names creates one subscription per path, and the returned function unsubscribes them all. A number-bearing array (`['tags', 0]`) is one segments path, not a name list — the same rule `trigger` uses.
176
+ Matching follows the event's shape. `'change'` walks the path tree: the default `'branch'` scope wakes a `'tags'` subscriber when any `tags.*` descendant is written, while `'leaf'` matches only the exact key and its ancestors. `'validating'` carries a path per async validator round and narrows by path exactly like `'change'` — the imperative counterpart of a per-field validating indicator. `'errors'` and `'touched'` always match the exact key — another field's error never wakes this subscriber. `'submitting'`/`'submitCount'`/`'submitSuccessful'`/`'disabled'` are payload-less, so a `name` narrows nothing. An array of names creates one subscription per path, and the returned function unsubscribes them all. A number-bearing array (`['tags', 0]`) is one segments path, not a name list — the same rule `trigger` uses.
163
177
 
164
- **`subscribe` vs `useWatch`:** `useWatch` (and the `useValue`/`useError`/… readers built on it) feeds rendering — it returns a snapshot and re-renders the component when it changes. `subscribe` runs imperative code and renders nothing. Use `subscribe` for linkages and effects; reach for a hook only when the watched value itself must appear on screen.
178
+ **`subscribe` vs `useWatch`:** `useWatch` (and the `useValue`/`useError`/… readers built on it) feeds rendering — it returns a snapshot and re-renders the component when it changes. `subscribe` runs imperative code and renders nothing. Use `subscribe` for linkages and effects; reach for a hook only when the watched value itself must appear on screen. Its first argument is the form (`useWatch(form, 'change', getter)`), matching every other hook's context shape; the raw emitter remains accepted for back-compat. `useWatch` itself takes an optional fourth argument — `isEqual(prev, next)` — aimed at wide-scope getters that return a fresh reference per call (a whole-values selector, say): on each event the getter recomputes, and an equal verdict keeps the cached snapshot without notifying React at all — no render, not even a bailed-out one (the same contract TanStack's `useSelector` `compare` option has).
165
179
 
166
180
  ### `useFieldArray`
167
181
 
@@ -384,6 +398,19 @@ function Profile({onSave}) {
384
398
 
385
399
  It is `false` for the whole async `onSubmit` span (not just the validation pass) and whenever any field holds an error — client validation or server backfill (`setServerErrors` lands there too). Deliberately no dirty or validating semantics: an untouched-but-clean form can submit. The snapshot recomputes on either input's event and re-renders only when the boolean itself flips. The underlying readers stay exported — `useIsSubmitting`, `useHasErrors`, `useSubmitCount` — for UIs that need the parts separately.
386
400
 
401
+ For the validation span itself reach for `useIsValidating(form)` — `true` while any field's async validator or pending debounce window is open and while the form-level validate round is in flight (the same marks `trigger` and submit wait out). The classic consumers are a spinner and a double-click guard on the same button:
402
+
403
+ ```jsx
404
+ import {useIsValidating} from 'react-f0rm';
405
+
406
+ const isValidating = useIsValidating(form);
407
+ <button disabled={!canSubmit || isValidating} onClick={submit}>
408
+ {isValidating ? 'Checking…' : 'Save'}
409
+ </button>
410
+ ```
411
+
412
+ `useCanSubmit` stays validating-free on purpose (see above), so combine the two flags when you want the stricter gate. The submission-outcome sibling `useIsSubmitSuccessful(form)` is exported alongside: `true` once `onSubmit`/`onValidSubmit` resolved without throwing, `false` when validation failed or a handler threw, `undefined` before the first submit — the usual success-banner/redirect trigger.
413
+
387
414
  ### Focusing the first error
388
415
 
389
416
  After a failed submit, the offending field is focused automatically — pass `shouldFocusError: false` (on `<Form>` or `handleSubmit`) to disable; it defaults to `true`. Custom validation failures focus the first errored field through a `'focusError'` event that bound fields (like `Field`) subscribe to; native constraint failures focus the submitted form's first `:invalid` control directly.
@@ -481,11 +508,17 @@ if (await trigger(form, 'email')) {
481
508
 
482
509
  Without `name` the scope is all fields plus the form-level `validate` result; with `name` only those fields' own errors count and form-level `validate` is skipped (RHF semantics). Fire-and-forget callers may ignore the promise — the validator kicks still happen synchronously.
483
510
 
511
+ The third argument opts into touched marking: `trigger(form, name, {shouldTouch: true})` marks every path in the triggered scope — the given names, or all registered fields when `name` is omitted — as touched once the round settles, whether validation passed or failed (react-hook-form's `trigger` semantics). Omitted, `trigger` stays validate-only:
512
+
513
+ ```jsx
514
+ await trigger(form, 'email', {shouldTouch: true}); // 'email' is now touched, error or not
515
+ ```
516
+
484
517
  ### Async validation
485
518
 
486
519
  Async validators are first-class. Two knobs keep them cheap and race-free:
487
520
 
488
- **`validateDebounce`** (on `Field`, `useField` or any bound component) delays a field's validation kicks by the given milliseconds; only the last kick inside the window runs the validator. While the timer is pending the field counts as *validating*, so `trigger` and submit wait the window out instead of racing it. The form-level `validate` gets the same contract through `validateDebounce` on `createForm`/`useForm` (see [Form-level validation](#form-level-validation)).
521
+ **`validateDebounce`** (on `Field`, `useField` or any bound component) delays a field's validation kicks by the given milliseconds; only the last kick inside the window runs the validator. While the timer is pending the field counts as *validating*, so `trigger` and submit wait the window out instead of racing it. The `required` rule is exempt: it runs synchronously on every kick, so a required failure shows immediately — and while it fails, the field's other validation is skipped. The form-level `validate` gets the same contract through `validateDebounce` on `createForm`/`useForm` (see [Form-level validation](#form-level-validation)).
489
522
 
490
523
  **`meta.signal`** — every validator's second argument carries `{form, path, signal}`. The `AbortSignal` fires as soon as the round is superseded (a newer round started, or the field unregistered), so async validators can cancel their underlying work instead of racing a stale result home:
491
524
 
@@ -551,7 +584,7 @@ Committed baselines follow the form's lifecycle: `reset`, `setInitialValues` and
551
584
 
552
585
  ### Writing as a user change (`changeValue`)
553
586
 
554
- `setValue` is the imperative channel — `shouldValidate` kicks the field's validator unconditionally, ignoring any mode. `changeValue` is the user-change channel: the write routes through the mounted field's own `onChange`, so it fires exactly the validation a user typing into the field would fire — the field's effective `mode` (per-field override included) and the form's `reValidateMode`. With no mounted field on the path it degrades to a plain `setValue`.
587
+ `setValue` is the imperative channel — `shouldValidate` kicks the field's validator unconditionally, ignoring any mode. `changeValue` is the user-change channel: the write rides the same core pipeline a user typing into the field would fire (`userChangeByPath` + the field-mode registry `useField` registers its mode override on mount), so it fires exactly the validation a user typing would fire — the field's effective `mode` (per-field override included) and the form's `reValidateMode`. With no mounted field on the path it degrades to a plain `setValue`.
555
588
 
556
589
  ```jsx
557
590
  import {changeValue} from 'react-f0rm';
@@ -561,7 +594,7 @@ import {changeValue} from 'react-f0rm';
561
594
  changeValue(form, 'email', 'a@b.com');
562
595
  ```
563
596
 
564
- This is the channel component libraries need when they hand a control a plain setter bound to a field (a `Control`/controlled-bridge over `useField`'s value): the mode gating — per-field override and live-error view — lives inside `useField`'s `onChange` closure and cannot be rebuilt from public form state, so `useField` publishes its `onChange` on the form (`form.changeHandlers`) and `changeValue`/`changeValueByPath` route through it.
597
+ This is the channel component libraries need when they hand a control a plain setter bound to a field (a `Control`/controlled-bridge over `useField`'s value): the mode gating — per-field override and live-error view — lives in the core's user-change pipeline, not in public form state, so the write must route through it rather than through a raw `setValue`.
565
598
 
566
599
  `changeValue` takes the same options object as `setValue` (see [`setValue` options](#setvalue-options)). With a field mounted on the path, `shouldDirty: false` applies — the write lands as a commit while the field's own mode gating keeps driving validation, which is the point of this channel (`shouldValidate`/`shouldTouch` have no meaning there: forcing them would defeat the gating). With no mounted field, the options forward to the plain `setValue` fallback wholesale:
567
600
 
@@ -610,10 +643,10 @@ The optional top-level `messages` record overrides messages per rule type (`min`
610
643
 
611
644
  Semantics:
612
645
 
613
- - A failing `required` short-circuits the rest — an empty value reports only its `required` error, not a full panel.
646
+ - A failing `required` short-circuits the rest — an empty value reports only its `required` error, not a full panel — and skips `validate` entirely for that kick: the async check never sees an empty value.
647
+ - `required` runs synchronously on every kick, even under a positive `validateDebounce`: its error shows on the keystroke and clears as soon as the value is filled.
614
648
  - Every other failing rule collects into one ordered `FieldError[]` (see [Multiple errors per field](#multiple-errors-per-field)).
615
- - `rules` composes with `validate`: rules run first, then `validate` (awaited when async), merging both sources' errors with rules ahead.
616
- - Rules ride the exact same pipeline as `validate` — `mode`, `reValidateMode`, `validateDebounce` and `meta.signal` all apply unchanged.
649
+ - The other rules compose with `validate`: they run first, then `validate` (awaited when async), merging both sources' errors with rules ahead. They ride the same pipeline as `validate` — `mode`, `reValidateMode`, `validateDebounce` and `meta.signal` all apply unchanged.
617
650
 
618
651
  Rules vs native constraints: HTML attributes (`required`, `type="email"`, `min`, …) keep running through the browser's `checkValidity`, whose bubble remains the pre-submit fallback. `rules` is the state-side alternative — failures are queryable (`getErrors`, `error`, `errors`), renderable by any UI, and carry your own messages. Prefer `rules` whenever the error text must be controlled.
619
652
 
@@ -653,6 +686,21 @@ const form = useForm({
653
686
 
654
687
  The `AbortSignal` fires as soon as the round is superseded — a newer round started, which under a positive `validateDebounce` means a kick landed during the in-flight round's window — so async validators can cancel their underlying work instead of racing a stale result home. Stale results are dropped independently by the round gate, so validators that ignore the signal stay correct too. Without `validateDebounce` (`0`/omitted) the validate runs once per `trigger`/submit exactly as before; it still receives the meta argument, but nothing supersedes an immediate round, so its signal never fires.
655
688
 
689
+ #### Reading form-level errors
690
+
691
+ Errors that belong to no single field need a slot to land in: a form-level `validate` record may return a `_form` entry, and the Standard Schema adapter drops every path-less issue there (see [Schema validation](#schema-validation)). That reserved key is exported as `FORM_ERROR`, so the magic string never has to be hand-written:
692
+
693
+ ```jsx
694
+ import {FORM_ERROR, useFormError, useFormErrors} from 'react-f0rm';
695
+
696
+ function FormErrorBanner({form}) {
697
+ const error = useFormError(form); // first form-level error's message
698
+ return error ? <p role="alert">{error}</p> : null;
699
+ }
700
+ ```
701
+
702
+ `useFormError(form)` reads the slot's first message (`undefined` while clean) — the classic consumer is one banner above the submit button. `useFormErrors(form)` reads every error stored under the key (`FieldError[]`, stable reference between unrelated events). The imperative twins are `getError(form, FORM_ERROR)` and `getFieldErrors(form, FORM_ERROR)`, and writes go through the same `setError(form, FORM_ERROR, …)` every field uses.
703
+
656
704
  #### Re-running on dependent field changes (`validateDeps`)
657
705
 
658
706
  By default the form-level `validate` runs on `trigger` and submit only — a cross-field error stays on screen even after the user edits the field that would fix it. `validateDeps` declares the fields whose **user changes re-run the form-level `validate`**:
@@ -817,15 +865,37 @@ const article = useData<Article>() ?? undefined;
817
865
  const form = useForm({initialValues: articleToValues(article)});
818
866
  ```
819
867
 
868
+ ### Async initial values
869
+
870
+ `initialValues` accepts a Promise, or a thunk returning a value or Promise — react-hook-form's async `defaultValues` shape. The form starts empty with `isLoading: true`, and when the source resolves, its values become the baseline (setInitialValues semantics: value subscribers re-sync, dirty/touched start clean, `reset()` returns to the resolved values):
871
+
872
+ ```jsx
873
+ const form = useForm({
874
+ initialValues: () => fetchUser(id).then(u => ({name: u.name, email: u.email}))
875
+ });
876
+ const isLoading = useIsLoading(form); // also on useFormState(form).isLoading
877
+
878
+ if (isLoading) return <Spinner />;
879
+ return <Form form={form}>…</Form>;
880
+ ```
881
+
882
+ Notes:
883
+
884
+ - The thunk runs at create time — keep its identity stable (`useMemo`, module scope) when passing it inline; StrictMode double-invokes it in development, like every render-phase call.
885
+ - A rejected source keeps the form empty, flips `isLoading` off and logs the error in DEV — attach a `.catch` on the source to handle it.
886
+ - SSR renders the form empty with `isLoading: true` on both sides, so hydration matches; the values land client-side after the fetch (pass the server-resolved record to hydrate eagerly instead).
887
+
820
888
  ### Resetting
821
889
 
822
- `reset(form, initialValues?)` wipes values, errors, touched, tombstones and the submission flags (`isSubmitting`, `submitCount`, `isSubmitSuccessful`). The second argument installs a fresh baseline. The third opts into keeping slices of state through the reset:
890
+ `reset(form, initialValues?)` wipes values, errors, touched, tombstones and the submission flags (`isSubmitting`, `submitCount`, `isSubmitSuccessful`). The second argument installs a fresh baseline; omitted (or `undefined`), the form keeps its current `initialValues` and every field simply returns to its initial value — the plain `reset(form)` "undo everything" shape. The third opts into keeping slices of state through the reset:
823
891
 
824
892
  ```jsx
825
893
  import {reset} from 'react-f0rm';
826
894
 
895
+ reset(form); // back to the current initialValues
827
896
  reset(form, freshRecord); // full reset to the new baseline
828
897
  reset(form, freshRecord, {keepDirtyValues: true}); // dirty drafts survive
898
+ reset(form, undefined, {keepTouched: true}); // reset, keep touched flags
829
899
  ```
830
900
 
831
901
  `keepDirtyValues` is the refetch shape: reload the record from the server, but fields the user already edited keep their live values (dirtiness is measured against the pre-reset initialValues; clean fields fall back to the new baseline):
@@ -918,6 +988,38 @@ async function onSubmit(values) {
918
988
 
919
989
  The message then renders under the field through the same error machinery (`renderError`, `useError` — see [Accessibility](#accessibility)), and the field's `aria-invalid`/`aria-describedby` wiring kicks in automatically. Existing errors are cleared first — a fresh response describes the current state; pass `{keepExisting: true}` to layer instead. String values land as one error, string arrays as several; an empty array clears that field.
920
990
 
991
+ ## Server-side validation
992
+
993
+ The client-side gate is UX, not security — payloads must be re-validated where they arrive. `react-f0rm/server` is the entry for that: a separate module graph with zero React, safe to import from Server Actions, RSC and plain Node, and — like the resolvers and devtools — never re-exported from the main entry, so client builds that never validate server-side stay at baseline size. Its export is one function:
994
+
995
+ ```jsx
996
+ import {validateValues} from 'react-f0rm/server';
997
+
998
+ // A Server Action — or any handler that receives a payload
999
+ export async function saveProfile(values) {
1000
+ const {valid, values: parsed, errors} = await validateValues(values, {
1001
+ validate: values =>
1002
+ values.email.includes('@') ? undefined : {email: 'Invalid email'}
1003
+ });
1004
+ if (!valid) return {errors};
1005
+ return save(parsed);
1006
+ }
1007
+ ```
1008
+
1009
+ `validateValues(values, options?)` spins up one throwaway form from `options` (its `initialValues` forced to `values`), runs a whole-form `trigger`, and reads the outcome back — async validators and `validateDebounce` windows are awaited, so the result is settled, never a mid-flight snapshot. The rules come from `options.validate`, the form-level validator: field validators register through mounted fields and nothing is mounted on the server, so pass `standardSchemaFormValidator(schema)` (from `react-f0rm/resolvers/standard-schema`) or a hand-written `validate`. The result carries:
1010
+
1011
+ - `valid` — `trigger`'s boolean. An invalid payload is a normal outcome, never a rejection: both branches are interesting on the server (persist vs. bounce back).
1012
+ - `values` — the tree after the round. A schema validator's parsed output (coerce/transform included) becomes the baseline, so this is the tree to persist, not necessarily the object passed in.
1013
+ - `errors` — the flat `{path, type, message}` entries, the same list `getErrors` hands out on the client. A one-liner lands a failed round back on the client form through the [Server-side errors](#server-side-errors) channel:
1014
+
1015
+ ```jsx
1016
+ setServerErrors(form, Object.fromEntries(errors.map(e => [e.path, e.message])));
1017
+ ```
1018
+
1019
+ The entry also re-exports `VALIDATION_OUTCOME`/`ValidationOutcome` for building branded validator results server-side without importing the package root (which would drag the React graph back in).
1020
+
1021
+ TanStack Form's counterpart is `createServerValidate`; theirs wraps the round inside a generated server action, while `validateValues` stays a plain function over values. That is the same stance as the client bridge — no `action` prop, no generated handler ("Server Actions: bridge, not first-class" in [Which one should you use?](#which-one-should-you-use), and the [React 19 Server Actions guide](docs-site/docs/guides/react19-server-actions.md) for the why): react-f0rm composes into your framework's handler instead of owning it.
1022
+
921
1023
  ## TypeScript
922
1024
 
923
1025
  `FieldPath<T>` and `PathValue<T, P>` make field names and value types compile-time checked:
@@ -937,8 +1039,10 @@ type ValuesPath = FieldPath<Values>;
937
1039
  type UserName = PathValue<Values, 'user.name'>;
938
1040
 
939
1041
  function UserNameField() {
940
- // value is inferred as string; an unknown path degrades to `any`
941
- // (PathValueOf), keeping dynamic names usable
1042
+ // value is inferred as string; a path outside FieldPath<Values> — a
1043
+ // typo or an untyped string variable — is a compile error on these
1044
+ // generic APIs (react-hook-form parity). Segment arrays
1045
+ // (['user', 'name']) stay accepted and read as `any`.
942
1046
  const {value, onChange} = useField<Values, 'user.name'>({name: 'user.name'});
943
1047
  return <input value={value} onChange={e => onChange(e.target.value)} />;
944
1048
  }
@@ -946,6 +1050,11 @@ function UserNameField() {
946
1050
 
947
1051
  The same generics work on `getValue`/`setValue`/`getError` and the other path-taking helpers.
948
1052
 
1053
+ The strictness is on the generic path APIs (`useField`, `setValue`, `getValue`, `useValue`, …): an unknown path fails there. Two escape hatches stay deliberately wide, because their names are runtime-computed by nature:
1054
+
1055
+ - **Segment arrays** (`['items', 0]`): accepted everywhere, value reads as `any` — the dynamic-path escape hatch.
1056
+ - **Runtime-dynamic entry points**: `useFieldArray`, `useFieldArrayItem`, `removeField`, `setTouched`, `setFocus`, `trigger` and `clearErrors` take the wide `Name` type, so `name={dynamicString}` keeps compiling without casts.
1057
+
949
1058
  The default context is typed too — `useFormContext<Values>()` returns a `Form<Values>`, so downstream components drop the `any` dances (`eslint-disable no-unsafe-*`, value casts) without buying into `createFormContext`:
950
1059
 
951
1060
  ```tsx
@@ -1006,6 +1115,17 @@ import {Select} from 'react-f0rm';
1006
1115
  </Select>
1007
1116
  ```
1008
1117
 
1118
+ **File inputs.** The DOM keeps `<input type="file">`'s `value` read-only — it holds a fake file path string and throws if you assign to it, so the control cannot be driven like other inputs. The controlled model adapts by storing the selection itself: leave the input uncontrolled and commit the chosen `File` object on change:
1119
+
1120
+ ```jsx
1121
+ function AvatarPicker() {
1122
+ const {onChange} = useField({name: 'avatar'});
1123
+ return <input type="file" accept="image/*" onChange={e => onChange(e.target.files?.[0])} />;
1124
+ }
1125
+ // getValues(form).avatar is now the File itself — on submit it is
1126
+ // ready for the request body (FormData/multipart), no DOM read needed.
1127
+ ```
1128
+
1009
1129
  ## Server-side Rendering
1010
1130
 
1011
1131
  Form state lives in synchronously readable structures seeded from `initialValues`, and every subscription goes through `useSyncExternalStore` with a `getServerSnapshot` that computes the same snapshot as the client's first render. `renderToString` therefore renders form-driven components with their initial values out of the box, and `hydrateRoot` matches the server markup — no provider shims, no `typeof window` guards:
@@ -1,2 +1,2 @@
1
- "use strict";var e=require("react"),t=require("../form-DwuY91QB.cjs.js");function n(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 r,o=n(e),a={exports:{}},l={};var s,i,d={};var c=(i||(i=1,"production"===process.env.NODE_ENV?a.exports=function(){if(r)return l;r=1;var t=e,n="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},o=t.useState,a=t.useEffect,s=t.useLayoutEffect,i=t.useDebugValue;function d(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!n(e,r)}catch(e){return!0}}var c="undefined"==typeof window||void 0===window.document||void 0===window.document.createElement?function(e,t){return t()}:function(e,t){var n=t(),r=o({inst:{value:n,getSnapshot:t}}),l=r[0].inst,c=r[1];return s(function(){l.value=n,l.getSnapshot=t,d(l)&&c({inst:l})},[e,n,t]),a(function(){return d(l)&&c({inst:l}),e(function(){d(l)&&c({inst:l})})},[e]),i(n),n};return l.useSyncExternalStore=void 0!==t.useSyncExternalStore?t.useSyncExternalStore:c,l}():a.exports=(s||(s=1,"production"!==process.env.NODE_ENV&&function(){function t(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!r(e,n)}catch(e){return!0}}"undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());var n=e,r="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},o=n.useState,a=n.useEffect,l=n.useLayoutEffect,s=n.useDebugValue,i=!1,c=!1,f="undefined"==typeof window||void 0===window.document||void 0===window.document.createElement?function(e,t){return t()}:function(e,d){i||void 0===n.startTransition||(i=!0,console.error("You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release."));var f=d();if(!c){var p=d();r(f,p)||(console.error("The result of getSnapshot should be cached to avoid an infinite loop"),c=!0)}var u=(p=o({inst:{value:f,getSnapshot:d}}))[0].inst,m=p[1];return l(function(){u.value=f,u.getSnapshot=d,t(u)&&m({inst:u})},[e,f,d]),a(function(){return t(u)&&m({inst:u}),e(function(){t(u)&&m({inst:u})})},[e]),s(f),f};d.useSyncExternalStore=void 0!==n.useSyncExternalStore?n.useSyncExternalStore:f,"undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())}()),d)),a.exports);function f(n,r,o){return function(t,n){const r=e.useRef(null);null===r.current&&(r.current={hasValue:!1});const o=r.current,a=e.useRef(n);a.current=n;const l=e.useCallback(()=>(o.hasValue||(o.value=a.current(),o.hasValue=!0),o.value),[o]),s=e.useCallback(e=>(o.hasValue=!1,t(()=>{o.hasValue=!1,e()})),[t,o]);return c.useSyncExternalStore(s,l,l)}(e.useCallback(e=>t.on(n,r,e),[n,r]),o)}const p=e.createContext(null);p.Provider;e.createContext(null).Provider;function u({name:t,value:n,depth:r=0}){const[a,l]=e.useState(r<=1),s=void 0===t?null:o.createElement(o.Fragment,null,o.createElement("span",{className:"rf0-dt-key"},String(t)),o.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),i=e?"[":"{",d=e?"]":"}",c=a?"":`${i}…${d} ${t.length}`;return o.createElement("div",{className:"rf0-dt-row",style:{paddingLeft:12*r}},o.createElement("button",{type:"button",className:"rf0-dt-node-toggle","aria-expanded":a,onClick:()=>l(!a)},o.createElement("span",{className:"rf0-dt-caret"},a?"▾":"▸"),s,o.createElement("span",{className:"rf0-dt-punct"},a?i:c)),a&&o.createElement(o.Fragment,null,t.map(([e,t])=>o.createElement(u,{key:String(e),name:e,value:t,depth:r+1})),o.createElement("span",{className:"rf0-dt-punct",style:{paddingLeft:12*r}},d)))}return o.createElement("span",{className:"rf0-dt-row",style:{paddingLeft:12*r,display:"block"}},s,o.createElement(m,{value:n}))}function m({value:e}){return void 0===e?o.createElement("span",{className:"rf0-dt-null"},"undefined"):null===e?o.createElement("span",{className:"rf0-dt-null"},"null"):"string"==typeof e?o.createElement("span",{className:"rf0-dt-string"},'"',e,'"'):"boolean"==typeof e?o.createElement("span",{className:"rf0-dt-boolean"},String(e)):o.createElement("span",{className:"rf0-dt-number"},String(e))}const b="react-f0rm-devtools-style";!function(){if("undefined"==typeof document)return;if(document.getElementById(b))return;const e=document.createElement("style");e.id=b,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 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:n,position:r="top-right"}){const a=e.useContext(p),l=n??a;if(!l)throw new Error("<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.");const[s,i]=e.useState(!0),[d,c]=e.useState("values"),m=e.useId().replace(/[^a-zA-Z0-9-]/g,""),b=f(l.emitter,"change",t.getValues.bind(null,l)),y=f(l.emitter,"errors",t.getErrors.bind(null,l)),E=function(e){return f(e.emitter,"touched",t.getTouchedFields.bind(null,e))}(l),v=function(e){return f(e.emitter,"change",t.getDirtyFields.bind(null,e))}(l),w=function(e){return f(e.emitter,"submitting",()=>e.isSubmitting)}(l),k=function(e){return f(e.emitter,"submitCount",()=>e.submitCount)}(l),S=f(l.emitter,"submitSuccessful",()=>l.isSubmitSuccessful);if(!s)return o.createElement("button",{type:"button",className:`rf0-dt-badge rf0-dt-badge--${r}${y.length>0?" rf0-dt-badge--has-errors":""}`,"aria-expanded":!1,"aria-label":`Open react-f0rm devtools (${y.length} errors)`,onClick:()=>i(!0)},"f0",o.createElement("span",{className:"rf0-dt-dot"}));const O={values:h(b),errors:y.length,touched:E.length,dirty:Object.keys(v).length};return o.createElement("section",{className:`rf0-dt rf0-dt--${r}`,"aria-label":"react-f0rm devtools"},o.createElement("header",{className:"rf0-dt-header"},o.createElement("span",{className:"rf0-dt-title"},"react-f0rm"),o.createElement("button",{type:"button",className:"rf0-dt-headerbtn","aria-label":"Collapse devtools",onClick:()=>i(!1)},"–")),o.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(d)+t+g.length)%g.length];c(n),document.getElementById(`${m}-tab-${n}`)?.focus()}},g.map(e=>o.createElement("button",{key:e,id:`${m}-tab-${e}`,type:"button",role:"tab",className:"rf0-dt-tab"+("errors"===e?" rf0-dt-tab--danger":""),"aria-selected":d===e,"aria-controls":`${m}-panel-${e}`,tabIndex:d===e?0:-1,onClick:()=>c(e)},e,o.createElement("span",{className:"rf0-dt-tab-count"},O[e])))),o.createElement("div",{id:`${m}-panel-${d}`,role:"tabpanel","aria-labelledby":`${m}-tab-${d}`,className:"rf0-dt-panel"},"values"===d&&o.createElement(u,{value:b}),"errors"===d&&(0===y.length?o.createElement("p",{className:"rf0-dt-empty"},"no errors"):y.map(({path:e,type:t,message:n},r)=>o.createElement("div",{key:`${e}:${r}`,className:"rf0-dt-item"},o.createElement("span",{className:"rf0-dt-item-path"},e),o.createElement("span",{className:"rf0-dt-item-msg"},n),o.createElement("span",{className:"rf0-dt-item-tag"},t)))),"touched"===d&&(0===E.length?o.createElement("p",{className:"rf0-dt-empty"},"no touched fields"):E.map(e=>o.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--touched"},o.createElement("span",{className:"rf0-dt-item-path"},e)))),"dirty"===d&&(0===Object.keys(v).length?o.createElement("p",{className:"rf0-dt-empty"},"no dirty fields"):Object.keys(v).map(e=>o.createElement("div",{key:e,className:"rf0-dt-item rf0-dt-item--dirty"},o.createElement("span",{className:"rf0-dt-item-path"},e),o.createElement("span",{className:"rf0-dt-item-msg rf0-dt-item-msg--ok"},"changed"))))),o.createElement("p",{className:"rf0-dt-status","aria-live":"polite"},o.createElement("span",{className:w?"rf0-dt-on":void 0},"submitting ",o.createElement("b",null,String(w))),o.createElement("span",null,"submits ",o.createElement("b",null,k)),o.createElement("span",{className:x(S)},"ok"," ",o.createElement("b",null,void 0===S?"–":String(S)))),o.createElement("div",{className:"rf0-dt-actions"},o.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>t.reset(l,l.initialValues)},"Reset"),o.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>t.trigger(l)},"Validate")))};
1
+ "use strict";var e=require("react"),t=require("@for-fun/event-emitter"),n=require("../values-CDNAYEOB.cjs.js"),r=require("../errors-CxSjrWJO.cjs.js"),o=require("../validate-DAfz8Nbb.cjs.js");function a(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=a(e);function l({touched:e}){return Array.from(e,e=>JSON.parse(e).join("."))}function i(e){const t={};return function(e,t){for(const[n,o]of e.values){const a=JSON.parse(n);r.getDirtyBaseline(e,n,a)!==o&&t(a.join("."))}}(e,e=>{t[e]=!0}),t}function d(e){let t=r.dirtyFieldsCaches.get(e);if(t){if(t.version>0){const n=i(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:i(e)},r.dirtyFieldsCaches.set(e,t);return t.result}var c,f={exports:{}},u={};var p,b,m={};var g=(b||(b=1,"production"===process.env.NODE_ENV?f.exports=function(){if(c)return u;c=1;var t=e,n="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},r=t.useState,o=t.useEffect,a=t.useLayoutEffect,s=t.useDebugValue;function l(e){var t=e.getSnapshot;e=e.value;try{var r=t();return!n(e,r)}catch(e){return!0}}var i="undefined"==typeof window||void 0===window.document||void 0===window.document.createElement?function(e,t){return t()}:function(e,t){var n=t(),i=r({inst:{value:n,getSnapshot:t}}),d=i[0].inst,c=i[1];return a(function(){d.value=n,d.getSnapshot=t,l(d)&&c({inst:d})},[e,n,t]),o(function(){return l(d)&&c({inst:d}),e(function(){l(d)&&c({inst:d})})},[e]),s(n),n};return u.useSyncExternalStore=void 0!==t.useSyncExternalStore?t.useSyncExternalStore:i,u}():f.exports=(p||(p=1,"production"!==process.env.NODE_ENV&&function(){function t(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!r(e,n)}catch(e){return!0}}"undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());var n=e,r="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},o=n.useState,a=n.useEffect,s=n.useLayoutEffect,l=n.useDebugValue,i=!1,d=!1,c="undefined"==typeof window||void 0===window.document||void 0===window.document.createElement?function(e,t){return t()}:function(e,c){i||void 0===n.startTransition||(i=!0,console.error("You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release."));var f=c();if(!d){var u=c();r(f,u)||(console.error("The result of getSnapshot should be cached to avoid an infinite loop"),d=!0)}var p=(u=o({inst:{value:f,getSnapshot:c}}))[0].inst,b=u[1];return s(function(){p.value=f,p.getSnapshot=c,t(p)&&b({inst:p})},[e,f,c]),a(function(){return t(p)&&b({inst:p}),e(function(){t(p)&&b({inst:p})})},[e]),l(f),f};m.useSyncExternalStore=void 0!==n.useSyncExternalStore?n.useSyncExternalStore:c,"undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())}()),m)),f.exports);function x(n,r,o,a){const s="emitter"in n?n.emitter:n;return function(t,n,r){const o=e.useRef(null);null===o.current&&(o.current={hasValue:!1});const a=o.current,s=e.useRef(n);s.current=n;const l=e.useRef(r);l.current=r;const i=e.useCallback(()=>(a.hasValue||(a.value=s.current(),a.hasValue=!0),a.value),[a]),d=e.useCallback(e=>(a.hasValue=!1,t(()=>{const t=l.current;if(t&&a.hasValue){const n=s.current();if(t(a.value,n))return;return a.value=n,void e()}a.hasValue=!1,e()})),[t,a]);return g.useSyncExternalStore(d,i,i)}(e.useCallback(e=>t.on(s,r,e),[s,r]),o,a)}function h(e){return x(e,"touched",l.bind(null,e))}const v=e.createContext(null);v.Provider;e.createContext(null).Provider;function y({name:t,value:n,depth:r=0}){const[o,a]=e.useState(r<=1),l=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),i=e?"[":"{",d=e?"]":"}",c=o?"":`${i}…${d} ${t.length}`;return s.createElement("div",{className:"rf0-dt-row",style:{paddingLeft:12*r}},s.createElement("button",{type:"button",className:"rf0-dt-node-toggle","aria-expanded":o,onClick:()=>a(!o)},s.createElement("span",{className:"rf0-dt-caret"},o?"▾":"▸"),l,s.createElement("span",{className:"rf0-dt-punct"},o?i:c)),o&&s.createElement(s.Fragment,null,t.map(([e,t])=>s.createElement(y,{key:String(e),name:e,value:t,depth:r+1})),s.createElement("span",{className:"rf0-dt-punct",style:{paddingLeft:12*r}},d)))}return s.createElement("span",{className:"rf0-dt-row",style:{paddingLeft:12*r,display:"block"}},l,s.createElement(E,{value:n}))}function E({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 w="react-f0rm-devtools-style";const O=["values","errors","touched","dirty"];function k(e){if(void 0!==e)return e?"rf0-dt-ok":"rf0-dt-err"}function S(e){if(null===e||"object"!=typeof e)return 1;let t=0;for(const n of Object.values(e))t+=S(n);return t}exports.Devtools=function({form:t,position:a="top-right"}){!function(){if("undefined"==typeof document)return;if(document.getElementById(w))return;const e=document.createElement("style");e.id=w,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 l=e.useContext(v),i=t??l;if(!i)throw new Error("<Devtools> needs a form: pass the `form` prop or render it inside a <Form> / FormProvider.");const[c,f]=e.useState(!0),[u,p]=e.useState("values"),b=e.useId().replace(/[^a-zA-Z0-9-]/g,""),m=x(i,"change",n.getValues.bind(null,i)),g=x(i,"errors",r.getErrors.bind(null,i)),E=h(i),N=function(e){return x(e,"change",d.bind(null,e))}(i),_=function(e){return x(e,"submitting",()=>e.isSubmitting)}(i),C=function(e){return x(e,"submitCount",()=>e.submitCount)}(i),j=x(i,"submitSuccessful",()=>i.isSubmitSuccessful);if(!c)return s.createElement("button",{type:"button",className:`rf0-dt-badge rf0-dt-badge--${a}${g.length>0?" rf0-dt-badge--has-errors":""}`,"aria-expanded":!1,"aria-label":`Open react-f0rm devtools (${g.length} errors)`,onClick:()=>f(!0)},"f0",s.createElement("span",{className:"rf0-dt-dot"}));const L={values:S(m),errors:g.length,touched:E.length,dirty:Object.keys(N).length};return s.createElement("section",{className:`rf0-dt rf0-dt--${a}`,"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:()=>f(!1)},"–")),s.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=O[(O.indexOf(u)+t+O.length)%O.length];p(n),document.getElementById(`${b}-tab-${n}`)?.focus()}},O.map(e=>s.createElement("button",{key:e,id:`${b}-tab-${e}`,type:"button",role:"tab",className:"rf0-dt-tab"+("errors"===e?" rf0-dt-tab--danger":""),"aria-selected":u===e,"aria-controls":`${b}-panel-${e}`,tabIndex:u===e?0:-1,onClick:()=>p(e)},e,s.createElement("span",{className:"rf0-dt-tab-count"},L[e])))),s.createElement("div",{id:`${b}-panel-${u}`,role:"tabpanel","aria-labelledby":`${b}-tab-${u}`,className:"rf0-dt-panel"},"values"===u&&s.createElement(y,{value:m}),"errors"===u&&(0===g.length?s.createElement("p",{className:"rf0-dt-empty"},"no errors"):g.map(({path:e,type:t,message:n},r)=>s.createElement("div",{key:`${e}:${r}`,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)))),"touched"===u&&(0===E.length?s.createElement("p",{className:"rf0-dt-empty"},"no touched fields"):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)))),"dirty"===u&&(0===Object.keys(N).length?s.createElement("p",{className:"rf0-dt-empty"},"no dirty fields"):Object.keys(N).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"))))),s.createElement("p",{className:"rf0-dt-status","aria-live":"polite"},s.createElement("span",{className:_?"rf0-dt-on":void 0},"submitting ",s.createElement("b",null,String(_))),s.createElement("span",null,"submits ",s.createElement("b",null,C)),s.createElement("span",{className:k(j)},"ok"," ",s.createElement("b",null,void 0===j?"–":String(j)))),s.createElement("div",{className:"rf0-dt-actions"},s.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>n.reset(i,i.initialValues)},"Reset"),s.createElement("button",{type:"button",className:"rf0-dt-action",onClick:()=>o.trigger(i)},"Validate")))};
2
2
  //# sourceMappingURL=index.cjs.js.map