juxscript 1.0.104 → 1.0.106

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,6 +102,56 @@ 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
  */
@@ -114,15 +173,9 @@ export class JuxCompiler {
114
173
  } else if (entry.name === 'structure.css') {
115
174
  const componentName = relativePath || 'base';
116
175
  const content = fs.readFileSync(fullPath, 'utf8');
117
-
118
- // Add comment header for each component's CSS
119
- cssContents.push(`/* ═══════════════════════════════════════════════════════════════
120
- * Component: ${componentName}
121
- * Source: ${relPath}
122
- * ═══════════════════════════════════════════════════════════════ */\n`);
176
+ cssContents.push(`/* Component: ${componentName} */\n`);
123
177
  cssContents.push(content);
124
178
  cssContents.push('\n');
125
-
126
179
  components.push(componentName);
127
180
  }
128
181
  }
@@ -134,39 +187,35 @@ export class JuxCompiler {
134
187
  return { bundlePath: null, components: [] };
135
188
  }
136
189
 
137
- // Create bundled CSS file
138
190
  const cssDistDir = path.join(this.distDir, 'css');
139
191
  fs.mkdirSync(cssDistDir, { recursive: true });
140
192
 
141
- const bundleHeader = `/**
142
- * JUX Component Styles Bundle
143
- * Auto-generated - Do not edit directly
144
- *
145
- * Components included:
146
- * ${components.map(c => ` - ${c}`).join('\n * ')}
147
- *
148
- * Generated: ${new Date().toISOString()}
149
- */\n\n`;
193
+ const bundledCss = `/* JUX Components CSS Bundle */\n\n` + cssContents.join('\n');
194
+ fs.writeFileSync(path.join(cssDistDir, 'jux-components.css'), bundledCss);
150
195
 
151
- const bundledCss = bundleHeader + cssContents.join('\n');
152
- const bundlePath = path.join(cssDistDir, 'jux-components.css');
196
+ console.log(`📄 Bundled ${components.length} component CSS files`);
153
197
 
154
- fs.writeFileSync(bundlePath, bundledCss);
155
-
156
- console.log(`📄 Bundled ${components.length} CSS files → css/jux-components.css`);
157
-
158
- return {
159
- bundlePath: './css/jux-components.css',
160
- components
161
- };
198
+ return { bundlePath: './css/jux-components.css', components };
162
199
  }
163
200
 
164
201
  /**
165
- * Generate single CSS link for the bundled stylesheet
202
+ * Generate CSS links including theme if configured
166
203
  */
167
204
  generateCssLinks(cssBundle) {
168
- if (!cssBundle.bundlePath) return '';
169
- 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');
170
219
  }
171
220
 
