react-f0rm 1.2.0 → 1.3.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 (56) hide show
  1. package/README.md +107 -24
  2. package/dist/devtools/index.cjs.js +1 -1
  3. package/dist/devtools/index.cjs.js.map +1 -1
  4. package/dist/devtools/index.mjs +1 -1
  5. package/dist/devtools/index.mjs.map +1 -1
  6. package/dist/errors-BKrUdpfI.cjs.js +2 -0
  7. package/dist/errors-BKrUdpfI.cjs.js.map +1 -0
  8. package/dist/errors-CrQBddrJ.mjs +2 -0
  9. package/dist/errors-CrQBddrJ.mjs.map +1 -0
  10. package/dist/{form-CvmWHUrd.d.ts → form-CeKSBs31.d.ts} +68 -5
  11. package/dist/index.cjs.js +1 -1
  12. package/dist/index.cjs.js.map +1 -1
  13. package/dist/index.d.ts +344 -49
  14. package/dist/index.mjs +1 -1
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.umd.js +379 -283
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/index.umd.min.js +2 -2
  19. package/dist/index.umd.min.js.map +1 -1
  20. package/dist/persist.cjs.js +1 -1
  21. package/dist/persist.cjs.js.map +1 -1
  22. package/dist/persist.mjs +1 -1
  23. package/dist/persist.mjs.map +1 -1
  24. package/dist/resolvers/standard-schema.cjs.js +1 -1
  25. package/dist/resolvers/standard-schema.mjs +1 -1
  26. package/dist/resolvers/yup.cjs.js +1 -1
  27. package/dist/resolvers/yup.d.ts +1 -1
  28. package/dist/resolvers/yup.mjs +1 -1
  29. package/dist/resolvers/zod.cjs.js +1 -1
  30. package/dist/resolvers/zod.d.ts +1 -1
  31. package/dist/resolvers/zod.mjs +1 -1
  32. package/dist/server/index.cjs.js +1 -1
  33. package/dist/server/index.cjs.js.map +1 -1
  34. package/dist/server/index.d.ts +1 -1
  35. package/dist/server/index.mjs +1 -1
  36. package/dist/server/index.mjs.map +1 -1
  37. package/dist/{validate-B1Gdjeaq.mjs → validate-CNtuUhmk.mjs} +2 -2
  38. package/dist/validate-CNtuUhmk.mjs.map +1 -0
  39. package/dist/{validate-DAfz8Nbb.cjs.js → validate-Cl4ksNFu.cjs.js} +2 -2
  40. package/dist/validate-Cl4ksNFu.cjs.js.map +1 -0
  41. package/dist/{validate-CUmNZqg6.d.ts → validate-nksgv1pR.d.ts} +37 -3
  42. package/dist/values-Cu6awQOJ.cjs.js +2 -0
  43. package/dist/values-Cu6awQOJ.cjs.js.map +1 -0
  44. package/dist/values-DRY-a32G.mjs +2 -0
  45. package/dist/values-DRY-a32G.mjs.map +1 -0
  46. package/package.json +20 -9
  47. package/dist/errors-CxSjrWJO.cjs.js +0 -2
  48. package/dist/errors-CxSjrWJO.cjs.js.map +0 -1
  49. package/dist/errors-CzWtwjO0.mjs +0 -2
  50. package/dist/errors-CzWtwjO0.mjs.map +0 -1
  51. package/dist/validate-B1Gdjeaq.mjs.map +0 -1
  52. package/dist/validate-DAfz8Nbb.cjs.js.map +0 -1
  53. package/dist/values-B1IV-6V4.mjs +0 -2
  54. package/dist/values-B1IV-6V4.mjs.map +0 -1
  55. package/dist/values-CDNAYEOB.cjs.js +0 -2
  56. package/dist/values-CDNAYEOB.cjs.js.map +0 -1
