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,772 @@
1
+ // ─── Main Generator ─────────────────────────────────────────────────
2
+ export function generateStaticSite(project) {
3
+ const componentMap = new Map();
4
+ for (const comp of project.components) {
5
+ componentMap.set(comp.name, comp);
6
+ // Also map by lowercase / kebab variants
7
+ componentMap.set(comp.name.toLowerCase(), comp);
8
+ }
9
+ const isMultiPage = project.routes.length > 1;
10
+ // Collect all CSS
11
+ const cssBlocks = [...project.globalCSS];
12
+ for (const comp of project.components) {
13
+ if (comp.css)
14
+ cssBlocks.push(comp.css);
15
+ }
16
+ const mergedCSS = cssBlocks.join('\n\n');
17
+ // Generate JavaScript
18
+ const jsBlocks = [];
19
+ if (isMultiPage) {
20
+ // Generate hash-based SPA routing
21
+ jsBlocks.push(generateHashRouter(project.routes, componentMap));
22
+ }
23
+ // Generate vanilla JS for interactive components (with deduplication)
24
+ const globalStateVars = new Map();
25
+ const globalHandlers = new Map();
26
+ const globalEffects = [];
27
+ for (const comp of project.components) {
28
+ if (comp.isInteractive) {
29
+ // Merge state vars (deduplicate by name)
30
+ for (const [name, initial] of comp.stateVars) {
31
+ if (!globalStateVars.has(name)) {
32
+ globalStateVars.set(name, initial);
33
+ }
34
+ }
35
+ // Merge handlers (deduplicate by name)
36
+ for (const [name, body] of comp.eventHandlers) {
37
+ if (!globalHandlers.has(name)) {
38
+ globalHandlers.set(name, body);
39
+ }
40
+ }
41
+ // Collect effects
42
+ for (const effect of comp.effects) {
43
+ globalEffects.push(effect);
44
+ }
45
+ }
46
+ }
47
+ // Resolve the body HTML first so we can detect interactive patterns
48
+ let bodyHtml = '';
49
+ const comp = project.routes.length > 0
50
+ ? componentMap.get(project.routes[0].componentName) || project.components[0]
51
+ : project.components[0];
52
+ if (comp) {
53
+ bodyHtml = resolveTemplate(comp, componentMap);
54
+ }
55
+ // Auto-detect interactive patterns from HTML/CSS and generate proper vanilla JS
56
+ // This replaces the broken React-to-vanilla handler conversion for detected patterns
57
+ const autoJS = generateInteractiveJS(bodyHtml, mergedCSS);
58
+ if (autoJS) {
59
+ jsBlocks.push(autoJS);
60
+ }
61
+ // Build consolidated JS from React state/handlers, but skip names
62
+ // that are already defined in the auto-detected JS to avoid duplicates
63
+ if (globalStateVars.size > 0 || globalHandlers.size > 0 || globalEffects.length > 0) {
64
+ // Extract variable and function names declared in auto-detected JS
65
+ const autoJSNames = new Set();
66
+ if (autoJS) {
67
+ // Match: let/const/var name, function name(
68
+ const declPattern = /(?:let|const|var)\s+(\w+)|function\s+(\w+)\s*\(/g;
69
+ let m;
70
+ while ((m = declPattern.exec(autoJS)) !== null) {
71
+ autoJSNames.add(m[1] || m[2]);
72
+ }
73
+ }
74
+ // Filter out conflicting names from consolidated JS inputs
75
+ const filteredVars = new Map();
76
+ for (const [name, val] of globalStateVars) {
77
+ if (!autoJSNames.has(name))
78
+ filteredVars.set(name, val);
79
+ }
80
+ const filteredHandlers = new Map();
81
+ for (const [name, body] of globalHandlers) {
82
+ if (!autoJSNames.has(name))
83
+ filteredHandlers.set(name, body);
84
+ }
85
+ if (filteredVars.size > 0 || filteredHandlers.size > 0 || globalEffects.length > 0) {
86
+ jsBlocks.push(generateConsolidatedJS(filteredVars, filteredHandlers, globalEffects));
87
+ }
88
+ }
89
+ const mergedJS = jsBlocks.join('\n\n');
90
+ // Generate pages
91
+ const pages = [];
92
+ if (isMultiPage) {
93
+ // Single HTML file with hash routing
94
+ const mainHtml = generateSPAPage(project, componentMap, mergedCSS, mergedJS);
95
+ pages.push({
96
+ fileName: 'index.html',
97
+ html: mainHtml,
98
+ routePath: '/',
99
+ });
100
+ }
101
+ else {
102
+ if (comp) {
103
+ const html = generateSinglePage(project, comp, componentMap, mergedCSS, mergedJS);
104
+ pages.push({
105
+ fileName: 'index.html',
106
+ html,
107
+ routePath: '/',
108
+ });
109
+ }
110
+ }
111
+ return { pages, css: mergedCSS, js: mergedJS, assets: [] };
112
+ }
113
+ // ─── Single Page Generation ─────────────────────────────────────────
114
+ function generateSinglePage(project, mainComponent, componentMap, css, js) {
115
+ const bodyContent = resolveTemplate(mainComponent, componentMap);
116
+ return buildHTMLDocument({
117
+ title: project.title || project.projectName,
118
+ externalStyles: project.externalStyles,
119
+ externalScripts: project.externalScripts,
120
+ preconnectLinks: project.preconnectLinks || [],
121
+ css,
122
+ body: bodyContent,
123
+ js,
124
+ });
125
+ }
126
+ // ─── SPA (Multi-Page with Hash Router) ──────────────────────────────
127
+ function generateSPAPage(project, componentMap, css, js) {
128
+ // Find the index/shell component (usually has navbar + footer)
129
+ const indexRoute = project.routes.find(r => r.isIndex);
130
+ const shellComponent = indexRoute ? componentMap.get(indexRoute.componentName) : project.components[0];
131
+ // Build the shell HTML with a content container
132
+ let shellHtml = '';
133
+ if (shellComponent) {
134
+ shellHtml = resolveTemplate(shellComponent, componentMap);
135
+ // Try to find a main content area to inject the router outlet
136
+ if (!shellHtml.includes('id="app-content"')) {
137
+ // Wrap the shell content to add a router outlet marker
138
+ shellHtml = shellHtml.replace(/(<main[^>]*>)([\s\S]*?)(<\/main>)/i, '$1<div id="app-content">$2</div>$3');
139
+ if (!shellHtml.includes('id="app-content"')) {
140
+ // No <main> found, append content div
141
+ shellHtml += '\n<div id="app-content"></div>';
142
+ }
143
+ }
144
+ }
145
+ // Build page template objects for the router
146
+ const pageTemplates = [];
147
+ for (const route of project.routes) {
148
+ if (route.isIndex)
149
+ continue;
150
+ const comp = componentMap.get(route.componentName);
151
+ if (!comp)
152
+ continue;
153
+ const pageHtml = resolveTemplate(comp, componentMap);
154
+ const escapedHtml = pageHtml.replace(/`/g, '\\`').replace(/\$/g, '\\$');
155
+ pageTemplates.push(` '${route.path}': \`${escapedHtml}\``);
156
+ }
157
+ // Generate the full hash router JS
158
+ const routerJS = `
159
+ // ─── Hash-Based SPA Router ─────────────────────────────────────────
160
+ const routes = {
161
+ ${pageTemplates.join(',\n')}
162
+ };
163
+
164
+ function navigateTo(path) {
165
+ window.location.hash = '#' + path;
166
+ }
167
+
168
+ function handleRoute() {
169
+ const hash = window.location.hash.slice(1) || '/';
170
+ const content = document.getElementById('app-content');
171
+ if (!content) return;
172
+
173
+ if (routes[hash]) {
174
+ content.innerHTML = routes[hash];
175
+ }
176
+ // Re-initialize interactive elements after route change
177
+ if (typeof initInteractive === 'function') initInteractive();
178
+ // Re-initialize Lucide icons for new content
179
+ if (typeof lucide !== 'undefined') lucide.createIcons();
180
+ }
181
+
182
+ window.addEventListener('hashchange', handleRoute);
183
+ window.addEventListener('DOMContentLoaded', handleRoute);
184
+ `;
185
+ // Auto-initialize Lucide icons if used
186
+ const usesLucide = shellHtml.includes('data-lucide');
187
+ const lucideInit = usesLucide ? `
188
+ // Initialize Lucide icons
189
+ if (typeof lucide !== 'undefined') {
190
+ lucide.createIcons();
191
+ }
192
+ ` : '';
193
+ const fullJS = routerJS + '\n' + js + '\n' + lucideInit;
194
+ return buildHTMLDocument({
195
+ title: project.title || project.projectName,
196
+ externalStyles: project.externalStyles,
197
+ externalScripts: project.externalScripts,
198
+ preconnectLinks: project.preconnectLinks || [],
199
+ css,
200
+ body: shellHtml,
201
+ js: fullJS,
202
+ });
203
+ }
204
+ // ─── Template Resolution ────────────────────────────────────────────
205
+ /**
206
+ * Convert framework template to plain HTML and inline child components.
207
+ */
208
+ function resolveTemplate(component, componentMap, depth = 0) {
209
+ if (depth > 10)
210
+ return '<!-- max depth reached -->';
211
+ let html = component.template;
212
+ // Convert framework-specific template syntax to HTML
213
+ html = frameworkTemplateToHTML(html, component.framework);
214
+ // Inline child components (resolve <ChildComponent /> tags)
215
+ // Skip framework wrapper components that should be stripped, not resolved
216
+ const skipComponents = new Set(['ErrorBoundary', 'Suspense', 'Provider', 'StrictMode']);
217
+ for (const childName of component.childComponents) {
218
+ if (skipComponents.has(childName))
219
+ continue;
220
+ const child = componentMap.get(childName) || componentMap.get(childName.toLowerCase());
221
+ if (child) {
222
+ const childHtml = resolveTemplate(child, componentMap, depth + 1);
223
+ // Replace both self-closing and paired tags
224
+ html = html.replace(new RegExp(`<${childName}\\s*\\/?>`, 'g'), childHtml);
225
+ html = html.replace(new RegExp(`<${childName}[^>]*>[\\s\\S]*?<\\/${childName}>`, 'g'), childHtml);
226
+ // Also try kebab-case (Vue/Angular)
227
+ const kebab = childName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
228
+ html = html.replace(new RegExp(`<${kebab}\\s*\\/?>`, 'g'), childHtml);
229
+ html = html.replace(new RegExp(`<${kebab}[^>]*>[\\s\\S]*?<\\/${kebab}>`, 'g'), childHtml);
230
+ // Angular selector: app-kebab
231
+ html = html.replace(new RegExp(`<app-${kebab}\\s*\\/?>`, 'g'), childHtml);
232
+ html = html.replace(new RegExp(`<app-${kebab}[^>]*>[\\s\\S]*?<\\/app-${kebab}>`, 'g'), childHtml);
233
+ }
234
+ }
235
+ return html;
236
+ }
237
+ /**
238
+ * Convert framework-specific template syntax back to standard HTML.
239
+ */
240
+ function frameworkTemplateToHTML(template, framework) {
241
+ let html = template;
242
+ switch (framework) {
243
+ case 'react':
244
+ case 'nextjs':
245
+ html = jsxToHTML(html);
246
+ break;
247
+ case 'vue':
248
+ html = vueTemplateToHTML(html);
249
+ break;
250
+ case 'svelte':
251
+ html = svelteTemplateToHTML(html);
252
+ break;
253
+ case 'angular':
254
+ html = angularTemplateToHTML(html);
255
+ break;
256
+ }
257
+ // Remove ErrorBoundary wrapper (React-specific error handling)
258
+ html = html.replace(/<ErrorBoundary[^>]*>([\s\S]*?)<\/ErrorBoundary>/g, '$1');
259
+ html = html.replace(/<ErrorBoundary[^>]*\/>/g, '');
260
+ // Remove Suspense wrapper
261
+ html = html.replace(/<Suspense[^>]*>([\s\S]*?)<\/Suspense>/g, '$1');
262
+ // Common: convert router links back to hash links
263
+ // <Link to="/path"> → <a href="#/path">
264
+ html = html.replace(/<Link\s+(?:to|href)="([^"]*)"([^>]*)>([\s\S]*?)<\/Link>/g, '<a href="#$1"$2>$3</a>');
265
+ // <router-link to="/path"> → <a href="#/path">
266
+ html = html.replace(/<router-link\s+to="([^"]*)"([^>]*)>([\s\S]*?)<\/router-link>/g, '<a href="#$1"$2>$3</a>');
267
+ // routerLink="/path" → href="#/path"
268
+ html = html.replace(/routerLink="([^"]*)"/g, 'href="#$1"');
269
+ return html;
270
+ }
271
+ // ─── JSX → HTML ─────────────────────────────────────────────────────
272
+ function jsxToHTML(jsx) {
273
+ let html = jsx;
274
+ // className → class
275
+ html = html.replace(/\bclassName=/g, 'class=');
276
+ // htmlFor → for
277
+ html = html.replace(/\bhtmlFor=/g, 'for=');
278
+ // tabIndex → tabindex
279
+ html = html.replace(/\btabIndex=/g, 'tabindex=');
280
+ // style={{...}} → style="..." (handle nested braces carefully)
281
+ html = html.replace(/style=\{\{([\s\S]*?)\}\}/g, (_, styles) => {
282
+ const cssProps = styles.split(',').map((p) => {
283
+ const colonIdx = p.indexOf(':');
284
+ if (colonIdx === -1)
285
+ return '';
286
+ const key = p.slice(0, colonIdx).trim().replace(/['"]/g, '');
287
+ const val = p.slice(colonIdx + 1).trim().replace(/['"]/g, '');
288
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
289
+ return `${cssKey}: ${val}`;
290
+ }).filter(Boolean).join('; ');
291
+ return `style="${cssProps}"`;
292
+ });
293
+ // {/* comment */} → <!-- comment -->
294
+ html = html.replace(/\{\/\*\s*([\s\S]*?)\s*\*\/\}/g, '<!-- $1 -->');
295
+ // Remove JSX string expressions: {' '} → space, {'\n'} → newline
296
+ html = html.replace(/\{'\\n'\}/g, '\n');
297
+ html = html.replace(/\{' '\}/g, ' ');
298
+ html = html.replace(/\{"([^"]*)"\}/g, '$1');
299
+ html = html.replace(/\{'([^']*)'\}/g, '$1');
300
+ // Remove JSX template literal expressions: {`text`} → text
301
+ html = html.replace(/\{`([^`]*)`\}/g, '$1');
302
+ // Remove JSX ternary placeholders: {condition ? 'a' : 'b'} → remove
303
+ html = html.replace(/\{[^{}]*\?[^{}]*:[^{}]*\}/g, '');
304
+ // onInput={(e) => ...} → oninput="..."
305
+ html = html.replace(/onInput=\{[^}]*\}/g, '');
306
+ // onChange={(e) => ...} → onchange="..."
307
+ html = html.replace(/onChange=\{[^}]*\}/g, '');
308
+ // onSubmit={(e) => { e.preventDefault(); ... }} → onsubmit="..."
309
+ html = html.replace(/onSubmit=\{\([^)]*\)\s*=>\s*\{\s*[^}]*\}\}/g, '');
310
+ // onClick={(e) => e.preventDefault()} → onclick="event.preventDefault()"
311
+ html = html.replace(/onClick=\{\(e\)\s*=>\s*e\.preventDefault\(\)\}/g, 'onclick="event.preventDefault()"');
312
+ // Complex onClick with scrollIntoView → onclick
313
+ html = html.replace(/onClick=\{\(e\)\s*=>\s*\{\s*e\.preventDefault\(\);\s*document\.getElementById\('([^']+)'\)\?\.scrollIntoView\(\{[^}]*\}\);\s*\}\}/g, 'onclick="event.preventDefault(); document.getElementById(\'$1\')?.scrollIntoView({behavior:\'smooth\'})"');
314
+ // onClick={() => { handler(); }} → onclick="handler()"
315
+ html = html.replace(/onClick=\{\(\)\s*=>\s*\{\s*(\w+)\(([^)]*)\);?\s*\}\}/g, 'onclick="$1($2)"');
316
+ // onClick={() => handler()} → onclick="handler()"
317
+ html = html.replace(/onClick=\{\(\)\s*=>\s*(\w+)\(([^)]*)\)\s*\}/g, 'onclick="$1($2)"');
318
+ // onClick={handler} → onclick="handler()"
319
+ html = html.replace(/onClick=\{(\w+)\}/g, 'onclick="$1()"');
320
+ // Generic: onClick={(e) => { ... }} → onclick="..."
321
+ html = html.replace(/onClick=\{\([^)]*\)\s*=>\s*\{([^}]*)\}\}/g, (_, body) => `onclick="${body.trim().replace(/"/g, "'")}"`);
322
+ // Remove any remaining JSX event handler bindings: onXxx={...}
323
+ html = html.replace(/\bon[A-Z]\w+=\{[^}]*\}/g, '');
324
+ // Remove key={...} and key="..." props (React internal)
325
+ html = html.replace(/\s+key=\{[^}]*\}/g, '');
326
+ html = html.replace(/\s+key="[^"]*"/g, '');
327
+ // Remove ref={...} props
328
+ html = html.replace(/\s+ref=\{[^}]*\}/g, '');
329
+ // Remove dangerouslySetInnerHTML
330
+ html = html.replace(/\s+dangerouslySetInnerHTML=\{[^}]*\}/g, '');
331
+ // Remove remaining JSX expression wrappers {expr} → expr (for text content)
332
+ // Be careful not to break HTML attributes
333
+ html = html.replace(/>(\s*)\{([^{}]+)\}(\s*)</g, '>$1$2$3<');
334
+ // Fix double-encoded HTML entities: &amp;amp; → &amp;, &amp;aspect → &aspect
335
+ html = html.replace(/&amp;(amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\da-fA-F]+;)/g, '&$1');
336
+ // Also handle cases where & in URLs was encoded to &amp;
337
+ html = html.replace(/&amp;(?=\w+=)/g, '&');
338
+ // Remove (as HTMLInputElement) type assertions in text
339
+ html = html.replace(/\(([^)]+)\s+as\s+\w+\)/g, '$1');
340
+ html = html.replace(/\bas\s+HTML\w+Element\b/g, '');
341
+ // Remove JSX attribute bindings: value={expr}, src={expr}, etc.
342
+ html = html.replace(/\b(value|src|alt|href|id|name|placeholder|type|disabled|checked|readOnly)=\{([^}]*)\}/g, (_, attr, val) => {
343
+ // If it's a simple string or variable, use as attribute value
344
+ const clean = val.replace(/['"]/g, '').trim();
345
+ if (/^[a-zA-Z_$][\w.]*$/.test(clean) || /^['"]/.test(val)) {
346
+ return `${attr}="${clean}"`;
347
+ }
348
+ return `${attr}=""`;
349
+ });
350
+ // Remove stray JSX fragment tags and export statements leaked from parsing
351
+ html = html.replace(/\s*<\/>\s*\)\s*;?\s*/g, '');
352
+ html = html.replace(/^\s*<>\s*/gm, '');
353
+ html = html.replace(/\s*export\s+default\s+\w+;?\s*/g, '');
354
+ // Remove utility function TODO comment blocks
355
+ html = html.replace(/\/\*[\s\S]*?\*\//g, '');
356
+ // Remove any remaining JSX expression attributes: attr={...}
357
+ html = html.replace(/\s+\w+=\{[^}]*\}/g, '');
358
+ // Clean up orphaned closing braces on their own line (from removed JSX expressions)
359
+ html = html.replace(/^\s*\}\s*$/gm, '');
360
+ // Clean up extra blank lines
361
+ html = html.replace(/\n{3,}/g, '\n\n');
362
+ // Clean up empty attribute values from removed bindings
363
+ html = html.replace(/\s{2,}>/g, '>');
364
+ return html;
365
+ }
366
+ // ─── Vue Template → HTML ────────────────────────────────────────────
367
+ function vueTemplateToHTML(template) {
368
+ let html = template;
369
+ // @click="handler" → onclick="handler()"
370
+ html = html.replace(/@click="(\w+)\(([^)]*)\)"/g, 'onclick="$1($2)"');
371
+ html = html.replace(/@click="(\w+)"/g, 'onclick="$1()"');
372
+ // @change, @submit, etc.
373
+ html = html.replace(/@(\w+)(?:\.prevent)?="([^"]+)"/g, 'on$1="$2"');
374
+ // :class="..." → class="..." (simplified — strips binding)
375
+ html = html.replace(/:class="'([^']+)'"/g, 'class="$1"');
376
+ html = html.replace(/:class="([^"]+)"/g, 'class="$1"');
377
+ // :style="{ ... }" → style="..."
378
+ html = html.replace(/:style="\{([^}]*)\}"/g, (_, styles) => {
379
+ const cssProps = styles.split(',').map((p) => {
380
+ const colonIdx = p.indexOf(':');
381
+ if (colonIdx === -1)
382
+ return '';
383
+ const key = p.slice(0, colonIdx).trim();
384
+ const val = p.slice(colonIdx + 1).trim().replace(/['"]/g, '');
385
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
386
+ return `${cssKey}: ${val}`;
387
+ }).filter(Boolean).join('; ');
388
+ return `style="${cssProps}"`;
389
+ });
390
+ // {{ expr }} → (removed, or kept as text)
391
+ html = html.replace(/\{\{\s*([^}]+)\s*\}\}/g, '');
392
+ // v-if, v-for, v-show → removed (static HTML)
393
+ html = html.replace(/\s*v-if="[^"]*"/g, '');
394
+ html = html.replace(/\s*v-else/g, '');
395
+ html = html.replace(/\s*v-show="[^"]*"/g, '');
396
+ html = html.replace(/\s*v-for="[^"]*"/g, '');
397
+ html = html.replace(/\s*:key="[^"]*"/g, '');
398
+ html = html.replace(/\s*v-html="[^"]*"/g, '');
399
+ html = html.replace(/\s*v-model="[^"]*"/g, '');
400
+ return html;
401
+ }
402
+ // ─── Svelte Template → HTML ─────────────────────────────────────────
403
+ function svelteTemplateToHTML(template) {
404
+ let html = template;
405
+ // onclick={handler} → onclick="handler()"
406
+ html = html.replace(/onclick=\{(\w+)\}/g, 'onclick="$1()"');
407
+ html = html.replace(/onclick=\{\(\)\s*=>\s*(\w+)\(([^)]*)\)\}/g, 'onclick="$1($2)"');
408
+ html = html.replace(/onchange=\{([^}]+)\}/g, 'onchange="$1"');
409
+ // {#if ...} ... {/if} → keep content (static)
410
+ html = html.replace(/\{#if\s+[^}]+\}/g, '');
411
+ html = html.replace(/\{:else(?:\s+if\s+[^}]+)?\}/g, '');
412
+ html = html.replace(/\{\/if\}/g, '');
413
+ // {#each ...} ... {/each} → keep content
414
+ html = html.replace(/\{#each\s+[^}]+\}/g, '');
415
+ html = html.replace(/\{\/each\}/g, '');
416
+ // {expr} in text → removed
417
+ html = html.replace(/>(\s*)\{([^{}]+)\}(\s*)</g, '>$1$3<');
418
+ return html;
419
+ }
420
+ // ─── Angular Template → HTML ────────────────────────────────────────
421
+ function angularTemplateToHTML(template) {
422
+ let html = template;
423
+ // (click)="handler()" → onclick="handler()"
424
+ html = html.replace(/\(click\)="([^"]+)"/g, 'onclick="$1"');
425
+ html = html.replace(/\(change\)="([^"]+)"/g, 'onchange="$1"');
426
+ html = html.replace(/\(submit\)="([^"]+)"/g, 'onsubmit="$1"');
427
+ html = html.replace(/\(keydown\)="([^"]+)"/g, 'onkeydown="$1"');
428
+ // [class]="..." → class="..."
429
+ html = html.replace(/\[class\]="([^"]+)"/g, 'class="$1"');
430
+ // [ngStyle]="..." → style="..."
431
+ html = html.replace(/\[ngStyle\]="\{([^}]*)\}"/g, (_, styles) => {
432
+ const cssProps = styles.split(',').map((p) => {
433
+ const colonIdx = p.indexOf(':');
434
+ if (colonIdx === -1)
435
+ return '';
436
+ const key = p.slice(0, colonIdx).trim();
437
+ const val = p.slice(colonIdx + 1).trim().replace(/['"]/g, '');
438
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
439
+ return `${cssKey}: ${val}`;
440
+ }).filter(Boolean).join('; ');
441
+ return `style="${cssProps}"`;
442
+ });
443
+ // {{ expr }} → removed
444
+ html = html.replace(/\{\{\s*[^}]+\s*\}\}/g, '');
445
+ // *ngIf, *ngFor → removed
446
+ html = html.replace(/\s*\*ngIf="[^"]*"/g, '');
447
+ html = html.replace(/\s*\*ngFor="[^"]*"/g, '');
448
+ html = html.replace(/\s*\[value\]="[^"]*"/g, '');
449
+ return html;
450
+ }
451
+ // ─── Auto-Detect Interactive Patterns ───────────────────────────────
452
+ /**
453
+ * Detect common interactive patterns in the resolved HTML and CSS,
454
+ * and generate proper vanilla JS to make them work.
455
+ */
456
+ function generateInteractiveJS(bodyHtml, css) {
457
+ const blocks = [];
458
+ // 1. Scroll-reveal animations: .reveal with animation-play-state:paused in CSS
459
+ if (css.includes('animation-play-state') && (bodyHtml.includes('class="') || bodyHtml.includes("class='"))) {
460
+ const hasRevealCSS = /\.reveal\s*\{[^}]*animation-play-state\s*:\s*paused/s.test(css);
461
+ const hasRevealClass = /\bclass="[^"]*\breveal\b/.test(bodyHtml) || /\bclass='[^']*\breveal\b/.test(bodyHtml);
462
+ if (hasRevealCSS && hasRevealClass) {
463
+ blocks.push(`// Intersection observer reveals
464
+ const revealObserver = new IntersectionObserver((entries) => {
465
+ entries.forEach(e => {
466
+ if (e.isIntersecting) {
467
+ e.target.style.animationPlayState = 'running';
468
+ revealObserver.unobserve(e.target);
469
+ }
470
+ });
471
+ }, { threshold: 0.08 });
472
+ document.querySelectorAll('.reveal').forEach(el => {
473
+ el.style.animationPlayState = 'paused';
474
+ revealObserver.observe(el);
475
+ });`);
476
+ }
477
+ }
478
+ // 2. Nav scroll class toggle (fixed nav that adds class on scroll)
479
+ if (bodyHtml.includes('id="nav"') || bodyHtml.match(/<nav[^>]*id="[^"]*"/)) {
480
+ const navId = bodyHtml.match(/<nav[^>]*id="([^"]*)"/)?.[1] || 'nav';
481
+ if (css.includes('.scrolled') || css.includes('nav.scrolled')) {
482
+ blocks.push(`// Nav scroll effect
483
+ window.addEventListener('scroll', () => {
484
+ document.getElementById('${navId}')?.classList.toggle('scrolled', window.scrollY > 20);
485
+ }, { passive: true });`);
486
+ }
487
+ }
488
+ // 3. Mobile menu toggle (hamburger + mobile menu)
489
+ if (bodyHtml.includes('id="ham"') && bodyHtml.includes('id="mob"')) {
490
+ blocks.push(`// Mobile menu toggle
491
+ const ham = document.getElementById('ham');
492
+ const mob = document.getElementById('mob');
493
+ if (ham && mob) {
494
+ ham.addEventListener('click', () => {
495
+ ham.classList.toggle('open');
496
+ mob.classList.toggle('open');
497
+ document.body.style.overflow = mob.classList.contains('open') ? 'hidden' : '';
498
+ });
499
+ }
500
+ function closeMob() {
501
+ const ham = document.getElementById('ham');
502
+ const mob = document.getElementById('mob');
503
+ if (ham) ham.classList.remove('open');
504
+ if (mob) mob.classList.remove('open');
505
+ document.body.style.overflow = '';
506
+ }`);
507
+ }
508
+ // 4. Tab switching (demo-tab / tab panels)
509
+ if (bodyHtml.includes('demo-tab') || bodyHtml.includes('data-tab')) {
510
+ blocks.push(`// Tab switching
511
+ document.querySelectorAll('[data-tab]').forEach(tab => {
512
+ tab.addEventListener('click', () => {
513
+ const id = tab.dataset.tab;
514
+ const parent = tab.closest('.demo-tabs, .tabs, .tab-container')?.parentElement || document;
515
+ parent.querySelectorAll('[data-tab]').forEach(t => t.classList.remove('active'));
516
+ parent.querySelectorAll('.demo-panel, .tab-panel').forEach(p => p.classList.remove('active'));
517
+ tab.classList.add('active');
518
+ const panel = document.getElementById('tab-' + id);
519
+ if (panel) panel.classList.add('active');
520
+ });
521
+ });`);
522
+ }
523
+ // 5. FAQ accordion
524
+ if (bodyHtml.includes('faq-item') && bodyHtml.includes('faq-q')) {
525
+ blocks.push(`// FAQ accordion
526
+ document.querySelectorAll('.faq-q').forEach(btn => {
527
+ btn.addEventListener('click', () => {
528
+ const item = btn.closest('.faq-item');
529
+ const isOpen = item?.classList.contains('open');
530
+ document.querySelectorAll('.faq-item').forEach(i => i.classList.remove('open'));
531
+ if (!isOpen && item) item.classList.add('open');
532
+ });
533
+ });`);
534
+ }
535
+ // 6. Billing toggle (pricing section)
536
+ if (bodyHtml.includes('id="btog"') || bodyHtml.includes('billing-toggle')) {
537
+ blocks.push(`// Billing toggle
538
+ let annual = false;
539
+ function toggleBilling() {
540
+ annual = !annual;
541
+ const btog = document.getElementById('btog');
542
+ if (btog) btog.classList.toggle('on', annual);
543
+ document.querySelectorAll('.pval[data-m]').forEach(el => {
544
+ el.textContent = annual ? (el.dataset.a || '') : (el.dataset.m || '');
545
+ });
546
+ const proPer = document.getElementById('proPer');
547
+ const teamPer = document.getElementById('teamPer');
548
+ if (proPer) proPer.textContent = annual ? 'per month · billed annually' : 'per month · billed monthly';
549
+ if (teamPer) teamPer.textContent = annual ? 'per month · billed annually' : 'per month · up to 10 devs';
550
+ }`);
551
+ }
552
+ // 7. CTA email form
553
+ if (bodyHtml.includes('input-group') || bodyHtml.includes('handleCta') || bodyHtml.includes('id="ctaEmail"')) {
554
+ blocks.push(`// CTA email form
555
+ function handleCta() {
556
+ const input = document.getElementById('ctaEmail') || document.querySelector('.input-group input');
557
+ if (!input) return;
558
+ const btn = input.nextElementSibling;
559
+ if (input.value && input.value.includes('@')) {
560
+ if (btn) { btn.textContent = "You're on the list ✓"; btn.style.background = '#3ecf6e'; }
561
+ input.value = '';
562
+ input.placeholder = "We'll be in touch soon.";
563
+ setTimeout(() => { if (btn) { btn.textContent = 'Get early access'; btn.style.background = ''; } }, 4000);
564
+ } else {
565
+ input.style.outline = '1px solid #e74c3c';
566
+ input.focus();
567
+ setTimeout(() => input.style.outline = '', 2000);
568
+ }
569
+ }
570
+ document.querySelector('.input-group input')?.addEventListener('keydown', e => {
571
+ if (e.key === 'Enter') handleCta();
572
+ });`);
573
+ }
574
+ // 8. Counter animation (data-target number counting)
575
+ if (bodyHtml.includes('data-target')) {
576
+ blocks.push(`// Counter animation
577
+ const counterObserver = new IntersectionObserver((entries) => {
578
+ entries.forEach(e => {
579
+ if (e.isIntersecting) {
580
+ const target = +e.target.dataset.target;
581
+ let count = 0;
582
+ const step = target / 40;
583
+ const timer = setInterval(() => {
584
+ count = Math.min(count + step, target);
585
+ e.target.textContent = Math.floor(count);
586
+ if (count >= target) clearInterval(timer);
587
+ }, 28);
588
+ counterObserver.unobserve(e.target);
589
+ }
590
+ });
591
+ }, { threshold: 0.5 });
592
+ document.querySelectorAll('[data-target]').forEach(c => counterObserver.observe(c));`);
593
+ }
594
+ // 9. Ripple button effects
595
+ if (css.includes('@keyframes ripple') || bodyHtml.includes('btn-primary') || bodyHtml.includes('plan-btn')) {
596
+ blocks.push(`// Ripple button effect
597
+ document.querySelectorAll('.btn-primary, .plan-btn, .nav-install').forEach(btn => {
598
+ btn.addEventListener('click', function(e) {
599
+ const r = document.createElement('span');
600
+ const rect = this.getBoundingClientRect();
601
+ r.style.cssText = 'position:absolute;border-radius:50%;background:rgba(255,255,255,0.18);width:80px;height:80px;pointer-events:none;transform:scale(0);animation:ripple 0.6s ease forwards;left:' + (e.clientX - rect.left - 40) + 'px;top:' + (e.clientY - rect.top - 40) + 'px';
602
+ this.style.position = 'relative';
603
+ this.style.overflow = 'hidden';
604
+ this.appendChild(r);
605
+ setTimeout(() => r.remove(), 600);
606
+ });
607
+ });`);
608
+ }
609
+ // 10. Smooth scroll for anchor links
610
+ if (bodyHtml.includes('scrollIntoView') || bodyHtml.includes('href="#')) {
611
+ blocks.push(`// Smooth scroll for anchor links
612
+ document.querySelectorAll('a[href^="#"]').forEach(link => {
613
+ link.addEventListener('click', (e) => {
614
+ const href = link.getAttribute('href');
615
+ if (href && href.length > 1) {
616
+ const target = document.querySelector(href);
617
+ if (target) {
618
+ e.preventDefault();
619
+ target.scrollIntoView({ behavior: 'smooth' });
620
+ }
621
+ }
622
+ });
623
+ });`);
624
+ }
625
+ if (blocks.length === 0)
626
+ return '';
627
+ return '// ─── Auto-detected Interactive Patterns ───\n\n' + blocks.join('\n\n');
628
+ }
629
+ // ─── JavaScript Generation ──────────────────────────────────────────
630
+ function cleanHandlerBody(body) {
631
+ let cleanBody = body;
632
+ // React: setX(prev => !prev) → x = !x
633
+ cleanBody = cleanBody.replace(/set(\w+)\(\s*(?:prev|p)\s*=>\s*!(?:prev|p)\s*\)/g, (_, varName) => {
634
+ const v = varName.charAt(0).toLowerCase() + varName.slice(1);
635
+ return `${v} = !${v}`;
636
+ });
637
+ // React: setX(value) → x = value (but not if value contains =>)
638
+ cleanBody = cleanBody.replace(/set(\w+)\(([^)=]+)\)/g, (_, varName, val) => {
639
+ if (val.includes('=>'))
640
+ return `set${varName}(${val})`; // skip complex setters
641
+ const v = varName.charAt(0).toLowerCase() + varName.slice(1);
642
+ return `${v} = ${val}`;
643
+ });
644
+ // Vue: x.value = y → x = y
645
+ cleanBody = cleanBody.replace(/(\w+)\.value\s*=/g, '$1 =');
646
+ cleanBody = cleanBody.replace(/(\w+)\.value\b/g, '$1');
647
+ // Angular: this.x.update(v => !v) → x = !x
648
+ cleanBody = cleanBody.replace(/this\.(\w+)\.update\(v\s*=>\s*!v\)/g, '$1 = !$1');
649
+ cleanBody = cleanBody.replace(/this\.(\w+)\.set\(([^)]+)\)/g, '$1 = $2');
650
+ // Angular: this.x() → x (signal read)
651
+ cleanBody = cleanBody.replace(/this\.(\w+)\(\)/g, '$1');
652
+ // Remove TODO comments
653
+ cleanBody = cleanBody.replace(/\/\/\s*TODO:.*$/gm, '');
654
+ // Remove React-specific patterns
655
+ cleanBody = cleanBody.replace(/e\.currentTarget\b/g, 'event.currentTarget');
656
+ cleanBody = cleanBody.replace(/e\.target\b/g, 'event.target');
657
+ cleanBody = cleanBody.replace(/e\.preventDefault\(\)/g, 'event.preventDefault()');
658
+ // Fix broken arrow in setTimeout: timeout = ( => expr) → setTimeout(() => expr)
659
+ cleanBody = cleanBody.replace(/timeout\s*=\s*\(\s*=>\s*(.+?),\s*(\d+)\)/g, 'setTimeout(() => $1, $2)');
660
+ // Fix: (prev => !prev) without setState wrapper → just toggle
661
+ cleanBody = cleanBody.replace(/\(\s*prev\s*=>\s*!prev\s*\)/g, '!value');
662
+ // Remove return () => { cleanup } patterns (React effect cleanup)
663
+ cleanBody = cleanBody.replace(/return\s*\(\)\s*=>\s*\{[\s\S]*?\};?/g, '');
664
+ return cleanBody;
665
+ }
666
+ function generateConsolidatedJS(stateVars, handlers, effects) {
667
+ const lines = [];
668
+ lines.push('// ─── Application State & Handlers ───');
669
+ // State variables
670
+ for (const [name, initial] of stateVars) {
671
+ // Clean up initial values
672
+ let cleanInitial = initial;
673
+ // Remove type annotations
674
+ cleanInitial = cleanInitial.replace(/<[^>]+>/g, '');
675
+ lines.push(`let ${name} = ${cleanInitial};`);
676
+ }
677
+ if (stateVars.size > 0)
678
+ lines.push('');
679
+ // Event handlers
680
+ for (const [name, body] of handlers) {
681
+ const cleanBody = cleanHandlerBody(body);
682
+ const cleanLines = cleanBody.split('\n').filter(l => l.trim());
683
+ if (cleanLines.length === 0) {
684
+ lines.push(`function ${name}() { /* TODO: implement */ }`);
685
+ }
686
+ else {
687
+ lines.push(`function ${name}(event) {`);
688
+ for (const line of cleanLines) {
689
+ lines.push(` ${line.trim()}`);
690
+ }
691
+ lines.push('}');
692
+ }
693
+ lines.push('');
694
+ }
695
+ // Effects → DOMContentLoaded
696
+ if (effects.length > 0) {
697
+ const cleanEffects = effects.map(e => cleanHandlerBody(e)).filter(e => e.trim());
698
+ if (cleanEffects.length > 0) {
699
+ lines.push(`document.addEventListener('DOMContentLoaded', function() {`);
700
+ for (const effect of cleanEffects) {
701
+ for (const line of effect.split('\n')) {
702
+ if (line.trim())
703
+ lines.push(` ${line.trim()}`);
704
+ }
705
+ }
706
+ lines.push('});');
707
+ }
708
+ }
709
+ return lines.join('\n');
710
+ }
711
+ function generateHashRouter(routes, componentMap) {
712
+ // The hash router is generated inline in the SPA page
713
+ // This function generates the init function that wires up link clicks
714
+ return `
715
+ function initInteractive() {
716
+ // Wire up hash navigation links
717
+ document.querySelectorAll('a[href^="#/"]').forEach(function(link) {
718
+ link.addEventListener('click', function(e) {
719
+ e.preventDefault();
720
+ var path = this.getAttribute('href');
721
+ if (path) window.location.hash = path.slice(1);
722
+ });
723
+ });
724
+ }
725
+ `;
726
+ }
727
+ function buildHTMLDocument(options) {
728
+ const headParts = [];
729
+ const bodyEndParts = [];
730
+ // Separate head scripts (Tailwind config, etc.) from body scripts
731
+ for (const src of options.externalScripts) {
732
+ if (src.includes('tailwindcss') || src.includes('tailwind')) {
733
+ // Tailwind CDN goes in head
734
+ headParts.push(` <script src="${src}"></script>`);
735
+ }
736
+ else {
737
+ bodyEndParts.push(` <script src="${src}"></script>`);
738
+ }
739
+ }
740
+ // Output preconnect hints before stylesheets (for font loading optimization)
741
+ const preconnectTags = (options.preconnectLinks || [])
742
+ .map(tag => ` ${tag}`)
743
+ .join('\n');
744
+ const extStyleLinks = options.externalStyles
745
+ .map(href => ` <link rel="stylesheet" href="${href}" />`)
746
+ .join('\n');
747
+ // Check if the project uses Tailwind (by looking at CSS for @tailwind directives)
748
+ const usesTailwind = options.css.includes('@tailwind') || headParts.length > 0;
749
+ // If using Tailwind but no CDN script found, add it
750
+ if (usesTailwind && headParts.length === 0) {
751
+ headParts.push(' <script src="https://cdn.tailwindcss.com"></script>');
752
+ }
753
+ const headScripts = headParts.length > 0 ? '\n' + headParts.join('\n') : '';
754
+ const bodyScripts = bodyEndParts.length > 0 ? '\n' + bodyEndParts.join('\n') : '';
755
+ return `<!DOCTYPE html>
756
+ <html lang="en">
757
+ <head>
758
+ <meta charset="UTF-8" />
759
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
760
+ <title>${options.title}</title>${headScripts}
761
+ ${preconnectTags ? preconnectTags + '\n' : ''}${extStyleLinks}
762
+ <link rel="stylesheet" href="styles.css" />
763
+ </head>
764
+ <body>
765
+ ${options.body}
766
+ ${bodyScripts}
767
+ <script src="app.js"></script>
768
+ </body>
769
+ </html>
770
+ `;
771
+ }
772
+ //# sourceMappingURL=html-generator.js.map