formcn 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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/index.js +71 -0
  4. package/generators/form-generator.js +152 -0
  5. package/generators/form-ui-templates.js +183 -0
  6. package/generators/multi-form-generator.js +257 -0
  7. package/generators/schema-generator.js +89 -0
  8. package/package.json +46 -0
  9. package/test/README.md +73 -0
  10. package/test/components.json +22 -0
  11. package/test/eslint.config.js +23 -0
  12. package/test/index.html +13 -0
  13. package/test/package-lock.json +4759 -0
  14. package/test/package.json +46 -0
  15. package/test/public/vite.svg +1 -0
  16. package/test/src/App.css +42 -0
  17. package/test/src/App.tsx +7 -0
  18. package/test/src/assets/react.svg +1 -0
  19. package/test/src/components/ui/button.tsx +62 -0
  20. package/test/src/components/ui/checkbox.tsx +32 -0
  21. package/test/src/components/ui/field.tsx +246 -0
  22. package/test/src/components/ui/input-group.tsx +170 -0
  23. package/test/src/components/ui/input.tsx +21 -0
  24. package/test/src/components/ui/label.tsx +22 -0
  25. package/test/src/components/ui/radio-group.tsx +43 -0
  26. package/test/src/components/ui/select.tsx +188 -0
  27. package/test/src/components/ui/separator.tsx +28 -0
  28. package/test/src/components/ui/textarea.tsx +18 -0
  29. package/test/src/index.css +123 -0
  30. package/test/src/lib/utils.ts +6 -0
  31. package/test/src/main.tsx +10 -0
  32. package/test/tsconfig.app.json +33 -0
  33. package/test/tsconfig.json +13 -0
  34. package/test/tsconfig.node.json +26 -0
  35. package/test/vite.config.ts +14 -0
  36. package/utils/ensurePackages.js +62 -0
  37. package/utils/lib.js +22 -0
  38. package/utils/prompts.js +200 -0
  39. package/utils/tailwind-presets.js +132 -0
  40. package/utils/templates.js +103 -0
  41. package/utils/test.js +136 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fares Galal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # formcn