package/README.md CHANGED
@@ -11,7 +11,8 @@ Coming from TanStack Form? [Migrating from TanStack Form](./docs/from-tanstack-f
11
11
 
12
12
  ## Features
13
13
 
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).
14
+ - **Field-level subscriptions.** Editing one field re-renders exactly that field's component, not the whole form. State is read through React's native `useSyncExternalStore`, so snapshots stay consistent under concurrent rendering (no tearing).
15
+ - **One shared runtime dependency.** The event core is `@for-fun/event-emitter` (a base library by the same author, ~2.3 KB gzip standalone) kept **external** in the ESM/CJS builds — an app that already depends on it dedupes the copy, and the emitted `Form.emitter` handle interops with the package's own `on()`/`emit()` types. `useSyncExternalStore` comes from React itself — the peer range is `react >=18`.
15
16
  - **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
17
  - **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
18
  - **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.
@@ -19,7 +20,10 @@ Coming from TanStack Form? [Migrating from TanStack Form](./docs/from-tanstack-f
19
20
  - **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
21
  - **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).
21
22
  - **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.
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.
23
+ - **Mount validation.** `createForm({validateOnMount: true})` (or per field) kicks every field's validator once after mount, so errors show on an untouched form; with an async `initialValues` source the kicks wait for the resolved baseline instead of validating the empty shell.
24
+ - **Form-level status channel.** `setStatus`/`useStatus` carry non-field state — server session flags, wizard steps, account-level errors — through the same event core (Formik's `status` role).
25
+ - **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. `asyncAlways` (field or form level) keeps a field's validator running even when its `required` gate failed, landing both verdicts per-source.
26
+ - **Declarative rules, native and store-side.** `rules` (`required`/`min`/`max`/`minLength`/`maxLength`/`pattern`/`validate` callbacks) compile into the store-based pipeline — queryable errors, your messages, `renderError`/`aria-invalid` — and the declarative subset renders as native constraint attributes (`required`, `minLength`, `pattern`, …) for `:invalid` styling and screen-reader hints. A user-passed attribute always wins over the derived one.
23
27
  - **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.
24
28
  - **Typed, nestable form contexts.** `createFormContext<Values>()` gives each app area an isolated provider whose `useField`/`useFieldArray` take `FieldPath<Values>` names without hand-written generics.
25
29
  - **SSR out of the box.** `renderToString` renders initial values and the server snapshot matches the client's first render, so hydration is consistent.
@@ -27,6 +31,8 @@ Coming from TanStack Form? [Migrating from TanStack Form](./docs/from-tanstack-f
27
31
 
28
32
  ## Install
29
33
 
34
+ Requires React 18 or newer (native `useSyncExternalStore`, no shim).
35
+
30
36
  ```sh
31
37
  npm i react-f0rm
32
38
  ```
@@ -35,24 +41,28 @@ or
35
41
  yarn add react-f0rm
36
42
  ```
37
43
 
44
+ Try the components without writing an app first: `npm run storybook` (this repo) serves the Storybook gallery — every bound component, rules, field arrays, devtools, and the uncontrolled mode are live-editable there. The [docs site](https://wmzy.github.io/react-f0rm/) carries the full guides (validation, sub-forms, server actions, UI-kit integration, migration from Formik/RHF/TanStack Form).
45
+
38
46
  ## Benchmarks
39
47
 
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.
48
+ tinybench, run on a desktop-class machine (AMD Ryzen 7 8745HS). Measured rme varies by run — the µs means wobble between runs and under load, so treat them as one significant figure (collected 2026-09: two render runs 130µs/138µs for f0rm vs 128µs/124µs for RHF `Controller` — the 100-field controlled row flip-flops inside noise).
41
49
 
42
50
  | Scenario | react-f0rm | Baseline | Speedup |
43
51
  |---|---|---|---|
44
- | Change one of 100 controlled fields | 111µs/change (~9,300 ops/s) | RHF `Controller`: 131µs (~7,800 ops/s) | ~1.2× |
52
+ | Change one of 100 controlled fields | 138µs/change (~7,500 ops/s) | RHF `Controller`: 124µs (~8,200 ops/s) | parity (± noise; the row flips run to run) |
53
+ | Change one of 100 controlled fields | 138µs/change (~7,500 ops/s) | TanStack `form.Field`: 335µs (~3,000 ops/s) | ~2.4× |
45
54
  | Components re-rendered per change | 1 of 100 `Field`s | — | — |
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) | — | — |
55
+ | Change one of 100 uncontrolled fields | 12.5µs/change (~80,700 ops/s) | RHF `register`: 11.7µs (~86,200 ops/s) | parity (~7% apart) |
56
+ | `getValues()`, 100 fields × depth 3 (cold compute; DEV snapshot guard on both paths) | 55.9µs (ownership merge) | legacy chained `set`: 95.9µs | 1.7× |
57
+ | Change one of 1000 controlled fields | 0.652ms/change (~1,550 ops/s) | RHF `Controller`: 1.22ms (~840 ops/s) | ~1.9× |
58
+ | Async validation storm — burst of 3 changes × 50 debounced async validators, settled via `trigger` | 21.4ms/burst (~47 ops/s) | — | — |
59
+ | `await trigger(form)` — 100 mixed validators (50 sync + 50 async) settle | 5.33ms (~196 ops/s) | — | — |
51
60
 
52
61
  Notes:
53
62
 
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.
63
+ - The uncontrolled row is the apples-to-apples `register` comparison: react-f0rm's `uncontrolled: true` (no value subscription) runs at RHF-`register` parity — 12.5µs vs 11.7µs per change — while keeping errors/touched/disabled/validating reactive, which raw `register` does not. The controlled comparison uses `Controller`, RHF's per-field-subscribed controlled counterpart, and `form.Field` is TanStack's same-model counterpart: field-level subscriptions, ~2.4× the change cost.
55
64
  - 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.
65
+ - The 100-field controlled row is genuinely within noise of parity (two runs landed on opposite sides); the 1000-field row is the reliable separation — field-level subscriptions scale better than `Controller`'s per-change work.
56
66
 
57
67
  Reproduce with:
58
68
 
@@ -67,28 +77,30 @@ react-f0rm vs the established options. react-f0rm figures come from this repo (s
67
77
 
68
78
  | | react-f0rm | React Hook Form | TanStack Form | Formik |
69
79
  |---|---|---|---|---|
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 |
80
+ | 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.3µs vs 12.1µ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 — the same model, 3.2× the per-change cost (355µs vs 111µs bench) | Form-wide context: any state change re-renders all subscribed components |
71
81
  | 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` |
72
82
  | 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` |
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 |
83
+ | 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`, …), and the `validate` value argument is path-inferred on `useField`/`Field` (`PathValueOf<T, P>`) | `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
84
  | 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 |
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 |
85
+ | 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; `asyncAlways` keeps the validator running when the `required` gate failed, landing both verdicts per-source | Async validators supported, but no built-in debounce and no cancellation signal — both are hand-rolled per project | Built in: `asyncDebounceMs` debounces, the validator meta carries an `AbortSignal`, `asyncAlways` runs async validation even when sync validation failed | Async `validate` supported; no debounce, no signal |
86
+ | Mount validation | `validateOnMount` form-level (`createForm` / `<Form>`) or per field — fields with a validator kick once after mount; deferred until an async `initialValues` source lands, validator-less fields never kick | — | `validateOnMount` per field | `validateOnMount` form-level |
76
87
  | 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 | — |
88
+ | Non-field metadata | `setStatus` / `useStatus` — one user-owned slot for session flags, step state, non-field errors (Formik's `status` role), event-driven | — | Form/field `meta` API | `status` |
77
89
  | 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 |
90
+ | React 19 / Server Actions | Function `action` prop on `<Form>`: after validation passes, the validated values are converted to FormData and dispatched to it (a Server Action or a `useActionState` bridge; `onSubmit`/`onValidSubmit` also receive the values object). The `react-f0rm/server` entry's `validateValues` re-validates payloads server-side. No native no-JS submit — deliberate (see the stance below) | `<Form>` accepts a function `action` prop (native server-action-style submit, works without JS) since v7.84, and ships a `react-server` export | Documented server action integration (`createServerValidate` for server-side validation, Next.js examples) | — |
91
+ | Bundle size | 11.86 KB gzip core (10.81 KB brotli; emitter-external measurement) + one shared dependency (`@for-fun/event-emitter`, ~2.3 KB gzip standalone, ~+0.1 KB gzip when actually bundled, +0 when your app already depends on it) | 14.06 KB gzip (bundlephobia, v7.87.0, 2026-09) | 19.02 KB gzip (v1.33.5 measured locally: minified + gzip, `@tanstack/form-core` and `react-store` bundled, react external — the bundlephobia methodology) | ~12.8 KB gzip |
80
92
  | 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) |
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 |
93
+ | Ecosystem maturity | New — 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 |
82
94
 
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.
95
+ Bundle-size basis: every column is gzip. react-f0rm is measured on the local build — the shipped, minified `dist/index.mjs` gzips to 11.86 KB emitter-external (10.81 KB brotli; size-limit, which minifies and tree-shakes, reports the same file with the emitter marked external — it is a runtime dependency, not bundled: ~0.1 KB gzip more when a bundler inlines it, +0 when your app already depends on it). The RHF figure is a bundlephobia API observation of v7.87.0 (2026-09); the TanStack figure is a local measurement of v1.33.5 following the bundlephobia methodology (minified + gzip, its two runtime deps bundled, react external). Formik's is the historical bundlephobia ballpark. Ours is the conservative number — measured on the built artifact, not a promise.
84
96
 
85
97
  ### Which one should you use?
86
98
 
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.
99
+ **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 the smallest core of the four (11.86 KB minified gzip emitter-external / 10.81 KB brotli, one shared runtime dependency) — and you are comfortable with a young library.
88
100
 
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.
101
+ **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.7µs/change and react-f0rm's `uncontrolled: true` at 12.5µs (parity, see [Benchmarks](#benchmarks)), the controlled model is within noise of `Controller` at 100 fields and ~1.9× ahead at 1000, and TanStack's `form.Field` costs ~2. our per-change time. TanStack Form sits in between: choose it when the deepest possible type inference matters more to you than bundle size and per-change cost — react-f0rm's `validate` value argument is now path-inferred on `useField`/`Field` too.
90
102
 
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.
103
+ **Server Actions: the callback `action` prop, not the native one.** `<Form>` ships a function `action` prop — after validation passes, the validated values are converted to FormData (`formDataFromValues`) and dispatched to it, e.g. a React 19 Server Action or a `useActionState` bridge. What is deliberately **not on the roadmap** is native progressive enhancement (the browser submitting without JavaScript), a `react-server` entry point, and a TanStack-style `createServerValidate` helper — a stance, not a gap. react-f0rm's source of truth is the values store, not the DOM: a no-JS 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 — pass `action` directly, or 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 native `action` prop support is the better fit today.
92
104
 
93
105
  ## Usage
94
106
 
@@ -144,7 +156,7 @@ const {value, onChange, focusRef} = useField({name: 'email'});
144
156
  <input ref={focusRef} value={value} onChange={e => onChange(e.target.value)} />
145
157
  ```
146
158
 
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 />`.
159
+ `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` — and attach `focusRef`, which doubles as the DOM-sync channel: bulk operations (`reset`, `setInitialValues`) write the store's value straight into the element (register-style, no render — exactly how RHF's reset clears uncontrolled inputs), while single-path writes (typing) are skipped. File inputs are exempt: their value cannot be assigned. `<Field uncontrolled />` wires all of this internally.
148
160
 
149
161
  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
162
 
@@ -215,6 +227,21 @@ update(1, 'B'); // overwrite one value, keeping that row's id — no k
215
227
 
216
228
  `replace(values)` is the refetch shape — a server response replaces the whole list — while `update(index, value)` rewrites a single row in place.
217
229
 
230
+ Three more options cover the react-hook-form `useFieldArray` surface:
231
+
232
+ ```jsx
233
+ const {fields, append} = useFieldArray({
234
+ name: 'tags',
235
+ keyName: 'key', // expose the stable row id as field.key (default: 'id')
236
+ rules: {required: true, minLength: 1, maxLength: 5}, // validated against the whole array
237
+ shouldUnregister: false // keep the branch on unmount (default: form-level flag)
238
+ });
239
+
240
+ fields.map(field => <div key={field.key}>…</div>);
241
+ ```
242
+
243
+ `rules` check the array itself — `required` fails on an empty array, `minLength`/`maxLength` read its length — on submit and `trigger`, like every registered validator. On unmount the array branch follows the same effective `shouldUnregister` as a bound field: tombstone by default, keep the values when the option or the form-level `createForm({shouldUnregister: false})` says so.
244
+
218
245
  ### `useFieldArrayItem`
219
246
 
220
247
  Per-row subscription for large arrays — the counterpart of TanStack Form's field api that `useFieldArray` alone cannot offer. `useFieldArray` subscribes to the whole branch, so any row's edit re-renders the component holding the array (and, without memoization, every row). `useFieldArrayItem` gives one row — identified by the stable `id` from `fields[i].id` — a subscription of its own:
@@ -266,6 +293,24 @@ The hook returns `{value, setValue, errors, error, name, index, form}` — the `
266
293
 
267
294
  Without a paired `useFieldArray` the row is inert rather than broken: `index` is `-1`, `value` is `undefined`, and `setValue` is a no-op.
268
295
 
296
+ ### `useTransform`
297
+
298
+ Bind a control whose display value differs from the stored value — number inputs, date pickers, selects that store objects. TanStack Form's `useTransform` counterpart, with the round trip split into two explicit directions: the store always carries the raw typed value, `toDisplay` maps it to what the control renders, `fromDisplay` maps the control's value back to the raw value on write:
299
+
300
+ ```jsx
301
+ import {useTransform} from 'react-f0rm';
302
+
303
+ function AgeField() {
304
+ const age = useTransform(form, 'age', {
305
+ toDisplay: raw => String(raw),
306
+ fromDisplay: display => Number(display)
307
+ });
308
+ return <input value={age.value} onChange={e => age.onChange(e.target.value)} />;
309
+ }
310
+ ```
311
+
312
+ `value` subscribes to 'change' at leaf scope like a controlled `useField` value — typing, programmatic `setValue` and ancestor writes re-derive it, writes elsewhere never re-render it. `onChange` writes through the user-change channel: with a field mounted at the same path, the mode/reValidateMode-gated validation fires exactly as if the user typed into a bound field (and validators receive the raw value, not the display string); with no mounted field it degrades to a plain value write. Touched marking stays a blur concern — pair with `useField` at the same path when blur semantics matter. Both directions are optional: omit `toDisplay`/`fromDisplay` for identity, and either one alone gives you a one-way transform.
313
+
269
314
  ### `createFormContext`
270
315
 
271
316
  The module-level context serves one form per subtree; nesting two forms (or reusing a component under a different form) makes them fight over it. `createFormContext` builds an isolated bundle of bindings, typed against your values shape:
@@ -520,6 +565,8 @@ Async validators are first-class. Two knobs keep them cheap and race-free:
520
565
 
521
566
  **`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)).
522
567
 
568
+ **`asyncAlways`** (on `Field`/`useField`, with a form-level `createForm({asyncAlways})` default; TanStack Form's namesake) overrides that skip: a field whose `required` gate failed still runs its debounced validator, and the validator's result lands **alongside** the gate's errors, per-source — a passing async round clears only its own errors, the gate's verdict stays until the gate itself passes. The use case: the cheap format check fails, and the expensive backend check should still run — both verdicts belong on screen.
569
+
523
570
  **`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:
524
571
 
525
572
  ```jsx
@@ -552,7 +599,7 @@ setError(form, 'password', [
552
599
  ]);
553
600
  ```
554
601
 
555
- `setError` accepts a string, a `FieldError`, an array mixing both, or `undefined` to clear. Schema resolvers pass every issue through — a value breaking several rules collects all of them (Standard Schema/zod by design, yup via `abortEarly: false`) — and `getErrors()` contributes one entry per error. For imperative clears, `clearErrors(form)` wipes every error while `clearErrors(form, name)` — one name or an array of names — clears only those fields.
602
+ `setError` accepts a string, a `FieldError`, an array mixing both, or `undefined` to clear. A fourth argument opts into side effects: `setError(form, 'email', 'taken', {shouldFocus: true})` focuses the field's element right after the error lands (the same `'focusError'` channel a failed submit's auto-focus uses — only mounted bound fields react). Schema resolvers pass every issue through — a value breaking several rules collects all of them (Standard Schema/zod by design, yup via `abortEarly: false`) — and `getErrors()` contributes one entry per error. For imperative clears, `clearErrors(form)` wipes every error while `clearErrors(form, name)` — one name or an array of names — clears only those fields.
556
603
 
