juxscript 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +292 -0
  2. package/bin/cli.js +149 -0
  3. package/lib/adapters/base-adapter.js +35 -0
  4. package/lib/adapters/index.js +33 -0
  5. package/lib/adapters/mysql-adapter.js +65 -0
  6. package/lib/adapters/postgres-adapter.js +70 -0
  7. package/lib/adapters/sqlite-adapter.js +56 -0
  8. package/lib/components/app.ts +124 -0
  9. package/lib/components/button.ts +136 -0
  10. package/lib/components/card.ts +205 -0
  11. package/lib/components/chart.ts +125 -0
  12. package/lib/components/code.ts +242 -0
  13. package/lib/components/container.ts +282 -0
  14. package/lib/components/data.ts +105 -0
  15. package/lib/components/docs-data.json +1211 -0
  16. package/lib/components/error-handler.ts +285 -0
  17. package/lib/components/footer.ts +146 -0
  18. package/lib/components/header.ts +167 -0
  19. package/lib/components/hero.ts +170 -0
  20. package/lib/components/import.ts +430 -0
  21. package/lib/components/input.ts +175 -0
  22. package/lib/components/layout.ts +113 -0
  23. package/lib/components/list.ts +392 -0
  24. package/lib/components/main.ts +111 -0
  25. package/lib/components/menu.ts +170 -0
  26. package/lib/components/modal.ts +216 -0
  27. package/lib/components/nav.ts +136 -0
  28. package/lib/components/node.ts +200 -0
  29. package/lib/components/reactivity.js +104 -0
  30. package/lib/components/script.ts +152 -0
  31. package/lib/components/sidebar.ts +168 -0
  32. package/lib/components/style.ts +129 -0
  33. package/lib/components/table.ts +279 -0
  34. package/lib/components/tabs.ts +191 -0
  35. package/lib/components/theme.ts +97 -0
  36. package/lib/components/view.ts +174 -0
  37. package/lib/jux.ts +203 -0
  38. package/lib/layouts/default.css +260 -0
  39. package/lib/layouts/default.jux +8 -0
  40. package/lib/layouts/figma.css +334 -0
  41. package/lib/layouts/figma.jux +0 -0
  42. package/lib/layouts/notion.css +258 -0
  43. package/lib/styles/base-theme.css +186 -0
  44. package/lib/styles/dark-theme.css +144 -0
  45. package/lib/styles/global.css +1131 -0
  46. package/lib/styles/light-theme.css +144 -0
  47. package/lib/styles/tokens/dark.css +86 -0
  48. package/lib/styles/tokens/light.css +86 -0
  49. package/lib/themes/dark.css +86 -0
  50. package/lib/themes/light.css +86 -0
  51. package/lib/utils/path-resolver.js +23 -0
  52. package/machinery/compiler.js +262 -0
  53. package/machinery/doc-generator.js +160 -0
  54. package/machinery/generators/css.js +128 -0
  55. package/machinery/generators/html.js +108 -0
  56. package/machinery/imports.js +155 -0
  57. package/machinery/server.js +185 -0
  58. package/machinery/validators/file-validator.js +123 -0
  59. package/machinery/watcher.js +148 -0
  60. package/package.json +58 -0
  61. package/types/globals.d.ts +16 -0