2
+
3
+ [![npm version](https://img.shields.io/npm/v/formcn.svg)](https://www.npmjs.com/package/formcn)
4
+ [![npm downloads](https://img.shields.io/npm/dw/formcn.svg)](https://www.npmjs.com/package/formcn)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > Schema-driven React form generator built on top of React Hook Form, Zod, and shadcn/ui.
8
+
9
+ formcn generates fully typed, validated React form components from an interactive CLI workflow.
10
+ It is designed to reduce boilerplate while staying aligned with shadcn/ui conventions.
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - Zero-configuration form generation
17
+ - Type-safe schemas using Zod
18
+ - shadcn/ui component integration
19
+ - Single-step and multi-step forms
20
+ - Schema-first validation
21
+ - Interactive CLI workflow
22
+ - Automatic dependency detection and installation
23
+ - Predefined templates for common use cases
24
+
25
+ ---
26
+
27
+ ## Prerequisites
28
+
29
+ Before using formcn, ensure you have:
30
+
31
+ - Node.js 18+
32
+ - A React project with TypeScript
33
+ - shadcn/ui initialized in your project
34
+
35
+ Initialize shadcn/ui if needed:
36
+
37
+ ```bash
38
+ npx shadcn@latest init
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ Install globally using npm:
46
+
47
+ ```bash
48
+ npm install -g formcn
49
+ ```
50
+
51
+ Or using yarn:
52
+
53
+ ```bash
54
+ yarn global add formcn
55
+ ```
56
+
57
+ Or run directly with npx:
58
+
59
+ ```bash
60
+ npx formcn
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Usage
66
+
67
+ Run the CLI:
68
+
69
+ ```bash
70
+ formcn
71
+ ```
72
+
73
+ The interactive workflow will guide you through:
74
+
75
+ 1. Form name
76
+ 2. Form type (single-step or multi-step)
77
+ 3. Template selection
78
+ 4. Field definitions and validation rules
79
+ 5. Style preset selection
80
+
81
+ ### Example
82
+
83
+ ```bash
84
+ $ formcn
85
+
86
+ formcn
87
+
88
+ ✔ react-hook-form detected
89
+ ✔ @hookform/resolvers detected
90
+ ✔ zod detected
91
+ ✔ shadcn/ui components detected
92
+
93
+ Form name: register
94
+ Form type: single step
95
+ Template: registration
96
+ Preset: default
97
+
98
+ ✔ Form generated at src/components/forms/register
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Generated Output
104
+
105
+ ### Single-step form
106
+
107
+ ```txt
108
+ src/components/forms/{formName}/
109
+ ├── schema.ts
110
+ └── form.tsx
111
+ ```
112
+
113
+ ### Multi-step form
114
+
115
+ ```txt
116
+ src/components/forms/user-registration/
117
+ ├── personal-info-schema.ts
118
+ ├── personalInfoStep.tsx
119
+ ├── account-details-schema.ts
120
+ ├── accountDetailsStep.tsx
121
+ ├── contact-info-schema.ts
122
+ ├── contactInfoStep.tsx
123
+ └── UserRegistrationForm.tsx
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Supported Field Types
129
+
130
+ - Text
131
+ - Email
132
+ - Password (with optional confirmation)
133
+ - Number
134
+ - URL
135
+ - Textarea
136
+ - Select
137
+ - Checkbox
138
+ - Radio
139
+ - Date
140
+
141
+ ---
142
+
143
+ ## Templates
144
+
145
+ ### Single-step
146
+
147
+ - Registration
148
+ - Login
149
+ - Contact
150
+
151
+ ### Multi-step
152
+
153
+ - Registration (personal info, account details, contact info)
154
+
155
+ ---
156
+
157
+ ## Style Presets
158
+
159
+ ### Single-step
160
+
161
+ - `default` — minimal layout with subtle borders and spacing
162
+
163
+ ### Multi-step
164
+
165
+ - `minimal`
166
+ - `sidebarStepper`
167
+ - `softType`
168
+ - `stepperTop`
169
+
170
+ ---
171
+
172
+ ## Example Output
173
+
174
+ ### Schema (`schema.ts`)
175
+
176
+ ```ts
177
+ import { z } from "zod";
178
+
179
+ export const schema = z
180
+ .object({
181
+ first_name: z.string().min(1, "First Name is required"),
182
+ last_name: z.string().min(1, "Last Name is required"),
183
+ email: z.string().email("Invalid email"),
184
+ password: z.string().min(8, "Password must be at least 8 characters"),
185
+ passwordConfirmation: z.string(),
186
+ age: z.coerce.number().min(18, "Age must be at least 18"),
187
+ website: z.string().url("Invalid URL").optional().or(z.literal("")),
188
+ bio: z.string().optional(),
189
+ country: z.union([z.literal("us"), z.literal("ca"), z.literal("uk")], {
190
+ error: "Please select a country",
191
+ }),
192
+ newsletter: z.boolean().optional(),
193
+ })
194
+ .refine((data) => data.password === data.passwordConfirmation, {
195
+ message: "Passwords do not match",
196
+ path: ["passwordConfirmation"],
197
+ });
198
+
199
+ export type SchemaFormValues = z.infer<typeof schema>;
200
+ ```
201
+
202
+ ### Form (`form.tsx`)
203
+
204
+ ```tsx
205
+ "use client";
206
+
207
+ import { useForm } from "react-hook-form";
208
+ import { zodResolver } from "@hookform/resolvers/zod";
209
+ import { Button } from "@/components/ui/button";
210
+ import { FieldGroup } from "@/components/ui/field";
211
+ import { schema, type SchemaFormValues } from "./schema";
212
+
213
+ export default function RegisterForm() {
214
+ const form = useForm<SchemaFormValues>({
215
+ resolver: zodResolver(schema),
216
+ });
217
+
218
+ function onSubmit(values: SchemaFormValues) {
219
+ console.log(values);
220
+ }
221
+
222
+ return (
223
+ <form
224
+ onSubmit={form.handleSubmit(onSubmit)}
225
+ className="space-y-6 max-w-3xl mx-auto border rounded-lg p-6"
226
+ >
227
+ <FieldGroup />
228
+ <div className="flex justify-end">
229
+ <Button type="submit" disabled={!form.formState.isValid}>
230
+ Submit
231
+ </Button>
232
+ </div>
233
+ </form>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Troubleshooting
241
+
242
+ ### Command not found
243
+
244
+ Ensure your global npm bin directory is in your PATH:
245
+
246
+ ```bash
247
+ npm config get prefix
248
+ ```
249
+
250
+ ### shadcn/ui not detected
251
+
252
+ ```bash
253
+ npx shadcn@latest init
254
+ ```
255
+
256
+ ### Missing dependencies
257
+
258
+ ```bash
259
+ npm install react-hook-form @hookform/resolvers zod
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Contributing
265
+
266
+ Contributions are welcome.
267
+
268
+ 1. Fork the repository
269
+ 2. Create a feature branch
270
+ 3. Commit your changes
271
+ 4. Push the branch
272
+ 5. Open a pull request
273
+
274
+ ---
275
+
276
+ ## License
277
+
278
+ MIT License
279
+
280
+ ---
281
+
282
+ ## Author
283
+
284
+ **Fares Galal**
285
+
286
+ ---
287
+
288
+ ## Acknowledgments
289
+
290
+ - React Hook Form
291
+ - Zod
292
+ - shadcn/ui
293
+ - Tailwind CSS
294
+ - TypeScript
package/bin/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { intro, outro, select } from "@clack/prompts";
2
+ import {
3
+ askFormType,
4
+ askFields,
5
+ askFormName,
6
+ askSteps,
7
+ askFormTemplate,
8
+ askFormPreset,
9
+ } from "../utils/prompts.js";
10
+ import { generateSingleForm } from "../generators/form-generator.js";
11
+ import { generateMultiForm } from "../generators/multi-form-generator.js";
12
+ import {
13
+ singleFormPresets,
14
+ multiFormPresets,
15
+ } from "../utils/tailwind-presets.js";
16
+ import {
17
+ SINGLE_FIELD_TEMPLATES,
18
+ MULTI_STEP_TEMPLATES,
19
+ } from "../utils/templates.js";
20
+ import { ensurePackages } from "../utils/ensurePackages.js";
21
+
22
+ intro("formcn");
23
+
24
+ await ensurePackages();
25
+
26
+ const formName = await askFormName();
27
+ const formType = await askFormType();
28
+
29
+ const formTemplateChoice = await askFormTemplate(formType);
30
+
31
+ let presetKey;
32
+
33
+ if (formTemplateChoice === "template") {
34
+ if (formType === "single") {
35
+ const templateKeys = Object.keys(SINGLE_FIELD_TEMPLATES);
36
+ const templateChoice = await select({
37
+ message: "Choose a template:",
38
+ options: templateKeys.map((key) => ({ value: key, label: key })),
39
+ });
40
+ presetKey = await askFormPreset(singleFormPresets);
41
+ await generateSingleForm({
42
+ formName,
43
+ fields: SINGLE_FIELD_TEMPLATES[templateChoice],
44
+ presetKey,
45
+ });
46
+ } else {
47
+ const templateKeys = Object.keys(MULTI_STEP_TEMPLATES);
48
+ const templateChoice = await select({
49
+ message: "Choose a template:",
50
+ options: templateKeys.map((key) => ({ value: key, label: key })),
51
+ });
52
+ presetKey = await askFormPreset(multiFormPresets);
53
+ await generateMultiForm({
54
+ formName,
55
+ steps: MULTI_STEP_TEMPLATES[templateChoice],
56
+ presetKey,
57
+ });
58
+ }
59
+ } else {
60
+ if (formType === "single") {
61
+ const fields = await askFields();
62
+ presetKey = await askFormPreset(singleFormPresets);
63
+ await generateSingleForm({ formName, fields, presetKey });
64
+ } else {
65
+ presetKey = await askFormPreset(multiFormPresets);
66
+ const steps = await askSteps();
67
+ await generateMultiForm({ formName, steps, presetKey });
68
+ }
69
+ }
70
+
71
+ outro("✅ Form created successfully");
@@ -0,0 +1,152 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { generateZodSchema } from "./schema-generator.js";
4
+ import { toPascalCaseForm } from "../utils/lib.js";
5
+ import {
6
+ generateImports,
7
+ getComponentUsage,
8
+ getDefaultValue,
9
+ renderError,
10
+ renderInputByType,
11
+ } from "./form-ui-templates.js";
12
+ import { singleFormPresets } from "../utils/tailwind-presets.js";
13
+ import { confirm, isCancel } from "@clack/prompts";
14
+
15
+ export async function generateSingleForm({ formName, fields, presetKey }) {
16
+ const baseDir = fs.existsSync(path.resolve("src"))
17
+ ? path.resolve("src/components/forms")
18
+ : path.resolve("components/forms");
19
+
20
+ const formDir = path.join(baseDir, formName);
21
+
22
+ const exists = await fs.pathExists(formDir);
23
+
24
+ if (exists) {
25
+ const overwrite = await confirm({
26
+ message: `Form "${formName}" already exists. Overwrite it?`,
27
+ initialValue: false,
28
+ });
29
+
30
+ if (isCancel(overwrite) || !overwrite) {
31
+ console.log("❌ Operation cancelled.");
32
+ process.exit(0);
33
+ }
34
+ await fs.remove(formDir);
35
+ }
36
+
37
+ try {
38
+ await fs.ensureDir(formDir);
39
+
40
+ const enhancedFields = addPasswordConfirmation(fields);
41
+
42
+ await fs.writeFile(
43
+ path.join(formDir, "schema.ts"),
44
+ generateZodSchema(enhancedFields)
45
+ );
46
+
47
+ await fs.writeFile(
48
+ path.join(formDir, "form.tsx"),
49
+ generateFormComponent(formName, enhancedFields, presetKey)
50
+ );
51
+
52
+ console.log(`✅ Form generated at ${formDir}`);
53
+ } catch (error) {
54
+ await fs.remove(formDir);
55
+ console.error("❌ Failed to generate form. Changes reverted.");
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ function addPasswordConfirmation(fields) {
61
+ return fields.flatMap((field) =>
62
+ field.type === "password" && field.confirm
63
+ ? [
64
+ field,
65
+ {
66
+ ...field,
67
+ name: `${field.name}Confirmation`,
68
+ label: "Confirm Password",
69
+ isConfirmation: true,
70
+ confirmationFor: field.name,
71
+ },
72
+ ]
73
+ : field
74
+ );
75
+ }
76
+
77
+ function generateFormComponent(formName, fields, presetKey) {
78
+ const uses = getComponentUsage(fields);
79
+ const componentName = toPascalCaseForm(formName);
80
+ const preset = singleFormPresets[presetKey];
81
+ if (!preset) throw new Error(`Single form preset "${presetKey}" not found`);
82
+
83
+ const renderControllerContent = (f) => `
84
+ <FieldLabel htmlFor="${f.name}">${f.label}${
85
+ f.required ? ' <span className="text-red-500">*</span>' : ""
86
+ }
87
+ </FieldLabel>
88
+ ${renderInputByType(f)}
89
+ ${renderError()}
90
+ `;
91
+
92
+ const renderFields = fields
93
+ .map((f) => {
94
+ const isCheckbox = f.type === "checkbox";
95
+
96
+ return `
97
+ <Controller
98
+ name="${f.name}"
99
+ control={form.control}
100
+ render={({ field, fieldState }) => (
101
+ ${
102
+ isCheckbox
103
+ ? `
104
+ <>
105
+ ${renderControllerContent(f, false)}
106
+ </>
107
+ `
108
+ : `
109
+ <Field data-invalid={fieldState.invalid}>
110
+ ${renderControllerContent(f)}
111
+ </Field>
112
+ `
113
+ }
114
+ )}
115
+ />
116
+ `;
117
+ })
118
+ .join("\n");
119
+
120
+ return `
121
+ "use client";
122
+
123
+ ${generateImports(uses)}
124
+ import { schema, type SchemaFormValues } from "./schema";
125
+
126
+ export default function ${componentName}() {
127
+ const form = useForm<SchemaFormValues>({
128
+ resolver: zodResolver(schema),
129
+ defaultValues: {
130
+ ${fields.map(getDefaultValue).join(",\n ")}
131
+ },
132
+ });
133
+
134
+ function onSubmit(data: SchemaFormValues) {
135
+ console.log(data);
136
+ }
137
+
138
+ return (
139
+ <form onSubmit={form.handleSubmit(onSubmit)} className="${preset.form}">
140
+ <FieldGroup>
141
+ ${renderFields}
142
+ </FieldGroup>
143
+ <div className="${preset.buttonsWrapper}">
144
+ <Button type="submit" disabled={!form.formState.isValid}>
145
+ Submit
146
+ </Button>
147
+ </div>
148
+ </form>
149
+ );
150
+ }
151
+ `.trim();
152
+ }
@@ -0,0 +1,183 @@
1
+ export const INPUT_TEMPLATES = {
2
+ textarea: (f) => `<Textarea
3
+ {...field}
4
+ id="${f.name}"
5
+ rows={4}
6
+ ${f.required ? "required" : ""}
7
+ aria-invalid={fieldState.invalid}
8
+ />`,
9
+
10
+ select: (
11
+ f
12
+ ) => `<Select value={field.value ?? ""} onValueChange={field.onChange}>
13
+ <SelectTrigger id="${f.name}" aria-invalid={fieldState.invalid}>
14
+ <SelectValue placeholder="Select ${f.label}" />
15
+ </SelectTrigger>
16
+ <SelectContent>
17
+ ${f.options
18
+ .map(
19
+ (opt) => `<SelectItem value="${opt.value}">${opt.label}</SelectItem>`
20
+ )
21
+ .join("\n ")}
22
+ </SelectContent>
23
+ </Select>`,
24
+
25
+ checkbox: (f) => `<FieldGroup data-slot="checkbox-group">
26
+ <Field orientation="horizontal">
27
+ <Checkbox
28
+ id="${f.name}"
29
+ name={field.name}
30
+ checked={field.value}
31
+ onCheckedChange={field.onChange}
32
+ ${f.required ? "required" : ""}
33
+ />
34
+ <FieldLabel htmlFor="${f.name}" className="font-normal">
35
+ ${f.label}
36
+ </FieldLabel>
37
+ </Field>
38
+ </FieldGroup>`,
39
+
40
+ radio: (
41
+ f
42
+ ) => `<RadioGroup value={field.value ?? ""} onValueChange={(val) => field.onChange(val === "" ? undefined : val)}>
43
+ ${f.options
44
+ .map(
45
+ (opt) => `<Field orientation="horizontal">
46
+ <RadioGroupItem value="${opt.value}" id="${f.name}-${opt.value}" />
47
+ <FieldLabel htmlFor="${f.name}-${opt.value}">${opt.label}</FieldLabel>
48
+ </Field>`
49
+ )
50
+ .join("\n ")}
51
+ </RadioGroup>`,
52
+
53
+ number: (f) => `<Input
54
+ id="${f.name}"
55
+ type="number"
56
+ autoComplete="${f.autocomplete ?? "off"}"
57
+ ${f.required ? "required" : ""}
58
+ aria-invalid={fieldState.invalid}
59
+ value={field.value ?? ""}
60
+ onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
61
+ onBlur={field.onBlur}
62
+ ref={field.ref}
63
+ />`,
64
+
65
+ default: (f) => {
66
+ const type = ["text", "email", "password", "date"].includes(f.type)
67
+ ? f.type
68
+ : "text";
69
+ const defaultAutoComplete = {
70
+ text: "off",
71
+ email: "email",
72
+ password: "new-password",
73
+ date: "bday",
74
+ }[type];
75
+ return `<Input
76
+ {...field}
77
+ id="${f.name}"
78
+ type="${type}"
79
+ autoComplete="${f.autocomplete ?? defaultAutoComplete}"
80
+ ${f.required ? "required" : ""}
81
+ aria-invalid={fieldState.invalid}
82
+ />`;
83
+ },
84
+ };
85
+
86
+ export const COMPONENT_USAGE_MAP = {
87
+ textarea: { Textarea: true },
88
+ select: {
89
+ Select: true,
90
+ SelectTrigger: true,
91
+ SelectValue: true,
92
+ SelectContent: true,
93
+ SelectItem: true,
94
+ },
95
+ checkbox: { Checkbox: true },
96
+ radio: { RadioGroup: true, RadioGroupItem: true },
97
+ default: { Input: true },
98
+ };
99
+
100
+ export function renderInputByType(f) {
101
+ return INPUT_TEMPLATES[f.type]?.(f) || INPUT_TEMPLATES.default(f);
102
+ }
103
+
104
+ export function getComponentUsage(fields) {
105
+ const usage = {};
106
+
107
+ fields.forEach((f) => {
108
+ const components =
109
+ COMPONENT_USAGE_MAP[f.type] || COMPONENT_USAGE_MAP.default;
110
+ Object.assign(usage, components);
111
+ });
112
+
113
+ return usage;
114
+ }
115
+
116
+ export function generateImports(uses = {}, mode = "single") {
117
+ const importSet = new Set();
118
+
119
+ const componentImports = {
120
+ Input: `import { Input } from "@/components/ui/input";`,
121
+ Textarea: `import { Textarea } from "@/components/ui/textarea";`, // <--- add this
122
+ Select: `import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";`,
123
+ Checkbox: `import { Checkbox } from "@/components/ui/checkbox";`,
124
+ RadioGroup: `import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";`,
125
+ };
126
+
127
+ if (mode === "single") {
128
+ importSet.add(`import { useForm, Controller } from "react-hook-form";`);
129
+ importSet.add(`import { Button } from "@/components/ui/button";`);
130
+ importSet.add(`import { zodResolver } from "@hookform/resolvers/zod";`);
131
+ importSet.add(
132
+ `import { Field, FieldLabel, FieldError, FieldGroup } from "@/components/ui/field";`
133
+ );
134
+
135
+ Object.entries(uses).forEach(([component, used]) => {
136
+ if (used && componentImports[component])
137
+ importSet.add(componentImports[component]);
138
+ });
139
+ }
140
+
141
+ if (mode === "step") {
142
+ importSet.add(
143
+ `import { Controller, useFormContext } from "react-hook-form";`
144
+ );
145
+ importSet.add(
146
+ `import { Field, FieldLabel, FieldError, FieldGroup } from "@/components/ui/field";`
147
+ );
148
+
149
+ Object.entries(uses).forEach(([component, used]) => {
150
+ if (used && componentImports[component])
151
+ importSet.add(componentImports[component]);
152
+ });
153
+ }
154
+
155
+ if (mode === "multi") {
156
+ importSet.add(`import { useForm, FormProvider } from "react-hook-form";`);
157
+ importSet.add(`import { Button } from "@/components/ui/button";`);
158
+ importSet.add(`import { zodResolver } from "@hookform/resolvers/zod";`);
159
+ }
160
+
161
+ return Array.from(importSet).join("\n");
162
+ }
163
+
164
+ export function getDefaultValue(field) {
165
+ if (field.type === "checkbox") return `${field.name}: false`;
166
+ if (
167
+ field.type === "number" ||
168
+ field.type === "select" ||
169
+ field.type === "radio"
170
+ )
171
+ return `${field.name}: undefined`;
172
+ return `${field.name}: ""`;
173
+ }
174
+
175
+ export function renderError() {
176
+ return `
177
+ {fieldState.invalid && (
178
+ <FieldError className="text-left">
179
+ {fieldState.error?.message}
180
+ </FieldError>
181
+ )}
182
+ `;
183
+ }