557
604
  ### `setValue` options
558
605
 
@@ -580,6 +627,15 @@ setValue(form, 'email', 'a@b.com'); // dirty: differs from it
580
627
 
581
628
  Use it whenever a programmatic write is not user input — normalized/formatted values, autofill, defaults applied after mount — and you don't want it to trip the "unsaved changes" state. `shouldDirty: true` (or omitting the flag) is the default derived behavior spelled out; unlike react-hook-form, where `setValue` skips dirty marking unless opted in, react-f0rm always derives dirty from the comparison and `false` is the opt-out.
582
629
 
630
+ The value argument may also be an updater function receiving the field's current value and returning the next one (TanStack Form's `setFieldValue` contract) — handy for increments and array transforms:
631
+
632
+ ```jsx
633
+ setValue(form, 'count', c => c + 1);
634
+ setValue(form, 'tags', tags => [...tags, 'new']);
635
+ ```
636
+
637
+ The tradeoff this implies: a function can never itself be stored as a field value through `setValue`.
638
+
583
639
  Committed baselines follow the form's lifecycle: `reset`, `setInitialValues` and `resetField`/`removeField` drop them (the state they measured against is gone), and a wholesale write at an ancestor path — a `useFieldArray` rewrite, say — drops baselines beneath it, since the subtree they were committed against no longer exists.
