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
@@ -0,0 +1,200 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { select, text, confirm, isCancel } from "@clack/prompts";
4
+
5
+ export async function askFormName() {
6
+ const baseDir = fs.existsSync(path.resolve("src"))
7
+ ? path.resolve("src/components/forms")
8
+ : path.resolve("components/forms");
9
+
10
+ const formName = await text({
11
+ message: "Form name (the component will be suffixed with 'Form')",
12
+ placeholder: "register → RegisterForm",
13
+ validate(value) {
14
+ if (!value) return "Form name is required";
15
+
16
+ if (!/^[a-zA-Z0-9-_]+$/.test(value)) {
17
+ return "Use only letters, numbers, dashes or underscores";
18
+ }
19
+
20
+ const formDir = path.join(baseDir, value);
21
+
22
+ if (fs.pathExistsSync(formDir)) {
23
+ return `A form named "${value}" already exists`;
24
+ }
25
+ },
26
+ });
27
+
28
+ if (isCancel(formName)) {
29
+ console.log("❌ Operation cancelled.");
30
+ process.exit(0);
31
+ }
32
+
33
+ return formName;
34
+ }
35
+ export async function askFormType() {
36
+ const result = await select({
37
+ message: "Form type?",
38
+ options: [
39
+ { value: "single", label: "Single step" },
40
+ { value: "multi", label: "Multi step" },
41
+ ],
42
+ });
43
+
44
+ if (isCancel(result)) {
45
+ console.log("❌ Operation cancelled.");
46
+ process.exit(0);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ export async function askFormTemplate(formType) {
52
+ const choice = await select({
53
+ message: `Do you want a ready template for the ${formType} form, or create manually?`,
54
+ options: [
55
+ { value: "template", label: "Use ready template" },
56
+ { value: "manual", label: "Create manually" },
57
+ ],
58
+ });
59
+
60
+ if (isCancel(choice)) {
61
+ console.log("❌ Operation cancelled.");
62
+ process.exit(0);
63
+ }
64
+
65
+ return choice;
66
+ }
67
+
68
+ const FIELD_TYPES = [
69
+ { value: "text", label: "Text" },
70
+ { value: "email", label: "Email" },
71
+ { value: "password", label: "Password" },
72
+ { value: "number", label: "Number" },
73
+ { value: "url", label: "URL" },
74
+ { value: "textarea", label: "Textarea" },
75
+ { value: "select", label: "Select" },
76
+ { value: "checkbox", label: "Checkbox" },
77
+ { value: "radio", label: "Radio" },
78
+ { value: "date", label: "Date" },
79
+ ];
80
+
81
+ export async function askFields() {
82
+ const fields = [];
83
+
84
+ while (true) {
85
+ const type = await select({
86
+ message: "Field type?",
87
+ options: FIELD_TYPES,
88
+ });
89
+ if (isCancel(type)) {
90
+ console.log("❌ Operation cancelled.");
91
+ process.exit(0);
92
+ }
93
+
94
+ const label = await text({
95
+ message: 'Label (e.g. "First Name" → "first_name" as field name)',
96
+ validate(value) {
97
+ if (!value.trim()) return "Label cannot be empty.";
98
+ },
99
+ });
100
+ if (isCancel(label)) {
101
+ console.log("❌ Operation cancelled.");
102
+ process.exit(0);
103
+ }
104
+
105
+ const name = label.trim().toLowerCase().replace(/\s+/g, "_");
106
+
107
+ const required = await confirm({ message: "Required?" });
108
+ if (isCancel(required)) {
109
+ console.log("❌ Operation cancelled.");
110
+ process.exit(0);
111
+ }
112
+
113
+ let options;
114
+
115
+ if (type === "select" || type === "radio") {
116
+ options = [];
117
+
118
+ while (true) {
119
+ const labelInput = await text({
120
+ message: 'Option label? (e.g. "Active User" → "active_user")',
121
+ validate(value) {
122
+ const transformed = value.trim().toLowerCase().replace(/\s+/g, "_");
123
+ if (!value.trim()) return "Option label cannot be empty.";
124
+ if (options.some((o) => o.value === transformed))
125
+ return `Option "${transformed}" already exists.`;
126
+ },
127
+ });
128
+ if (isCancel(labelInput)) {
129
+ console.log("❌ Operation cancelled.");
130
+ process.exit(0);
131
+ }
132
+
133
+ const label = labelInput
134
+ .split(" ")
135
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
136
+ .join(" ");
137
+
138
+ const value = label.trim().toLowerCase().replace(/\s+/g, "_");
139
+
140
+ options.push({ label, value });
141
+
142
+ const more = await confirm({ message: "Add another option?" });
143
+ if (isCancel(more) || !more) break;
144
+ }
145
+ }
146
+
147
+ fields.push({
148
+ type,
149
+ name,
150
+ label,
151
+ required,
152
+ options,
153
+ });
154
+
155
+ const addMore = await confirm({ message: "Add another field?" });
156
+ if (isCancel(addMore) || !addMore) break;
157
+ }
158
+
159
+ return fields;
160
+ }
161
+
162
+ export async function askSteps() {
163
+ const steps = [];
164
+
165
+ while (true) {
166
+ const stepName = await text({
167
+ message: "Step name?",
168
+ validate(value) {
169
+ if (!value.trim()) return "Step Name cannot be empty.";
170
+ },
171
+ });
172
+ if (isCancel(stepName)) {
173
+ console.log("❌ Operation cancelled.");
174
+ process.exit(0);
175
+ }
176
+
177
+ console.log(`\nAdd fields for step: ${stepName}\n`);
178
+ const fields = await askFields();
179
+
180
+ steps.push({ stepName, fields });
181
+
182
+ const addMoreSteps = await confirm({ message: "Add another step?" });
183
+ if (isCancel(addMoreSteps) || !addMoreSteps) break;
184
+ }
185
+
186
+ return steps;
187
+ }
188
+
189
+ export async function askFormPreset(presets) {
190
+ const choices = Object.keys(presets).map((key) => ({
191
+ value: key,
192
+ label: key,
193
+ }));
194
+ const preset = await select({
195
+ message: "Choose a form preset:",
196
+ options: choices,
197
+ });
198
+
199
+ return preset;
200
+ }
@@ -0,0 +1,132 @@
1
+ export const singleFormPresets = {
2
+ default: {
3
+ form: "space-y-6 max-w-3xl mx-auto bg-white p-6 rounded-lg border border-slate-200 shadow-sm",
4
+ buttonsWrapper: "flex justify-end gap-2 pt-4 border-t border-slate-100",
5
+ },
6
+ };
7
+
8
+ export const multiFormPresets = {
9
+ minimal: {
10
+ form: "w-full max-w-4xl mx-auto bg-white text-zinc-900 p-8 rounded-2xl border border-zinc-200 shadow-xl",
11
+ buttonsWrapper: "flex justify-between mt-10",
12
+ stepper: `<div className="mb-4 space-y-2">
13
+ <div className="flex gap-1 mb-4">
14
+ {steps.map((_, i) => (
15
+ <div
16
+ key={i}
17
+ className={\`h-1 flex-1 rounded-full transition-all duration-500 \${
18
+ i <= currentStep ? "bg-zinc-900" : "bg-zinc-100"
19
+ }\`}
20
+ />
21
+ ))}
22
+ </div>
23
+ <h2 className="text-2xl font-bold tracking-tight capitalize text-start">
24
+ {currentStepData.title}
25
+ </h2>
26
+ </div>`,
27
+ },
28
+ sidebarStepper: {
29
+ form: "w-full max-w-4xl mx-auto flex bg-white rounded-2xl border border-slate-200 shadow-xl overflow-hidden min-h-[500px]",
30
+ buttonsWrapper:
31
+ "flex justify-between items-center mt-10 pt-8 border-t border-slate-100",
32
+ step: "flex-1 p-8",
33
+ stepper: `
34
+ <div className="w-72 bg-slate-900 p-8 text-white space-y-8">
35
+ <div className="space-y-1 flex flex-col items-start">
36
+ <h3 className="text-lg font-bold">Registration</h3>
37
+ <p className="text-slate-400 text-xs">Complete all steps to join us.</p>
38
+ </div>
39
+
40
+ <div className="space-y-6">
41
+ {steps.map((step, i) => {
42
+ const isCompleted = i < currentStep
43
+ const isActive = i === currentStep
44
+
45
+ return (
46
+ <div key={i} className="flex gap-4 items-center">
47
+ <div className="mt-1">
48
+ {isCompleted ? (
49
+ <div className="w-5 h-5 rounded-full bg-emerald-400 flex items-center justify-center text-[10px] font-bold text-slate-900">
50
+
51
+ </div>
52
+ ) : (
53
+ <div
54
+ className={
55
+ "w-5 h-5 rounded-full border-2 flex items-center justify-center text-[10px] font-bold " +
56
+ (isActive
57
+ ? "border-white bg-white text-slate-900"
58
+ : "border-slate-700 text-slate-500")
59
+ }
60
+ >
61
+ {i + 1}
62
+ </div>
63
+ )}
64
+ </div>
65
+
66
+ <p
67
+ className={
68
+ "text-xs font-bold uppercase tracking-wider " +
69
+ (i <= currentStep ? "text-white" : "text-slate-600")
70
+ }
71
+ >
72
+ {step.title}
73
+ </p>
74
+ </div>
75
+ )
76
+ })}
77
+ </div>
78
+ </div>
79
+ `,
80
+ },
81
+ softType: {
82
+ form: "w-full max-w-2xl mx-auto bg-white p-12 rounded-[2.5rem] shadow-[0_20px_50px_rgba(0,0,0,0.05)] border border-slate-50/50",
83
+ buttonsWrapper: "flex gap-4 mt-10",
84
+ stepper: ` <div className="flex flex-col items-start justify-start mb-4">
85
+ <h2 className="text-xl font-bold text-slate-800">
86
+ Step {currentStep + 1} of {steps.length}
87
+ </h2>
88
+ <p className="text-slate-400 text-sm capitalize">{currentStepData.title}</p>
89
+ </div>`,
90
+ },
91
+ stepperTop: {
92
+ form: "w-full max-w-2xl mx-auto bg-white rounded-2xl border p-8 border-slate-200 shadow-sm overflow-hidden",
93
+ buttonsWrapper: "flex justify-end gap-3 mt-10",
94
+ stepper: `
95
+ <div className="py-6">
96
+ <div className="relative flex justify-between">
97
+ <div className="absolute top-4 left-0 w-full h-0.5 bg-slate-200 z-0">
98
+ <div
99
+ className="h-full bg-slate-600 transition-all duration-500"
100
+ style={{ width: \`\${(currentStep / (steps.length - 1)) * 100}%\` }}
101
+ />
102
+ </div>
103
+
104
+ {steps.map((step, i) => (
105
+ <div key={i} className="relative z-10 flex flex-col items-center group">
106
+ <div
107
+ className={
108
+ "w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all duration-300 border-2 " +
109
+ (i < currentStep
110
+ ? "bg-slate-600 border-slate-600 text-white"
111
+ : i === currentStep
112
+ ? "bg-white border-slate-600 text-slate-600"
113
+ : "bg-white border-slate-300 text-slate-400")
114
+ }
115
+ >
116
+ {i + 1}
117
+ </div>
118
+ <span
119
+ className={
120
+ "mt-2 text-[10px] font-bold uppercase tracking-wider transition-colors " +
121
+ (i <= currentStep ? "text-slate-600" : "text-slate-400")
122
+ }
123
+ >
124
+ {step.title.split(" ")[0]}
125
+ </span>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ `,
131
+ },
132
+ };
@@ -0,0 +1,103 @@
1
+ export const SINGLE_FIELD_TEMPLATES = {
2
+ registration: [
3
+ { name: "first_name", type: "text", label: "First Name", required: true },
4
+ { name: "last_name", type: "text", label: "Last Name", required: true },
5
+ { name: "email", type: "email", label: "Email", required: true },
6
+ {
7
+ name: "password",
8
+ type: "password",
9
+ label: "Password",
10
+ required: true,
11
+ confirm: true,
12
+ },
13
+ ],
14
+ login: [
15
+ { name: "email", type: "email", label: "Email", required: true },
16
+ {
17
+ name: "password",
18
+ type: "password",
19
+ label: "Password",
20
+ required: true,
21
+ confirm: false,
22
+ },
23
+ ],
24
+ contact: [
25
+ { name: "full_name", type: "text", label: "Full Name", required: true },
26
+ { name: "email", type: "email", label: "Email", required: true },
27
+ { name: "subject", type: "text", label: "Subject", required: true },
28
+ { name: "message", type: "textarea", label: "Message", required: true },
29
+ ],
30
+ };
31
+
32
+ export const MULTI_STEP_TEMPLATES = {
33
+ registration: [
34
+ {
35
+ stepName: "Personal Info",
36
+ fields: [
37
+ {
38
+ name: "first_name",
39
+ type: "text",
40
+ label: "First Name",
41
+ required: true,
42
+ },
43
+ { name: "last_name", type: "text", label: "Last Name", required: true },
44
+ {
45
+ name: "gender",
46
+ type: "radio",
47
+ label: "Gender",
48
+ required: true,
49
+ options: [
50
+ { label: "Male", value: "male" },
51
+ { label: "Female", value: "female" },
52
+ { label: "Other", value: "other" },
53
+ ],
54
+ },
55
+ { name: "birthdate", type: "date", label: "Birthdate", required: true },
56
+ ],
57
+ },
58
+ {
59
+ stepName: "Account Details",
60
+ fields: [
61
+ { name: "email", type: "email", label: "Email", required: true },
62
+ {
63
+ name: "password",
64
+ type: "password",
65
+ label: "Password",
66
+ required: true,
67
+ },
68
+ {
69
+ name: "password_confirmation",
70
+ type: "password",
71
+ label: "Confirm Password",
72
+ required: true,
73
+ isConfirmation: true,
74
+ confirmationFor: "password",
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ stepName: "Contact Info",
80
+ fields: [
81
+ {
82
+ name: "phone",
83
+ type: "number",
84
+ label: "Phone Number",
85
+ required: true,
86
+ },
87
+ { name: "address", type: "text", label: "Address", required: true },
88
+ { name: "city", type: "text", label: "City", required: true },
89
+ {
90
+ name: "country",
91
+ type: "select",
92
+ label: "Country",
93
+ required: true,
94
+ options: [
95
+ { label: "United States", value: "us" },
96
+ { label: "Canada", value: "ca" },
97
+ { label: "United Kingdom", value: "uk" },
98
+ ],
99
+ },
100
+ ],
101
+ },
102
+ ],
103
+ };
package/utils/test.js ADDED
@@ -0,0 +1,136 @@
1
+ export const FAST_MODE = false;
2
+
3
+ export const FAST_FORMS = [
4
+ {
5
+ formName: "testSingleAllTypes",
6
+ type: "single",
7
+ presetKey: "default",
8
+ steps: [
9
+ {
10
+ stepName: "main",
11
+ fields: [
12
+ {
13
+ name: "first_name",
14
+ label: "First Name",
15
+ type: "text",
16
+ required: true,
17
+ },
18
+ { name: "email", label: "Email", type: "email", required: true },
19
+ {
20
+ name: "password",
21
+ label: "Password",
22
+ type: "password",
23
+ required: true,
24
+ },
25
+ { name: "age", label: "Age", type: "number", required: false },
26
+ {
27
+ name: "birthdate",
28
+ label: "Birthdate",
29
+ type: "date",
30
+ required: true,
31
+ },
32
+ { name: "website", label: "Website", type: "url", required: false },
33
+ {
34
+ name: "subscribe",
35
+ label: "Subscribe",
36
+ type: "checkbox",
37
+ required: false,
38
+ },
39
+ {
40
+ name: "gender",
41
+ label: "Gender",
42
+ type: "radio",
43
+ required: true,
44
+ options: [
45
+ { label: "Male", value: "male" },
46
+ { label: "Female", value: "female" },
47
+ ],
48
+ },
49
+ {
50
+ name: "country",
51
+ label: "Country",
52
+ type: "select",
53
+ required: true,
54
+ options: [
55
+ { label: "USA", value: "usa" },
56
+ { label: "UK", value: "uk" },
57
+ { label: "UAE", value: "uae" },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ },
64
+
65
+ // Multi-step form with all types split across steps
66
+ {
67
+ formName: "test",
68
+ type: "multi",
69
+ presetKey: "minimal", // stepperTop | minimal | sidebarStepper | softType
70
+ steps: [
71
+ {
72
+ stepName: "personal",
73
+ fields: [
74
+ {
75
+ name: "first_name",
76
+ label: "First Name",
77
+ type: "text",
78
+ required: true,
79
+ },
80
+ { name: "email", label: "Email", type: "email", required: true },
81
+ {
82
+ name: "password",
83
+ label: "Password",
84
+ type: "password",
85
+ required: true,
86
+ },
87
+ ],
88
+ },
89
+ {
90
+ stepName: "details",
91
+ fields: [
92
+ { name: "age", label: "Age", type: "number", required: false },
93
+ {
94
+ name: "birthdate",
95
+ label: "Birthdate",
96
+ type: "date",
97
+ required: true,
98
+ },
99
+ { name: "website", label: "Website", type: "url", required: false },
100
+ ],
101
+ },
102
+ {
103
+ stepName: "preferences",
104
+ fields: [
105
+ {
106
+ name: "subscribe",
107
+ label: "Subscribe",
108
+ type: "checkbox",
109
+ required: false,
110
+ },
111
+ {
112
+ name: "gender",
113
+ label: "Gender",
114
+ type: "radio",
115
+ required: true,
116
+ options: [
117
+ { label: "Male", value: "male" },
118
+ { label: "Female", value: "female" },
119
+ ],
120
+ },
121
+ {
122
+ name: "country",
123
+ label: "Country",
124
+ type: "select",
125
+ required: true,
126
+ options: [
127
+ { label: "USA", value: "usa" },
128
+ { label: "UK", value: "uk" },
129
+ { label: "UAE", value: "uae" },
130
+ ],
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ },
136
+ ];