codemeld 2.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.
Files changed (243) hide show
  1. package/README.md +514 -0
  2. package/bin/cli.js +2 -0
  3. package/dist/ai/agent.d.ts +124 -0
  4. package/dist/ai/agent.d.ts.map +1 -0
  5. package/dist/ai/agent.js +289 -0
  6. package/dist/ai/agent.js.map +1 -0
  7. package/dist/ai/index.d.ts +10 -0
  8. package/dist/ai/index.d.ts.map +1 -0
  9. package/dist/ai/index.js +10 -0
  10. package/dist/ai/index.js.map +1 -0
  11. package/dist/ai/prompts.d.ts +35 -0
  12. package/dist/ai/prompts.d.ts.map +1 -0
  13. package/dist/ai/prompts.js +166 -0
  14. package/dist/ai/prompts.js.map +1 -0
  15. package/dist/ai/refinement-loop.d.ts +29 -0
  16. package/dist/ai/refinement-loop.d.ts.map +1 -0
  17. package/dist/ai/refinement-loop.js +180 -0
  18. package/dist/ai/refinement-loop.js.map +1 -0
  19. package/dist/ai/tools.d.ts +17 -0
  20. package/dist/ai/tools.d.ts.map +1 -0
  21. package/dist/ai/tools.js +353 -0
  22. package/dist/ai/tools.js.map +1 -0
  23. package/dist/ai/visual-compare.d.ts +43 -0
  24. package/dist/ai/visual-compare.d.ts.map +1 -0
  25. package/dist/ai/visual-compare.js +176 -0
  26. package/dist/ai/visual-compare.js.map +1 -0
  27. package/dist/cli.d.ts +3 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +179 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/converter.d.ts +10 -0
  32. package/dist/converter.d.ts.map +1 -0
  33. package/dist/converter.js +836 -0
  34. package/dist/converter.js.map +1 -0
  35. package/dist/deconverter.d.ts +19 -0
  36. package/dist/deconverter.d.ts.map +1 -0
  37. package/dist/deconverter.js +188 -0
  38. package/dist/deconverter.js.map +1 -0
  39. package/dist/frameworks/angular-adapter.d.ts +27 -0
  40. package/dist/frameworks/angular-adapter.d.ts.map +1 -0
  41. package/dist/frameworks/angular-adapter.js +617 -0
  42. package/dist/frameworks/angular-adapter.js.map +1 -0
  43. package/dist/frameworks/index.d.ts +10 -0
  44. package/dist/frameworks/index.d.ts.map +1 -0
  45. package/dist/frameworks/index.js +21 -0
  46. package/dist/frameworks/index.js.map +1 -0
  47. package/dist/frameworks/nextjs-adapter.d.ts +22 -0
  48. package/dist/frameworks/nextjs-adapter.d.ts.map +1 -0
  49. package/dist/frameworks/nextjs-adapter.js +392 -0
  50. package/dist/frameworks/nextjs-adapter.js.map +1 -0
  51. package/dist/frameworks/react-adapter.d.ts +21 -0
  52. package/dist/frameworks/react-adapter.d.ts.map +1 -0
  53. package/dist/frameworks/react-adapter.js +71 -0
  54. package/dist/frameworks/react-adapter.js.map +1 -0
  55. package/dist/frameworks/svelte-adapter.d.ts +27 -0
  56. package/dist/frameworks/svelte-adapter.d.ts.map +1 -0
  57. package/dist/frameworks/svelte-adapter.js +519 -0
  58. package/dist/frameworks/svelte-adapter.js.map +1 -0
  59. package/dist/frameworks/types.d.ts +78 -0
  60. package/dist/frameworks/types.d.ts.map +1 -0
  61. package/dist/frameworks/types.js +2 -0
  62. package/dist/frameworks/types.js.map +1 -0
  63. package/dist/frameworks/vue-adapter.d.ts +34 -0
  64. package/dist/frameworks/vue-adapter.d.ts.map +1 -0
  65. package/dist/frameworks/vue-adapter.js +632 -0
  66. package/dist/frameworks/vue-adapter.js.map +1 -0
  67. package/dist/generators/accessibility-generator.d.ts +43 -0
  68. package/dist/generators/accessibility-generator.d.ts.map +1 -0
  69. package/dist/generators/accessibility-generator.js +507 -0
  70. package/dist/generators/accessibility-generator.js.map +1 -0
  71. package/dist/generators/asset-handler.d.ts +14 -0
  72. package/dist/generators/asset-handler.d.ts.map +1 -0
  73. package/dist/generators/asset-handler.js +79 -0
  74. package/dist/generators/asset-handler.js.map +1 -0
  75. package/dist/generators/build-verifier.d.ts +8 -0
  76. package/dist/generators/build-verifier.d.ts.map +1 -0
  77. package/dist/generators/build-verifier.js +64 -0
  78. package/dist/generators/build-verifier.js.map +1 -0
  79. package/dist/generators/component-extractor.d.ts +25 -0
  80. package/dist/generators/component-extractor.d.ts.map +1 -0
  81. package/dist/generators/component-extractor.js +146 -0
  82. package/dist/generators/component-extractor.js.map +1 -0
  83. package/dist/generators/component-generator.d.ts +12 -0
  84. package/dist/generators/component-generator.d.ts.map +1 -0
  85. package/dist/generators/component-generator.js +724 -0
  86. package/dist/generators/component-generator.js.map +1 -0
  87. package/dist/generators/deploy-generator.d.ts +9 -0
  88. package/dist/generators/deploy-generator.d.ts.map +1 -0
  89. package/dist/generators/deploy-generator.js +409 -0
  90. package/dist/generators/deploy-generator.js.map +1 -0
  91. package/dist/generators/error-boundary.d.ts +5 -0
  92. package/dist/generators/error-boundary.d.ts.map +1 -0
  93. package/dist/generators/error-boundary.js +59 -0
  94. package/dist/generators/error-boundary.js.map +1 -0
  95. package/dist/generators/form-generator.d.ts +42 -0
  96. package/dist/generators/form-generator.d.ts.map +1 -0
  97. package/dist/generators/form-generator.js +662 -0
  98. package/dist/generators/form-generator.js.map +1 -0
  99. package/dist/generators/hooks-generator.d.ts +40 -0
  100. package/dist/generators/hooks-generator.d.ts.map +1 -0
  101. package/dist/generators/hooks-generator.js +297 -0
  102. package/dist/generators/hooks-generator.js.map +1 -0
  103. package/dist/generators/html-generator.d.ts +27 -0
  104. package/dist/generators/html-generator.d.ts.map +1 -0
  105. package/dist/generators/html-generator.js +772 -0
  106. package/dist/generators/html-generator.js.map +1 -0
  107. package/dist/generators/jquery-converter.d.ts +41 -0
  108. package/dist/generators/jquery-converter.d.ts.map +1 -0
  109. package/dist/generators/jquery-converter.js +594 -0
  110. package/dist/generators/jquery-converter.js.map +1 -0
  111. package/dist/generators/pattern-implementer.d.ts +26 -0
  112. package/dist/generators/pattern-implementer.d.ts.map +1 -0
  113. package/dist/generators/pattern-implementer.js +336 -0
  114. package/dist/generators/pattern-implementer.js.map +1 -0
  115. package/dist/generators/performance-generator.d.ts +51 -0
  116. package/dist/generators/performance-generator.d.ts.map +1 -0
  117. package/dist/generators/performance-generator.js +428 -0
  118. package/dist/generators/performance-generator.js.map +1 -0
  119. package/dist/generators/router-generator.d.ts +21 -0
  120. package/dist/generators/router-generator.d.ts.map +1 -0
  121. package/dist/generators/router-generator.js +178 -0
  122. package/dist/generators/router-generator.js.map +1 -0
  123. package/dist/generators/scaffolder.d.ts +28 -0
  124. package/dist/generators/scaffolder.d.ts.map +1 -0
  125. package/dist/generators/scaffolder.js +266 -0
  126. package/dist/generators/scaffolder.js.map +1 -0
  127. package/dist/generators/seo-generator.d.ts +29 -0
  128. package/dist/generators/seo-generator.d.ts.map +1 -0
  129. package/dist/generators/seo-generator.js +223 -0
  130. package/dist/generators/seo-generator.js.map +1 -0
  131. package/dist/generators/test-generator.d.ts +19 -0
  132. package/dist/generators/test-generator.d.ts.map +1 -0
  133. package/dist/generators/test-generator.js +398 -0
  134. package/dist/generators/test-generator.js.map +1 -0
  135. package/dist/generators/type-generator.d.ts +33 -0
  136. package/dist/generators/type-generator.d.ts.map +1 -0
  137. package/dist/generators/type-generator.js +663 -0
  138. package/dist/generators/type-generator.js.map +1 -0
  139. package/dist/index.d.ts +23 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +12 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/parsers/css-processor.d.ts +23 -0
  144. package/dist/parsers/css-processor.d.ts.map +1 -0
  145. package/dist/parsers/css-processor.js +129 -0
  146. package/dist/parsers/css-processor.js.map +1 -0
  147. package/dist/parsers/framework-parser.d.ts +48 -0
  148. package/dist/parsers/framework-parser.d.ts.map +1 -0
  149. package/dist/parsers/framework-parser.js +770 -0
  150. package/dist/parsers/framework-parser.js.map +1 -0
  151. package/dist/parsers/html-parser.d.ts +12 -0
  152. package/dist/parsers/html-parser.d.ts.map +1 -0
  153. package/dist/parsers/html-parser.js +444 -0
  154. package/dist/parsers/html-parser.js.map +1 -0
  155. package/dist/parsers/js-analyzer.d.ts +199 -0
  156. package/dist/parsers/js-analyzer.d.ts.map +1 -0
  157. package/dist/parsers/js-analyzer.js +680 -0
  158. package/dist/parsers/js-analyzer.js.map +1 -0
  159. package/dist/parsers/js-resolver.d.ts +8 -0
  160. package/dist/parsers/js-resolver.d.ts.map +1 -0
  161. package/dist/parsers/js-resolver.js +45 -0
  162. package/dist/parsers/js-resolver.js.map +1 -0
  163. package/dist/parsers/tailwind-detector.d.ts +23 -0
  164. package/dist/parsers/tailwind-detector.d.ts.map +1 -0
  165. package/dist/parsers/tailwind-detector.js +104 -0
  166. package/dist/parsers/tailwind-detector.js.map +1 -0
  167. package/dist/tests/advanced-features.test.d.ts +2 -0
  168. package/dist/tests/advanced-features.test.d.ts.map +1 -0
  169. package/dist/tests/advanced-features.test.js +235 -0
  170. package/dist/tests/advanced-features.test.js.map +1 -0
  171. package/dist/tests/css-modules.test.d.ts +2 -0
  172. package/dist/tests/css-modules.test.d.ts.map +1 -0
  173. package/dist/tests/css-modules.test.js +61 -0
  174. package/dist/tests/css-modules.test.js.map +1 -0
  175. package/dist/tests/css-processor.test.d.ts +2 -0
  176. package/dist/tests/css-processor.test.d.ts.map +1 -0
  177. package/dist/tests/css-processor.test.js +48 -0
  178. package/dist/tests/css-processor.test.js.map +1 -0
  179. package/dist/tests/html-parser.test.d.ts +2 -0
  180. package/dist/tests/html-parser.test.d.ts.map +1 -0
  181. package/dist/tests/html-parser.test.js +78 -0
  182. package/dist/tests/html-parser.test.js.map +1 -0
  183. package/dist/tests/integration.test.d.ts +2 -0
  184. package/dist/tests/integration.test.d.ts.map +1 -0
  185. package/dist/tests/integration.test.js +65 -0
  186. package/dist/tests/integration.test.js.map +1 -0
  187. package/dist/tests/js-analyzer.test.d.ts +2 -0
  188. package/dist/tests/js-analyzer.test.d.ts.map +1 -0
  189. package/dist/tests/js-analyzer.test.js +58 -0
  190. package/dist/tests/js-analyzer.test.js.map +1 -0
  191. package/dist/tests/naming.test.d.ts +2 -0
  192. package/dist/tests/naming.test.d.ts.map +1 -0
  193. package/dist/tests/naming.test.js +43 -0
  194. package/dist/tests/naming.test.js.map +1 -0
  195. package/dist/tests/router-generator.test.d.ts +2 -0
  196. package/dist/tests/router-generator.test.d.ts.map +1 -0
  197. package/dist/tests/router-generator.test.js +60 -0
  198. package/dist/tests/router-generator.test.js.map +1 -0
  199. package/dist/tui/chat.d.ts +13 -0
  200. package/dist/tui/chat.d.ts.map +1 -0
  201. package/dist/tui/chat.js +499 -0
  202. package/dist/tui/chat.js.map +1 -0
  203. package/dist/tui/design-guide.d.ts +41 -0
  204. package/dist/tui/design-guide.d.ts.map +1 -0
  205. package/dist/tui/design-guide.js +184 -0
  206. package/dist/tui/design-guide.js.map +1 -0
  207. package/dist/tui/input.d.ts +30 -0
  208. package/dist/tui/input.d.ts.map +1 -0
  209. package/dist/tui/input.js +239 -0
  210. package/dist/tui/input.js.map +1 -0
  211. package/dist/tui/renderer.d.ts +48 -0
  212. package/dist/tui/renderer.d.ts.map +1 -0
  213. package/dist/tui/renderer.js +212 -0
  214. package/dist/tui/renderer.js.map +1 -0
  215. package/dist/tui/tools.d.ts +14 -0
  216. package/dist/tui/tools.d.ts.map +1 -0
  217. package/dist/tui/tools.js +1370 -0
  218. package/dist/tui/tools.js.map +1 -0
  219. package/dist/types.d.ts +93 -0
  220. package/dist/types.d.ts.map +1 -0
  221. package/dist/types.js +2 -0
  222. package/dist/types.js.map +1 -0
  223. package/dist/utils/config.d.ts +20 -0
  224. package/dist/utils/config.d.ts.map +1 -0
  225. package/dist/utils/config.js +33 -0
  226. package/dist/utils/config.js.map +1 -0
  227. package/dist/utils/formatter.d.ts +5 -0
  228. package/dist/utils/formatter.d.ts.map +1 -0
  229. package/dist/utils/formatter.js +68 -0
  230. package/dist/utils/formatter.js.map +1 -0
  231. package/dist/utils/logger.d.ts +8 -0
  232. package/dist/utils/logger.d.ts.map +1 -0
  233. package/dist/utils/logger.js +19 -0
  234. package/dist/utils/logger.js.map +1 -0
  235. package/dist/utils/naming.d.ts +17 -0
  236. package/dist/utils/naming.d.ts.map +1 -0
  237. package/dist/utils/naming.js +48 -0
  238. package/dist/utils/naming.js.map +1 -0
  239. package/dist/utils/report.d.ts +56 -0
  240. package/dist/utils/report.d.ts.map +1 -0
  241. package/dist/utils/report.js +339 -0
  242. package/dist/utils/report.js.map +1 -0
  243. package/package.json +61 -0
