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.
- package/README.md +514 -0
- package/bin/cli.js +2 -0
- package/dist/ai/agent.d.ts +124 -0
- package/dist/ai/agent.d.ts.map +1 -0
- package/dist/ai/agent.js +289 -0
- package/dist/ai/agent.js.map +1 -0
- package/dist/ai/index.d.ts +10 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +10 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/prompts.d.ts +35 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +166 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/refinement-loop.d.ts +29 -0
- package/dist/ai/refinement-loop.d.ts.map +1 -0
- package/dist/ai/refinement-loop.js +180 -0
- package/dist/ai/refinement-loop.js.map +1 -0
- package/dist/ai/tools.d.ts +17 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +353 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/ai/visual-compare.d.ts +43 -0
- package/dist/ai/visual-compare.d.ts.map +1 -0
- package/dist/ai/visual-compare.js +176 -0
- package/dist/ai/visual-compare.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/converter.d.ts +10 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +836 -0
- package/dist/converter.js.map +1 -0
- package/dist/deconverter.d.ts +19 -0
- package/dist/deconverter.d.ts.map +1 -0
- package/dist/deconverter.js +188 -0
- package/dist/deconverter.js.map +1 -0
- package/dist/frameworks/angular-adapter.d.ts +27 -0
- package/dist/frameworks/angular-adapter.d.ts.map +1 -0
- package/dist/frameworks/angular-adapter.js +617 -0
- package/dist/frameworks/angular-adapter.js.map +1 -0
- package/dist/frameworks/index.d.ts +10 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +21 -0
- package/dist/frameworks/index.js.map +1 -0
- package/dist/frameworks/nextjs-adapter.d.ts +22 -0
- package/dist/frameworks/nextjs-adapter.d.ts.map +1 -0
- package/dist/frameworks/nextjs-adapter.js +392 -0
- package/dist/frameworks/nextjs-adapter.js.map +1 -0
- package/dist/frameworks/react-adapter.d.ts +21 -0
- package/dist/frameworks/react-adapter.d.ts.map +1 -0
- package/dist/frameworks/react-adapter.js +71 -0
- package/dist/frameworks/react-adapter.js.map +1 -0
- package/dist/frameworks/svelte-adapter.d.ts +27 -0
- package/dist/frameworks/svelte-adapter.d.ts.map +1 -0
- package/dist/frameworks/svelte-adapter.js +519 -0
- package/dist/frameworks/svelte-adapter.js.map +1 -0
- package/dist/frameworks/types.d.ts +78 -0
- package/dist/frameworks/types.d.ts.map +1 -0
- package/dist/frameworks/types.js +2 -0
- package/dist/frameworks/types.js.map +1 -0
- package/dist/frameworks/vue-adapter.d.ts +34 -0
- package/dist/frameworks/vue-adapter.d.ts.map +1 -0
- package/dist/frameworks/vue-adapter.js +632 -0
- package/dist/frameworks/vue-adapter.js.map +1 -0
- package/dist/generators/accessibility-generator.d.ts +43 -0
- package/dist/generators/accessibility-generator.d.ts.map +1 -0
- package/dist/generators/accessibility-generator.js +507 -0
- package/dist/generators/accessibility-generator.js.map +1 -0
- package/dist/generators/asset-handler.d.ts +14 -0
- package/dist/generators/asset-handler.d.ts.map +1 -0
- package/dist/generators/asset-handler.js +79 -0
- package/dist/generators/asset-handler.js.map +1 -0
- package/dist/generators/build-verifier.d.ts +8 -0
- package/dist/generators/build-verifier.d.ts.map +1 -0
- package/dist/generators/build-verifier.js +64 -0
- package/dist/generators/build-verifier.js.map +1 -0
- package/dist/generators/component-extractor.d.ts +25 -0
- package/dist/generators/component-extractor.d.ts.map +1 -0
- package/dist/generators/component-extractor.js +146 -0
- package/dist/generators/component-extractor.js.map +1 -0
- package/dist/generators/component-generator.d.ts +12 -0
- package/dist/generators/component-generator.d.ts.map +1 -0
- package/dist/generators/component-generator.js +724 -0
- package/dist/generators/component-generator.js.map +1 -0
- package/dist/generators/deploy-generator.d.ts +9 -0
- package/dist/generators/deploy-generator.d.ts.map +1 -0
- package/dist/generators/deploy-generator.js +409 -0
- package/dist/generators/deploy-generator.js.map +1 -0
- package/dist/generators/error-boundary.d.ts +5 -0
- package/dist/generators/error-boundary.d.ts.map +1 -0
- package/dist/generators/error-boundary.js +59 -0
- package/dist/generators/error-boundary.js.map +1 -0
- package/dist/generators/form-generator.d.ts +42 -0
- package/dist/generators/form-generator.d.ts.map +1 -0
- package/dist/generators/form-generator.js +662 -0
- package/dist/generators/form-generator.js.map +1 -0
- package/dist/generators/hooks-generator.d.ts +40 -0
- package/dist/generators/hooks-generator.d.ts.map +1 -0
- package/dist/generators/hooks-generator.js +297 -0
- package/dist/generators/hooks-generator.js.map +1 -0
- package/dist/generators/html-generator.d.ts +27 -0
- package/dist/generators/html-generator.d.ts.map +1 -0
- package/dist/generators/html-generator.js +772 -0
- package/dist/generators/html-generator.js.map +1 -0
- package/dist/generators/jquery-converter.d.ts +41 -0
- package/dist/generators/jquery-converter.d.ts.map +1 -0
- package/dist/generators/jquery-converter.js +594 -0
- package/dist/generators/jquery-converter.js.map +1 -0
- package/dist/generators/pattern-implementer.d.ts +26 -0
- package/dist/generators/pattern-implementer.d.ts.map +1 -0
- package/dist/generators/pattern-implementer.js +336 -0
- package/dist/generators/pattern-implementer.js.map +1 -0
- package/dist/generators/performance-generator.d.ts +51 -0
- package/dist/generators/performance-generator.d.ts.map +1 -0
- package/dist/generators/performance-generator.js +428 -0
- package/dist/generators/performance-generator.js.map +1 -0
- package/dist/generators/router-generator.d.ts +21 -0
- package/dist/generators/router-generator.d.ts.map +1 -0
- package/dist/generators/router-generator.js +178 -0
- package/dist/generators/router-generator.js.map +1 -0
- package/dist/generators/scaffolder.d.ts +28 -0
- package/dist/generators/scaffolder.d.ts.map +1 -0
- package/dist/generators/scaffolder.js +266 -0
- package/dist/generators/scaffolder.js.map +1 -0
- package/dist/generators/seo-generator.d.ts +29 -0
- package/dist/generators/seo-generator.d.ts.map +1 -0
- package/dist/generators/seo-generator.js +223 -0
- package/dist/generators/seo-generator.js.map +1 -0
- package/dist/generators/test-generator.d.ts +19 -0
- package/dist/generators/test-generator.d.ts.map +1 -0
- package/dist/generators/test-generator.js +398 -0
- package/dist/generators/test-generator.js.map +1 -0
- package/dist/generators/type-generator.d.ts +33 -0
- package/dist/generators/type-generator.d.ts.map +1 -0
- package/dist/generators/type-generator.js +663 -0
- package/dist/generators/type-generator.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/css-processor.d.ts +23 -0
- package/dist/parsers/css-processor.d.ts.map +1 -0
- package/dist/parsers/css-processor.js +129 -0
- package/dist/parsers/css-processor.js.map +1 -0
- package/dist/parsers/framework-parser.d.ts +48 -0
- package/dist/parsers/framework-parser.d.ts.map +1 -0
- package/dist/parsers/framework-parser.js +770 -0
- package/dist/parsers/framework-parser.js.map +1 -0
- package/dist/parsers/html-parser.d.ts +12 -0
- package/dist/parsers/html-parser.d.ts.map +1 -0
- package/dist/parsers/html-parser.js +444 -0
- package/dist/parsers/html-parser.js.map +1 -0
- package/dist/parsers/js-analyzer.d.ts +199 -0
- package/dist/parsers/js-analyzer.d.ts.map +1 -0
- package/dist/parsers/js-analyzer.js +680 -0
- package/dist/parsers/js-analyzer.js.map +1 -0
- package/dist/parsers/js-resolver.d.ts +8 -0
- package/dist/parsers/js-resolver.d.ts.map +1 -0
- package/dist/parsers/js-resolver.js +45 -0
- package/dist/parsers/js-resolver.js.map +1 -0
- package/dist/parsers/tailwind-detector.d.ts +23 -0
- package/dist/parsers/tailwind-detector.d.ts.map +1 -0
- package/dist/parsers/tailwind-detector.js +104 -0
- package/dist/parsers/tailwind-detector.js.map +1 -0
- package/dist/tests/advanced-features.test.d.ts +2 -0
- package/dist/tests/advanced-features.test.d.ts.map +1 -0
- package/dist/tests/advanced-features.test.js +235 -0
- package/dist/tests/advanced-features.test.js.map +1 -0
- package/dist/tests/css-modules.test.d.ts +2 -0
- package/dist/tests/css-modules.test.d.ts.map +1 -0
- package/dist/tests/css-modules.test.js +61 -0
- package/dist/tests/css-modules.test.js.map +1 -0
- package/dist/tests/css-processor.test.d.ts +2 -0
- package/dist/tests/css-processor.test.d.ts.map +1 -0
- package/dist/tests/css-processor.test.js +48 -0
- package/dist/tests/css-processor.test.js.map +1 -0
- package/dist/tests/html-parser.test.d.ts +2 -0
- package/dist/tests/html-parser.test.d.ts.map +1 -0
- package/dist/tests/html-parser.test.js +78 -0
- package/dist/tests/html-parser.test.js.map +1 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +65 -0
- package/dist/tests/integration.test.js.map +1 -0
- package/dist/tests/js-analyzer.test.d.ts +2 -0
- package/dist/tests/js-analyzer.test.d.ts.map +1 -0
- package/dist/tests/js-analyzer.test.js +58 -0
- package/dist/tests/js-analyzer.test.js.map +1 -0
- package/dist/tests/naming.test.d.ts +2 -0
- package/dist/tests/naming.test.d.ts.map +1 -0
- package/dist/tests/naming.test.js +43 -0
- package/dist/tests/naming.test.js.map +1 -0
- package/dist/tests/router-generator.test.d.ts +2 -0
- package/dist/tests/router-generator.test.d.ts.map +1 -0
- package/dist/tests/router-generator.test.js +60 -0
- package/dist/tests/router-generator.test.js.map +1 -0
- package/dist/tui/chat.d.ts +13 -0
- package/dist/tui/chat.d.ts.map +1 -0
- package/dist/tui/chat.js +499 -0
- package/dist/tui/chat.js.map +1 -0
- package/dist/tui/design-guide.d.ts +41 -0
- package/dist/tui/design-guide.d.ts.map +1 -0
- package/dist/tui/design-guide.js +184 -0
- package/dist/tui/design-guide.js.map +1 -0
- package/dist/tui/input.d.ts +30 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +239 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/tui/renderer.d.ts +48 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +212 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/tools.d.ts +14 -0
- package/dist/tui/tools.d.ts.map +1 -0
- package/dist/tui/tools.js +1370 -0
- package/dist/tui/tools.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +20 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +33 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/formatter.d.ts +5 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +68 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +19 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/naming.d.ts +17 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +48 -0
- package/dist/utils/naming.js.map +1 -0
- package/dist/utils/report.d.ts +56 -0
- package/dist/utils/report.d.ts.map +1 -0
- package/dist/utils/report.js +339 -0
- package/dist/utils/report.js.map +1 -0
- 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; → &, &aspect → &aspect
|
|
335
|
+
html = html.replace(/&(amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\da-fA-F]+;)/g, '&$1');
|
|
336
|
+
// Also handle cases where & in URLs was encoded to &
|
|
337
|
+
html = html.replace(/&(?=\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
|