@beignet/react-hook-form 0.0.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/react-hook-form
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # @beignet/react-hook-form
2
+
3
+ > React Hook Form integration for Beignet
4
+
5
+ This package provides automatic form validation using your contract's body schema. Works with any [Standard Schema](https://github.com/standard-schema/standard-schema) library (Zod, Valibot, ArkType, etc.).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @beignet/react-hook-form @beignet/core react-hook-form @hookform/resolvers react
11
+ ```
12
+
13
+
14
+ ## TypeScript requirements
15
+
16
+ This package requires TypeScript 5.0 or higher for proper type inference.
17
+
18
+ ## Usage
19
+
20
+ ### Basic form
21
+
22
+ ```tsx
23
+ import { createReactHookForm } from "@beignet/react-hook-form";
24
+ import { createTodo } from "@/features/todos/contracts";
25
+
26
+ const rhf = createReactHookForm();
27
+
28
+ function CreateTodoForm() {
29
+ const { useForm } = rhf(createTodo);
30
+ const form = useForm({
31
+ defaultValues: {
32
+ title: "",
33
+ completed: false,
34
+ },
35
+ });
36
+
37
+ const onSubmit = form.handleSubmit((values) => {
38
+ // values is typed as: { title: string; completed?: boolean }
39
+ console.log("Creating todo:", values);
40
+ });
41
+
42
+ return (
43
+ <form onSubmit={onSubmit}>
44
+ <input
45
+ {...form.register("title")}
46
+ placeholder="What needs to be done?"
47
+ />
48
+ {form.formState.errors.title && (
49
+ <p className="error">{form.formState.errors.title.message}</p>
50
+ )}
51
+
52
+ <label>
53
+ <input type="checkbox" {...form.register("completed")} />
54
+ Completed
55
+ </label>
56
+
57
+ <button type="submit" disabled={form.formState.isSubmitting}>
58
+ Create Todo
59
+ </button>
60
+ </form>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### With React Query mutation
66
+
67
+ ```tsx
68
+ import { createReactHookForm } from "@beignet/react-hook-form";
69
+ import { useMutation } from "@tanstack/react-query";
70
+ import { rq } from "@/client/rq";
71
+ import { createTodo } from "@/features/todos/contracts";
72
+
73
+ const rhf = createReactHookForm();
74
+
75
+ function CreateTodoForm() {
76
+ const { useForm } = rhf(createTodo);
77
+ const form = useForm({
78
+ defaultValues: { title: "" },
79
+ });
80
+
81
+ const mutation = useMutation(
82
+ rq(createTodo).mutationOptions()
83
+ );
84
+
85
+ const onSubmit = form.handleSubmit((values) => {
86
+ mutation.mutate({ body: values });
87
+ });
88
+
89
+ return (
90
+ <form onSubmit={onSubmit}>
91
+ <input {...form.register("title")} placeholder="Title" />
92
+ {form.formState.errors.title && (
93
+ <p>{form.formState.errors.title.message}</p>
94
+ )}
95
+
96
+ <button type="submit" disabled={mutation.isPending}>
97
+ {mutation.isPending ? "Creating..." : "Create"}
98
+ </button>
99
+
100
+ {mutation.isError && (
101
+ <p className="error">{mutation.error.message}</p>
102
+ )}
103
+ </form>
104
+ );
105
+ }
106
+ ```
107
+
108
+ ### Disabling validation
109
+
110
+ If you need to disable the schema resolver (e.g., for partial form handling):
111
+
112
+ ```tsx
113
+ const { useForm } = rhf(createTodo);
114
+ const form = useForm({
115
+ resolverEnabled: false, // Disable schema validation
116
+ defaultValues: { title: "" },
117
+ });
118
+ ```
119
+
120
+ ### Using contract config directly
121
+
122
+ You can pass either a contract builder or its config:
123
+
124
+ ```tsx
125
+ import { createReactHookForm } from "@beignet/react-hook-form";
126
+ import { createTodo } from "@/features/todos/contracts";
127
+
128
+ const rhf = createReactHookForm();
129
+
130
+ // Using ContractBuilder directly
131
+ const { useForm } = rhf(createTodo);
132
+
133
+ // Or using the contract config
134
+ const { useForm } = rhf(createTodo.config);
135
+ ```
136
+
137
+ ## API reference
138
+
139
+ ### `createReactHookForm()`
140
+
141
+ Creates a React Hook Form adapter factory.
142
+
143
+ ```ts
144
+ const rhf = createReactHookForm();
145
+ ```
146
+
147
+ ### `rhf(contract)`
148
+
149
+ Creates a React Hook Form adapter for a contract.
150
+
151
+ ```ts
152
+ const adapter = rhf(createTodo);
153
+ ```
154
+
155
+ ### `adapter.useForm(props?)`
156
+
157
+ Returns a React Hook Form `useForm` result with the contract's body schema as resolver.
158
+
159
+ ```ts
160
+ const form = adapter.useForm({
161
+ defaultValues?: { ... },
162
+ resolverEnabled?: boolean, // default: true
163
+ // ...other React Hook Form options
164
+ });
165
+ ```
166
+
167
+ ## Type inference
168
+
169
+ Form values are automatically typed based on the contract's body schema:
170
+
171
+ ```ts
172
+ // Contract definition
173
+ const createTodo = todos
174
+ .post("/api/todos")
175
+ .body(z.object({
176
+ title: z.string().min(1),
177
+ description: z.string().optional(),
178
+ completed: z.boolean().optional(),
179
+ }))
180
+ .responses({ 201: TodoSchema });
181
+
182
+ // Form values are inferred
183
+ const rhf = createReactHookForm();
184
+ const form = rhf(createTodo).useForm();
185
+ form.register("title"); // ✓ Valid
186
+ form.register("description"); // ✓ Valid
187
+ form.register("invalid"); // ✗ Type error
188
+ ```
189
+
190
+ ## Standard Schema support
191
+
192
+ This package uses the `@hookform/resolvers/standard-schema` resolver, which works with any Standard Schema compatible library:
193
+
194
+ - **Zod** - `z.object({ ... })`
195
+ - **Valibot** - `v.object({ ... })`
196
+ - **ArkType** - `type({ ... })`
197
+
198
+ ## Validation behavior
199
+
200
+ The resolver validates:
201
+
202
+ 1. **On blur** - When a field loses focus
203
+ 2. **On change** - After first submission attempt
204
+ 3. **On submit** - Before calling your submit handler
205
+
206
+ Validation errors are available via `form.formState.errors`:
207
+
208
+ ```tsx
209
+ {form.formState.errors.title && (
210
+ <span className="error">
211
+ {form.formState.errors.title.message}
212
+ </span>
213
+ )}
214
+ ```
215
+
216
+ ## Complete example
217
+
218
+ ```tsx
219
+ import { createReactHookForm } from "@beignet/react-hook-form";
220
+ import { useMutation } from "@tanstack/react-query";
221
+ import { rq } from "@/client/rq";
222
+ import { updateProfile } from "@/features/profile/contracts";
223
+
224
+ const rhf = createReactHookForm();
225
+
226
+ function ProfileForm({ profile }) {
227
+ const { useForm } = rhf(updateProfile);
228
+ const form = useForm({
229
+ defaultValues: {
230
+ name: profile.name,
231
+ email: profile.email,
232
+ bio: profile.bio ?? "",
233
+ },
234
+ });
235
+
236
+ const mutation = useMutation(
237
+ rq(updateProfile).mutationOptions({
238
+ onSuccess: () => {
239
+ toast.success("Profile updated!");
240
+ },
241
+ })
242
+ );
243
+
244
+ const onSubmit = form.handleSubmit((values) => {
245
+ mutation.mutate({ body: values });
246
+ });
247
+
248
+ const { errors, isDirty, isSubmitting } = form.formState;
249
+
250
+ return (
251
+ <form onSubmit={onSubmit}>
252
+ <div>
253
+ <label htmlFor="name">Name</label>
254
+ <input id="name" {...form.register("name")} />
255
+ {errors.name && <span className="error">{errors.name.message}</span>}
256
+ </div>
257
+
258
+ <div>
259
+ <label htmlFor="email">Email</label>
260
+ <input id="email" type="email" {...form.register("email")} />
261
+ {errors.email && <span className="error">{errors.email.message}</span>}
262
+ </div>
263
+
264
+ <div>
265
+ <label htmlFor="bio">Bio</label>
266
+ <textarea id="bio" {...form.register("bio")} />
267
+ {errors.bio && <span className="error">{errors.bio.message}</span>}
268
+ </div>
269
+
270
+ <button type="submit" disabled={!isDirty || isSubmitting}>
271
+ {isSubmitting ? "Saving..." : "Save Changes"}
272
+ </button>
273
+ </form>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ## Related packages
279
+
280
+ - [`@beignet/core/contracts`](https://beignet.dev/contracts) - Core contract definitions
281
+ - [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
282
+ - [`@beignet/core/client`](https://beignet.dev/client) - HTTP client
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,44 @@
1
+ import { type ContractLike, type HttpContractConfig, type InferOutput, type ResolveContract, type StandardSchemaV1 } from "@beignet/core/contracts";
2
+ import { type FieldValues, type UseFormProps, type UseFormReturn } from "react-hook-form";
3
+ /**
4
+ * Infer form values from contract body
5
+ */
6
+ type InferBody<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? InferOutput<TContract["body"]> & FieldValues : FieldValues;
7
+ type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
8
+ /**
9
+ * Options for React Hook Form with contract support
10
+ */
11
+ type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<FormValues<TContract>>, "resolver"> & {
12
+ /**
13
+ * Optional override for the resolver. If supplied, this is used instead of
14
+ * the default contract-based resolver.
15
+ */
16
+ resolver?: UseFormProps<FormValues<TContract>>["resolver"];
17
+ /**
18
+ * Enables or disables automatic resolver generation from the contract body schema.
19
+ * Defaults to true.
20
+ */
21
+ resolverEnabled?: boolean;
22
+ };
23
+ /**
24
+ * React Hook Form adapter for a contract
25
+ */
26
+ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> = {
27
+ /**
28
+ * Generate UseFormProps<FormValues<TContract>> for any RHF usage.
29
+ */
30
+ formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<FormValues<TContract>>;
31
+ /**
32
+ * Convenience wrapper around useForm(formOptions(props)).
33
+ */
34
+ useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<FormValues<TContract>>;
35
+ };
36
+ /**
37
+ * Create a React Hook Form adapter factory.
38
+ *
39
+ * Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
40
+ * create the adapter once, then bind individual contracts with `rhf(contract)`.
41
+ */
42
+ export declare function createReactHookForm(): <TContractLike extends ContractLike>(contract: TContractLike) => ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
43
+ export {};
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,KAAK,SAAS,CAAC,SAAS,SAAS,kBAAkB,IACjD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,GAC5C,WAAW,CAAC;AAElB,KAAK,UAAU,CAAC,SAAS,SAAS,kBAAkB,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC;AAE7E;;GAEG;AACH,KAAK,cAAc,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CAC9D,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EACnC,UAAU,CACX,GAAG;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAE3D;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,CAAC,SAAS,SAAS,kBAAkB,IAC3E;IACE;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEzC;;OAEG;IACH,OAAO,EAAE,CACP,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;CAC3C,CAAC;AAqDJ;;;;;GAKG;AACH,wBAAgB,mBAAmB,KACb,aAAa,SAAS,YAAY,EACpD,UAAU,aAAa,KACtB,4BAA4B,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAGhE"}
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { resolveContract, } from "@beignet/core/contracts";
2
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
3
+ import { useForm as rhfUseForm, } from "react-hook-form";
4
+ function createReactHookFormAdapter(contract) {
5
+ const resolvedContract = resolveContract(contract);
6
+ if (!resolvedContract.body) {
7
+ throw new Error(`rhf(${resolvedContract.name}): Contract has no body schema. ` +
8
+ "React Hook Form requires a body schema to generate form fields and validation. " +
9
+ "Use .body(schema) on the contract builder to define the request body shape.");
10
+ }
11
+ function formOptions(props) {
12
+ const bodySchema = resolvedContract.body;
13
+ const { resolverEnabled = true, resolver: resolverOverride, ...rest } = props ?? {};
14
+ const resolver = resolverOverride ??
15
+ (bodySchema && resolverEnabled
16
+ ? standardSchemaResolver(bodySchema)
17
+ : undefined);
18
+ return {
19
+ ...rest,
20
+ resolver,
21
+ };
22
+ }
23
+ function useForm(props) {
24
+ const options = formOptions(props);
25
+ return rhfUseForm(options);
26
+ }
27
+ return { formOptions, useForm };
28
+ }
29
+ /**
30
+ * Create a React Hook Form adapter factory.
31
+ *
32
+ * Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
33
+ * create the adapter once, then bind individual contracts with `rhf(contract)`.
34
+ */
35
+ export function createReactHookForm() {
36
+ return function rhf(contract) {
37
+ return createReactHookFormAdapter(contract);
38
+ };
39
+ }
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,eAAe,GAEhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAEL,OAAO,IAAI,UAAU,GAGtB,MAAM,iBAAiB,CAAC;AAoDzB,SAAS,0BAA0B,CACjC,QAAuB;IAEvB,MAAM,gBAAgB,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,OAAO,gBAAgB,CAAC,IAAI,kCAAkC;YAC5D,iFAAiF;YACjF,6EAA6E,CAChF,CAAC;IACJ,CAAC;IAID,SAAS,WAAW,CAClB,KAAsD;QAEtD,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAGvB,CAAC;QAEd,MAAM,EACJ,eAAe,GAAG,IAAI,EACtB,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,IAAI,EACR,GAAG,KAAK,IAAI,EAAE,CAAC;QAEhB,MAAM,QAAQ,GACZ,gBAAgB;YAChB,CAAC,UAAU,IAAI,eAAe;gBAC5B,CAAC,CAAC,sBAAsB,CAAC,UAAU,CAAC;gBACpC,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,OAAO;YACL,GAAI,IAA6B;YACjC,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,KAAsD;QAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,UAAU,CAAS,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,SAAS,GAAG,CACjB,QAAuB;QAEvB,OAAO,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@beignet/react-hook-form",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "React Hook Form integration for Beignet with Standard Schema support",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "contract",
33
+ "api",
34
+ "typescript",
35
+ "react-hook-form",
36
+ "standard-schema"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/taylorbryant/beignet.git",
42
+ "directory": "packages/react-hook-form"
43
+ },
44
+ "author": "Taylor Bryant",
45
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
46
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@hookform/resolvers": "^5.0.0",
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "react-hook-form": "^7.0.0"
58
+ },
59
+ "dependencies": {
60
+ "@beignet/core": "*"
61
+ },
62
+ "devDependencies": {
63
+ "@hookform/resolvers": "^5.2.2",
64
+ "@testing-library/dom": "^9.3.4",
65
+ "@testing-library/react": "^14.3.1",
66
+ "@types/bun": "^1.3.13",
67
+ "@types/node": "^20.10.0",
68
+ "@types/react": "^18.2.0",
69
+ "happy-dom": "^20.0.11",
70
+ "react": "^18.2.0",
71
+ "react-hook-form": "^7.48.0",
72
+ "typescript": "^5.3.0",
73
+ "zod": "^4.0.0"
74
+ }
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,130 @@
1
+ import {
2
+ type ContractLike,
3
+ type HttpContractConfig,
4
+ type InferOutput,
5
+ type ResolveContract,
6
+ resolveContract,
7
+ type StandardSchemaV1,
8
+ } from "@beignet/core/contracts";
9
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
10
+ import {
11
+ type FieldValues,
12
+ useForm as rhfUseForm,
13
+ type UseFormProps,
14
+ type UseFormReturn,
15
+ } from "react-hook-form";
16
+
17
+ /**
18
+ * Infer form values from contract body
19
+ */
20
+ type InferBody<TContract extends HttpContractConfig> =
21
+ TContract["body"] extends StandardSchemaV1
22
+ ? InferOutput<TContract["body"]> & FieldValues
23
+ : FieldValues;
24
+
25
+ type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
26
+
27
+ /**
28
+ * Options for React Hook Form with contract support
29
+ */
30
+ type RhfFormOptions<TContract extends HttpContractConfig> = Omit<
31
+ UseFormProps<FormValues<TContract>>,
32
+ "resolver"
33
+ > & {
34
+ /**
35
+ * Optional override for the resolver. If supplied, this is used instead of
36
+ * the default contract-based resolver.
37
+ */
38
+ resolver?: UseFormProps<FormValues<TContract>>["resolver"];
39
+
40
+ /**
41
+ * Enables or disables automatic resolver generation from the contract body schema.
42
+ * Defaults to true.
43
+ */
44
+ resolverEnabled?: boolean;
45
+ };
46
+
47
+ /**
48
+ * React Hook Form adapter for a contract
49
+ */
50
+ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
51
+ {
52
+ /**
53
+ * Generate UseFormProps<FormValues<TContract>> for any RHF usage.
54
+ */
55
+ formOptions: (
56
+ props?: RhfFormOptions<TContract>,
57
+ ) => UseFormProps<FormValues<TContract>>;
58
+
59
+ /**
60
+ * Convenience wrapper around useForm(formOptions(props)).
61
+ */
62
+ useForm: (
63
+ props?: RhfFormOptions<TContract>,
64
+ ) => UseFormReturn<FormValues<TContract>>;
65
+ };
66
+
67
+ function createReactHookFormAdapter<TContractLike extends ContractLike>(
68
+ contract: TContractLike,
69
+ ): ReactHookFormContractAdapter<ResolveContract<TContractLike>> {
70
+ const resolvedContract = resolveContract(contract);
71
+
72
+ if (!resolvedContract.body) {
73
+ throw new Error(
74
+ `rhf(${resolvedContract.name}): Contract has no body schema. ` +
75
+ "React Hook Form requires a body schema to generate form fields and validation. " +
76
+ "Use .body(schema) on the contract builder to define the request body shape.",
77
+ );
78
+ }
79
+
80
+ type Values = FormValues<ResolveContract<TContractLike>>;
81
+
82
+ function formOptions(
83
+ props?: RhfFormOptions<ResolveContract<TContractLike>>,
84
+ ): UseFormProps<Values> {
85
+ const bodySchema = resolvedContract.body as
86
+ | StandardSchemaV1<FieldValues>
87
+ | null
88
+ | undefined;
89
+
90
+ const {
91
+ resolverEnabled = true,
92
+ resolver: resolverOverride,
93
+ ...rest
94
+ } = props ?? {};
95
+
96
+ const resolver =
97
+ resolverOverride ??
98
+ (bodySchema && resolverEnabled
99
+ ? standardSchemaResolver(bodySchema)
100
+ : undefined);
101
+
102
+ return {
103
+ ...(rest as UseFormProps<Values>),
104
+ resolver,
105
+ };
106
+ }
107
+
108
+ function useForm(
109
+ props?: RhfFormOptions<ResolveContract<TContractLike>>,
110
+ ): UseFormReturn<Values> {
111
+ const options = formOptions(props);
112
+ return rhfUseForm<Values>(options);
113
+ }
114
+
115
+ return { formOptions, useForm };
116
+ }
117
+
118
+ /**
119
+ * Create a React Hook Form adapter factory.
120
+ *
121
+ * Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
122
+ * create the adapter once, then bind individual contracts with `rhf(contract)`.
123
+ */
124
+ export function createReactHookForm() {
125
+ return function rhf<TContractLike extends ContractLike>(
126
+ contract: TContractLike,
127
+ ): ReactHookFormContractAdapter<ResolveContract<TContractLike>> {
128
+ return createReactHookFormAdapter(contract);
129
+ };
130
+ }