@@ -0,0 +1,662 @@
1
+ // ─── Form Generator ────────────────────────────────────────────────
2
+ // Generates form handling code (hooks, validation, components, schemas)
3
+ // for converted React components.
4
+ // ─── HTML Detection ────────────────────────────────────────────────
5
+ /**
6
+ * Parse an HTML string and extract form field metadata from
7
+ * `<input>`, `<select>`, and `<textarea>` elements.
8
+ */
9
+ export function detectFormFields(html) {
10
+ const fields = [];
11
+ const seen = new Set();
12
+ // Pre-parse label associations: for="id" → label text
13
+ const labelMap = new Map();
14
+ const labelRegex = /<label[^>]*\bfor\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/label>/gi;
15
+ let labelMatch;
16
+ while ((labelMatch = labelRegex.exec(html)) !== null) {
17
+ const forId = labelMatch[1];
18
+ const text = stripTags(labelMatch[2]).trim();
19
+ if (forId && text) {
20
+ labelMap.set(forId, text);
21
+ }
22
+ }
23
+ // ── <input> elements ──────────────────────────────────────────
24
+ const inputRegex = /<input\b([^>]*?)\/?>/gi;
25
+ let match;
26
+ while ((match = inputRegex.exec(html)) !== null) {
27
+ const attrs = match[1];
28
+ const type = extractAttr(attrs, 'type') || 'text';
29
+ // Skip submit/reset/button inputs – they aren't data fields
30
+ if (['submit', 'reset', 'button', 'image'].includes(type))
31
+ continue;
32
+ const name = extractAttr(attrs, 'name') || extractAttr(attrs, 'id') || '';
33
+ if (!name || seen.has(name))
34
+ continue;
35
+ seen.add(name);
36
+ const id = extractAttr(attrs, 'id') || '';
37
+ const label = labelMap.get(id) ||
38
+ labelMap.get(name) ||
39
+ extractAttr(attrs, 'placeholder') ||
40
+ undefined;
41
+ fields.push({
42
+ name,
43
+ type,
44
+ label,
45
+ required: hasAttr(attrs, 'required'),
46
+ placeholder: extractAttr(attrs, 'placeholder') || undefined,
47
+ pattern: extractAttr(attrs, 'pattern') || undefined,
48
+ min: extractAttr(attrs, 'min') || undefined,
49
+ max: extractAttr(attrs, 'max') || undefined,
50
+ });
51
+ }
52
+ // ── <textarea> elements ───────────────────────────────────────
53
+ const textareaRegex = /<textarea\b([^>]*)>([\s\S]*?)<\/textarea>/gi;
54
+ while ((match = textareaRegex.exec(html)) !== null) {
55
+ const attrs = match[1];
56
+ const name = extractAttr(attrs, 'name') || extractAttr(attrs, 'id') || '';
57
+ if (!name || seen.has(name))
58
+ continue;
59
+ seen.add(name);
60
+ const id = extractAttr(attrs, 'id') || '';
61
+ const label = labelMap.get(id) ||
62
+ labelMap.get(name) ||
63
+ extractAttr(attrs, 'placeholder') ||
64
+ undefined;
65
+ fields.push({
66
+ name,
67
+ type: 'textarea',
68
+ label,
69
+ required: hasAttr(attrs, 'required'),
70
+ placeholder: extractAttr(attrs, 'placeholder') || undefined,
71
+ });
72
+ }
73
+ // ── <select> elements ─────────────────────────────────────────
74
+ const selectRegex = /<select\b([^>]*)>([\s\S]*?)<\/select>/gi;
75
+ while ((match = selectRegex.exec(html)) !== null) {
76
+ const attrs = match[1];
77
+ const inner = match[2];
78
+ const name = extractAttr(attrs, 'name') || extractAttr(attrs, 'id') || '';
79
+ if (!name || seen.has(name))
80
+ continue;
81
+ seen.add(name);
82
+ const id = extractAttr(attrs, 'id') || '';
83
+ const label = labelMap.get(id) || labelMap.get(name) || undefined;
84
+ const options = [];
85
+ const optRegex = /<option[^>]*\bvalue\s*=\s*["']([^"']*)["'][^>]*>/gi;
86
+ let optMatch;
87
+ while ((optMatch = optRegex.exec(inner)) !== null) {
88
+ if (optMatch[1])
89
+ options.push(optMatch[1]);
90
+ }
91
+ fields.push({
92
+ name,
93
+ type: 'select',
94
+ label,
95
+ required: hasAttr(attrs, 'required'),
96
+ options: options.length > 0 ? options : undefined,
97
+ });
98
+ }
99
+ return fields;
100
+ }
101
+ // ─── Hook Generation ───────────────────────────────────────────────
102
+ /**
103
+ * Generate a custom `useForm` hook that manages field values, touched
104
+ * state, errors, and submit handling.
105
+ *
106
+ * Returns code string exporting:
107
+ * `{ values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, isValid, reset }`
108
+ */
109
+ export function generateFormHook(formFields) {
110
+ const fieldNames = formFields.map((f) => f.name);
111
+ const initialValues = formFields
112
+ .map((f) => {
113
+ if (f.type === 'checkbox')
114
+ return ` ${f.name}: false,`;
115
+ if (f.type === 'number')
116
+ return ` ${f.name}: '',`;
117
+ if (f.type === 'file')
118
+ return ` ${f.name}: null as File | null,`;
119
+ return ` ${f.name}: '',`;
120
+ })
121
+ .join('\n');
122
+ const touchedInit = fieldNames.map((n) => ` ${n}: false,`).join('\n');
123
+ const errorsInit = fieldNames.map((n) => ` ${n}: '',`).join('\n');
124
+ return `import { useState, useCallback } from 'react';
125
+
126
+ interface FormValues {
127
+ ${formFields
128
+ .map((f) => {
129
+ if (f.type === 'checkbox')
130
+ return ` ${f.name}: boolean;`;
131
+ if (f.type === 'file')
132
+ return ` ${f.name}: File | null;`;
133
+ return ` ${f.name}: string;`;
134
+ })
135
+ .join('\n')}
136
+ }
137
+
138
+ interface FormErrors {
139
+ ${fieldNames.map((n) => ` ${n}: string;`).join('\n')}
140
+ }
141
+
142
+ interface FormTouched {
143
+ ${fieldNames.map((n) => ` ${n}: boolean;`).join('\n')}
144
+ }
145
+
146
+ type ValidateFn = (values: FormValues) => FormErrors;
147
+
148
+ const initialValues: FormValues = {
149
+ ${initialValues}
150
+ };
151
+
152
+ const initialTouched: FormTouched = {
153
+ ${touchedInit}
154
+ };
155
+
156
+ const initialErrors: FormErrors = {
157
+ ${errorsInit}
158
+ };
159
+
160
+ /**
161
+ * Custom form hook with validation, touched tracking, and submit handling.
162
+ */
163
+ export function useForm(validate: ValidateFn, onSubmit: (values: FormValues) => Promise<void> | void) {
164
+ const [values, setValues] = useState<FormValues>(initialValues);
165
+ const [errors, setErrors] = useState<FormErrors>(initialErrors);
166
+ const [touched, setTouched] = useState<FormTouched>(initialTouched);
167
+ const [isSubmitting, setIsSubmitting] = useState(false);
168
+
169
+ const isValid = Object.values(errors).every((e) => e === '');
170
+
171
+ const handleChange = useCallback(
172
+ (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
173
+ const { name, type } = e.target;
174
+ const value =
175
+ type === 'checkbox'
176
+ ? (e.target as HTMLInputElement).checked
177
+ : type === 'file'
178
+ ? (e.target as HTMLInputElement).files?.[0] ?? null
179
+ : e.target.value;
180
+
181
+ setValues((prev) => {
182
+ const next = { ...prev, [name]: value } as FormValues;
183
+ setErrors(validate(next));
184
+ return next;
185
+ });
186
+ },
187
+ [validate],
188
+ );
189
+
190
+ const handleBlur = useCallback(
191
+ (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
192
+ const { name } = e.target;
193
+ setTouched((prev) => ({ ...prev, [name]: true }));
194
+ setErrors(validate(values));
195
+ },
196
+ [validate, values],
197
+ );
198
+
199
+ const handleSubmit = useCallback(
200
+ async (e: React.FormEvent) => {
201
+ e.preventDefault();
202
+
203
+ // Mark all fields as touched
204
+ const allTouched = Object.keys(touched).reduce(
205
+ (acc, key) => ({ ...acc, [key]: true }),
206
+ {} as FormTouched,
207
+ );
208
+ setTouched(allTouched);
209
+
210
+ const validationErrors = validate(values);
211
+ setErrors(validationErrors);
212
+
213
+ if (Object.values(validationErrors).some((e) => e !== '')) return;
214
+
215
+ setIsSubmitting(true);
216
+ try {
217
+ await onSubmit(values);
218
+ } finally {
219
+ setIsSubmitting(false);
220
+ }
221
+ },
222
+ [values, touched, validate, onSubmit],
223
+ );
224
+
225
+ const reset = useCallback(() => {
226
+ setValues(initialValues);
227
+ setErrors(initialErrors);
228
+ setTouched(initialTouched);
229
+ setIsSubmitting(false);
230
+ }, []);
231
+
232
+ return { values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, isValid, reset };
233
+ }
234
+ `;
235
+ }
236
+ // ─── Validation Generation ─────────────────────────────────────────
237
+ /**
238
+ * Generate a `validate` function that checks every field based on its
239
+ * type and HTML validation attributes (required, pattern, min, max).
240
+ */
241
+ export function generateFormValidation(fields) {
242
+ const checks = fields
243
+ .map((f) => {
244
+ const lines = [];
245
+ const val = `values.${f.name}`;
246
+ // ── required ──────────────────────────────────────────
247
+ if (f.required) {
248
+ if (f.type === 'checkbox') {
249
+ lines.push(` if (!${val}) errors.${f.name} = '${labelFor(f)} is required';`);
250
+ }
251
+ else if (f.type === 'file') {
252
+ lines.push(` if (!${val}) errors.${f.name} = '${labelFor(f)} is required';`);
253
+ }
254
+ else {
255
+ lines.push(` if (!${val} || (typeof ${val} === 'string' && ${val}.trim() === '')) errors.${f.name} = '${labelFor(f)} is required';`);
256
+ }
257
+ }
258
+ // Type-specific validations (only when value is present)
259
+ const guard = f.type === 'file' || f.type === 'checkbox' ? '' : `${val} && `;
260
+ switch (f.type) {
261
+ case 'email':
262
+ lines.push(` if (${guard}!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${val})) errors.${f.name} = 'Please enter a valid email address';`);
263
+ break;
264
+ case 'password':
265
+ lines.push(` if (${guard}${val}.length < 8) errors.${f.name} = 'Password must be at least 8 characters';`);
266
+ lines.push(` else if (${guard}!/[A-Z]/.test(${val})) errors.${f.name} = 'Password must contain an uppercase letter';`);
267
+ lines.push(` else if (${guard}!/[a-z]/.test(${val})) errors.${f.name} = 'Password must contain a lowercase letter';`);
268
+ lines.push(` else if (${guard}!/[0-9]/.test(${val})) errors.${f.name} = 'Password must contain a number';`);
269
+ break;
270
+ case 'tel':
271
+ lines.push(` if (${guard}!/^[\\+]?[0-9\\s\\-\\(\\)]{7,15}$/.test(${val})) errors.${f.name} = 'Please enter a valid phone number';`);
272
+ break;
273
+ case 'url':
274
+ lines.push(` if (${guard}!/^https?:\\/\\/[^\\s]+\\.[^\\s]+$/.test(${val})) errors.${f.name} = 'Please enter a valid URL';`);
275
+ break;
276
+ case 'number': {
277
+ lines.push(` if (${guard}isNaN(Number(${val}))) errors.${f.name} = '${labelFor(f)} must be a number';`);
278
+ if (f.min !== undefined) {
279
+ lines.push(` else if (${guard}Number(${val}) < ${f.min}) errors.${f.name} = '${labelFor(f)} must be at least ${f.min}';`);
280
+ }
281
+ if (f.max !== undefined) {
282
+ lines.push(` else if (${guard}Number(${val}) > ${f.max}) errors.${f.name} = '${labelFor(f)} must be at most ${f.max}';`);
283
+ }
284
+ break;
285
+ }
286
+ default:
287
+ break;
288
+ }
289
+ // ── custom pattern ────────────────────────────────────
290
+ if (f.pattern && f.type !== 'file' && f.type !== 'checkbox') {
291
+ lines.push(` if (${guard}!/${f.pattern}/.test(${val})) errors.${f.name} = '${labelFor(f)} format is invalid';`);
292
+ }
293
+ return lines.join('\n');
294
+ })
295
+ .filter(Boolean)
296
+ .join('\n\n');
297
+ const fieldNames = fields.map((f) => f.name);
298
+ const errorsType = fieldNames.map((n) => ` ${n}: string;`).join('\n');
299
+ const errorsInit = fieldNames.map((n) => ` ${n}: '',`).join('\n');
300
+ return `interface FormValues {
301
+ ${fields
302
+ .map((f) => {
303
+ if (f.type === 'checkbox')
304
+ return ` ${f.name}: boolean;`;
305
+ if (f.type === 'file')
306
+ return ` ${f.name}: File | null;`;
307
+ return ` ${f.name}: string;`;
308
+ })
309
+ .join('\n')}
310
+ }
311
+
312
+ interface FormErrors {
313
+ ${errorsType}
314
+ }
315
+
316
+ /**
317
+ * Validate all form fields and return an errors object.
318
+ */
319
+ export function validate(values: FormValues): FormErrors {
320
+ const errors: FormErrors = {
321
+ ${errorsInit}
322
+ };
323
+
324
+ ${checks}
325
+
326
+ return errors;
327
+ }
328
+ `;
329
+ }
330
+ // ─── Component Generation ──────────────────────────────────────────
331
+ /**
332
+ * Generate a complete React form component with controlled inputs,
333
+ * inline validation errors, accessible labels, loading submit state,
334
+ * and file-upload drag-and-drop support.
335
+ */
336
+ export function generateFormComponent(fields, action) {
337
+ const componentName = 'GeneratedForm';
338
+ const inputElements = fields
339
+ .map((f) => {
340
+ const id = `field-${f.name}`;
341
+ const errorId = `${id}-error`;
342
+ const labelText = f.label || humanize(f.name);
343
+ // ── File upload with drag-and-drop ─────────────────────
344
+ if (f.type === 'file') {
345
+ return ` {/* ${labelText} */}
346
+ <div className="form-field">
347
+ <label htmlFor="${id}">${labelText}${f.required ? ' *' : ''}</label>
348
+ <div
349
+ className={\`drop-zone\${dragOver === '${f.name}' ? ' drop-zone--active' : ''}\`}
350
+ onDragOver={(e) => { e.preventDefault(); setDragOver('${f.name}'); }}
351
+ onDragLeave={() => setDragOver(null)}
352
+ onDrop={(e) => {
353
+ e.preventDefault();
354
+ setDragOver(null);
355
+ const file = e.dataTransfer.files[0];
356
+ if (file) {
357
+ const syntheticEvent = {
358
+ target: { name: '${f.name}', type: 'file', files: [file] },
359
+ } as unknown as React.ChangeEvent<HTMLInputElement>;
360
+ handleChange(syntheticEvent);
361
+ }
362
+ }}
363
+ >
364
+ <input
365
+ id="${id}"
366
+ name="${f.name}"
367
+ type="file"
368
+ onChange={handleChange}
369
+ aria-describedby={touched.${f.name} && errors.${f.name} ? '${errorId}' : undefined}
370
+ aria-invalid={touched.${f.name} && !!errors.${f.name}}
371
+ />
372
+ <p>Drag & drop a file here, or click to browse</p>
373
+ </div>
374
+ {touched.${f.name} && errors.${f.name} && (
375
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
376
+ )}
377
+ </div>`;
378
+ }
379
+ // ── Textarea ───────────────────────────────────────────
380
+ if (f.type === 'textarea') {
381
+ return ` {/* ${labelText} */}
382
+ <div className="form-field">
383
+ <label htmlFor="${id}">${labelText}${f.required ? ' *' : ''}</label>
384
+ <textarea
385
+ id="${id}"
386
+ name="${f.name}"
387
+ value={values.${f.name}}
388
+ onChange={handleChange}
389
+ onBlur={handleBlur}${f.placeholder ? `\n placeholder="${f.placeholder}"` : ''}${f.required ? '\n required' : ''}
390
+ aria-describedby={touched.${f.name} && errors.${f.name} ? '${errorId}' : undefined}
391
+ aria-invalid={touched.${f.name} && !!errors.${f.name}}
392
+ />
393
+ {touched.${f.name} && errors.${f.name} && (
394
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
395
+ )}
396
+ </div>`;
397
+ }
398
+ // ── Select ─────────────────────────────────────────────
399
+ if (f.type === 'select') {
400
+ const opts = (f.options || [])
401
+ .map((o) => ` <option value="${o}">${o}</option>`)
402
+ .join('\n');
403
+ return ` {/* ${labelText} */}
404
+ <div className="form-field">
405
+ <label htmlFor="${id}">${labelText}${f.required ? ' *' : ''}</label>
406
+ <select
407
+ id="${id}"
408
+ name="${f.name}"
409
+ value={values.${f.name}}
410
+ onChange={handleChange}
411
+ onBlur={handleBlur}${f.required ? '\n required' : ''}
412
+ aria-describedby={touched.${f.name} && errors.${f.name} ? '${errorId}' : undefined}
413
+ aria-invalid={touched.${f.name} && !!errors.${f.name}}
414
+ >
415
+ <option value="">Select...</option>
416
+ ${opts}
417
+ </select>
418
+ {touched.${f.name} && errors.${f.name} && (
419
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
420
+ )}
421
+ </div>`;
422
+ }
423
+ // ── Radio ──────────────────────────────────────────────
424
+ if (f.type === 'radio') {
425
+ const radios = (f.options || [])
426
+ .map((o) => ` <label>
427
+ <input
428
+ type="radio"
429
+ name="${f.name}"
430
+ value="${o}"
431
+ checked={values.${f.name} === '${o}'}
432
+ onChange={handleChange}
433
+ onBlur={handleBlur}
434
+ />
435
+ ${o}
436
+ </label>`)
437
+ .join('\n');
438
+ return ` {/* ${labelText} */}
439
+ <fieldset className="form-field">
440
+ <legend>${labelText}${f.required ? ' *' : ''}</legend>
441
+ ${radios}
442
+ {touched.${f.name} && errors.${f.name} && (
443
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
444
+ )}
445
+ </fieldset>`;
446
+ }
447
+ // ── Checkbox ───────────────────────────────────────────
448
+ if (f.type === 'checkbox') {
449
+ return ` {/* ${labelText} */}
450
+ <div className="form-field form-field--checkbox">
451
+ <label>
452
+ <input
453
+ id="${id}"
454
+ name="${f.name}"
455
+ type="checkbox"
456
+ checked={values.${f.name} as unknown as boolean}
457
+ onChange={handleChange}
458
+ onBlur={handleBlur}
459
+ aria-describedby={touched.${f.name} && errors.${f.name} ? '${errorId}' : undefined}
460
+ aria-invalid={touched.${f.name} && !!errors.${f.name}}
461
+ />
462
+ ${labelText}${f.required ? ' *' : ''}
463
+ </label>
464
+ {touched.${f.name} && errors.${f.name} && (
465
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
466
+ )}
467
+ </div>`;
468
+ }
469
+ // ── Hidden ─────────────────────────────────────────────
470
+ if (f.type === 'hidden') {
471
+ return ` <input type="hidden" name="${f.name}" value={values.${f.name}} />`;
472
+ }
473
+ // ── Default (text-like inputs) ─────────────────────────
474
+ return ` {/* ${labelText} */}
475
+ <div className="form-field">
476
+ <label htmlFor="${id}">${labelText}${f.required ? ' *' : ''}</label>
477
+ <input
478
+ id="${id}"
479
+ name="${f.name}"
480
+ type="${f.type}"
481
+ value={values.${f.name}}
482
+ onChange={handleChange}
483
+ onBlur={handleBlur}${f.placeholder ? `\n placeholder="${f.placeholder}"` : ''}${f.required ? '\n required' : ''}
484
+ aria-describedby={touched.${f.name} && errors.${f.name} ? '${errorId}' : undefined}
485
+ aria-invalid={touched.${f.name} && !!errors.${f.name}}
486
+ />
487
+ {touched.${f.name} && errors.${f.name} && (
488
+ <span id="${errorId}" className="field-error" role="alert">{errors.${f.name}}</span>
489
+ )}
490
+ </div>`;
491
+ })
492
+ .join('\n\n');
493
+ const hasFileField = fields.some((f) => f.type === 'file');
494
+ const dragOverState = hasFileField
495
+ ? `\n const [dragOver, setDragOver] = useState<string | null>(null);`
496
+ : '';
497
+ const actionAttr = action ? `\n action="${action}"` : '';
498
+ return `import React, { useState } from 'react';
499
+ import { useForm } from './useForm';
500
+ import { validate } from './validate';
501
+
502
+ /**
503
+ * ${componentName} – auto-generated form component.
504
+ */
505
+ export default function ${componentName}() {
506
+ const { values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, isValid, reset } = useForm(validate, async (values) => {
507
+ // TODO: implement form submission${action ? `\n // action: ${action}` : ''}
508
+ console.log('Form submitted', values);
509
+ });${dragOverState}
510
+
511
+ return (
512
+ <form
513
+ onSubmit={handleSubmit}${actionAttr}
514
+ noValidate
515
+ >
516
+ ${inputElements}
517
+
518
+ <div className="form-actions">
519
+ <button type="submit" disabled={isSubmitting || !isValid}>
520
+ {isSubmitting ? 'Submitting...' : 'Submit'}
521
+ </button>
522
+ <button type="button" onClick={reset} disabled={isSubmitting}>
523
+ Reset
524
+ </button>
525
+ </div>
526
+ </form>
527
+ );
528
+ }
529
+ `;
530
+ }
531
+ // ─── Zod Schema Generation ─────────────────────────────────────────
532
+ /**
533
+ * Generate a Zod validation schema string that mirrors the form fields.
534
+ * Maps field types to `z.string()`, `z.number()`, `z.boolean()`, etc.
535
+ * with appropriate refinements.
536
+ */
537
+ export function generateZodSchema(fields) {
538
+ const schemaFields = fields
539
+ .map((f) => {
540
+ const parts = [];
541
+ switch (f.type) {
542
+ case 'checkbox':
543
+ parts.push(`z.boolean()`);
544
+ if (f.required) {
545
+ parts.push(`.refine((v) => v === true, { message: '${labelFor(f)} is required' })`);
546
+ }
547
+ break;
548
+ case 'number':
549
+ parts.push(`z.coerce.number({ invalid_type_error: '${labelFor(f)} must be a number' })`);
550
+ if (f.min !== undefined)
551
+ parts.push(`.min(${f.min}, '${labelFor(f)} must be at least ${f.min}')`);
552
+ if (f.max !== undefined)
553
+ parts.push(`.max(${f.max}, '${labelFor(f)} must be at most ${f.max}')`);
554
+ break;
555
+ case 'email':
556
+ parts.push(`z.string()`);
557
+ if (f.required)
558
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
559
+ parts.push(`.email('Please enter a valid email address')`);
560
+ break;
561
+ case 'url':
562
+ parts.push(`z.string()`);
563
+ if (f.required)
564
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
565
+ parts.push(`.url('Please enter a valid URL')`);
566
+ break;
567
+ case 'password':
568
+ parts.push(`z.string()`);
569
+ if (f.required)
570
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
571
+ parts.push(`.min(8, 'Password must be at least 8 characters')`);
572
+ parts.push(`.regex(/[A-Z]/, 'Password must contain an uppercase letter')`);
573
+ parts.push(`.regex(/[a-z]/, 'Password must contain a lowercase letter')`);
574
+ parts.push(`.regex(/[0-9]/, 'Password must contain a number')`);
575
+ break;
576
+ case 'tel':
577
+ parts.push(`z.string()`);
578
+ if (f.required)
579
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
580
+ parts.push(`.regex(/^[\\+]?[0-9\\s\\-\\(\\)]{7,15}$/, 'Please enter a valid phone number')`);
581
+ break;
582
+ case 'file':
583
+ parts.push(`z.instanceof(File)`);
584
+ if (!f.required) {
585
+ parts.length = 0;
586
+ parts.push(`z.instanceof(File).nullable().optional()`);
587
+ }
588
+ break;
589
+ case 'date':
590
+ parts.push(`z.string()`);
591
+ if (f.required)
592
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
593
+ parts.push(`.refine((v) => !isNaN(Date.parse(v)), { message: 'Please enter a valid date' })`);
594
+ break;
595
+ case 'hidden':
596
+ parts.push(`z.string()`);
597
+ break;
598
+ default:
599
+ // text, textarea, select, radio, and any other string-based types
600
+ parts.push(`z.string()`);
601
+ if (f.required)
602
+ parts.push(`.min(1, '${labelFor(f)} is required')`);
603
+ if (f.pattern) {
604
+ parts.push(`.regex(/${f.pattern}/, '${labelFor(f)} format is invalid')`);
605
+ }
606
+ if (f.type === 'select' && f.options && f.options.length > 0) {
607
+ // replace z.string() with z.enum() for selects with known options
608
+ parts.length = 0;
609
+ const opts = f.options.map((o) => `'${o}'`).join(', ');
610
+ parts.push(`z.enum([${opts}])`);
611
+ if (!f.required) {
612
+ parts.push(`.optional()`);
613
+ }
614
+ }
615
+ break;
616
+ }
617
+ return ` ${f.name}: ${parts.join('')},`;
618
+ })
619
+ .join('\n');
620
+ return `import { z } from 'zod';
621
+
622
+ /**
623
+ * Zod schema for form validation.
624
+ */
625
+ export const formSchema = z.object({
626
+ ${schemaFields}
627
+ });
628
+
629
+ export type FormData = z.infer<typeof formSchema>;
630
+ `;
631
+ }
632
+ // ─── Internal Helpers ──────────────────────────────────────────────
633
+ /** Extract a single HTML attribute value from an attribute string. */
634
+ function extractAttr(attrs, name) {
635
+ // Handles both single and double quotes, as well as unquoted values
636
+ const regex = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i');
637
+ const m = regex.exec(attrs);
638
+ if (!m)
639
+ return null;
640
+ return m[1] ?? m[2] ?? m[3] ?? null;
641
+ }
642
+ /** Check whether a boolean attribute is present. */
643
+ function hasAttr(attrs, name) {
644
+ return new RegExp(`\\b${name}\\b`, 'i').test(attrs);
645
+ }
646
+ /** Strip HTML tags from a string. */
647
+ function stripTags(html) {
648
+ return html.replace(/<[^>]*>/g, '');
649
+ }
650
+ /** Derive a human-readable label for error messages. */
651
+ function labelFor(f) {
652
+ return f.label || humanize(f.name);
653
+ }
654
+ /** Convert a camelCase or snake_case name to a human-readable label. */
655
+ function humanize(name) {
656
+ return name
657
+ .replace(/([A-Z])/g, ' $1')
658
+ .replace(/[_-]/g, ' ')
659
+ .replace(/^\w/, (c) => c.toUpperCase())
660
+ .trim();
661
+ }
662
+ //# sourceMappingURL=form-generator.js.map