@@ -0,0 +1,262 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import esbuild from 'esbuild';
4
+ import { generateDocs } from './doc-generator.js';
5
+
6
+ export async function compileJuxFile(juxFilePath, options = {}) {
7
+ const { distDir, projectRoot, isServe = false } = options;
8
+
9
+ const relativePath = path.relative(projectRoot, juxFilePath);
10
+ const parsedPath = path.parse(relativePath);
11
+
12
+ // Output paths
13
+ const outputDir = path.join(distDir, parsedPath.dir);
14
+ const jsOutputPath = path.join(outputDir, `${parsedPath.name}.js`);
15
+ const htmlOutputPath = path.join(outputDir, `${parsedPath.name}.html`);
16
+
17
+ // Ensure output directory exists
18
+ if (!fs.existsSync(outputDir)) {
19
+ fs.mkdirSync(outputDir, { recursive: true });
20
+ }
21
+
22
+ console.log(`📝 Compiling: ${relativePath}`);
23
+
24
+ // Read the .jux file
25
+ const juxContent = fs.readFileSync(juxFilePath, 'utf-8');
26
+
27
+ // Calculate depth for relative paths
28
+ const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
29
+ const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
30
+ const styleBasePath = depth === 0 ? './lib/styles/' : '../'.repeat(depth) + 'lib/styles/';
31
+
32
+ // Transform imports
33
+ let transformedContent = juxContent;
34
+
35
+ // Replace common import patterns with calculated path
36
+ transformedContent = transformedContent.replace(
37
+ /from\s+['"]\.\.?\/lib\/jux\.js['"]/g,
38
+ `from '${libPath}'`
39
+ );
40
+
41
+ // Only inject import if:
42
+ // 1. File is not empty (ignoring whitespace and comments)
43
+ // 2. File uses 'jux.' but has no import statement
44
+ const contentWithoutComments = transformedContent
45
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
46
+ .replace(/\/\/.*/g, '') // Remove line comments
47
+ .trim();
48
+
49
+ const hasContent = contentWithoutComments.length > 0;
50
+ const usesJux = /\bjux\./g.test(contentWithoutComments);
51
+ const hasImport = /import\s+.*from/.test(transformedContent);
52
+
53
+ if (hasContent && usesJux && !hasImport) {
54
+ transformedContent = `import { jux } from '${libPath}';\n\n${transformedContent}`;
55
+ }
56
+
57
+ // Write the transformed JS
58
+ fs.writeFileSync(jsOutputPath, transformedContent);
59
+
60
+ console.log(` ✓ JS: ${path.relative(projectRoot, jsOutputPath)}`);
61
+
62
+ // Generate HTML with correct script path
63
+ const scriptPath = `./${parsedPath.name}.js`;
64
+
65
+ const html = `<!DOCTYPE html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
70
+ <title>${parsedPath.name}</title>
71
+
72
+ <!-- JUX Core Styles -->
73
+ <link rel="stylesheet" href="${styleBasePath}tokens/dark.css">
74
+ <link rel="stylesheet" href="${styleBasePath}global.css">
75
+ </head>
76
+ <body data-theme="dark">
77
+ <!-- App container -->
78
+ <div id="app" data-jux-page="${parsedPath.name}"></div>
79
+
80
+ <!-- Main content -->
81
+ <div id="appmain"></div>
82
+
83
+ <!-- Modal overlay -->
84
+ <div id="appmodal" aria-hidden="true" role="dialog"></div>
85
+
86
+ <script type="module" src="${scriptPath}"></script>
87
+ ${isServe ? `
88
+ <!-- Hot reload -->
89
+ <script type="module">
90
+ const ws = new WebSocket('ws://localhost:3001');
91
+ ws.onmessage = (msg) => {
92
+ const data = JSON.parse(msg.data);
93
+ if (data.type === 'reload') {
94
+ console.log('🔄 Reloading page...');
95
+ location.reload();
96
+ }
97
+ };
98
+ ws.onerror = () => console.warn('⚠️ WebSocket connection failed');
99
+ </script>
100
+ ` : ''}
101
+ </body>
102
+ </html>`;
103
+
104
+ fs.writeFileSync(htmlOutputPath, html);
105
+ console.log(` ✓ HTML: ${path.relative(projectRoot, htmlOutputPath)}`);
106
+
107
+ return { jsOutputPath, htmlOutputPath };
108
+ }
109
+
110
+ export async function copyLibToOutput(projectRoot, distDir) {
111
+ // Simplified lib path resolution
112
+ const libSrc = path.resolve(projectRoot, '../lib');
113
+
114
+ if (!fs.existsSync(libSrc)) {
115
+ throw new Error(`lib/ directory not found at ${libSrc}`);
116
+ }
117
+
118
+ const libDest = path.join(distDir, 'lib');
119
+
120
+ console.log('📦 Building TypeScript library...');
121
+ console.log(` From: ${libSrc}`);
122
+ console.log(` To: ${libDest}`);
123
+
124
+ if (fs.existsSync(libDest)) {
125
+ fs.rmSync(libDest, { recursive: true });
126
+ }
127
+
128
+ fs.mkdirSync(libDest, { recursive: true });
129
+
130
+ // Find all TypeScript entry points
131
+ const tsFiles = findFiles(libSrc, '.ts');
132
+
133
+ if (tsFiles.length === 0) {
134
+ console.warn('⚠️ No TypeScript files found in lib/');
135
+ return;
136
+ }
137
+
138
+ console.log(` Found ${tsFiles.length} TypeScript files`);
139
+
140
+ // Build all TypeScript files with esbuild
141
+ try {
142
+ await esbuild.build({
143
+ entryPoints: tsFiles,
144
+ bundle: false,
145
+ format: 'esm',
146
+ outdir: libDest,
147
+ outbase: libSrc,
148
+ platform: 'browser',
149
+ target: 'es2020',
150
+ loader: {
151
+ '.ts': 'ts'
152
+ },
153
+ logLevel: 'warning'
154
+ });
155
+
156
+ console.log(' ✓ TypeScript compiled to JavaScript');
157
+
158
+ // Copy non-TS files (CSS, HTML, etc.)
159
+ console.log(' Copying lib assets...');
160
+ copyNonTsFiles(libSrc, libDest);
161
+ console.log(' ✓ Lib assets copied');
162
+
163
+ } catch (err) {
164
+ console.error('❌ Failed to build TypeScript:', err.message);
165
+ throw err;
166
+ }
167
+
168
+ console.log('✅ Library ready\n');
169
+ }
170
+
171
+ export async function copyProjectAssets(projectRoot, distDir) {
172
+ console.log('📦 Copying project assets...');
173
+
174
+ // Find all CSS and JS files in project root (excluding node_modules, dist, .git)
175
+ const allFiles = [];
176
+ findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
177
+
178
+ console.log(` Found ${allFiles.length} asset file(s)`);
179
+
180
+ for (const srcPath of allFiles) {
181
+ const relativePath = path.relative(projectRoot, srcPath);
182
+ const destPath = path.join(distDir, relativePath);
183
+ const destDir = path.dirname(destPath);
184
+
185
+ // Create destination directory if needed
186
+ if (!fs.existsSync(destDir)) {
187
+ fs.mkdirSync(destDir, { recursive: true });
188
+ }
189
+
190
+ // Copy file
191
+ fs.copyFileSync(srcPath, destPath);
192
+ console.log(` ✓ ${relativePath}`);
193
+ }
194
+
195
+ console.log('✅ Project assets copied\n');
196
+ }
197
+
198
+ function findFiles(dir, extension, fileList = []) {
199
+ const files = fs.readdirSync(dir);
200
+
201
+ files.forEach(file => {
202
+ const filePath = path.join(dir, file);
203
+ const stat = fs.statSync(filePath);
204
+
205
+ if (stat.isDirectory()) {
206
+ findFiles(filePath, extension, fileList);
207
+ } else if (file.endsWith(extension)) {
208
+ fileList.push(filePath);
209
+ }
210
+ });
211
+
212
+ return fileList;
213
+ }
214
+
215
+ function copyNonTsFiles(src, dest) {
216
+ const entries = fs.readdirSync(src, { withFileTypes: true });
217
+
218
+ for (const entry of entries) {
219
+ const srcPath = path.join(src, entry.name);
220
+ const destPath = path.join(dest, entry.name);
221
+
222
+ if (entry.isDirectory()) {
223
+ if (!fs.existsSync(destPath)) {
224
+ fs.mkdirSync(destPath, { recursive: true });
225
+ }
226
+ copyNonTsFiles(srcPath, destPath);
227
+ } else if (entry.isFile()) {
228
+ const ext = path.extname(entry.name);
229
+ // Copy CSS, JSON, and JS files (but not .ts files)
230
+ if (ext === '.css' || ext === '.json' || (ext === '.js' && !srcPath.endsWith('.ts'))) {
231
+ fs.copyFileSync(srcPath, destPath);
232
+ console.log(` → Copied: ${path.relative(src, srcPath)}`);
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'dist', '.git', 'lib']) {
239
+ if (!fs.existsSync(dir)) return fileList;
240
+
241
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
242
+
243
+ for (const entry of entries) {
244
+ const fullPath = path.join(dir, entry.name);
245
+
246
+ if (entry.isDirectory()) {
247
+ // Skip excluded directories
248
+ if (excludeDirs.includes(entry.name)) {
249
+ continue;
250
+ }
251
+ findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
252
+ } else {
253
+ // Check if file has one of the desired extensions
254
+ const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
255
+ if (hasExtension) {
256
+ fileList.push(fullPath);
257
+ }
258
+ }
259
+ }
260
+
261
+ return fileList;
262
+ }
@@ -0,0 +1,160 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+
5
+ /**
6
+ * Generate documentation from TypeScript component files
7
+ */
8
+ export async function generateDocs(projectRoot) {
9
+ const libRoot = path.resolve(projectRoot, '../lib');
10
+ const componentsDir = path.join(libRoot, 'components');
11
+
12
+ console.log(` Scanning: ${componentsDir}`);
13
+
14
+ // Find all component TypeScript files
15
+ const componentFiles = glob.sync('*.ts', {
16
+ cwd: componentsDir,
17
+ absolute: true,
18
+ ignore: ['reactivity.js', 'error-handler.ts', 'docs.ts']
19
+ });
20
+
21
+ console.log(` Found ${componentFiles.length} component files`);
22
+
23
+ const components = [];
24
+
25
+ for (const filePath of componentFiles) {
26
+ const content = fs.readFileSync(filePath, 'utf-8');
27
+ const componentDoc = parseComponentFile(content, filePath);
28
+
29
+ if (componentDoc) {
30
+ components.push(componentDoc);
31
+ console.log(` ✓ ${componentDoc.name}`);
32
+ }
33
+ }
34
+
35
+ // Sort by category and name
36
+ components.sort((a, b) => {
37
+ const categoryCompare = (a.category || 'ZZZ').localeCompare(b.category || 'ZZZ');
38
+ if (categoryCompare !== 0) return categoryCompare;
39
+ return a.name.localeCompare(b.name);
40
+ });
41
+
42
+ // Generate the docs data
43
+ const docsData = {
44
+ components,
45
+ version: '1.0.0',
46
+ lastUpdated: new Date().toISOString()
47
+ };
48
+
49
+ // Write to JSON file
50
+ const outputPath = path.join(componentsDir, 'docs-data.json');
51
+ fs.writeFileSync(outputPath, JSON.stringify(docsData, null, 2));
52
+
53
+ console.log(` Generated: ${outputPath}`);
54
+
55
+ return docsData;
56
+ }
57
+
58
+ /**
59
+ * Parse a single component file
60
+ */
61
+ function parseComponentFile(content, filePath) {
62
+ const fileName = path.basename(filePath, '.ts');
63
+ const className = fileName.charAt(0).toUpperCase() + fileName.slice(1);
64
+
65
+ // Extract category from file content or infer
66
+ const category = inferCategory(className, content);
67
+
68
+ // Extract description from class JSDoc
69
+ const descMatch = content.match(/\/\*\*\s*\n\s*\*\s*([^\n*]+)/);
70
+ const description = descMatch ? descMatch[1].trim() : `${className} component`;
71
+
72
+ // Extract constructor/factory pattern
73
+ const factoryMatch = content.match(/export function\s+\w+\(([^)]*)\)/);
74
+ const constructorSig = factoryMatch
75
+ ? `jux.${fileName}(${factoryMatch[1]})`
76
+ : `new ${className}()`;
77
+
78
+ // Extract fluent methods
79
+ const fluentMethods = extractFluentMethods(content, className);
80
+
81
+ // Extract usage example from JSDoc
82
+ const exampleMatch = content.match(/\*\s+Usage:\s*\n\s*\*\s+(.+?)(?:\n|$)/);
83
+ let example = exampleMatch
84
+ ? exampleMatch[1].trim()
85
+ : `jux.${fileName}('id').render()`;
86
+
87
+ return {
88
+ name: className,
89
+ category,
90
+ description,
91
+ constructor: constructorSig,
92
+ fluentMethods,
93
+ example
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Extract fluent API methods from class
99
+ */
100
+ function extractFluentMethods(content, className) {
101
+ const methods = [];
102
+
103
+ // Match method patterns: methodName(params): this
104
+ const methodRegex = /^\s*(\w+)\(([^)]*)\):\s*this\s*\{/gm;
105
+ let match;
106
+
107
+ while ((match = methodRegex.exec(content)) !== null) {
108
+ const methodName = match[1];
109
+ const params = match[2];
110
+
111
+ // Skip private methods and constructor
112
+ if (methodName.startsWith('_') || methodName === 'constructor') continue;
113
+
114
+ // Clean up params
115
+ const cleanParams = params
116
+ .split(',')
117
+ .map(p => {
118
+ const parts = p.trim().split(':');
119
+ return parts[0].trim();
120
+ })
121
+ .filter(p => p)
122
+ .join(', ');
123
+
124
+ methods.push({
125
+ name: methodName,
126
+ params: cleanParams ? `(${cleanParams})` : '()',
127
+ returns: 'this',
128
+ description: `Set ${methodName}`
129
+ });
130
+ }
131
+
132
+ // Also look for render methods
133
+ const renderMatch = content.match(/^\s*render\(([^)]*)\):\s*(\w+)/m);
134
+ if (renderMatch && !methods.find(m => m.name === 'render')) {
135
+ methods.push({
136
+ name: 'render',
137
+ params: renderMatch[1] ? `(${renderMatch[1]})` : '()',
138
+ returns: renderMatch[2],
139
+ description: 'Render component to DOM'
140
+ });
141
+ }
142
+
143
+ return methods;
144
+ }
145
+
146
+ /**
147
+ * Infer category from component name or content
148
+ */
149
+ function inferCategory(name, content) {
150
+ const dataComponents = ['Table', 'List', 'Chart', 'Data'];
151
+ const coreComponents = ['App', 'Layout', 'Theme', 'Style', 'Script', 'Import'];
152
+
153
+ if (dataComponents.some(dc => name.includes(dc))) {
154
+ return 'Data Components';
155
+ }
156
+ if (coreComponents.includes(name)) {
157
+ return 'Core';
158
+ }
159
+ return 'UI Components';
160
+ }
@@ -0,0 +1,128 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import CleanCSS from 'clean-css';
5
+ import { FileValidator } from '../validators/file-validator.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ /**
11
+ * Generates CSS content from Jux configuration
12
+ * Handles: global.css, themes, imports, styleImports, and styleInline blocks
13
+ */
14
+ export function generateCSS(juxConfig, layoutParsed = null) {
15
+ const fileValidator = new FileValidator();
16
+ let cssOutput = '';
17
+
18
+ // 1. Add global.css (always first)
19
+ const globalCssPath = path.join(__dirname, '../../lib/global.css');
20
+ if (fs.existsSync(globalCssPath)) {
21
+ cssOutput += `/* Global Styles */\n`;
22
+ cssOutput += fs.readFileSync(globalCssPath, 'utf-8') + '\n\n';
23
+ console.log(` ✓ Included global.css`);
24
+ }
25
+
26
+ // 2. Add theme CSS (layout theme first, then page theme if different)
27
+ const themesToLoad = [];
28
+
29
+ if (layoutParsed?.config?.theme) {
30
+ themesToLoad.push({ theme: layoutParsed.config.theme, source: 'layout' });
31
+ }
32
+
33
+ if (juxConfig.theme && juxConfig.theme !== layoutParsed?.config?.theme) {
34
+ themesToLoad.push({ theme: juxConfig.theme, source: 'page' });
35
+ }
36
+
37
+ for (const { theme, source } of themesToLoad) {
38
+ const themePath = path.join(__dirname, '../../lib/themes', `${theme}.css`);
39
+ if (fs.existsSync(themePath)) {
40
+ cssOutput += `/* Theme: ${theme} (${source}) */\n`;
41
+ cssOutput += fs.readFileSync(themePath, 'utf-8') + '\n\n';
42
+ console.log(` ✓ Included theme: ${theme} (${source})`);
43
+ } else {
44
+ console.warn(` ⚠️ Theme not found: ${theme}`);
45
+ }
46
+ }
47
+
48
+ // 3. Process @import directives (CSS files only from layout, then page)
49
+ const allImports = [
50
+ ...(layoutParsed?.config?.import || []),
51
+ ...(juxConfig.import || [])
52
+ ];
53
+
54
+ if (allImports.length > 0) {
55
+ const { categorized } = fileValidator.categorizeImports(allImports);
56
+
57
+ // Only process CSS files
58
+ for (const cssImport of categorized.css) {
59
+ const resolvedPath = path.join(__dirname, '../../', cssImport);
60
+ if (fs.existsSync(resolvedPath)) {
61
+ cssOutput += `/* Import: ${cssImport} */\n`;
62
+ cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
63
+ console.log(` ✓ Included import: ${cssImport}`);
64
+ } else {
65
+ console.warn(` ⚠️ Import not found: ${cssImport} (resolved to ${resolvedPath})`);
66
+ }
67
+ }
68
+
69
+ // Log skipped JS imports (will be handled in HTML)
70
+ if (categorized.js.length > 0) {
71
+ console.log(` ℹ️ Skipped JS imports (will be added to HTML): ${categorized.js.length} file(s)`);
72
+ }
73
+ }
74
+
75
+ // 4. Process @style imports (CSS file references from layout, then page)
76
+ const allStyleImports = [
77
+ ...(layoutParsed?.config?.styleImports || []),
78
+ ...(juxConfig.styleImports || [])
79
+ ];
80
+
81
+ for (const styleImport of allStyleImports) {
82
+ // Handle URLs (CDN)
83
+ if (styleImport.startsWith('http://') || styleImport.startsWith('https://')) {
84
+ cssOutput += `/* External CSS: ${styleImport} */\n`;
85
+ cssOutput += `@import url('${styleImport}');\n\n`;
86
+ console.log(` ✓ Added CDN import: ${styleImport}`);
87
+ } else {
88
+ // Handle local files
89
+ const resolvedPath = path.join(__dirname, '../../', styleImport);
90
+ if (fs.existsSync(resolvedPath)) {
91
+ cssOutput += `/* Style Import: ${styleImport} */\n`;
92
+ cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
93
+ console.log(` ✓ Included style import: ${styleImport}`);
94
+ } else {
95
+ console.warn(` ⚠️ Style import not found: ${styleImport} (resolved to ${resolvedPath})`);
96
+ }
97
+ }
98
+ }
99
+
100
+ // 5. Process inline @style blocks (layout first, then page)
101
+ const allInlineStyles = [
102
+ ...(layoutParsed?.config?.styleInline || []),
103
+ ...(juxConfig.styleInline || [])
104
+ ];
105
+
106
+ if (allInlineStyles.length > 0) {
107
+ cssOutput += `/* Inline Styles (${allInlineStyles.length} block(s)) */\n`;
108
+
109
+ allInlineStyles.forEach((styleBlock, index) => {
110
+ const source = index < (layoutParsed?.config?.styleInline?.length || 0) ? 'layout' : 'page';
111
+
112
+ try {
113
+ const validatedStyle = fileValidator.validateStyleContent(styleBlock, `inline block #${index + 1}`);
114
+
115
+ if (!fileValidator.isEmptyStyle(validatedStyle)) {
116
+ cssOutput += `/* Inline Block #${index + 1} (${source}) */\n`;
117
+ cssOutput += validatedStyle + '\n\n';
118
+ }
119
+ } catch (error) {
120
+ console.error(` ❌ ${error.message}`);
121
+ }
122
+ });
123
+
124
+ console.log(` ✓ Included ${allInlineStyles.length} inline style block(s)`);
125
+ }
126
+
127
+ return cssOutput.trim();
128
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Generates a simple HTML wrapper for a .jux file
6
+ * No directives, no parsing - just a clean HTML shell
7
+ */
8
+ export function generateHTML(fileName, options = {}) {
9
+ const { fileDir = '.', pathPrefix = './', isServe = false } = options;
10
+
11
+ const prefix = pathPrefix;
12
+
13
+ // Page script path
14
+ const pageScriptPath = `${prefix}${fileName}.js`;
15
+
16
+ // Build app structure with semantic nodes
17
+ const appStructure = buildAppStructure(fileName);
18
+
19
+ // Hot reload script (only in serve mode)
20
+ const hotReloadScript = isServe ? `
21
+ <!-- Hot Reload -->
22
+ <script>
23
+ (function() {
24
+ const ws = new WebSocket('ws://' + location.host);
25
+ ws.onmessage = (event) => {
26
+ const data = JSON.parse(event.data);
27
+ if (data.type === 'reload') {
28
+ console.log('🔄 Reloading...');
29
+ location.reload();
30
+ }
31
+ };
32
+ ws.onclose = () => console.log('🔌 Hot reload disconnected');
33
+ })();
34
+ </script>` : '';
35
+
36
+ // Build complete HTML
37
+ const html = `<!DOCTYPE html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="UTF-8">
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
+ <title>${fileName}</title>
43
+
44
+ <!-- JUX Core Styles -->
45
+ <link rel="stylesheet" href="${prefix}lib/styles/tokens/dark.css">
46
+ <link rel="stylesheet" href="${prefix}lib/styles/global.css">
47
+ </head>
48
+ <body data-theme="dark">
49
+ ${appStructure}
50
+
51
+ <!-- Page Script -->
52
+ <script type="module" src="${pageScriptPath}"></script>${hotReloadScript}
53
+ </body>
54
+ </html>`;
55
+
56
+ return html;
57
+ }
58
+
59
+ /**
60
+ * Build semantic app structure - Always includes all nodes
61
+ */
62
+ function buildAppStructure(pageName) {
63
+ const page = pageName || 'index';
64
+
65
+ return ` <!-- App Container -->
66
+ <div id="app" data-jux-page="${page}">
67
+ <!-- Header -->
68
+ <header id="appheader">
69
+ <div id="appheader-logo"></div>
70
+ <nav id="appheader-nav"></nav>
71
+ <div id="appheader-actions"></div>
72
+ </header>
73
+
74
+ <!-- Subheader (breadcrumbs, tabs, etc.) -->
75
+ <div id="appsubheader">
76
+ <div id="appsubheader-breadcrumbs"></div>
77
+ <div id="appsubheader-tabs"></div>
78
+ <div id="appsubheader-actions"></div>
79
+ </div>
80
+
81
+ <!-- Sidebar -->
82
+ <aside id="appsidebar">
83
+ <div id="appsidebar-header"></div>
84
+ <div id="appsidebar-content"></div>
85
+ <div id="appsidebar-footer"></div>
86
+ </aside>
87
+
88
+ <!-- Main content area -->
89
+ <main id="appmain"></main>
90
+
91
+ <!-- Footer -->
92
+ <footer id="appfooter">
93
+ <div id="appfooter-content"></div>
94
+ <div id="appfooter-legal"></div>
95
+ </footer>
96
+ </div>
97
+
98
+ <!-- Modal overlay -->
99
+ <div id="appmodal" aria-hidden="true" role="dialog">
100
+ <div id="appmodal-backdrop"></div>
101
+ <div id="appmodal-container">
102
+ <button id="appmodal-close" aria-label="Close modal">&times;</button>
103
+ <header id="appmodal-header"></header>
104
+ <div id="appmodal-content"></div>
105
+ <footer id="appmodal-footer"></footer>
106
+ </div>
107
+ </div>`;
108
+ }