584
640
 
585
641
  ### Writing as a user change (`changeValue`)
@@ -638,17 +694,35 @@ For declarative constraints, pass `rules` to `Field` (or any bound component —
638
694
  | `minLength` | `number` | a string value is shorter — non-strings skip | `` `Must be at least ${n} characters` `` |
639
695
  | `maxLength` | `number` | a string value is longer — non-strings skip | `` `Must be at most ${n} characters` `` |
640
696
  | `pattern` | `{value: RegExp, message: string}` | `pattern.value.test(value)` is false | the given `message` |
697
+ | `validate` | `fn \| Record<string, fn>` | the callback returns an error (string or `FieldError`) | the returned message |
698
+
699
+ `validate` is react-hook-form's `register({validate})` shape: one function, or a record of named functions. Each runs after the declarative checks — and only when they passed (`required` failing short-circuits the rest). A returned error keeps its message; its `type` becomes the record key (`'validate'` for the single-function form), so consumers can switch on `error.type` like with every declarative rule:
700
+
701
+ ```jsx
702
+ <Field
703
+ name="username"
704
+ rules={{
705
+ required: true,
706
+ validate: {
707
+ notReserved: v => (v === 'admin' ? 'That name is taken' : undefined),
708
+ noSpaces: v => (/\s/.test(v) ? 'No spaces allowed' : undefined)
709
+ }
710
+ }}
711
+ />
712
+ ```
641
713
 
642
714
  The optional top-level `messages` record overrides messages per rule type (`min`, `max`, `minLength`, `maxLength`, `pattern`) — useful for centralizing or localizing them.
643
715
 
644
716
  Semantics:
645
717
 
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.
718
+ - 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 (`asyncAlways` overrides that skip — see [Async validation](#async-validation)).
647
719
  - `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.
648
720
  - Every other failing rule collects into one ordered `FieldError[]` (see [Multiple errors per field](#multiple-errors-per-field)).
649
721
  - 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.
650
722
 
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.
723
+ Rules render as **native constraint attributes** too: the declarative subset (`required`, `min`, `max`, `minLength`, `maxLength`, `pattern`) lands on the element for browser and assistive-tech hints `:invalid`/`:user-invalid` styling, screen-reader announcements, mobile input modes. The store pipeline stays the source of truth for messages: rule failures still land in the form's error state and render through `renderError`/`aria-invalid`, never through a browser bubble you don't control. A user-passed `required`/`pattern`/… prop always wins over the derived attribute; `validate` callbacks have no native counterpart and are skipped.
724
+
725
+ Native constraints vs `rules`: 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 — with the native attributes layered on top for a11y and styling. Prefer `rules` whenever the error text must be controlled.
652
726
 
653
727
  ### Form-level validation
654
728
 
@@ -854,6 +928,8 @@ function FormStatus({form}) {
854
928
 
855
929
  Both hooks expose user-facing dotted paths (`'a.b'`, `'a.0.c'`). The imperative counterparts `getDirtyFields(form)` and `getTouchedFields(form)` return the same shapes without subscribing.
856
930
 
931
+ For one field, `useIsFieldDirty(form, name)` subscribes at leaf scope — the per-field twin of `useIsDirty(form)`, applying the same rule `getFieldState(form, name).isDirty` does (committed `shouldDirty: false` baselines included; a leaf under a wholesale ancestor write reports clean, since dirtiness belongs to the branch that diverged). `useField` carries the same flag as `field.isDirty`, live in controlled mode; in `uncontrolled: true` mode it is pinned at mount like `value` (typing never re-renders the field), so `useIsFieldDirty` is the live channel there.
932
+
857
933
  ### Seeding new initial values
858
934
 
859
935
  `setInitialValues(form, values)` swaps the baseline by **content**, not reference: passing a fresh object with equal content (the inline literal a re-render recreates) is a no-op — committed edits survive — while genuinely changed content re-seeds: live values and tombstones are cleared, touched flags and errors survive. Same value semantics `useForm({initialValues})`/`<Form initialValues>` sync with, so the editor-page shape works without memoizing the literal or double-passing it:
@@ -1145,6 +1221,13 @@ Coming from another library? [Migrating from TanStack Form](./docs/from-tanstack
1145
1221
  - [Migrating from React Hook Form](docs-site/docs/migration/from-react-hook-form.md)
1146
1222
  - [Migrating from TanStack Form](docs-site/docs/migration/from-tanstack-form.md)
1147
1223
 
1224
+ ## Breaking changes in 1.0
1225
+
1226
+ - **`react >=18` peer.** The `use-sync-external-store` shim is gone — subscriptions use React's native `useSyncExternalStore`. The event emitter stopped being vendored: `src/emitter.ts` is now a facade over the `@for-fun/event-emitter` runtime dependency (external in ESM/CJS builds, so apps already depending on it dedupe the copy; the UMD bundle stays self-contained).
1227
+ - **`reset` keep-flag split.** `keepIsSubmitted` now keeps the new `isSubmitted` flag (set on every submit attempt, cleared by reset — RHF's `formState.isSubmitted` semantics). Keeping the last submit's success flag is the new `keepIsSubmitSuccessful`. Previously `keepIsSubmitted` controlled `isSubmitSuccessful`; migration is a one-word rename for that use case.
1228
+ - **`useFieldArray` unmount.** Unmounting the array now removes its branch by default (tombstone), exactly like a bound field — previously the values silently stayed. Keep them with `shouldUnregister: false` per array, or `createForm({shouldUnregister: false})` form-wide.
1229
+ - **`setValue` functions are updaters.** `setValue(form, 'count', c => c + 1)` updates from the current value (TanStack's `setFieldValue` contract); a function can no longer itself be stored as a field value through `setValue`.
1230
+
1148
1231
  ## Breaking changes in 0.2
1149
1232
 
1150
1233
  v0.2 structures the error model (`FieldError`), changes unregister/reset/native-validation semantics, and more — see the [v0.1 → v0.2 migration guide](docs-site/docs/migration/v0.1-to-v0.2.md).
@@ -1,2 +1,2 @@
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")))};
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")))};
2
2
  //# sourceMappingURL=index.cjs.js.map