juxscript 1.0.105 → 1.0.107

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.
@@ -19,10 +19,6 @@ export function Grid(id, options = {}) {
19
19
  };
20
20
  // @ts-ignore
21
21
  engine.injectCSS = (id, css) => skin.injectCSS(id, css);
22
- // --- Structural API ---
23
- // --- Tools ---
24
- // @ts-ignore
25
- engine.gridder = (active) => { engine.toggleGridder(active); return engine; };
26
22
  // --- Helpers ---
27
23
  // @ts-ignore
28
24
  engine.getCellId = (row, col) => `${id}-${row}-${col}`;
@@ -1,12 +1,150 @@
1
1
  import { JuxCompiler } from './compiler3.js';
2
2
  import path from 'path';
3
+ import fs from 'fs';
3
4
 
4
- // Resolve paths relative to CWD (user's project)
5
5
  const PROJECT_ROOT = process.cwd();
6
6
 
7
+ // ═══════════════════════════════════════════════════════════════
8
+ // LOAD CONFIG
9
+ // ═══════════════════════════════════════════════════════════════
10
+ const JUX_CONFIG_PATH = path.resolve(PROJECT_ROOT, 'juxconfig.js');
11
+ let rawConfig = {};
12
+
13
+ try {
14
+ rawConfig = (await import(JUX_CONFIG_PATH)).config;
15
+ console.log(`⚙️ Loaded config: ${JUX_CONFIG_PATH}`);
16
+ } catch (err) {
17
+ console.warn(`⚠️ No juxconfig.js found, using defaults`);
18
+ }
19
+
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // EXPLODE CONFIG INTO NAMED OBJECTS
22
+ // ═══════════════════════════════════════════════════════════════
23
+ const directories = {
24
+ source: rawConfig.directories?.source || './jux',
25
+ distribution: rawConfig.directories?.distribution || './.jux-dist',
26
+ themes: rawConfig.directories?.themes || './themes',
27
+ layouts: rawConfig.directories?.layouts || './themes/layouts',
28
+ assets: rawConfig.directories?.assets || './themes/assets'
29
+ };
30
+
31
+ const defaults = {
32
+ httpPort: rawConfig.defaults?.httpPort || 3000,
33
+ wsPort: rawConfig.defaults?.wsPort || 3001,
34
+ autoRoute: rawConfig.defaults?.autoRoute ?? true,
35
+ layout: rawConfig.defaults?.layout || null,
36
+ theme: rawConfig.defaults?.theme || null
37
+ };
38
+
39
+ // Resolve absolute paths
40
+ const paths = {
41
+ source: path.resolve(PROJECT_ROOT, directories.source),
42
+ distribution: path.resolve(PROJECT_ROOT, directories.distribution),
43
+ themes: path.resolve(PROJECT_ROOT, directories.source, directories.themes),
44
+ layouts: path.resolve(PROJECT_ROOT, directories.source, directories.layouts),
45
+ assets: path.resolve(PROJECT_ROOT, directories.source, directories.assets)
46
+ };
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // VALIDATE DIRECTORIES
50
+ // ═══════════════════════════════════════════════════════════════
51
+ console.log(`\n📁 Directory Check:`);
52
+
53
+ const dirStatus = {};
54
+ for (const [name, dirPath] of Object.entries(paths)) {
55
+ if (name === 'distribution') continue;
56
+
57
+ const exists = fs.existsSync(dirPath);
58
+ dirStatus[name] = exists;
59
+
60
+ const icon = exists ? '✓' : '✗';
61
+ const suffix = exists ? '' : ' (will be skipped)';
62
+ console.log(` ${icon} ${name}: ${dirPath}${suffix}`);
63
+ }
64
+
65
+ // ═══════════════════════════════════════════════════════════════
66
+ // RESOLVE DEFAULT LAYOUT & THEME
67
+ // ═══════════════════════════════════════════════════════════════
68
+ function resolveFile(filename, ...searchPaths) {
69
+ if (!filename) return null;
70
+
71
+ // First try the filename as-is
72
+ if (fs.existsSync(filename)) {
73
+ return path.resolve(filename);
74
+ }
75
+
76
+ // Try each search path
77
+ for (const searchPath of searchPaths) {
78
+ if (!searchPath || !fs.existsSync(searchPath)) continue;
79
+
80
+ const fullPath = path.resolve(searchPath, filename);
81
+ if (fs.existsSync(fullPath)) {
82
+ return fullPath;
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ console.log(`\n🎨 Defaults Resolution:`);
90
+
91
+ // Resolve layout
92
+ const layoutPath = defaults.layout ? resolveFile(
93
+ defaults.layout,
94
+ paths.layouts,
95
+ paths.source,
96
+ PROJECT_ROOT
97
+ ) : null;
98
+
99
+ if (defaults.layout) {
100
+ if (layoutPath) {
101
+ console.log(` ✓ layout: ${layoutPath}`);
102
+ } else {
103
+ console.log(` ✗ layout: "${defaults.layout}" not found, will be skipped`);
104
+ }
105
+ } else {
106
+ console.log(` - layout: not configured`);
107
+ }
108
+
109
+ // Resolve theme
110
+ const themePath = defaults.theme ? resolveFile(
111
+ defaults.theme,
112
+ paths.themes,
113
+ paths.source,
114
+ PROJECT_ROOT
115
+ ) : null;
116
+
117
+ if (defaults.theme) {
118
+ if (themePath) {
119
+ console.log(` ✓ theme: ${themePath}`);
120
+ } else {
121
+ console.log(` ✗ theme: "${defaults.theme}" not found, will be skipped`);
122
+ }
123
+ } else {
124
+ console.log(` - theme: not configured`);
125
+ }
126
+
127
+ // ═══════════════════════════════════════════════════════════════
128
+ // VALIDATE SOURCE DIRECTORY EXISTS
129
+ // ═══════════════════════════════════════════════════════════════
130
+ if (!dirStatus.source) {
131
+ console.error(`\n❌ Source directory not found: ${paths.source}`);
132
+ console.error(` Create the directory or update juxconfig.js`);
133
+ process.exit(1);
134
+ }
135
+
136
+ // ═══════════════════════════════════════════════════════════════
137
+ // RUN BUILD
138
+ // ═══════════════════════════════════════════════════════════════
139
+ console.log(`\n`);
140
+
7
141
  const compiler = new JuxCompiler({
8
- srcDir: path.resolve(PROJECT_ROOT, 'jux'),
9
- distDir: path.resolve(PROJECT_ROOT, '.jux-dist')
142
+ srcDir: paths.source,
143
+ distDir: paths.distribution,
144
+ layoutPath,
145
+ themePath,
146
+ defaults,
147
+ paths
10
148
  });
11
149
 
12
150
  compiler.build()
@@ -13,6 +13,10 @@ export class JuxCompiler {
13
13
  this.config = config;
14
14
  this.srcDir = config.srcDir || './jux';
15
15
  this.distDir = config.distDir || './.jux-dist';
16
+ this.layoutPath = config.layoutPath || null;
17
+ this.themePath = config.themePath || null;
18
+ this.defaults = config.defaults || {};
19
+ this.paths = config.paths || {};
16
20
  this._juxscriptExports = null;
17
21
  this._juxscriptPath = null;
18
22
  }
@@ -61,7 +65,7 @@ export class JuxCompiler {
61
65
 
62
66
  scanFiles() {
63
67
  const files = fs.readdirSync(this.srcDir)
64
- .filter(f => f.endsWith('.jux') || f.endsWith('.js'));
68
+ .filter(f => (f.endsWith('.jux') || f.endsWith('.js')) && !this.isAssetFile(f));
65
69
 
66
70
  const views = [], dataModules = [], sharedModules = [];
67
71
 
@@ -81,6 +85,11 @@ export class JuxCompiler {
81
85
  return { views, dataModules, sharedModules };
82
86
  }
83
87
 
88
+ isAssetFile(filename) {
89
+ const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
90
+ return assetExtensions.some(ext => filename.endsWith(ext));
91
+ }
92
+
84
93
  removeImports(code) {
85
94
  return code
86
95
  .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
@@ -93,44 +102,81 @@ export class JuxCompiler {
93
102
  return name.replace(/[^a-zA-Z0-9]/g, '_');
94
103
  }
95
104
 
105
+ /**
106
+ * Copy all source assets (themes, layouts, assets folders) to dist
107
+ */
108
+ copySourceAssets() {
109
+ const copied = { themes: 0, layouts: 0, assets: 0, other: 0 };
110
+
111
+ const copyDir = (srcDir, destDir, category) => {
112
+ if (!fs.existsSync(srcDir)) return 0;
113
+
114
+ let count = 0;
115
+ fs.mkdirSync(destDir, { recursive: true });
116
+
117
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ const srcPath = path.join(srcDir, entry.name);
120
+ const destPath = path.join(destDir, entry.name);
121
+
122
+ if (entry.isDirectory()) {
123
+ count += copyDir(srcPath, destPath, category);
124
+ } else {
125
+ fs.copyFileSync(srcPath, destPath);
126
+ count++;
127
+ }
128
+ }
129
+ return count;
130
+ };
131
+
132
+ // Copy themes folder
133
+ if (this.paths.themes && fs.existsSync(this.paths.themes)) {
134
+ copied.themes = copyDir(this.paths.themes, path.join(this.distDir, 'themes'), 'themes');
135
+ }
136
+
137
+ // Copy layouts folder (if separate from themes)
138
+ if (this.paths.layouts && fs.existsSync(this.paths.layouts) && this.paths.layouts !== this.paths.themes) {
139
+ copied.layouts = copyDir(this.paths.layouts, path.join(this.distDir, 'layouts'), 'layouts');
140
+ }
141
+
142
+ // Copy assets folder
143
+ if (this.paths.assets && fs.existsSync(this.paths.assets)) {
144
+ copied.assets = copyDir(this.paths.assets, path.join(this.distDir, 'assets'), 'assets');
145
+ }
146
+
147
+ const total = copied.themes + copied.layouts + copied.assets;
148
+ if (total > 0) {
149
+ console.log(`📂 Copied source assets: ${copied.themes} themes, ${copied.layouts} layouts, ${copied.assets} assets`);
150
+ }
151
+
152
+ return copied;
153
+ }
154
+
96
155
  /**
97
156
  * Collect and bundle all component CSS into a single file
98
157
  */
99
158
  copyJuxscriptCss() {
100
159
  const componentsDir = this.getComponentsDir();
101
- if (!componentsDir) {
102
- console.log('⚠️ Components directory not found, skipping CSS bundling');
103
- return { bundlePath: null, components: [] };
104
- }
105
-
106
- console.log(`📂 Scanning for CSS in: ${componentsDir}`);
160
+ if (!componentsDir) return { bundlePath: null, components: [] };
107
161
 
108
162
  const cssContents = [];
109
163
  const components = [];
110
164
 
111
165
  const walkDir = (dir, relativePath = '') => {
112
166
  if (!fs.existsSync(dir)) return;
113
-
114
167
  const entries = fs.readdirSync(dir, { withFileTypes: true });
115
168
  for (const entry of entries) {
116
169
  const fullPath = path.join(dir, entry.name);
117
- const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
118
-
170
+ const relPath = path.join(relativePath, entry.name);
119
171
  if (entry.isDirectory()) {
120
172
  walkDir(fullPath, relPath);
121
173
  } else if (entry.name === 'structure.css') {
122
174
  const componentName = relativePath || 'base';
123
175
  const content = fs.readFileSync(fullPath, 'utf8');
124
-
125
- // Add comment header for each component's CSS
126
- cssContents.push(`/* ═══════════════════════════════════════════════════════════════
127
- * Component: ${componentName}
128
- * ═══════════════════════════════════════════════════════════════ */`);
176
+ cssContents.push(`/* Component: ${componentName} */\n`);
129
177
  cssContents.push(content);
130
- cssContents.push('');
131
-
178
+ cssContents.push('\n');
132
179
  components.push(componentName);
133
- console.log(` 📄 Found: ${componentName}/structure.css`);
134
180
  }
135
181
  }
136
182
  };
@@ -138,43 +184,38 @@ export class JuxCompiler {
138
184
  walkDir(componentsDir);
139
185
 
140
186
  if (cssContents.length === 0) {
141
- console.log('⚠️ No CSS files found to bundle');
142
187
  return { bundlePath: null, components: [] };
143
188
  }
144
189
 
145
- // Create bundled CSS file
146
190
  const cssDistDir = path.join(this.distDir, 'css');
147
191
  fs.mkdirSync(cssDistDir, { recursive: true });
148
192
 
149
- const bundleHeader = `/**
150
- * JUX Component Styles Bundle
151
- * Auto-generated - Do not edit directly
152
- *
153
- * Components included: ${components.join(', ')}
154
- * Generated: ${new Date().toISOString()}
155
- */
156
-
157
- `;
158
-
159
- const bundledCss = bundleHeader + cssContents.join('\n\n');
160
- const bundlePath = path.join(cssDistDir, 'jux-components.css');
193
+ const bundledCss = `/* JUX Components CSS Bundle */\n\n` + cssContents.join('\n');
194
+ fs.writeFileSync(path.join(cssDistDir, 'jux-components.css'), bundledCss);
161
195
 
162
- fs.writeFileSync(bundlePath, bundledCss);
196
+ console.log(`📄 Bundled ${components.length} component CSS files`);
163
197
 
164
- console.log(`✅ Bundled ${components.length} CSS files → css/jux-components.css (${(bundledCss.length / 1024).toFixed(1)}KB)`);
165
-
166
- return {
167
- bundlePath: './css/jux-components.css',
168
- components
169
- };
198
+ return { bundlePath: './css/jux-components.css', components };
170
199
  }
171
200
 
172
201
  /**
173
- * Generate single CSS link for the bundled stylesheet
202
+ * Generate CSS links including theme if configured
174
203
  */
175
204
  generateCssLinks(cssBundle) {
176
- if (!cssBundle.bundlePath) return '';
177
- return ` <link rel="stylesheet" href="${cssBundle.bundlePath}">`;
205
+ const links = [];
206
+
207
+ // JUX components CSS
208
+ if (cssBundle.bundlePath) {
209
+ links.push(` <link rel="stylesheet" href="${cssBundle.bundlePath}">`);
210
+ }
211
+
212
+ // User theme CSS
213
+ if (this.themePath) {
214
+ const themeName = path.basename(this.themePath);
215
+ links.push(` <link rel="stylesheet" href="./themes/${themeName}" id="jux-theme">`);
216
+ }
217
+
218
+ return links.join('\n');
178
219
  }
179
220
 
180
221
  async loadJuxscriptExports() {
@@ -186,7 +227,6 @@ export class JuxCompiler {
186
227
  const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
187
228
  const exports = new Set();
188
229
 
189
- // Parse export statements
190
230
  for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
191
231
  match[1].split(',').forEach(exp => {
192
232
  const name = exp.trim().split(/\s+as\s+/)[0].trim();
@@ -223,16 +263,12 @@ export class JuxCompiler {
223
263
  return issues;
224
264
  }
225
265
 
226
- const juxscriptImports = new Set();
227
266
  const allImports = new Set();
228
267
 
229
268
  walk(ast, {
230
269
  ImportDeclaration(node) {
231
270
  node.specifiers.forEach(spec => {
232
271
  allImports.add(spec.local.name);
233
- if (node.source.value === 'juxscript') {
234
- juxscriptImports.add(spec.local.name);
235
- }
236
272
  });
237
273
  }
238
274
  });
@@ -259,6 +295,17 @@ export class JuxCompiler {
259
295
  return issues;
260
296
  }
261
297
 
298
+ /**
299
+ * Extract layout function name from layout file
300
+ */
301
+ getLayoutFunctionName() {
302
+ if (!this.layoutPath || !fs.existsSync(this.layoutPath)) return null;
303
+
304
+ const content = fs.readFileSync(this.layoutPath, 'utf8');
305
+ const match = content.match(/export\s+function\s+(\w+)/);
306
+ return match ? match[1] : null;
307
+ }
308
+
262
309
  generateEntryPoint(views, dataModules, sharedModules) {
263
310
  let entry = `// Auto-generated JUX entry point\n\n`;
264
311
  const allIssues = [];
@@ -273,6 +320,16 @@ export class JuxCompiler {
273
320
  }
274
321
  });
275
322
 
323
+ // Check layout file for juxscript imports too
324
+ if (this.layoutPath && fs.existsSync(this.layoutPath)) {
325
+ const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
326
+ for (const match of layoutContent.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
327
+ match[1].split(',').map(s => s.trim()).forEach(imp => {
328
+ if (imp) juxImports.add(imp);
329
+ });
330
+ }
331
+ }
332
+
276
333
  if (juxImports.size > 0) {
277
334
  entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
278
335
  }
@@ -292,6 +349,25 @@ export class JuxCompiler {
292
349
  entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
293
350
  }
294
351
 
352
+ // Add layout function if configured
353
+ const layoutFnName = this.getLayoutFunctionName();
354
+ if (layoutFnName && this.layoutPath) {
355
+ const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
356
+ const layoutCode = this.removeImports(layoutContent);
357
+ entry += `\n// --- DEFAULT LAYOUT ---\n`;
358
+ entry += layoutCode;
359
+ entry += `\n`;
360
+
361
+ // Store in source snapshot
362
+ const layoutFile = path.basename(this.layoutPath);
363
+ sourceSnapshot[layoutFile] = {
364
+ name: 'layout',
365
+ file: layoutFile,
366
+ content: layoutContent,
367
+ lines: layoutContent.split('\n')
368
+ };
369
+ }
370
+
295
371
  entry += `\n// --- VIEW FUNCTIONS ---\n`;
296
372
 
297
373
  views.forEach(v => {
@@ -320,6 +396,7 @@ export class JuxCompiler {
320
396
 
321
397
  this._sourceSnapshot = sourceSnapshot;
322
398
  this._validationIssues = allIssues;
399
+ this._layoutFnName = layoutFnName;
323
400
  entry += this._generateRouter(views);
324
401
  return entry;
325
402
  }
@@ -349,6 +426,11 @@ export class JuxCompiler {
349
426
  routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
350
427
  });
351
428
 
429
+ // Initialize layout call if configured
430
+ const layoutInit = this._layoutFnName
431
+ ? `\n// Initialize default layout\nif (typeof ${this._layoutFnName} === 'function') {\n ${this._layoutFnName}();\n}\n`
432
+ : '';
433
+
352
434
  return `
353
435
  // --- JUX SOURCE LOADER ---
354
436
  var __juxSources = null;
@@ -363,7 +445,6 @@ async function __juxLoadSources() {
363
445
  return __juxSources;
364
446
  }
365
447
 
366
- // Find source file from error stack
367
448
  function __juxFindSource(stack) {
368
449
  var match = stack.match(/render(\\w+)/);
369
450
  if (match) {
@@ -374,6 +455,14 @@ function __juxFindSource(stack) {
374
455
  }
375
456
  }
376
457
  }
458
+ // Also check for layout
459
+ if (stack.indexOf('Layout') > -1) {
460
+ for (var file in __juxSources || {}) {
461
+ if (file.indexOf('layout') > -1) {
462
+ return { file: file, source: __juxSources[file] };
463
+ }
464
+ }
465
+ }
377
466
  return null;
378
467
  }
379
468
 
@@ -385,39 +474,21 @@ var __juxErrorOverlay = {
385
474
  background: rgba(0, 0, 0, 0.4);
386
475
  display: flex; align-items: center; justify-content: center;
387
476
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
388
- opacity: 0;
389
- transition: opacity 0.2s ease-out;
390
- backdrop-filter: blur(2px);
477
+ opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
391
478
  }
392
479
  #__jux-error-overlay.visible { opacity: 1; }
393
480
  #__jux-error-overlay * { box-sizing: border-box; }
394
481
  .__jux-modal {
395
- background: #f8f9fa;
396
- border-radius: 4px;
482
+ background: #f8f9fa; border-radius: 4px;
397
483
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
398
- max-width: 80vw;
399
- width: 90%;
400
- max-height: 90vh;
401
- overflow: hidden;
402
- display: flex;
403
- flex-direction: column;
404
- transform: translateY(10px);
405
- transition: transform 0.2s ease-out;
484
+ max-width: 80vw; width: 90%; max-height: 90vh;
485
+ overflow: hidden; display: flex; flex-direction: column;
486
+ transform: translateY(10px); transition: transform 0.2s ease-out;
406
487
  }
407
488
  #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
408
- .__jux-header {
409
- background: #fff;
410
- padding: 20px 24px;
411
- border-bottom: 1px solid #e5e7eb;
412
- }
413
- .__jux-header h3 {
414
- margin: 0 0 6px; font-weight: 600; font-size: 11px;
415
- color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px;
416
- }
417
- .__jux-header h1 {
418
- margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626;
419
- line-height: 1.3;
420
- }
489
+ .__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
490
+ .__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
491
+ .__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
421
492
  .__jux-header .file-info { color: #6b7280; font-size: 13px; }
422
493
  .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
423
494
  .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
@@ -426,138 +497,73 @@ var __juxErrorOverlay = {
426
497
  .__jux-code-line.error { background: #fef2f2; }
427
498
  .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
428
499
  .__jux-code-line.context { background: #fefce8; }
429
- .__jux-line-num {
430
- min-width: 44px; padding: 4px 12px; text-align: right;
431
- color: #9ca3af; background: #f9fafb; user-select: none;
432
- border-right: 1px solid #e5e7eb; font-size: 12px;
433
- }
500
+ .__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
434
501
  .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
435
502
  .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
436
- .__jux-footer {
437
- padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb;
438
- display: flex; justify-content: space-between; align-items: center;
439
- }
503
+ .__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
440
504
  .__jux-tip { color: #6b7280; font-size: 12px; }
441
- .__jux-dismiss {
442
- background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
443
- padding: 8px 16px; border-radius: 6px; cursor: pointer;
444
- font-size: 13px; font-weight: 500; transition: background 0.15s;
445
- }
505
+ .__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
446
506
  .__jux-dismiss:hover { background: #e5e7eb; }
447
507
  .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
448
- .__jux-no-source pre {
449
- background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px;
450
- font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left;
451
- border: 1px solid #e5e7eb;
452
- }
508
+ .__jux-no-source pre { background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px; font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left; border: 1px solid #e5e7eb; }
453
509
  \`,
454
510
 
455
511
  show: async function(error, title) {
456
512
  title = title || 'Runtime Error';
457
-
458
513
  var existing = document.getElementById('__jux-error-overlay');
459
514
  if (existing) existing.remove();
460
-
461
515
  await __juxLoadSources();
462
516
 
463
517
  var overlay = document.createElement('div');
464
518
  overlay.id = '__jux-error-overlay';
465
-
466
519
  var stack = error && error.stack ? error.stack : '';
467
520
  var msg = error && error.message ? error.message : String(error);
468
-
469
521
  var found = __juxFindSource(stack);
470
- var sourceHtml = '';
471
- var fileInfo = '';
522
+ var sourceHtml = '', fileInfo = '';
472
523
 
473
524
  if (found && found.source && found.source.lines) {
474
525
  var lines = found.source.lines;
475
526
  fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
476
-
477
527
  var errorLineIndex = -1;
478
528
  var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
479
529
  if (errorMethod) {
480
530
  for (var i = 0; i < lines.length; i++) {
481
- if (lines[i].indexOf('.' + errorMethod[1]) > -1) {
482
- errorLineIndex = i;
483
- break;
484
- }
531
+ if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
485
532
  }
486
533
  }
487
-
488
534
  if (errorLineIndex === -1) {
489
535
  for (var i = 0; i < lines.length; i++) {
490
- if (lines[i].indexOf('throw ') > -1 || lines[i].indexOf('throw(') > -1) {
491
- errorLineIndex = i;
492
- break;
493
- }
536
+ if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
494
537
  }
495
538
  }
496
-
497
539
  var contextStart = Math.max(0, errorLineIndex - 3);
498
540
  var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
499
-
500
- if (errorLineIndex === -1) {
501
- contextStart = 0;
502
- contextEnd = Math.min(14, lines.length - 1);
503
- }
541
+ if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
504
542
 
505
543
  for (var i = contextStart; i <= contextEnd; i++) {
506
544
  var isError = (i === errorLineIndex);
507
545
  var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
508
546
  var lineClass = isError ? ' error' : (isContext ? ' context' : '');
509
547
  var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
510
- sourceHtml += '<div class="__jux-code-line' + lineClass + '">' +
511
- '<span class="__jux-line-num">' + (i + 1) + '</span>' +
512
- '<span class="__jux-line-code">' + lineCode + '</span>' +
513
- '</div>';
548
+ sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
514
549
  }
515
550
  } else {
516
- sourceHtml = '<div class="__jux-no-source">' +
517
- '<p>Could not locate source file for this error.</p>' +
518
- '<pre>' + stack + '</pre>' +
519
- '</div>';
551
+ sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
520
552
  }
521
553
 
522
- overlay.innerHTML = '<style>' + this.styles + '</style>' +
523
- '<div class="__jux-modal">' +
524
- '<div class="__jux-header">' +
525
- '<h3>' + title + '</h3>' +
526
- '<h1>' + msg + '</h1>' +
527
- fileInfo +
528
- '</div>' +
529
- '<div class="__jux-source">' +
530
- '<div class="__jux-code">' + sourceHtml + '</div>' +
531
- '</div>' +
532
- '<div class="__jux-footer">' +
533
- '<span class="__jux-tip">💡 Fix the error and save to reload</span>' +
534
- '<button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button>' +
535
- '</div>' +
536
- '</div>';
537
-
554
+ overlay.innerHTML = '<style>' + this.styles + '</style><div class="__jux-modal"><div class="__jux-header"><h3>' + title + '</h3><h1>' + msg + '</h1>' + fileInfo + '</div><div class="__jux-source"><div class="__jux-code">' + sourceHtml + '</div></div><div class="__jux-footer"><span class="__jux-tip">💡 Fix the error and save to reload</span><button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button></div></div>';
538
555
  document.body.appendChild(overlay);
539
-
540
- // Trigger transition
541
- requestAnimationFrame(function() {
542
- overlay.classList.add('visible');
543
- });
544
-
556
+ requestAnimationFrame(function() { overlay.classList.add('visible'); });
545
557
  console.error(title + ':', error);
546
558
  }
547
559
  };
548
560
 
549
- // Global error handlers
550
- window.addEventListener('error', function(e) {
551
- __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error');
552
- }, true);
553
-
554
- window.addEventListener('unhandledrejection', function(e) {
555
- __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection');
556
- }, true);
561
+ window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
562
+ window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
557
563
 
558
564
  // --- JUX ROUTER ---
559
565
  const routes = {\n${routeMap}};
560
-
566
+ ${layoutInit}
561
567
  async function navigate(path) {
562
568
  const view = routes[path];
563
569
  if (!view) {
@@ -565,15 +571,9 @@ async function navigate(path) {
565
571
  return;
566
572
  }
567
573
  document.getElementById('app').innerHTML = '';
568
-
569
574
  var overlay = document.getElementById('__jux-error-overlay');
570
575
  if (overlay) overlay.remove();
571
-
572
- try {
573
- await view();
574
- } catch (err) {
575
- __juxErrorOverlay.show(err, 'Jux Render Error');
576
- }
576
+ try { await view(); } catch (err) { __juxErrorOverlay.show(err, 'Jux Render Error'); }
577
577
  }
578
578
 
579
579
  document.addEventListener('click', e => {
@@ -608,9 +608,12 @@ navigate(location.pathname);
608
608
  fs.mkdirSync(this.distDir, { recursive: true });
609
609
 
610
610
  const { views, dataModules, sharedModules } = this.scanFiles();
611
- console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data\n`);
611
+ console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
612
+
613
+ // Copy source assets (themes, layouts, assets)
614
+ this.copySourceAssets();
612
615
 
613
- // Changed: Now returns bundle info instead of array of files
616
+ // Bundle juxscript component CSS
614
617
  const cssBundle = this.copyJuxscriptCss();
615
618
 
616
619
  // Copy data/shared modules to dist
@@ -624,7 +627,6 @@ navigate(location.pathname);
624
627
  const entryPath = path.join(this.distDir, 'entry.js');
625
628
  fs.writeFileSync(entryPath, entryContent);
626
629
 
627
- // Write source snapshot for dev tools
628
630
  const snapshotPath = path.join(this.distDir, '__jux_sources.json');
629
631
  fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
630
632
  console.log(`📸 Source snapshot written`);
@@ -658,7 +660,7 @@ navigate(location.pathname);
658
660
  return { success: false, errors: [{ message: err.message }], warnings: [] };
659
661
  }
660
662
 
661
- // Generate index.html with single CSS link
663
+ // Generate HTML with layout container sibling to app
662
664
  const html = `<!DOCTYPE html>
663
665
  <html lang="en">
664
666
  <head>
@@ -669,12 +671,12 @@ ${this.generateCssLinks(cssBundle)}
669
671
  <script type="module" src="./bundle.js"></script>
670
672
  </head>
671
673
  <body>
674
+ <div id="jux-layout"></div>
672
675
  <div id="app"></div>
673
676
  </body>
674
677
  </html>`;
675
678
  fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
676
679
 
677
- // Cleanup temp files
678
680
  fs.unlinkSync(entryPath);
679
681
  fs.rmSync(juxDistDir, { recursive: true, force: true });
680
682
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "./index.js",