@sirmekus/kwado 1.0.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 (2) hide show
  1. package/README.md +575 -0
  2. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,575 @@
1
+ # Kwado
2
+
3
+ A React hook that unifies **Zod** schema validation, **react-hook-form** state management, and **Oku** HTTP submission into one concise, declarative API - with an extensible response-handler pipeline that keeps app-specific logic (toasts, error mappers, redirects) fully under your control.
4
+
5
+ ---
6
+
7
+ ## Why does this exist?
8
+
9
+ Zod and react-hook-form are individually excellent libraries, but combining them for a typical form-with-submission always produces the same boilerplate: wire the resolver, write a submit handler, call `handleSubmit`, check the response status, branch into success/error paths. Do this across dozens of forms in a project and you have a maintenance problem - inconsistent patterns, duplicated branching, and scattered HTTP logic.
10
+
11
+ `kwado` eliminates that by treating the full lifecycle - **validation → submission → response routing** — as a single, composable unit.
12
+
13
+ ---
14
+
15
+ ## The problem in plain code
16
+
17
+ Here is a login form written with the three libraries used separately:
18
+
19
+ ```tsx
20
+ import { useForm } from 'react-hook-form';
21
+ import { zodResolver } from '@hookform/resolvers/zod';
22
+ import { z } from 'zod';
23
+ import { post } from '@sirmekus/oku';
24
+
25
+ // 1. Schema defined once …
26
+ const loginSchema = z.object({
27
+ email: z.string().email(),
28
+ password: z.string().min(8),
29
+ });
30
+
31
+ function LoginForm() {
32
+ // 2. … but the resolver must be wired manually on every form
33
+ const form = useForm<z.infer<typeof loginSchema>>({
34
+ resolver: zodResolver(loginSchema),
35
+ });
36
+
37
+ const { setValue, setError, reset } = form;
38
+
39
+ // 3. Submit handler written by hand every time
40
+ const handleSubmit = async (data: z.infer<typeof loginSchema>) => {
41
+ let response;
42
+ try {
43
+ response = await post({ url: '/api/auth/login', data, method: 'POST' });
44
+ } catch (err) {
45
+ // oku rejects on non-2xx — have to catch and re-shape
46
+ response = err;
47
+ }
48
+
49
+ // 4. Status branching duplicated across every form
50
+ if (response.status === 'success') {
51
+ // For instance,
52
+ toast.success(response.data.message);
53
+ router.push('/dashboard');
54
+ } else {
55
+ if (response.statusCode === 422) {
56
+ doSomething(response.data, setError);
57
+ } else {
58
+ toast.error(response.data?.message ?? 'Login failed');
59
+ }
60
+ }
61
+ };
62
+
63
+ // 5. handleSubmit wrapper required every time
64
+ return (
65
+ <form onSubmit={form.handleSubmit(handleSubmit)}>
66
+ <input {...form.register('email')} />
67
+ <input {...form.register('password')} type="password" />
68
+ <button disabled={form.formState.isSubmitting}>Log in</button>
69
+ </form>
70
+ );
71
+ }
72
+ ```
73
+
74
+ That is **five distinct wiring steps** repeated on every form. The branching logic is particularly painful — it is ad-hoc, hard to reuse, and easily diverges between forms.
75
+
76
+ ---
77
+
78
+ ## The same form with `kwado`
79
+
80
+ ```tsx
81
+ import { useZodForm, whenStatusCode, whenSuccess, whenError } from '@sirmekus/kwado';
82
+ import { z } from 'zod';
83
+
84
+ const loginSchema = z.object({
85
+ email: z.string().email(),
86
+ password: z.string().min(8),
87
+ });
88
+
89
+ function LoginForm() {
90
+ const { register, onSubmit, formState: { errors, isSubmitting } } = useZodForm(
91
+ loginSchema,
92
+ {
93
+ endpoint: '/api/auth/login',
94
+ responseHandlers: [
95
+ whenStatusCode(422, (res, { setError }) => applyLaravelErrors(res.data, setError)),
96
+ whenSuccess((res) => { toast.success(res.data.message); router.push('/dashboard'); }),
97
+ whenError((res) => toast.error(res.data?.message ?? 'Login failed')),
98
+ ],
99
+ },
100
+ );
101
+
102
+ return (
103
+ <form onSubmit={onSubmit}>
104
+ <input {...register('email')} />
105
+ <input {...register('password')} type="password" />
106
+ <button disabled={isSubmitting}>Log in</button>
107
+ </form>
108
+ );
109
+ }
110
+ ```
111
+
112
+ **What disappeared:**
113
+
114
+ | Eliminated boilerplate | How |
115
+ |---|---|
116
+ | `resolver: zodResolver(schema)` | Wired automatically from the schema argument |
117
+ | `form.handleSubmit(handler)` | `onSubmit` is returned pre-bound |
118
+ | `try/catch` around oku | Normalised internally; both HTTP errors and network failures reach the pipeline |
119
+ | Manual `if/else` on `response.status` | Replaced by the `responseHandlers` pipeline |
120
+ | Closing over `setValue`, `setError`, `reset` | Available as `helpers` in every callback |
121
+
122
+ The schema is the single source of truth. TypeScript infers the field types from it automatically — no `z.infer<typeof schema>` needed at the call site.
123
+
124
+ ---
125
+
126
+ ## Installation
127
+
128
+ ```bash
129
+ npm install @sirmekus/kwado
130
+ # or
131
+ pnpm add @sirmekus/kwado
132
+ # or
133
+ bun add @sirmekus/kwado
134
+ ```
135
+
136
+ Install peer dependencies if not already present:
137
+
138
+ ```bash
139
+ npm install zod react-hook-form @hookform/resolvers @sirmekus/oku
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Quick start
145
+
146
+ ```tsx
147
+ import { useZodForm, whenSuccess, whenError } from '@sirmekus/kwado';
148
+ import { z } from 'zod';
149
+
150
+ const contactSchema = z.object({
151
+ name: z.string().min(1, 'Name is required'),
152
+ email: z.string().email(),
153
+ message: z.string().min(10),
154
+ });
155
+
156
+ function ContactForm() {
157
+ const { register, onSubmit, formState: { errors, isSubmitting } } = useZodForm(
158
+ contactSchema,
159
+ {
160
+ endpoint: '/api/contact',
161
+ responseHandlers: [
162
+ whenSuccess(() => alert('Message sent!')),
163
+ whenError((res) => alert(res.data?.message)),
164
+ ],
165
+ },
166
+ );
167
+
168
+ return (
169
+ <form onSubmit={onSubmit}>
170
+ <input {...register('name')} />
171
+ {errors.name && <p>{errors.name.message}</p>}
172
+
173
+ <input {...register('email')} />
174
+ {errors.email && <p>{errors.email.message}</p>}
175
+
176
+ <textarea {...register('message')} />
177
+ {errors.message && <p>{errors.message.message}</p>}
178
+
179
+ <button type="submit" disabled={isSubmitting}>
180
+ {isSubmitting ? 'Sending…' : 'Send'}
181
+ </button>
182
+ </form>
183
+ );
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Core concept: the response-handler pipeline
190
+
191
+ Instead of writing `if/else` branches inside a submit handler, you declare an ordered list of handlers. Each one has two parts:
192
+
193
+ - **`detect`** — a predicate that inspects the response and returns `true` if this handler owns it.
194
+ - **`handle`** — an async-capable function that reacts to the response.
195
+
196
+ The pipeline walks the list in order. The **first handler whose `detect` returns `true`** is executed and the rest — including the fallback `onSuccess` / `onError` — are skipped. This makes the response logic explicit, isolated, and easy to reorder or extract into reusable modules.
197
+
198
+ ```
199
+ HTTP response
200
+
201
+
202
+ ┌─────────────┐ detect → false
203
+ │ handler[0] │──────────────────► skip
204
+ └─────────────┘
205
+ │ detect → true
206
+
207
+ handle() ◄── setError, reset, redirect, toast, anything
208
+
209
+ end (handlers[1..n] and onSuccess/onError are not called)
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Handler factory functions
215
+
216
+ All factories are exported from the package and accept an optional async `handle` function that receives the response object and a `helpers` object.
217
+
218
+ ### `whenStatusCode(code, handle)`
219
+
220
+ Fires when `response.statusCode` equals `code`.
221
+
222
+ ```ts
223
+ // Map 422 validation errors back onto form fields (Laravel, Django, etc.)
224
+ whenStatusCode(422, (res, { setError }) => applyMyServerErrors(res.data, setError))
225
+
226
+ // Redirect on session expiry
227
+ whenStatusCode(401, () => router.push('/login'))
228
+
229
+ // Show a specific message for conflict errors
230
+ whenStatusCode(409, (res) => toast.warning(res.data.message))
231
+ ```
232
+
233
+ ### `whenStatusRange(min, max, handle)`
234
+
235
+ Fires when `min ≤ response.statusCode ≤ max`. Useful for class-level handling.
236
+
237
+ ```ts
238
+ // Report all 5xx errors to your monitoring service
239
+ whenStatusRange(500, 599, (res) => Sentry.captureMessage(res.data?.message))
240
+ ```
241
+
242
+ ### `whenSuccess(handle)`
243
+
244
+ Fires when `response.status === 'success'`.
245
+
246
+ ```ts
247
+ whenSuccess((res) => {
248
+ toast.success(res.data.message ?? 'Done!');
249
+ router.push('/dashboard');
250
+ })
251
+ ```
252
+
253
+ ### `whenError(handle)`
254
+
255
+ Fires when `response.status === 'error'`.
256
+
257
+ ```ts
258
+ whenError((res) => toast.error(res.data?.message ?? 'Something went wrong'))
259
+ ```
260
+
261
+ ### `whenResponse(detect, handle)`
262
+
263
+ Fully custom predicate. Use this when the other factories cannot express what you need — for example, matching on response body content.
264
+
265
+ ```ts
266
+ // Handle a soft "action required" response that arrives as a 200
267
+ whenResponse(
268
+ (res) => res.data?.action === 'TWO_FACTOR_REQUIRED',
269
+ () => router.push('/auth/2fa'),
270
+ )
271
+
272
+ // Handle business-logic flags in the response body
273
+ whenResponse(
274
+ (res) => res.data?.requiresEmailVerification === true,
275
+ (res, { reset }) => { reset(); router.push('/verify-email'); },
276
+ )
277
+ ```
278
+
279
+ ### `always(handle)`
280
+
281
+ Unconditional catch-all. Always fires. Place it **last** in the list.
282
+
283
+ ```ts
284
+ responseHandlers: [
285
+ whenStatusCode(422, applyErrors),
286
+ whenSuccess(redirectToDashboard),
287
+ always((_, { reset }) => reset()), // clean up the form no matter what
288
+ ]
289
+ ```
290
+
291
+ ---
292
+
293
+ ## `useZodForm` API reference
294
+
295
+ ```ts
296
+ function useZodForm<T extends ZodRawShape>(
297
+ schema: ZodObject<T>,
298
+ options: UseZodFormOptions<z.infer<ZodObject<T>>>,
299
+ )
300
+ ```
301
+
302
+ ### Options
303
+
304
+ | Option | Type | Description |
305
+ |---|---|---|
306
+ | `endpoint` | `string` | URL for the built-in oku HTTP call. Ignored when `submit` is provided. |
307
+ | `method` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | HTTP method. Defaults to `'POST'`. Ignored when `submit` is provided. |
308
+ | `defaultValues` | `Partial<TForm>` | Initial field values passed to react-hook-form. |
309
+ | `transform` | `(data: TForm) => unknown` | Reshape the validated payload before it is sent. |
310
+ | `responseHandlers` | `ResponseHandler[]` | Ordered response-handler pipeline (see above). |
311
+ | `onSuccess` | `(res, helpers) => void` | Fallback called on success when no handler matched. |
312
+ | `onError` | `(err, helpers) => void` | Fallback called on error when no handler matched, or on network failure. |
313
+ | `submit` | `(payload) => Promise<ResponseLike>` | Replace oku with any custom HTTP function. |
314
+ | `requestOptions` | `Record<string, unknown>` | Extra options forwarded verbatim to oku (headers, credentials, etc.). |
315
+ | `onBeforeSubmit` | `() => void` | Called immediately before submission starts. |
316
+ | `onAfterSubmit` | `() => void` | Called immediately after submission ends (always paired with `onBeforeSubmit`). |
317
+
318
+ ### Return value
319
+
320
+ `useZodForm` spreads the entire return value of react-hook-form's `useForm` onto its own return object, so **every function and property that `useForm` exposes is available directly** from `useZodForm` — no secondary form reference needed.
321
+
322
+ #### react-hook-form surface (all available, unchanged)
323
+
324
+ | Property / method | Description |
325
+ |---|---|
326
+ | `register(name, options?)` | Register a field and return its ref, `onChange`, `onBlur`, and `name` props. |
327
+ | `control` | `Controller` / `useController` integration object. |
328
+ | `formState` | Reactive state bag: `errors`, `isSubmitting`, `isValid`, `isDirty`, `dirtyFields`, `touchedFields`, `isLoading`, and more. |
329
+ | `watch(name?)` | Subscribe to field value changes. Returns the current value or an object of all values. |
330
+ | `getValues(name?)` | Read field values without subscribing to re-renders. |
331
+ | `setValue(name, value, options?)` | Imperatively set a field value. |
332
+ | `setError(name, error, options?)` | Manually set a field error (e.g. from a server response). |
333
+ | `clearErrors(name?)` | Clear one or all field errors. |
334
+ | `reset(values?, options?)` | Reset the form to its default values (or supplied values). |
335
+ | `resetField(name, options?)` | Reset a single field. |
336
+ | `trigger(name?)` | Manually trigger validation on one or all fields. |
337
+ | `setFocus(name, options?)` | Programmatically focus a registered field. |
338
+ | `unregister(name, options?)` | Unregister a field and optionally remove its value. |
339
+ | `getFieldState(name)` | Read the dirty/invalid/error state of a specific field. |
340
+ | `handleSubmit(fn, onError?)` | Wrap a custom handler with react-hook-form's validation gate. Not needed in normal usage since `onSubmit` is pre-bound, but available if you need a second submission path. |
341
+
342
+ All `formState` properties react-hook-form documents - `errors`, `isSubmitting`, `isValid`, `isDirty`, `isLoading`, `isSubmitSuccessful`, `submitCount`, `dirtyFields`, `touchedFields` - are accessible through `formState` exactly as they are in plain react-hook-form.
343
+
344
+ ```tsx
345
+ const {
346
+ register,
347
+ control,
348
+ watch,
349
+ setValue,
350
+ getValues,
351
+ setError,
352
+ clearErrors,
353
+ reset,
354
+ trigger,
355
+ setFocus,
356
+ formState: { errors, isSubmitting, isValid, isDirty },
357
+ onSubmit, // ← added by useZodForm
358
+ helpers, // ← added by useZodForm
359
+ } = useZodForm(schema, options);
360
+ ```
361
+
362
+ #### Added by `useZodForm`
363
+
364
+ | Property | Description |
365
+ |---|---|
366
+ | `onSubmit` | Pre-bound handler for `<form onSubmit={onSubmit}>`. Runs validation, submission, and the full pipeline. `formState.isSubmitting` is `true` for its entire async duration. |
367
+ | `helpers` | A curated object — `{ setValue, setError, reset, getValues, watch, trigger, clearErrors }` — forwarded into every `responseHandlers` callback and `onSuccess` / `onError`, so you can imperatively update the form from inside response logic without closing over the full form object. |
368
+
369
+ ---
370
+
371
+ ## Recipes
372
+
373
+ ### Pre-filling a form for editing
374
+
375
+ ```tsx
376
+ const { register, onSubmit } = useZodForm(userSchema, {
377
+ endpoint: `/api/users/${userId}`,
378
+ method: 'PUT',
379
+ defaultValues: existingUser, // pre-populates every field
380
+ responseHandlers: [
381
+ whenSuccess(() => toast.success('Profile updated')),
382
+ whenError((res) => toast.error(res.data?.message)),
383
+ ],
384
+ });
385
+ ```
386
+
387
+ ### Transforming the payload before submission
388
+
389
+ ```tsx
390
+ const { register, onSubmit } = useZodForm(signupSchema, {
391
+ endpoint: '/api/auth/signup',
392
+ transform: (data) => ({
393
+ ...data,
394
+ // strip the UI-only confirmation field
395
+ passwordConfirmation: undefined,
396
+ // inject a device fingerprint
397
+ deviceId: getDeviceFingerprint(),
398
+ }),
399
+ responseHandlers: [
400
+ whenStatusCode(422, (res, { setError }) => applyLaravelErrors(res.data, setError)),
401
+ whenSuccess(() => router.push('/dashboard')),
402
+ ],
403
+ });
404
+ ```
405
+
406
+ ### Global loading indicator with `onBeforeSubmit` / `onAfterSubmit`
407
+
408
+ ```tsx
409
+ const { register, onSubmit } = useZodForm(schema, {
410
+ endpoint: '/api/data',
411
+ onBeforeSubmit: () => globalLoadingStore.setLoading(true),
412
+ onAfterSubmit: () => globalLoadingStore.setLoading(false),
413
+ onSuccess: () => toast.success('Saved'),
414
+ });
415
+ ```
416
+
417
+ ### File uploads
418
+
419
+ oku automatically switches from JSON to `FormData` when any field value is a `File` or `FileList`, so no extra configuration is needed:
420
+
421
+ ```tsx
422
+ const uploadSchema = z.object({
423
+ title: z.string(),
424
+ file: z.instanceof(File),
425
+ });
426
+
427
+ const { register, onSubmit } = useZodForm(uploadSchema, {
428
+ endpoint: '/api/upload',
429
+ method: 'POST',
430
+ // oku detects the File and sends multipart/form-data automatically
431
+ responseHandlers: [
432
+ whenSuccess((res) => toast.success(`Uploaded: ${res.data.filename}`)),
433
+ whenError((res) => toast.error(res.data?.message)),
434
+ ],
435
+ });
436
+ ```
437
+
438
+ ### Custom HTTP client (bypass oku entirely)
439
+
440
+ Use `submit` to plug in any async function — fetch, axios, GraphQL, a mock — as long as it returns a `ResponseLike` object.
441
+
442
+ ```tsx
443
+ const { register, onSubmit } = useZodForm(schema, {
444
+ submit: async (payload) => {
445
+ const res = await axios.post('/api/login', payload);
446
+ return {
447
+ status: res.status < 400 ? 'success' : 'error',
448
+ statusCode: res.status,
449
+ data: res.data,
450
+ };
451
+ },
452
+ responseHandlers: [
453
+ whenSuccess(() => router.push('/dashboard')),
454
+ whenError((res) => toast.error(res.data?.message)),
455
+ ],
456
+ });
457
+ ```
458
+
459
+ ### Reusable handler modules
460
+
461
+ Because handlers are plain objects (`{ detect, handle }`), you can define them once and share them across forms:
462
+
463
+ ```ts
464
+ // lib/formHandlers.ts
465
+ import { whenStatusCode, whenStatusRange } from '@sirmekus/kwado';
466
+
467
+ export const handleValidationErrors = (setError) =>
468
+ whenStatusCode(422, (res) => applyLaravelErrors(res.data, setError));
469
+
470
+ export const reportServerErrors =
471
+ whenStatusRange(500, 599, (res) => Sentry.captureMessage(res.data?.message));
472
+
473
+ export const handleSessionExpiry =
474
+ whenStatusCode(401, () => router.push('/login'));
475
+ ```
476
+
477
+ ```tsx
478
+ // In any form — setError is received as a helper argument, not closed over
479
+ import { reportServerErrors, handleSessionExpiry } from '@/lib/formHandlers';
480
+
481
+ const { register, onSubmit } = useZodForm(schema, {
482
+ endpoint: '/api/resource',
483
+ responseHandlers: [
484
+ whenStatusCode(422, (res, { setError }) => applyLaravelErrors(res.data, setError)),
485
+ reportServerErrors,
486
+ handleSessionExpiry,
487
+ whenSuccess(() => toast.success('Saved!')),
488
+ ],
489
+ });
490
+ ```
491
+
492
+ ---
493
+
494
+ ## TypeScript
495
+
496
+ The hook is fully generic. Field names, types, and `formState.errors` are all inferred from your Zod schema with no manual annotation required.
497
+
498
+ ```ts
499
+ const schema = z.object({
500
+ email: z.string().email(),
501
+ password: z.string().min(8),
502
+ });
503
+
504
+ const { register, formState: { errors } } = useZodForm(schema, { ... });
505
+
506
+ // errors.email — fully typed, no 'any'
507
+ // errors.password — fully typed
508
+ // errors.typo — TypeScript error ✗
509
+ ```
510
+
511
+ You can also type the response data for full inference inside handlers by passing it as the second type argument:
512
+
513
+ ```ts
514
+ interface LoginResponse {
515
+ token: string;
516
+ user: { id: number; name: string };
517
+ message: string;
518
+ }
519
+
520
+ const { onSubmit } = useZodForm<typeof loginSchema['shape'], LoginResponse>(loginSchema, {
521
+ endpoint: '/api/auth/login',
522
+ responseHandlers: [
523
+ whenSuccess<LoginResponse>((res) => {
524
+ // res.data is LoginResponse — fully typed
525
+ localStorage.setItem('token', res.data.token);
526
+ console.log(res.data.user.name);
527
+ }),
528
+ ],
529
+ });
530
+ ```
531
+
532
+ ---
533
+
534
+ ## How the response pipeline handles oku's rejection model
535
+
536
+ oku resolves on 2xx and **rejects** on non-2xx HTTP responses and network failures. `kwado` catches both cases transparently:
537
+
538
+ - **Non-2xx HTTP response** — oku rejects with a `ResponseObject`. The hook detects the shape (`status`, `statusCode`, `data`) and feeds it through the `responseHandlers` pipeline as normal, so `whenStatusCode(422, ...)`, `whenError(...)`, etc. all work without any extra handling on your part.
539
+ - **Network failure** (connection refused, timeout) — the error is a plain `Error` object with no `status` / `statusCode`. The hook detects this and calls `onError` directly, bypassing the pipeline.
540
+
541
+ You never need to write a `try/catch` around `useZodForm`.
542
+
543
+ ---
544
+
545
+ ## Peer dependencies
546
+
547
+ | Package | Version |
548
+ |---|---|
549
+ | `react` | `>=18.0.0` |
550
+ | `react-hook-form` | `>=7.0.0` |
551
+ | `@hookform/resolvers` | `>=3.0.0` |
552
+ | `zod` | `>=3.0.0` |
553
+ | `@sirmekus/oku` | `>=1.0.0` |
554
+
555
+ ---
556
+
557
+ ## Building from source
558
+
559
+ ```bash
560
+ npm install
561
+ npm run build # emits dist/ (CJS + ESM + .d.ts)
562
+ npm run typecheck # tsc --noEmit
563
+ ```
564
+
565
+ The build uses [tsup](https://tsup.egoist.dev/) and produces:
566
+
567
+ - `dist/index.js` — CommonJS
568
+ - `dist/index.mjs` — ESM
569
+ - `dist/index.d.ts` — TypeScript declarations
570
+
571
+ ---
572
+
573
+ ## License
574
+
575
+ MIT
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@sirmekus/kwado",
3
+ "version": "1.0.0",
4
+ "description": "Unify Zod validation, react-hook-form, and oku HTTP submission in one hook — with an extensible response-handler pipeline.",
5
+ "keywords": [
6
+ "react",
7
+ "zod",
8
+ "react-hook-form",
9
+ "oku",
10
+ "form",
11
+ "validation",
12
+ "typescript",
13
+ "hooks"
14
+ ],
15
+ "author": "sirmekus (aka Emmy Boy)",
16
+ "license": "MIT",
17
+ "sideEffects": false,
18
+ "main": "./dist/index.js",
19
+ "module": "./dist/index.mjs",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "peerDependencies": {
40
+ "@hookform/resolvers": ">=3.0.0",
41
+ "@sirmekus/oku": ">=1.0.0",
42
+ "react": ">=18.0.0",
43
+ "react-hook-form": ">=7.0.0",
44
+ "zod": ">=3.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@hookform/resolvers": "^5.0.1",
48
+ "@sirmekus/oku": "^1.0.0",
49
+ "@types/react": "^18.3.0",
50
+ "react": "^18.3.0",
51
+ "react-hook-form": "^7.56.0",
52
+ "tsup": "^8.4.0",
53
+ "typescript": "^5.7.0",
54
+ "zod": "^3.24.3"
55
+ }
56
+ }