172
221
  async loadJuxscriptExports() {
@@ -178,7 +227,6 @@ export class JuxCompiler {
178
227
  const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
179
228
  const exports = new Set();
180
229
 
181
- // Parse export statements
182
230
  for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
183
231
  match[1].split(',').forEach(exp => {
184
232
  const name = exp.trim().split(/\s+as\s+/)[0].trim();
@@ -215,16 +263,12 @@ export class JuxCompiler {
215
263
  return issues;
216
264
  }
217
265
 
218
- const juxscriptImports = new Set();
219
266
  const allImports = new Set();
220
267
 
221
268
  walk(ast, {
222
269
  ImportDeclaration(node) {
223
270
  node.specifiers.forEach(spec => {
224
271
  allImports.add(spec.local.name);
225
- if (node.source.value === 'juxscript') {
226
- juxscriptImports.add(spec.local.name);
227
- }
228
272
  });
229
273
  }
230
274
  });
@@ -251,6 +295,17 @@ export class JuxCompiler {
251
295
  return issues;
252
296
  }
253
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
+
254
309
  generateEntryPoint(views, dataModules, sharedModules) {
255
310
  let entry = `// Auto-generated JUX entry point\n\n`;
256
311
  const allIssues = [];
@@ -265,6 +320,16 @@ export class JuxCompiler {
265
320
  }
266
321
  });
267
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
+
268
333
  if (juxImports.size > 0) {
269
334
  entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
270
335
  }
@@ -284,6 +349,25 @@ export class JuxCompiler {
284
349
  entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
285
350
  }
286
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
+
287
371
  entry += `\n// --- VIEW FUNCTIONS ---\n`;
288
372
 
289
373
  views.forEach(v => {
@@ -312,6 +396,7 @@ export class JuxCompiler {
312
396
 
313
397
  this._sourceSnapshot = sourceSnapshot;
314
398
  this._validationIssues = allIssues;
399
+ this._layoutFnName = layoutFnName;
315
400
  entry += this._generateRouter(views);
316
401
  return entry;
317
402
  }
@@ -341,6 +426,11 @@ export class JuxCompiler {
341
426
  routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
342
427
  });
343
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
+
344
434
  return `
345
435
  // --- JUX SOURCE LOADER ---
346
436
  var __juxSources = null;
@@ -355,7 +445,6 @@ async function __juxLoadSources() {
355
445
  return __juxSources;
356
446
  }
357
447
 
358
- // Find source file from error stack
359
448
  function __juxFindSource(stack) {
360
449
  var match = stack.match(/render(\\w+)/);
361
450
  if (match) {
@@ -366,6 +455,14 @@ function __juxFindSource(stack) {
366
455
  }
367
456
  }
368
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
+ }
369
466
  return null;
370
467
  }
371
468
 
@@ -377,39 +474,21 @@ var __juxErrorOverlay = {
377
474
  background: rgba(0, 0, 0, 0.4);
378
475
  display: flex; align-items: center; justify-content: center;
379
476
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
380
- opacity: 0;
381
- transition: opacity 0.2s ease-out;
382
- backdrop-filter: blur(2px);
477
+ opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
383
478
  }
384
479
  #__jux-error-overlay.visible { opacity: 1; }
385
480
  #__jux-error-overlay * { box-sizing: border-box; }
386
481
  .__jux-modal {
387
- background: #f8f9fa;
388
- border-radius: 4px;
482
+ background: #f8f9fa; border-radius: 4px;
389
483
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
390
- max-width: 80vw;
391
- width: 90%;
392
- max-height: 90vh;
393
- overflow: hidden;
394
- display: flex;
395
- flex-direction: column;
396
- transform: translateY(10px);
397
- 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;
398
487
  }
399
488
  #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
400
- .__jux-header {
401
- background: #fff;
402
- padding: 20px 24px;
403
- border-bottom: 1px solid #e5e7eb;
404
- }
405
- .__jux-header h3 {
406
- margin: 0 0 6px; font-weight: 600; font-size: 11px;
407
- color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px;
408
- }
409
- .__jux-header h1 {
410
- margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626;
411
- line-height: 1.3;
412
- }
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; }
413
492
  .__jux-header .file-info { color: #6b7280; font-size: 13px; }
414
493
  .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
415
494
  .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
@@ -418,138 +497,73 @@ var __juxErrorOverlay = {
418
497
  .__jux-code-line.error { background: #fef2f2; }
419
498
  .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
420
499
  .__jux-code-line.context { background: #fefce8; }
421
- .__jux-line-num {
422
- min-width: 44px; padding: 4px 12px; text-align: right;
423
- color: #9ca3af; background: #f9fafb; user-select: none;
424
- border-right: 1px solid #e5e7eb; font-size: 12px;
425
- }
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; }
426
501
  .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
427
502
  .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
428
- .__jux-footer {
429
- padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb;
430
- display: flex; justify-content: space-between; align-items: center;
431
- }
503
+ .__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
432
504
  .__jux-tip { color: #6b7280; font-size: 12px; }
433
- .__jux-dismiss {
434
- background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
435
- padding: 8px 16px; border-radius: 6px; cursor: pointer;
436
- font-size: 13px; font-weight: 500; transition: background 0.15s;
437
- }
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; }
438
506
  .__jux-dismiss:hover { background: #e5e7eb; }
439
507
  .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
440
- .__jux-no-source pre {
441
- background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px;
442
- font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left;
443
- border: 1px solid #e5e7eb;
444
- }
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; }
445
509
  \`,
446
510
 
447
511
  show: async function(error, title) {
448
512
  title = title || 'Runtime Error';
449
-
450
513
  var existing = document.getElementById('__jux-error-overlay');
451
514
  if (existing) existing.remove();
452
-
453
515
  await __juxLoadSources();
454
516
 
455
517
  var overlay = document.createElement('div');
456
518
  overlay.id = '__jux-error-overlay';
457
-
458
519
  var stack = error && error.stack ? error.stack : '';
459
520
  var msg = error && error.message ? error.message : String(error);
460
-
461
521
  var found = __juxFindSource(stack);
462
- var sourceHtml = '';
463
- var fileInfo = '';
522
+ var sourceHtml = '', fileInfo = '';
464
523
 
465
524
  if (found && found.source && found.source.lines) {
466
525
  var lines = found.source.lines;
467
526
  fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
468
-
469
527
  var errorLineIndex = -1;
470
528
  var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
471
529
  if (errorMethod) {
472
530
  for (var i = 0; i < lines.length; i++) {
473
- if (lines[i].indexOf('.' + errorMethod[1]) > -1) {
474
- errorLineIndex = i;
475
- break;
476
- }
531
+ if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
477
532
  }
478
533
  }
479
-
480
534
  if (errorLineIndex === -1) {
481
535
  for (var i = 0; i < lines.length; i++) {
482
- if (lines[i].indexOf('throw ') > -1 || lines[i].indexOf('throw(') > -1) {
483
- errorLineIndex = i;
484
- break;
485
- }
536
+ if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
486
537
  }
487
538
  }
488
-
489
539
  var contextStart = Math.max(0, errorLineIndex - 3);
490
540
  var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
491
-
492
- if (errorLineIndex === -1) {
493
- contextStart = 0;
494
- contextEnd = Math.min(14, lines.length - 1);
495
- }
541
+ if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
496
542
 
497
543
  for (var i = contextStart; i <= contextEnd; i++) {
498
544
  var isError = (i === errorLineIndex);
499
545
  var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
500
546
  var lineClass = isError ? ' error' : (isContext ? ' context' : '');
501
547
  var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
502
- sourceHtml += '<div class="__jux-code-line' + lineClass + '">' +
503
- '<span class="__jux-line-num">' + (i + 1) + '</span>' +
504
- '<span class="__jux-line-code">' + lineCode + '</span>' +
505
- '</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>';
506
549
  }
507
550
  } else {
508
- sourceHtml = '<div class="__jux-no-source">' +
509
- '<p>Could not locate source file for this error.</p>' +
510
- '<pre>' + stack + '</pre>' +
511
- '</div>';
551
+ sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
512
552
  }
513
553
 
514
- overlay.innerHTML = '<style>' + this.styles + '</style>' +
515
- '<div class="__jux-modal">' +
516
- '<div class="__jux-header">' +
517
- '<h3>' + title + '</h3>' +
518
- '<h1>' + msg + '</h1>' +
519
- fileInfo +
520
- '</div>' +
521
- '<div class="__jux-source">' +
522
- '<div class="__jux-code">' + sourceHtml + '</div>' +
523
- '</div>' +
524
- '<div class="__jux-footer">' +
525
- '<span class="__jux-tip">💡 Fix the error and save to reload</span>' +
526
- '<button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button>' +
527
- '</div>' +
528
- '</div>';
529
-
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>';
530
555
  document.body.appendChild(overlay);
531
-
532
- // Trigger transition
533
- requestAnimationFrame(function() {
534
- overlay.classList.add('visible');
535
- });
536
-
556
+ requestAnimationFrame(function() { overlay.classList.add('visible'); });
537
557
  console.error(title + ':', error);
538
558
  }
539
559
  };
540
560
 
541
- // Global error handlers
542
- window.addEventListener('error', function(e) {
543
- __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error');
544
- }, true);
545
-
546
- window.addEventListener('unhandledrejection', function(e) {
547
- __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection');
548
- }, 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);
549
563
 
550
564
  // --- JUX ROUTER ---
551
565
  const routes = {\n${routeMap}};
552
-
566
+ ${layoutInit}
553
567
  async function navigate(path) {
554
568
  const view = routes[path];
555
569
  if (!view) {
@@ -557,15 +571,9 @@ async function navigate(path) {
557
571
  return;
558
572
  }
559
573
  document.getElementById('app').innerHTML = '';
560
-
561
574
  var overlay = document.getElementById('__jux-error-overlay');
562
575
  if (overlay) overlay.remove();
563
-
564
- try {
565
- await view();
566
- } catch (err) {
567
- __juxErrorOverlay.show(err, 'Jux Render Error');
568
- }
576
+ try { await view(); } catch (err) { __juxErrorOverlay.show(err, 'Jux Render Error'); }
569
577
  }
570
578
 
571
579
  document.addEventListener('click', e => {
@@ -600,9 +608,12 @@ navigate(location.pathname);
600
608
  fs.mkdirSync(this.distDir, { recursive: true });
601
609
 
602
610
  const { views, dataModules, sharedModules } = this.scanFiles();
603
- 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();
604
615
 
605
- // Changed: Now returns bundle info instead of array of files
616
+ // Bundle juxscript component CSS
606
617
  const cssBundle = this.copyJuxscriptCss();
607
618
 
608
619
  // Copy data/shared modules to dist
@@ -616,7 +627,6 @@ navigate(location.pathname);
616
627
  const entryPath = path.join(this.distDir, 'entry.js');
617
628
  fs.writeFileSync(entryPath, entryContent);
618
629
 
619
- // Write source snapshot for dev tools
620
630
  const snapshotPath = path.join(this.distDir, '__jux_sources.json');
621
631
  fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
622
632
  console.log(`📸 Source snapshot written`);
@@ -650,7 +660,7 @@ navigate(location.pathname);
650
660
  return { success: false, errors: [{ message: err.message }], warnings: [] };
651
661
  }
652
662
 
653
- // Generate index.html with single CSS link
663
+ // Generate HTML with layout container sibling to app
654
664
  const html = `<!DOCTYPE html>
655
665
  <html lang="en">
656
666
  <head>
@@ -661,12 +671,12 @@ ${this.generateCssLinks(cssBundle)}
661
671
  <script type="module" src="./bundle.js"></script>
662
672
  </head>
663
673
  <body>
674
+ <div id="jux-layout"></div>
664
675
  <div id="app"></div>
665
676
  </body>
666
677
  </html>`;
667
678
  fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
668
679
 
669
- // Cleanup temp files
670
680
  fs.unlinkSync(entryPath);
671
681
  fs.rmSync(juxDistDir, { recursive: true, force: true });
672
682
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.104",
3
+ "version": "1.0.106",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "./index.js",