react-formsteps-core 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,36 @@
1
+ Copyright (c) 2025 Franxx
2
+ GitHub: https://github.com/franklinjunior23
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software to use it for personal, educational, or open source projects,
6
+ subject to the following conditions:
7
+
8
+ 1. ATTRIBUTION REQUIRED: The above copyright notice and this permission notice
9
+ must be included in all copies or substantial portions of the Software.
10
+
11
+ 2. NON-COMMERCIAL: This software may not be sold, sublicensed, or used as part
12
+ of any commercial product or service without explicit written permission
13
+ from the original author (Franxx).
14
+
15
+ 3. NO REBRANDING: You may not remove or alter the original authorship credits,
16
+ rename this library and republish it as your own, or claim original
17
+ authorship of this work.
18
+
19
+ 4. NO REPUBLISHING: You may not publish this package or any modified version
20
+ of it to npm, GitHub Packages, or any other package registry under a
21
+ different name or ownership without explicit written permission from
22
+ the original author.
23
+
24
+ 5. CONTRIBUTIONS WELCOME: Contributions via pull requests are welcome. By
25
+ submitting a contribution, you agree that the original author (Franxx)
26
+ retains full ownership and copyright of the project, including your
27
+ contributions.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
33
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
+
36
+ For commercial use or any other permissions contact the author directly.
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # react-formsteps
2
+
3
+ > Headless, type-safe multi-step form library for React — built on [react-hook-form](https://react-hook-form.com/) and [Zod](https://zod.dev/).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/react-formsteps-core?style=flat-square&color=blue)](https://www.npmjs.com/package/react-formsteps-core)
6
+ [![npm version](https://img.shields.io/npm/v/react-formsteps-ui?style=flat-square&color=blue)](https://www.npmjs.com/package/react-formsteps-ui)
7
+ [![license](https://img.shields.io/badge/license-Custom-orange?style=flat-square)](./LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
9
+
10
+ ---
11
+
12
+ ## Packages
13
+
14
+ | Package | Version | Description |
15
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------- |
16
+ | [`react-formsteps-core`](./packages/core) | ![npm](https://img.shields.io/npm/v/react-formsteps-core?style=flat-square) | Headless hooks + context. No UI, no styles. |
17
+ | [`react-formsteps-ui`](./packages/ui) | ![npm](https://img.shields.io/npm/v/react-formsteps-ui?style=flat-square) | Optional pre-built React components. |
18
+
19
+ ---
20
+
21
+ ## Features
22
+
23
+ - **Headless** — zero UI imposed. Works with any design system.
24
+ - **Per-step validation** — validates only the current step's Zod schema before advancing.
25
+ - **Type-safe** — strict TypeScript throughout. Types flow from Zod schemas into your fields.
26
+ - **Built on react-hook-form** — full compatibility with the RHF ecosystem.
27
+ - **Flexible** — use the hooks alone or drop in the ready-made components.
28
+ - **Tiny** — tree-shakeable, no CSS bundled.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ### Core only (headless)
35
+
36
+ ```bash
37
+ # npm
38
+ npm install react-formsteps-core react-hook-form zod @hookform/resolvers
39
+
40
+ # pnpm
41
+ pnpm add react-formsteps-core react-hook-form zod @hookform/resolvers
42
+
43
+ # yarn
44
+ yarn add react-formsteps-core react-hook-form zod @hookform/resolvers
45
+ ```
46
+
47
+ ### With UI components
48
+
49
+ ```bash
50
+ # npm
51
+ npm install react-formsteps-core react-formsteps-ui react-hook-form zod @hookform/resolvers
52
+
53
+ # pnpm
54
+ pnpm add react-formsteps-core react-formsteps-ui react-hook-form zod @hookform/resolvers
55
+ ```
56
+
57
+ ### Peer dependencies
58
+
59
+ | Dependency | Version |
60
+ | --------------------- | ------- |
61
+ | `react` | `>=18` |
62
+ | `react-dom` | `>=18` |
63
+ | `react-hook-form` | `>=7` |
64
+ | `zod` | `>=3` |
65
+ | `@hookform/resolvers` | `>=3` |
66
+
67
+ ---
68
+
69
+ ## Quick start
70
+
71
+ ### Headless — `useSteps` + `useStepForm`
72
+
73
+ ```tsx
74
+ import { useSteps, useStepForm } from 'react-formsteps-core';
75
+ import { z } from 'zod';
76
+
77
+ const schemas = [
78
+ z.object({ firstName: z.string().min(1, 'Required'), lastName: z.string().min(1, 'Required') }),
79
+ z.object({ email: z.string().email('Enter a valid email') }),
80
+ z.object({ password: z.string().min(8, 'Min. 8 characters') }),
81
+ ];
82
+
83
+ export function RegistrationForm() {
84
+ const { currentStep, next, prev, isFirst, isLast, progress, totalSteps } = useSteps({
85
+ totalSteps: schemas.length,
86
+ });
87
+
88
+ const { form, nextWithValidation, isValidating } = useStepForm({
89
+ schema: schemas[currentStep],
90
+ onNext: (data) => console.log('Step data:', data),
91
+ });
92
+
93
+ const handleNext = async () => {
94
+ const ok = await nextWithValidation();
95
+ if (ok && !isLast) next();
96
+ if (ok && isLast) console.log('All done!', form.getValues());
97
+ };
98
+
99
+ return (
100
+ <form>
101
+ {/* Progress */}
102
+ <div>
103
+ Step {currentStep + 1} of {totalSteps} — {progress}%
104
+ </div>
105
+ <progress value={progress} max={100} />
106
+
107
+ {/* Step 1 */}
108
+ {currentStep === 0 && (
109
+ <>
110
+ <input {...form.register('firstName')} placeholder="First name" />
111
+ <input {...form.register('lastName')} placeholder="Last name" />
112
+ </>
113
+ )}
114
+
115
+ {/* Step 2 */}
116
+ {currentStep === 1 && <input {...form.register('email')} placeholder="Email" />}
117
+
118
+ {/* Step 3 */}
119
+ {currentStep === 2 && (
120
+ <input {...form.register('password')} type="password" placeholder="Password" />
121
+ )}
122
+
123
+ {/* Navigation */}
124
+ <button type="button" onClick={prev} disabled={isFirst}>
125
+ Back
126
+ </button>
127
+ <button type="button" onClick={handleNext} disabled={isValidating}>
128
+ {isLast ? 'Submit' : 'Next'}
129
+ </button>
130
+ </form>
131
+ );
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ### With UI components — `<Steps>` + `<Step>`
138
+
139
+ ```tsx
140
+ import { Steps, Step, StepBar, StepNav } from 'react-formsteps-ui';
141
+ import { z } from 'zod';
142
+
143
+ const schema1 = z.object({ name: z.string().min(1, 'Required') });
144
+ const schema2 = z.object({ email: z.string().email() });
145
+ const schema3 = z.object({ password: z.string().min(8) });
146
+
147
+ export function RegistrationForm() {
148
+ return (
149
+ <Steps
150
+ schemas={[schema1, schema2, schema3]}
151
+ onSubmit={(data) => console.log('Submitted:', data)}
152
+ >
153
+ <Step title="Personal info">{/* your fields */}</Step>
154
+
155
+ <Step title="Contact">{/* your fields */}</Step>
156
+
157
+ <Step title="Security">{/* your fields */}</Step>
158
+ </Steps>
159
+ );
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## API
166
+
167
+ ### `useSteps(options)`
168
+
169
+ Manages step navigation state.
170
+
171
+ ```ts
172
+ const {
173
+ currentStep, // number — 0-indexed
174
+ totalSteps, // number
175
+ isFirst, // boolean
176
+ isLast, // boolean
177
+ next, // () => void
178
+ prev, // () => void
179
+ goTo, // (index: number) => void
180
+ progress, // number — 0 to 100
181
+ } = useSteps({ totalSteps: 3, initialStep?: 0, onComplete?: () => void });
182
+ ```
183
+
184
+ | Option | Type | Default | Description |
185
+ | ------------- | ------------ | ------- | -------------------------------------------------------- |
186
+ | `totalSteps` | `number` | — | **Required.** Must be a positive integer |
187
+ | `initialStep` | `number` | `0` | Starting step index |
188
+ | `onComplete` | `() => void` | — | Called once when the user first arrives at the last step |
189
+
190
+ ---
191
+
192
+ ### `useStepForm(options)`
193
+
194
+ Integrates react-hook-form with per-step Zod validation.
195
+
196
+ ```ts
197
+ const {
198
+ form, // UseFormReturn<z.infer<typeof schema>>
199
+ nextWithValidation, // () => Promise<boolean>
200
+ isValidating, // boolean
201
+ } = useStepForm({ schema, defaultValues?, onNext? });
202
+ ```
203
+
204
+ | Option | Type | Description |
205
+ | --------------- | --------------------------- | --------------------------------------------- |
206
+ | `schema` | `ZodType` | **Required.** Zod schema for the current step |
207
+ | `defaultValues` | `Partial<z.infer<TSchema>>` | Initial field values |
208
+ | `onNext` | `(data) => void` | Called with validated data when advancing |
209
+
210
+ ---
211
+
212
+ ### `useStepsContext()`
213
+
214
+ Access step state from any component inside a `<Steps>` or `<StepsProvider>`.
215
+
216
+ ```ts
217
+ import { useStepsContext } from 'react-formsteps-core';
218
+
219
+ const { currentStep, totalSteps, next, prev, goTo, formData } = useStepsContext();
220
+ ```
221
+
222
+ ---
223
+
224
+ ### `<StepsProvider>` — headless context
225
+
226
+ Use the context without the UI package.
227
+
228
+ ```tsx
229
+ import { StepsProvider, useStepsContext } from 'react-formsteps-core';
230
+
231
+ <StepsProvider schemas={[schema1, schema2]} onSubmit={handleSubmit}>
232
+ <MyCustomWizard />
233
+ </StepsProvider>;
234
+ ```
235
+
236
+ ---
237
+
238
+ ### Utilities
239
+
240
+ ```ts
241
+ import { validateStep, mergeSchemas, validateAllSteps } from 'react-formsteps-core';
242
+
243
+ // Validate a single step — never throws
244
+ const result = await validateStep(schema, data);
245
+ // { success: boolean, data?, errors?: Record<string, string> }
246
+
247
+ // Merge multiple ZodObject schemas into one
248
+ const fullSchema = mergeSchemas([step1Schema, step2Schema, step3Schema]);
249
+
250
+ // Validate all accumulated data against merged schemas
251
+ const result = await validateAllSteps(schemas, allData);
252
+ ```
253
+
254
+ ---
255
+
256
+ ### UI Components
257
+
258
+ | Component | Description |
259
+ | ----------- | ----------------------------------------------------------- |
260
+ | `<Steps>` | Root provider. Pass `schemas` and `onSubmit`. |
261
+ | `<Step>` | Wrapper for each step's content. Accepts optional `title`. |
262
+ | `<StepBar>` | Progress bar with optional step labels. |
263
+ | `<StepNav>` | Back / Next / Submit buttons with built-in validation gate. |
264
+
265
+ ---
266
+
267
+ ## TypeScript
268
+
269
+ All APIs are fully typed. Enable `strict: true` in your `tsconfig.json` for the best experience.
270
+
271
+ ```json
272
+ {
273
+ "compilerOptions": {
274
+ "strict": true
275
+ }
276
+ }
277
+ ```
278
+
279
+ Import types directly:
280
+
281
+ ```ts
282
+ import type {
283
+ StepSchema,
284
+ StepConfig,
285
+ StepsContextValue,
286
+ UseStepsOptions,
287
+ UseStepsReturn,
288
+ UseStepFormOptions,
289
+ UseStepFormReturn,
290
+ StepsProviderProps,
291
+ } from 'react-formsteps-core';
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Contributing
297
+
298
+ Contributions via pull requests are welcome. Please open an issue first to discuss significant changes.
299
+
300
+ By submitting a contribution you agree that the original author (Franxx) retains full ownership and copyright of the project, including your contributions.
301
+
302
+ ---
303
+
304
+ ## License
305
+
306
+ Copyright (c) 2025 Franxx — see [LICENSE](./LICENSE) for full terms.
307
+
308
+ This software is free for personal, educational, and open source use. **Commercial use requires explicit written permission from the author.** See the license for details.
309
+
310
+ ---
311
+
312
+ <p align="center">Made by <a href="https://github.com/franklinjunior23">Franxx</a></p>
@@ -0,0 +1,85 @@
1
+ import { ZodType, z, ZodObject } from 'zod';
2
+ import { UseFormReturn, FieldValues } from 'react-hook-form';
3
+ import React$1 from 'react';
4
+
5
+ type StepSchema = ZodType<Record<string, unknown>>;
6
+ interface StepConfig {
7
+ id: string;
8
+ label?: string;
9
+ schema?: StepSchema;
10
+ }
11
+ interface StepsContextValue {
12
+ currentStep: number;
13
+ totalSteps: number;
14
+ isFirst: boolean;
15
+ isLast: boolean;
16
+ next: () => void;
17
+ prev: () => void;
18
+ goTo: (index: number) => void;
19
+ formData: Record<string, unknown>;
20
+ setStepData: (step: number, data: Record<string, unknown>) => void;
21
+ schemas: StepSchema[];
22
+ }
23
+ interface UseStepsOptions {
24
+ totalSteps: number;
25
+ initialStep?: number;
26
+ onComplete?: () => void;
27
+ }
28
+ interface UseStepsReturn {
29
+ currentStep: number;
30
+ totalSteps: number;
31
+ isFirst: boolean;
32
+ isLast: boolean;
33
+ next: () => void;
34
+ prev: () => void;
35
+ goTo: (index: number) => void;
36
+ progress: number;
37
+ }
38
+ interface UseStepFormOptions<TSchema extends ZodType = ZodType> {
39
+ schema: TSchema;
40
+ defaultValues?: Partial<z.infer<TSchema>>;
41
+ onNext?: (data: z.infer<TSchema>) => void;
42
+ }
43
+ interface UseStepFormReturn<TSchema extends ZodType = ZodType> {
44
+ form: UseFormReturn<z.infer<TSchema> & FieldValues>;
45
+ nextWithValidation: () => Promise<boolean>;
46
+ isValidating: boolean;
47
+ }
48
+ interface StepsProviderProps {
49
+ children: React.ReactNode;
50
+ schemas: StepSchema[];
51
+ onSubmit?: (data: Record<string, unknown>) => void;
52
+ initialStep?: number;
53
+ }
54
+
55
+ declare function useSteps({ totalSteps, initialStep, onComplete, }: UseStepsOptions): UseStepsReturn;
56
+
57
+ declare function useStepForm<TSchema extends ZodType>({ schema, defaultValues, onNext, }: UseStepFormOptions<TSchema>): UseStepFormReturn<TSchema>;
58
+
59
+ declare const StepsContext: React$1.Context<StepsContextValue | null>;
60
+ declare function useStepsContext(): StepsContextValue;
61
+ declare function StepsProvider({ children, schemas, initialStep }: StepsProviderProps): React$1.FunctionComponentElement<React$1.ProviderProps<StepsContextValue | null>>;
62
+
63
+ interface ValidateStepResult {
64
+ success: boolean;
65
+ errors?: Record<string, string>;
66
+ data?: Record<string, unknown>;
67
+ }
68
+ declare function validateStep(schema: ZodType, data: Record<string, unknown>): Promise<ValidateStepResult>;
69
+
70
+ type AnyZodObject = ZodObject<any, any, any>;
71
+ /**
72
+ * Merges multiple Zod schemas into a single ZodObject schema.
73
+ * Only merges ZodObject schemas; other schema types are skipped.
74
+ */
75
+ declare function mergeSchemas(schemas: ZodType[]): AnyZodObject;
76
+ /**
77
+ * Validates all accumulated form data against the merged schema.
78
+ */
79
+ declare function validateAllSteps(schemas: ZodType[], data: Record<string, unknown>): Promise<{
80
+ success: boolean;
81
+ data?: Record<string, unknown>;
82
+ errors?: Record<string, string>;
83
+ }>;
84
+
85
+ export { type StepConfig, type StepSchema, StepsContext, type StepsContextValue, StepsProvider, type StepsProviderProps, type UseStepFormOptions, type UseStepFormReturn, type UseStepsOptions, type UseStepsReturn, mergeSchemas, useStepForm, useSteps, useStepsContext, validateAllSteps, validateStep };
@@ -0,0 +1,85 @@
1
+ import { ZodType, z, ZodObject } from 'zod';
2
+ import { UseFormReturn, FieldValues } from 'react-hook-form';
3
+ import React$1 from 'react';
4
+
5
+ type StepSchema = ZodType<Record<string, unknown>>;
6
+ interface StepConfig {
7
+ id: string;
8
+ label?: string;
9
+ schema?: StepSchema;
10
+ }
11
+ interface StepsContextValue {
12
+ currentStep: number;
13
+ totalSteps: number;
14
+ isFirst: boolean;
15
+ isLast: boolean;
16
+ next: () => void;
17
+ prev: () => void;
18
+ goTo: (index: number) => void;
19
+ formData: Record<string, unknown>;
20
+ setStepData: (step: number, data: Record<string, unknown>) => void;
21
+ schemas: StepSchema[];
22
+ }
23
+ interface UseStepsOptions {
24
+ totalSteps: number;
25
+ initialStep?: number;
26
+ onComplete?: () => void;
27
+ }
28
+ interface UseStepsReturn {
29
+ currentStep: number;
30
+ totalSteps: number;
31
+ isFirst: boolean;
32
+ isLast: boolean;
33
+ next: () => void;
34
+ prev: () => void;
35
+ goTo: (index: number) => void;
36
+ progress: number;
37
+ }
38
+ interface UseStepFormOptions<TSchema extends ZodType = ZodType> {
39
+ schema: TSchema;
40
+ defaultValues?: Partial<z.infer<TSchema>>;
41
+ onNext?: (data: z.infer<TSchema>) => void;
42
+ }
43
+ interface UseStepFormReturn<TSchema extends ZodType = ZodType> {
44
+ form: UseFormReturn<z.infer<TSchema> & FieldValues>;
45
+ nextWithValidation: () => Promise<boolean>;
46
+ isValidating: boolean;
47
+ }
48
+ interface StepsProviderProps {
49
+ children: React.ReactNode;
50
+ schemas: StepSchema[];
51
+ onSubmit?: (data: Record<string, unknown>) => void;
52
+ initialStep?: number;
53
+ }
54
+
55
+ declare function useSteps({ totalSteps, initialStep, onComplete, }: UseStepsOptions): UseStepsReturn;
56
+
57
+ declare function useStepForm<TSchema extends ZodType>({ schema, defaultValues, onNext, }: UseStepFormOptions<TSchema>): UseStepFormReturn<TSchema>;
58
+
59
+ declare const StepsContext: React$1.Context<StepsContextValue | null>;
60
+ declare function useStepsContext(): StepsContextValue;
61
+ declare function StepsProvider({ children, schemas, initialStep }: StepsProviderProps): React$1.FunctionComponentElement<React$1.ProviderProps<StepsContextValue | null>>;
62
+
63
+ interface ValidateStepResult {
64
+ success: boolean;
65
+ errors?: Record<string, string>;
66
+ data?: Record<string, unknown>;
67
+ }
68
+ declare function validateStep(schema: ZodType, data: Record<string, unknown>): Promise<ValidateStepResult>;
69
+
70
+ type AnyZodObject = ZodObject<any, any, any>;
71
+ /**
72
+ * Merges multiple Zod schemas into a single ZodObject schema.
73
+ * Only merges ZodObject schemas; other schema types are skipped.
74
+ */
75
+ declare function mergeSchemas(schemas: ZodType[]): AnyZodObject;
76
+ /**
77
+ * Validates all accumulated form data against the merged schema.
78
+ */
79
+ declare function validateAllSteps(schemas: ZodType[], data: Record<string, unknown>): Promise<{
80
+ success: boolean;
81
+ data?: Record<string, unknown>;
82
+ errors?: Record<string, string>;
83
+ }>;
84
+
85
+ export { type StepConfig, type StepSchema, StepsContext, type StepsContextValue, StepsProvider, type StepsProviderProps, type UseStepFormOptions, type UseStepFormReturn, type UseStepsOptions, type UseStepsReturn, mergeSchemas, useStepForm, useSteps, useStepsContext, validateAllSteps, validateStep };
package/dist/index.js ADDED
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ StepsContext: () => StepsContext,
34
+ StepsProvider: () => StepsProvider,
35
+ mergeSchemas: () => mergeSchemas,
36
+ useStepForm: () => useStepForm,
37
+ useSteps: () => useSteps,
38
+ useStepsContext: () => useStepsContext,
39
+ validateAllSteps: () => validateAllSteps,
40
+ validateStep: () => validateStep
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // src/hooks/useSteps.ts
45
+ var import_react = require("react");
46
+ function useSteps({
47
+ totalSteps,
48
+ initialStep = 0,
49
+ onComplete
50
+ }) {
51
+ if (totalSteps <= 0) {
52
+ throw new Error("[useSteps] totalSteps must be a positive integer");
53
+ }
54
+ const [currentStep, setCurrentStep] = (0, import_react.useState)(initialStep);
55
+ const next = (0, import_react.useCallback)(() => {
56
+ setCurrentStep((prev2) => {
57
+ const nextStep = Math.min(prev2 + 1, totalSteps - 1);
58
+ if (nextStep === totalSteps - 1 && prev2 !== totalSteps - 1) {
59
+ onComplete?.();
60
+ }
61
+ return nextStep;
62
+ });
63
+ }, [totalSteps, onComplete]);
64
+ const prev = (0, import_react.useCallback)(() => {
65
+ setCurrentStep((prev2) => Math.max(prev2 - 1, 0));
66
+ }, []);
67
+ const goTo = (0, import_react.useCallback)(
68
+ (index) => {
69
+ if (index >= 0 && index < totalSteps) {
70
+ setCurrentStep(index);
71
+ }
72
+ },
73
+ [totalSteps]
74
+ );
75
+ const progress = totalSteps > 1 ? Math.round(currentStep / (totalSteps - 1) * 100) : 100;
76
+ return {
77
+ currentStep,
78
+ totalSteps,
79
+ isFirst: currentStep === 0,
80
+ isLast: currentStep === totalSteps - 1,
81
+ next,
82
+ prev,
83
+ goTo,
84
+ progress
85
+ };
86
+ }
87
+
88
+ // src/hooks/useStepForm.ts
89
+ var import_react2 = require("react");
90
+ var import_react_hook_form = require("react-hook-form");
91
+ var import_zod = require("@hookform/resolvers/zod");
92
+ function useStepForm({
93
+ schema,
94
+ defaultValues,
95
+ onNext
96
+ }) {
97
+ const [isValidating, setIsValidating] = (0, import_react2.useState)(false);
98
+ const form = (0, import_react_hook_form.useForm)({
99
+ resolver: (0, import_zod.zodResolver)(schema),
100
+ // Cast needed: react-hook-form expects FieldValues but z.infer<TSchema> satisfies it at runtime
101
+ defaultValues,
102
+ mode: "onSubmit"
103
+ });
104
+ const nextWithValidation = (0, import_react2.useCallback)(async () => {
105
+ setIsValidating(true);
106
+ try {
107
+ const isValid = await form.trigger();
108
+ if (isValid) {
109
+ const data = form.getValues();
110
+ onNext?.(data);
111
+ return true;
112
+ }
113
+ return false;
114
+ } finally {
115
+ setIsValidating(false);
116
+ }
117
+ }, [form, onNext]);
118
+ return {
119
+ form,
120
+ nextWithValidation,
121
+ isValidating
122
+ };
123
+ }
124
+
125
+ // src/context/StepsContext.ts
126
+ var import_react3 = __toESM(require("react"));
127
+ var StepsContext = (0, import_react3.createContext)(null);
128
+ function useStepsContext() {
129
+ const ctx = (0, import_react3.useContext)(StepsContext);
130
+ if (!ctx) {
131
+ throw new Error("useStepsContext must be used within a StepsProvider");
132
+ }
133
+ return ctx;
134
+ }
135
+ function StepsProvider({ children, schemas, initialStep = 0 }) {
136
+ const totalSteps = schemas.length;
137
+ const [currentStep, setCurrentStep] = (0, import_react3.useState)(initialStep);
138
+ const [formData, setFormData] = (0, import_react3.useState)({});
139
+ const next = (0, import_react3.useCallback)(
140
+ () => setCurrentStep((s) => Math.min(s + 1, totalSteps - 1)),
141
+ [totalSteps]
142
+ );
143
+ const prev = (0, import_react3.useCallback)(() => setCurrentStep((s) => Math.max(s - 1, 0)), []);
144
+ const goTo = (0, import_react3.useCallback)(
145
+ (index) => {
146
+ if (index >= 0 && index < totalSteps) setCurrentStep(index);
147
+ },
148
+ [totalSteps]
149
+ );
150
+ const setStepData = (0, import_react3.useCallback)((_step, data) => {
151
+ setFormData((prev2) => ({ ...prev2, ...data }));
152
+ }, []);
153
+ const contextValue = (0, import_react3.useMemo)(
154
+ () => ({
155
+ currentStep,
156
+ totalSteps,
157
+ isFirst: currentStep === 0,
158
+ isLast: currentStep === totalSteps - 1,
159
+ next,
160
+ prev,
161
+ goTo,
162
+ formData,
163
+ setStepData,
164
+ schemas
165
+ }),
166
+ [currentStep, totalSteps, next, prev, goTo, formData, setStepData, schemas]
167
+ );
168
+ return import_react3.default.createElement(StepsContext.Provider, { value: contextValue }, children);
169
+ }
170
+
171
+ // src/utils/validateStep.ts
172
+ var import_zod2 = require("zod");
173
+ async function validateStep(schema, data) {
174
+ try {
175
+ const parsed = await schema.parseAsync(data);
176
+ return { success: true, data: parsed };
177
+ } catch (err) {
178
+ if (err instanceof import_zod2.ZodError) {
179
+ const errors = {};
180
+ for (const issue of err.issues) {
181
+ const path = issue.path.join(".");
182
+ errors[path] = issue.message;
183
+ }
184
+ return { success: false, errors };
185
+ }
186
+ return { success: false, errors: { _root: "Validation failed" } };
187
+ }
188
+ }
189
+
190
+ // src/utils/mergeSchemas.ts
191
+ var import_zod3 = require("zod");
192
+ function mergeSchemas(schemas) {
193
+ let merged = import_zod3.z.object({});
194
+ for (const schema of schemas) {
195
+ if (schema instanceof import_zod3.ZodObject) {
196
+ merged = merged.merge(schema);
197
+ }
198
+ }
199
+ return merged;
200
+ }
201
+ async function validateAllSteps(schemas, data) {
202
+ const mergedSchema = mergeSchemas(schemas);
203
+ try {
204
+ const parsed = await mergedSchema.parseAsync(data);
205
+ return { success: true, data: parsed };
206
+ } catch (err) {
207
+ const { ZodError: ZodError2 } = await import("zod");
208
+ if (err instanceof ZodError2) {
209
+ const errors = {};
210
+ for (const issue of err.issues) {
211
+ errors[issue.path.join(".")] = issue.message;
212
+ }
213
+ return { success: false, errors };
214
+ }
215
+ return { success: false };
216
+ }
217
+ }
218
+ // Annotate the CommonJS export names for ESM import in node:
219
+ 0 && (module.exports = {
220
+ StepsContext,
221
+ StepsProvider,
222
+ mergeSchemas,
223
+ useStepForm,
224
+ useSteps,
225
+ useStepsContext,
226
+ validateAllSteps,
227
+ validateStep
228
+ });
229
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/hooks/useSteps.ts","../src/hooks/useStepForm.ts","../src/context/StepsContext.ts","../src/utils/validateStep.ts","../src/utils/mergeSchemas.ts"],"sourcesContent":["// Hooks\nexport { useSteps } from './hooks/useSteps';\nexport { useStepForm } from './hooks/useStepForm';\n\n// Context\nexport { StepsContext, useStepsContext, StepsProvider } from './context/StepsContext';\n\n// Utils\nexport { validateStep } from './utils/validateStep';\nexport { mergeSchemas, validateAllSteps } from './utils/mergeSchemas';\n\n// Types\nexport type {\n StepConfig,\n StepSchema,\n StepsContextValue,\n UseStepsOptions,\n UseStepsReturn,\n UseStepFormOptions,\n UseStepFormReturn,\n StepsProviderProps,\n} from './types';\n","import { useState, useCallback } from 'react';\nimport { UseStepsOptions, UseStepsReturn } from '../types';\n\nexport function useSteps({\n totalSteps,\n initialStep = 0,\n onComplete,\n}: UseStepsOptions): UseStepsReturn {\n if (totalSteps <= 0) {\n throw new Error('[useSteps] totalSteps must be a positive integer');\n }\n\n const [currentStep, setCurrentStep] = useState(initialStep);\n\n const next = useCallback(() => {\n setCurrentStep((prev) => {\n const nextStep = Math.min(prev + 1, totalSteps - 1);\n if (nextStep === totalSteps - 1 && prev !== totalSteps - 1) {\n onComplete?.();\n }\n return nextStep;\n });\n }, [totalSteps, onComplete]);\n\n const prev = useCallback(() => {\n setCurrentStep((prev) => Math.max(prev - 1, 0));\n }, []);\n\n const goTo = useCallback(\n (index: number) => {\n if (index >= 0 && index < totalSteps) {\n setCurrentStep(index);\n }\n },\n [totalSteps]\n );\n\n const progress = totalSteps > 1 ? Math.round((currentStep / (totalSteps - 1)) * 100) : 100;\n\n return {\n currentStep,\n totalSteps,\n isFirst: currentStep === 0,\n isLast: currentStep === totalSteps - 1,\n next,\n prev,\n goTo,\n progress,\n };\n}\n","import { useState, useCallback } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { ZodType, z } from 'zod';\nimport { UseStepFormOptions, UseStepFormReturn } from '../types';\n\nexport function useStepForm<TSchema extends ZodType>({\n schema,\n defaultValues,\n onNext,\n}: UseStepFormOptions<TSchema>): UseStepFormReturn<TSchema> {\n const [isValidating, setIsValidating] = useState(false);\n\n const form = useForm<z.infer<TSchema>>({\n resolver: zodResolver(schema),\n // Cast needed: react-hook-form expects FieldValues but z.infer<TSchema> satisfies it at runtime\n defaultValues: defaultValues as z.infer<TSchema>,\n mode: 'onSubmit',\n });\n\n const nextWithValidation = useCallback(async (): Promise<boolean> => {\n setIsValidating(true);\n try {\n const isValid = await form.trigger();\n if (isValid) {\n const data = form.getValues();\n onNext?.(data);\n return true;\n }\n return false;\n } finally {\n setIsValidating(false);\n }\n }, [form, onNext]);\n\n return {\n form,\n nextWithValidation,\n isValidating,\n };\n}\n","import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';\nimport { StepsContextValue, StepsProviderProps } from '../types';\n\nexport const StepsContext = createContext<StepsContextValue | null>(null);\n\nexport function useStepsContext(): StepsContextValue {\n const ctx = useContext(StepsContext);\n if (!ctx) {\n throw new Error('useStepsContext must be used within a StepsProvider');\n }\n return ctx;\n}\n\nexport function StepsProvider({ children, schemas, initialStep = 0 }: StepsProviderProps) {\n // We derive totalSteps from schemas length for headless usage\n const totalSteps = schemas.length;\n const [currentStep, setCurrentStep] = useState(initialStep);\n const [formData, setFormData] = useState<Record<string, unknown>>({});\n\n const next = useCallback(\n () => setCurrentStep((s) => Math.min(s + 1, totalSteps - 1)),\n [totalSteps]\n );\n const prev = useCallback(() => setCurrentStep((s) => Math.max(s - 1, 0)), []);\n const goTo = useCallback(\n (index: number) => {\n if (index >= 0 && index < totalSteps) setCurrentStep(index);\n },\n [totalSteps]\n );\n\n const setStepData = useCallback((_step: number, data: Record<string, unknown>) => {\n setFormData((prev) => ({ ...prev, ...data }));\n }, []);\n\n const contextValue: StepsContextValue = useMemo(\n () => ({\n currentStep,\n totalSteps,\n isFirst: currentStep === 0,\n isLast: currentStep === totalSteps - 1,\n next,\n prev,\n goTo,\n formData,\n setStepData,\n schemas,\n }),\n [currentStep, totalSteps, next, prev, goTo, formData, setStepData, schemas]\n );\n\n return React.createElement(StepsContext.Provider, { value: contextValue }, children);\n}\n","import { ZodType, ZodError } from 'zod';\n\nexport interface ValidateStepResult {\n success: boolean;\n errors?: Record<string, string>;\n data?: Record<string, unknown>;\n}\n\nexport async function validateStep(\n schema: ZodType,\n data: Record<string, unknown>\n): Promise<ValidateStepResult> {\n try {\n const parsed = await schema.parseAsync(data);\n return { success: true, data: parsed as Record<string, unknown> };\n } catch (err) {\n if (err instanceof ZodError) {\n const errors: Record<string, string> = {};\n for (const issue of err.issues) {\n const path = issue.path.join('.');\n errors[path] = issue.message;\n }\n return { success: false, errors };\n }\n return { success: false, errors: { _root: 'Validation failed' } };\n }\n}\n","import { z, ZodType, ZodObject, ZodTypeAny } from 'zod';\n\n// ZodObject generics (shape, unknownKeys, catchall) vary across schemas after `.merge()`,\n// making a precise generic type impractical here. `any` is intentional and safe.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyZodObject = ZodObject<any, any, any>;\n\n/**\n * Merges multiple Zod schemas into a single ZodObject schema.\n * Only merges ZodObject schemas; other schema types are skipped.\n */\nexport function mergeSchemas(schemas: ZodType[]): AnyZodObject {\n let merged: AnyZodObject = z.object({});\n\n for (const schema of schemas) {\n if (schema instanceof ZodObject) {\n merged = merged.merge(schema as ZodObject<Record<string, ZodTypeAny>>);\n }\n }\n\n return merged;\n}\n\n/**\n * Validates all accumulated form data against the merged schema.\n */\nexport async function validateAllSteps(\n schemas: ZodType[],\n data: Record<string, unknown>\n): Promise<{ success: boolean; data?: Record<string, unknown>; errors?: Record<string, string> }> {\n const mergedSchema = mergeSchemas(schemas);\n try {\n const parsed = await mergedSchema.parseAsync(data);\n return { success: true, data: parsed as Record<string, unknown> };\n } catch (err) {\n const { ZodError } = await import('zod');\n if (err instanceof ZodError) {\n const errors: Record<string, string> = {};\n for (const issue of err.issues) {\n errors[issue.path.join('.')] = issue.message;\n }\n return { success: false, errors };\n }\n return { success: false };\n }\n}\n"],"mappings":"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","names":["prev","import_react","import_react","prev","React","import_zod","import_zod","ZodError"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,185 @@
1
+ // src/hooks/useSteps.ts
2
+ import { useState, useCallback } from "react";
3
+ function useSteps({
4
+ totalSteps,
5
+ initialStep = 0,
6
+ onComplete
7
+ }) {
8
+ if (totalSteps <= 0) {
9
+ throw new Error("[useSteps] totalSteps must be a positive integer");
10
+ }
11
+ const [currentStep, setCurrentStep] = useState(initialStep);
12
+ const next = useCallback(() => {
13
+ setCurrentStep((prev2) => {
14
+ const nextStep = Math.min(prev2 + 1, totalSteps - 1);
15
+ if (nextStep === totalSteps - 1 && prev2 !== totalSteps - 1) {
16
+ onComplete?.();
17
+ }
18
+ return nextStep;
19
+ });
20
+ }, [totalSteps, onComplete]);
21
+ const prev = useCallback(() => {
22
+ setCurrentStep((prev2) => Math.max(prev2 - 1, 0));
23
+ }, []);
24
+ const goTo = useCallback(
25
+ (index) => {
26
+ if (index >= 0 && index < totalSteps) {
27
+ setCurrentStep(index);
28
+ }
29
+ },
30
+ [totalSteps]
31
+ );
32
+ const progress = totalSteps > 1 ? Math.round(currentStep / (totalSteps - 1) * 100) : 100;
33
+ return {
34
+ currentStep,
35
+ totalSteps,
36
+ isFirst: currentStep === 0,
37
+ isLast: currentStep === totalSteps - 1,
38
+ next,
39
+ prev,
40
+ goTo,
41
+ progress
42
+ };
43
+ }
44
+
45
+ // src/hooks/useStepForm.ts
46
+ import { useState as useState2, useCallback as useCallback2 } from "react";
47
+ import { useForm } from "react-hook-form";
48
+ import { zodResolver } from "@hookform/resolvers/zod";
49
+ function useStepForm({
50
+ schema,
51
+ defaultValues,
52
+ onNext
53
+ }) {
54
+ const [isValidating, setIsValidating] = useState2(false);
55
+ const form = useForm({
56
+ resolver: zodResolver(schema),
57
+ // Cast needed: react-hook-form expects FieldValues but z.infer<TSchema> satisfies it at runtime
58
+ defaultValues,
59
+ mode: "onSubmit"
60
+ });
61
+ const nextWithValidation = useCallback2(async () => {
62
+ setIsValidating(true);
63
+ try {
64
+ const isValid = await form.trigger();
65
+ if (isValid) {
66
+ const data = form.getValues();
67
+ onNext?.(data);
68
+ return true;
69
+ }
70
+ return false;
71
+ } finally {
72
+ setIsValidating(false);
73
+ }
74
+ }, [form, onNext]);
75
+ return {
76
+ form,
77
+ nextWithValidation,
78
+ isValidating
79
+ };
80
+ }
81
+
82
+ // src/context/StepsContext.ts
83
+ import React, { createContext, useContext, useState as useState3, useCallback as useCallback3, useMemo } from "react";
84
+ var StepsContext = createContext(null);
85
+ function useStepsContext() {
86
+ const ctx = useContext(StepsContext);
87
+ if (!ctx) {
88
+ throw new Error("useStepsContext must be used within a StepsProvider");
89
+ }
90
+ return ctx;
91
+ }
92
+ function StepsProvider({ children, schemas, initialStep = 0 }) {
93
+ const totalSteps = schemas.length;
94
+ const [currentStep, setCurrentStep] = useState3(initialStep);
95
+ const [formData, setFormData] = useState3({});
96
+ const next = useCallback3(
97
+ () => setCurrentStep((s) => Math.min(s + 1, totalSteps - 1)),
98
+ [totalSteps]
99
+ );
100
+ const prev = useCallback3(() => setCurrentStep((s) => Math.max(s - 1, 0)), []);
101
+ const goTo = useCallback3(
102
+ (index) => {
103
+ if (index >= 0 && index < totalSteps) setCurrentStep(index);
104
+ },
105
+ [totalSteps]
106
+ );
107
+ const setStepData = useCallback3((_step, data) => {
108
+ setFormData((prev2) => ({ ...prev2, ...data }));
109
+ }, []);
110
+ const contextValue = useMemo(
111
+ () => ({
112
+ currentStep,
113
+ totalSteps,
114
+ isFirst: currentStep === 0,
115
+ isLast: currentStep === totalSteps - 1,
116
+ next,
117
+ prev,
118
+ goTo,
119
+ formData,
120
+ setStepData,
121
+ schemas
122
+ }),
123
+ [currentStep, totalSteps, next, prev, goTo, formData, setStepData, schemas]
124
+ );
125
+ return React.createElement(StepsContext.Provider, { value: contextValue }, children);
126
+ }
127
+
128
+ // src/utils/validateStep.ts
129
+ import { ZodError } from "zod";
130
+ async function validateStep(schema, data) {
131
+ try {
132
+ const parsed = await schema.parseAsync(data);
133
+ return { success: true, data: parsed };
134
+ } catch (err) {
135
+ if (err instanceof ZodError) {
136
+ const errors = {};
137
+ for (const issue of err.issues) {
138
+ const path = issue.path.join(".");
139
+ errors[path] = issue.message;
140
+ }
141
+ return { success: false, errors };
142
+ }
143
+ return { success: false, errors: { _root: "Validation failed" } };
144
+ }
145
+ }
146
+
147
+ // src/utils/mergeSchemas.ts
148
+ import { z, ZodObject } from "zod";
149
+ function mergeSchemas(schemas) {
150
+ let merged = z.object({});
151
+ for (const schema of schemas) {
152
+ if (schema instanceof ZodObject) {
153
+ merged = merged.merge(schema);
154
+ }
155
+ }
156
+ return merged;
157
+ }
158
+ async function validateAllSteps(schemas, data) {
159
+ const mergedSchema = mergeSchemas(schemas);
160
+ try {
161
+ const parsed = await mergedSchema.parseAsync(data);
162
+ return { success: true, data: parsed };
163
+ } catch (err) {
164
+ const { ZodError: ZodError2 } = await import("zod");
165
+ if (err instanceof ZodError2) {
166
+ const errors = {};
167
+ for (const issue of err.issues) {
168
+ errors[issue.path.join(".")] = issue.message;
169
+ }
170
+ return { success: false, errors };
171
+ }
172
+ return { success: false };
173
+ }
174
+ }
175
+ export {
176
+ StepsContext,
177
+ StepsProvider,
178
+ mergeSchemas,
179
+ useStepForm,
180
+ useSteps,
181
+ useStepsContext,
182
+ validateAllSteps,
183
+ validateStep
184
+ };
185
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useSteps.ts","../src/hooks/useStepForm.ts","../src/context/StepsContext.ts","../src/utils/validateStep.ts","../src/utils/mergeSchemas.ts"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { UseStepsOptions, UseStepsReturn } from '../types';\n\nexport function useSteps({\n totalSteps,\n initialStep = 0,\n onComplete,\n}: UseStepsOptions): UseStepsReturn {\n if (totalSteps <= 0) {\n throw new Error('[useSteps] totalSteps must be a positive integer');\n }\n\n const [currentStep, setCurrentStep] = useState(initialStep);\n\n const next = useCallback(() => {\n setCurrentStep((prev) => {\n const nextStep = Math.min(prev + 1, totalSteps - 1);\n if (nextStep === totalSteps - 1 && prev !== totalSteps - 1) {\n onComplete?.();\n }\n return nextStep;\n });\n }, [totalSteps, onComplete]);\n\n const prev = useCallback(() => {\n setCurrentStep((prev) => Math.max(prev - 1, 0));\n }, []);\n\n const goTo = useCallback(\n (index: number) => {\n if (index >= 0 && index < totalSteps) {\n setCurrentStep(index);\n }\n },\n [totalSteps]\n );\n\n const progress = totalSteps > 1 ? Math.round((currentStep / (totalSteps - 1)) * 100) : 100;\n\n return {\n currentStep,\n totalSteps,\n isFirst: currentStep === 0,\n isLast: currentStep === totalSteps - 1,\n next,\n prev,\n goTo,\n progress,\n };\n}\n","import { useState, useCallback } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { ZodType, z } from 'zod';\nimport { UseStepFormOptions, UseStepFormReturn } from '../types';\n\nexport function useStepForm<TSchema extends ZodType>({\n schema,\n defaultValues,\n onNext,\n}: UseStepFormOptions<TSchema>): UseStepFormReturn<TSchema> {\n const [isValidating, setIsValidating] = useState(false);\n\n const form = useForm<z.infer<TSchema>>({\n resolver: zodResolver(schema),\n // Cast needed: react-hook-form expects FieldValues but z.infer<TSchema> satisfies it at runtime\n defaultValues: defaultValues as z.infer<TSchema>,\n mode: 'onSubmit',\n });\n\n const nextWithValidation = useCallback(async (): Promise<boolean> => {\n setIsValidating(true);\n try {\n const isValid = await form.trigger();\n if (isValid) {\n const data = form.getValues();\n onNext?.(data);\n return true;\n }\n return false;\n } finally {\n setIsValidating(false);\n }\n }, [form, onNext]);\n\n return {\n form,\n nextWithValidation,\n isValidating,\n };\n}\n","import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';\nimport { StepsContextValue, StepsProviderProps } from '../types';\n\nexport const StepsContext = createContext<StepsContextValue | null>(null);\n\nexport function useStepsContext(): StepsContextValue {\n const ctx = useContext(StepsContext);\n if (!ctx) {\n throw new Error('useStepsContext must be used within a StepsProvider');\n }\n return ctx;\n}\n\nexport function StepsProvider({ children, schemas, initialStep = 0 }: StepsProviderProps) {\n // We derive totalSteps from schemas length for headless usage\n const totalSteps = schemas.length;\n const [currentStep, setCurrentStep] = useState(initialStep);\n const [formData, setFormData] = useState<Record<string, unknown>>({});\n\n const next = useCallback(\n () => setCurrentStep((s) => Math.min(s + 1, totalSteps - 1)),\n [totalSteps]\n );\n const prev = useCallback(() => setCurrentStep((s) => Math.max(s - 1, 0)), []);\n const goTo = useCallback(\n (index: number) => {\n if (index >= 0 && index < totalSteps) setCurrentStep(index);\n },\n [totalSteps]\n );\n\n const setStepData = useCallback((_step: number, data: Record<string, unknown>) => {\n setFormData((prev) => ({ ...prev, ...data }));\n }, []);\n\n const contextValue: StepsContextValue = useMemo(\n () => ({\n currentStep,\n totalSteps,\n isFirst: currentStep === 0,\n isLast: currentStep === totalSteps - 1,\n next,\n prev,\n goTo,\n formData,\n setStepData,\n schemas,\n }),\n [currentStep, totalSteps, next, prev, goTo, formData, setStepData, schemas]\n );\n\n return React.createElement(StepsContext.Provider, { value: contextValue }, children);\n}\n","import { ZodType, ZodError } from 'zod';\n\nexport interface ValidateStepResult {\n success: boolean;\n errors?: Record<string, string>;\n data?: Record<string, unknown>;\n}\n\nexport async function validateStep(\n schema: ZodType,\n data: Record<string, unknown>\n): Promise<ValidateStepResult> {\n try {\n const parsed = await schema.parseAsync(data);\n return { success: true, data: parsed as Record<string, unknown> };\n } catch (err) {\n if (err instanceof ZodError) {\n const errors: Record<string, string> = {};\n for (const issue of err.issues) {\n const path = issue.path.join('.');\n errors[path] = issue.message;\n }\n return { success: false, errors };\n }\n return { success: false, errors: { _root: 'Validation failed' } };\n }\n}\n","import { z, ZodType, ZodObject, ZodTypeAny } from 'zod';\n\n// ZodObject generics (shape, unknownKeys, catchall) vary across schemas after `.merge()`,\n// making a precise generic type impractical here. `any` is intentional and safe.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyZodObject = ZodObject<any, any, any>;\n\n/**\n * Merges multiple Zod schemas into a single ZodObject schema.\n * Only merges ZodObject schemas; other schema types are skipped.\n */\nexport function mergeSchemas(schemas: ZodType[]): AnyZodObject {\n let merged: AnyZodObject = z.object({});\n\n for (const schema of schemas) {\n if (schema instanceof ZodObject) {\n merged = merged.merge(schema as ZodObject<Record<string, ZodTypeAny>>);\n }\n }\n\n return merged;\n}\n\n/**\n * Validates all accumulated form data against the merged schema.\n */\nexport async function validateAllSteps(\n schemas: ZodType[],\n data: Record<string, unknown>\n): Promise<{ success: boolean; data?: Record<string, unknown>; errors?: Record<string, string> }> {\n const mergedSchema = mergeSchemas(schemas);\n try {\n const parsed = await mergedSchema.parseAsync(data);\n return { success: true, data: parsed as Record<string, unknown> };\n } catch (err) {\n const { ZodError } = await import('zod');\n if (err instanceof ZodError) {\n const errors: Record<string, string> = {};\n for (const issue of err.issues) {\n errors[issue.path.join('.')] = issue.message;\n }\n return { success: false, errors };\n }\n return { success: false };\n }\n}\n"],"mappings":"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","names":["prev","useState","useCallback","useState","useCallback","prev","ZodError"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "react-formsteps-core",
3
+ "version": "0.1.0",
4
+ "description": "Headless multi-step form logic for React",
5
+ "keywords": [
6
+ "react",
7
+ "form",
8
+ "wizard",
9
+ "steps",
10
+ "headless"
11
+ ],
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "test": "vitest run",
30
+ "lint": "eslint src --ext .ts,.tsx"
31
+ },
32
+ "peerDependencies": {
33
+ "@hookform/resolvers": ">=3",
34
+ "react": ">=18",
35
+ "react-dom": ">=18",
36
+ "react-hook-form": ">=7",
37
+ "zod": ">=3"
38
+ },
39
+ "devDependencies": {
40
+ "@hookform/resolvers": "^3.3.4",
41
+ "@testing-library/react": "^14.2.2",
42
+ "@testing-library/react-hooks": "^8.0.1",
43
+ "@types/react": "^18.2.73",
44
+ "@types/react-dom": "^18.2.23",
45
+ "@vitest/coverage-v8": "^4.1.0",
46
+ "jsdom": "^29.0.1",
47
+ "react": "^18.2.0",
48
+ "react-dom": "^18.2.0",
49
+ "react-hook-form": "^7.51.2",
50
+ "tsup": "^8.0.2",
51
+ "typescript": "^5.4.5",
52
+ "vitest": "^1.4.0",
53
+ "zod": "^3.22.4"
54
+ }
55
+ }