juxscript 1.1.3 → 1.1.4

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 (63) hide show
  1. package/machinery/build3.js +7 -91
  2. package/machinery/compiler3.js +3 -209
  3. package/package.json +19 -5
  4. package/lib/components/alert.ts +0 -200
  5. package/lib/components/app.ts +0 -247
  6. package/lib/components/badge.ts +0 -101
  7. package/lib/components/base/BaseComponent.ts +0 -421
  8. package/lib/components/base/FormInput.ts +0 -227
  9. package/lib/components/button.ts +0 -178
  10. package/lib/components/card.ts +0 -173
  11. package/lib/components/chart.ts +0 -231
  12. package/lib/components/checkbox.ts +0 -242
  13. package/lib/components/code.ts +0 -123
  14. package/lib/components/container.ts +0 -140
  15. package/lib/components/data.ts +0 -135
  16. package/lib/components/datepicker.ts +0 -234
  17. package/lib/components/dialog.ts +0 -172
  18. package/lib/components/divider.ts +0 -100
  19. package/lib/components/dropdown.ts +0 -186
  20. package/lib/components/element.ts +0 -267
  21. package/lib/components/fileupload.ts +0 -309
  22. package/lib/components/grid.ts +0 -291
  23. package/lib/components/guard.ts +0 -92
  24. package/lib/components/heading.ts +0 -96
  25. package/lib/components/helpers.ts +0 -41
  26. package/lib/components/hero.ts +0 -224
  27. package/lib/components/icon.ts +0 -178
  28. package/lib/components/icons.ts +0 -464
  29. package/lib/components/include.ts +0 -410
  30. package/lib/components/input.ts +0 -457
  31. package/lib/components/list.ts +0 -419
  32. package/lib/components/loading.ts +0 -100
  33. package/lib/components/menu.ts +0 -275
  34. package/lib/components/modal.ts +0 -284
  35. package/lib/components/nav.ts +0 -257
  36. package/lib/components/paragraph.ts +0 -97
  37. package/lib/components/progress.ts +0 -159
  38. package/lib/components/radio.ts +0 -278
  39. package/lib/components/req.ts +0 -303
  40. package/lib/components/script.ts +0 -41
  41. package/lib/components/select.ts +0 -252
  42. package/lib/components/sidebar.ts +0 -275
  43. package/lib/components/style.ts +0 -41
  44. package/lib/components/switch.ts +0 -246
  45. package/lib/components/table.ts +0 -1249
  46. package/lib/components/tabs.ts +0 -250
  47. package/lib/components/theme-toggle.ts +0 -293
  48. package/lib/components/tooltip.ts +0 -144
  49. package/lib/components/view.ts +0 -190
  50. package/lib/components/write.ts +0 -272
  51. package/lib/layouts/default.css +0 -260
  52. package/lib/layouts/figma.css +0 -334
  53. package/lib/reactivity/state.ts +0 -78
  54. package/lib/utils/fetch.ts +0 -553
  55. package/machinery/ast.js +0 -347
  56. package/machinery/build.js +0 -466
  57. package/machinery/bundleAssets.js +0 -0
  58. package/machinery/bundleJux.js +0 -0
  59. package/machinery/bundleVendors.js +0 -0
  60. package/machinery/doc-generator.js +0 -136
  61. package/machinery/imports.js +0 -155
  62. package/machinery/ts-shim.js +0 -46
  63. package/machinery/validators/file-validator.js +0 -123
@@ -18,31 +18,23 @@ try {
18
18
  }
19
19
 
20
20
  // ═══════════════════════════════════════════════════════════════
21
- // EXPLODE CONFIG INTO NAMED OBJECTS
21
+ // EXPLODE CONFIG
22
22
  // ═══════════════════════════════════════════════════════════════
23
23
  const directories = {
24
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'
25
+ distribution: rawConfig.directories?.distribution || './.jux-dist'
29
26
  };
30
27
 
31
28
  const defaults = {
32
29
  httpPort: rawConfig.defaults?.httpPort || 3000,
33
30
  wsPort: rawConfig.defaults?.wsPort || 3001,
34
- autoRoute: rawConfig.defaults?.autoRoute ?? true,
35
- layout: rawConfig.defaults?.layout || null,
36
- theme: rawConfig.defaults?.theme || null
31
+ autoRoute: rawConfig.defaults?.autoRoute ?? true
37
32
  };
38
33
 
39
34
  // Resolve absolute paths
40
35
  const paths = {
41
36
  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)
37
+ distribution: path.resolve(PROJECT_ROOT, directories.distribution)
46
38
  };
47
39
 
48
40
  // ═══════════════════════════════════════════════════════════════
@@ -50,84 +42,10 @@ const paths = {
50
42
  // ═══════════════════════════════════════════════════════════════
51
43
  console.log(`\n📁 Directory Check:`);
52
44
 
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
- }
45
+ if (fs.existsSync(paths.source)) {
46
+ console.log(` ✓ source: ${paths.source}`);
105
47
  } 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) {
48
+ console.log(` source: ${paths.source} (not found)`);
131
49
  console.error(`\n❌ Source directory not found: ${paths.source}`);
132
50
  console.error(` Create the directory or update juxconfig.js`);
133
51
  process.exit(1);
@@ -141,8 +59,6 @@ console.log(`\n`);
141
59
  const compiler = new JuxCompiler({
142
60
  srcDir: paths.source,
143
61
  distDir: paths.distribution,
144
- layoutPath,
145
- themePath,
146
62
  defaults,
147
63
  paths
148
64
  });
@@ -13,8 +13,6 @@ 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
16
  this.defaults = config.defaults || {};
19
17
  this.paths = config.paths || {};
20
18
  this._juxscriptExports = null;
@@ -94,163 +92,8 @@ export class JuxCompiler {
94
92
  }
95
93
 
96
94
  /**
97
- * Copy all source assets (themes, layouts, assets folders) to dist
95
+ * Generate entry point without layout/theme logic
98
96
  */
99
- copySourceAssets() {
100
- const copied = { themes: 0, layouts: 0, assets: 0, other: 0 };
101
-
102
- const copyDir = (srcDir, destDir, category) => {
103
- if (!fs.existsSync(srcDir)) return 0;
104
-
105
- let count = 0;
106
- fs.mkdirSync(destDir, { recursive: true });
107
-
108
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
109
- for (const entry of entries) {
110
- const srcPath = path.join(srcDir, entry.name);
111
- const destPath = path.join(destDir, entry.name);
112
-
113
- if (entry.isDirectory()) {
114
- count += copyDir(srcPath, destPath, category);
115
- } else {
116
- fs.copyFileSync(srcPath, destPath);
117
- count++;
118
- }
119
- }
120
- return count;
121
- };
122
-
123
- // Copy themes folder
124
- if (this.paths.themes && fs.existsSync(this.paths.themes)) {
125
- copied.themes = copyDir(this.paths.themes, path.join(this.distDir, 'themes'), 'themes');
126
- }
127
-
128
- // Copy layouts folder (if separate from themes)
129
- if (this.paths.layouts && fs.existsSync(this.paths.layouts) && this.paths.layouts !== this.paths.themes) {
130
- copied.layouts = copyDir(this.paths.layouts, path.join(this.distDir, 'layouts'), 'layouts');
131
- }
132
-
133
- // Copy assets folder
134
- if (this.paths.assets && fs.existsSync(this.paths.assets)) {
135
- copied.assets = copyDir(this.paths.assets, path.join(this.distDir, 'assets'), 'assets');
136
- }
137
-
138
- const total = copied.themes + copied.layouts + copied.assets;
139
- if (total > 0) {
140
- console.log(`📂 Copied source assets: ${copied.themes} themes, ${copied.layouts} layouts, ${copied.assets} assets`);
141
- }
142
-
143
- return copied;
144
- }
145
-
146
- /**
147
- * Generate CSS links including theme if configured
148
- */
149
- generateCssLinks() {
150
- const links = [];
151
-
152
- // Note: Component CSS is no longer bundled (handled by Styler)
153
-
154
- // User theme CSS
155
- if (this.themePath) {
156
- const themeName = path.basename(this.themePath);
157
- links.push(` <link rel="stylesheet" href="./themes/${themeName}" id="jux-theme">`);
158
- console.log(`🎨 🎨 🎨 🎨 Included theme: ${themeName} 🎨 🎨 🎨 🎨 `);
159
- }
160
-
161
- return links.join('\n');
162
- }
163
-
164
- async loadJuxscriptExports() {
165
- if (this._juxscriptExports) return this._juxscriptExports;
166
-
167
- try {
168
- const juxscriptPath = this.findJuxscriptPath();
169
- if (juxscriptPath) {
170
- const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
171
- const exports = new Set();
172
-
173
- for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
174
- match[1].split(',').forEach(exp => {
175
- const name = exp.trim().split(/\s+as\s+/)[0].trim();
176
- if (name) exports.add(name);
177
- });
178
- }
179
-
180
- this._juxscriptExports = [...exports];
181
- if (this._juxscriptExports.length > 0) {
182
- console.log(`📦 juxscript exports: ${this._juxscriptExports.join(', ')}`);
183
- }
184
- }
185
- } catch (err) {
186
- this._juxscriptExports = [];
187
- }
188
-
189
- return this._juxscriptExports;
190
- }
191
-
192
- validateViewCode(viewName, code) {
193
- const issues = [];
194
-
195
- let ast;
196
- try {
197
- ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
198
- } catch (parseError) {
199
- issues.push({
200
- type: 'error',
201
- view: viewName,
202
- line: parseError.loc?.line || 0,
203
- message: `Syntax error: ${parseError.message}`,
204
- code: ''
205
- });
206
- return issues;
207
- }
208
-
209
- const allImports = new Set();
210
-
211
- walk(ast, {
212
- ImportDeclaration(node) {
213
- node.specifiers.forEach(spec => {
214
- allImports.add(spec.local.name);
215
- });
216
- }
217
- });
218
-
219
- // Default known facades/components if load fails
220
- const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
221
-
222
- walk(ast, {
223
- Identifier(node, parent) {
224
- if (parent?.type === 'CallExpression' && parent.callee === node) {
225
- const name = node.name;
226
- // Updated: Removed strict Capitalization check to allow facades (lowercase)
227
- if (!allImports.has(name) && knownComponents.includes(name)) {
228
- issues.push({
229
- type: 'warning',
230
- view: viewName,
231
- line: node.loc?.start?.line || 0,
232
- message: `"${name}" is used but not imported from juxscript`,
233
- code: ''
234
- });
235
- }
236
- }
237
- }
238
- });
239
-
240
- return issues;
241
- }
242
-
243
- /**
244
- * Extract layout function name from layout file
245
- */
246
- getLayoutFunctionName() {
247
- if (!this.layoutPath || !fs.existsSync(this.layoutPath)) return null;
248
-
249
- const content = fs.readFileSync(this.layoutPath, 'utf8');
250
- const match = content.match(/export\s+function\s+(\w+)/);
251
- return match ? match[1] : null;
252
- }
253
-
254
97
  generateEntryPoint(views, dataModules, sharedModules) {
255
98
  let entry = `// Auto-generated JUX entry point\n\n`;
256
99
  const allIssues = [];
@@ -265,16 +108,6 @@ export class JuxCompiler {
265
108
  }
266
109
  });
267
110
 
268
- // Check layout file for juxscript imports too
269
- if (this.layoutPath && fs.existsSync(this.layoutPath)) {
270
- const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
271
- for (const match of layoutContent.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
272
- match[1].split(',').map(s => s.trim()).forEach(imp => {
273
- if (imp) juxImports.add(imp);
274
- });
275
- }
276
- }
277
-
278
111
  if (juxImports.size > 0) {
279
112
  entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
280
113
  }
@@ -294,25 +127,6 @@ export class JuxCompiler {
294
127
  entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
295
128
  }
296
129
 
297
- // Add layout function if configured
298
- const layoutFnName = this.getLayoutFunctionName();
299
- if (layoutFnName && this.layoutPath) {
300
- const layoutContent = fs.readFileSync(this.layoutPath, 'utf8');
301
- const layoutCode = this.removeImports(layoutContent);
302
- entry += `\n// --- DEFAULT LAYOUT ---\n`;
303
- entry += layoutCode;
304
- entry += `\n`;
305
-
306
- // Store in source snapshot
307
- const layoutFile = path.basename(this.layoutPath);
308
- sourceSnapshot[layoutFile] = {
309
- name: 'layout',
310
- file: layoutFile,
311
- content: layoutContent,
312
- lines: layoutContent.split('\n')
313
- };
314
- }
315
-
316
130
  entry += `\n// --- VIEW FUNCTIONS ---\n`;
317
131
 
318
132
  views.forEach(v => {
@@ -341,7 +155,6 @@ export class JuxCompiler {
341
155
 
342
156
  this._sourceSnapshot = sourceSnapshot;
343
157
  this._validationIssues = allIssues;
344
- this._layoutFnName = layoutFnName;
345
158
  entry += this._generateRouter(views);
346
159
  return entry;
347
160
  }
@@ -371,11 +184,6 @@ export class JuxCompiler {
371
184
  routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
372
185
  });
373
186
 
374
- // Initialize layout call if configured
375
- const layoutInit = this._layoutFnName
376
- ? `\n// Initialize default layout\nif (typeof ${this._layoutFnName} === 'function') {\n ${this._layoutFnName}();\n}\n`
377
- : '';
378
-
379
187
  return `
380
188
  // --- JUX SOURCE LOADER ---
381
189
  var __juxSources = null;
@@ -400,14 +208,6 @@ function __juxFindSource(stack) {
400
208
  }
401
209
  }
402
210
  }
403
- // Also check for layout
404
- if (stack.indexOf('Layout') > -1) {
405
- for (var file in __juxSources || {}) {
406
- if (file.indexOf('layout') > -1) {
407
- return { file: file, source: __juxSources[file] };
408
- }
409
- }
410
- }
411
211
  return null;
412
212
  }
413
213
 
@@ -508,7 +308,7 @@ window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.sh
508
308
 
509
309
  // --- JUX ROUTER ---
510
310
  const routes = {\n${routeMap}};
511
- ${layoutInit}
311
+
512
312
  async function navigate(path) {
513
313
  const view = routes[path];
514
314
  if (!view) {
@@ -555,9 +355,6 @@ navigate(location.pathname);
555
355
  const { views, dataModules, sharedModules } = this.scanFiles();
556
356
  console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
557
357
 
558
- // Copy source assets (themes, layouts, assets)
559
- this.copySourceAssets();
560
-
561
358
  // Copy data/shared modules to dist
562
359
  const juxDistDir = path.join(this.distDir, 'jux');
563
360
  fs.mkdirSync(juxDistDir, { recursive: true });
@@ -602,7 +399,6 @@ navigate(location.pathname);
602
399
  return { success: false, errors: [{ message: err.message }], warnings: [] };
603
400
  }
604
401
 
605
- // Generate HTML with layout container sibling to app
606
402
  const html = `<!DOCTYPE html>
607
403
  <html lang="en">
608
404
  <head>
@@ -612,9 +408,7 @@ navigate(location.pathname);
612
408
  <script type="module" src="./bundle.js"></script>
613
409
  </head>
614
410
  <body>
615
- <div id="jux-layout">
616
- <div id="app"></div>
617
- </div>
411
+ <div id="app"></div>
618
412
  </body>
619
413
  </html>`;
620
414
  fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",
@@ -19,14 +19,25 @@
19
19
  "./package.json": "./package.json"
20
20
  },
21
21
  "files": [
22
- "bin",
23
- "lib",
24
- "machinery",
25
22
  "index.js",
26
23
  "index.d.ts",
24
+ "bin",
25
+ "create/*.jux",
26
+ "lib/**/*.js",
27
+ "lib/**/*.d.ts",
28
+ "machinery/build3.js",
29
+ "machinery/compiler3.js",
30
+ "machinery/config.js",
31
+ "machinery/serve.js",
32
+ "machinery/watcher.js",
33
+ "types",
27
34
  "juxconfig.example.js",
28
35
  "README.md",
29
- "LICENSE"
36
+ "LICENSE",
37
+ "!lib/**/*.test.js",
38
+ "!lib/**/*.spec.js",
39
+ "!lib/previews/**",
40
+ "!**/*.map"
30
41
  ],
31
42
  "bin": {
32
43
  "jux": "./bin/cli.js"
@@ -46,6 +57,7 @@
46
57
  },
47
58
  "dependencies": {
48
59
  "acorn": "^8.15.0",
60
+ "astray": "^1.1.1",
49
61
  "axios": "^1.6.0",
50
62
  "chart.js": "^4.5.1",
51
63
  "esbuild": "^0.19.0",
@@ -54,10 +66,12 @@
54
66
  },
55
67
  "devDependencies": {
56
68
  "@types/express": "^4.17.17",
69
+ "@types/jsdom": "^27.0.0",
57
70
  "@types/node": "^20.0.0",
58
71
  "@types/ws": "^8.5.5",
59
72
  "acorn-walk": "^8.3.4",
60
73
  "jsdom": "^27.4.0",
74
+ "ts-node": "^10.9.2",
61
75
  "typescript": "^5.0.0"
62
76
  }
63
77
  }
@@ -1,200 +0,0 @@
1
- import { BaseComponent } from './base/BaseComponent.js';
2
- import { renderIcon } from './icons.js';
3
-
4
- // Event definitions
5
- const TRIGGER_EVENTS = [] as const;
6
- const CALLBACK_EVENTS = ['dismiss'] as const;
7
-
8
- export interface AlertOptions {
9
- content?: string;
10
- type?: 'info' | 'success' | 'warning' | 'error';
11
- dismissible?: boolean;
12
- icon?: string;
13
- style?: string;
14
- class?: string;
15
- }
16
-
17
- type AlertState = {
18
- content: string;
19
- type: string;
20
- dismissible: boolean;
21
- icon: string;
22
- visible: boolean;
23
- style: string;
24
- class: string;
25
- };
26
-
27
- export class Alert extends BaseComponent<AlertState> {
28
- private _alert: HTMLElement | null = null;
29
-
30
- constructor(id: string, options: AlertOptions = {}) {
31
- super(id, {
32
- content: options.content ?? '',
33
- type: options.type ?? 'info',
34
- dismissible: options.dismissible ?? true,
35
- icon: options.icon ?? '',
36
- visible: true,
37
- style: options.style ?? '',
38
- class: options.class ?? ''
39
- });
40
- }
41
-
42
- protected getTriggerEvents(): readonly string[] {
43
- return TRIGGER_EVENTS;
44
- }
45
-
46
- protected getCallbackEvents(): readonly string[] {
47
- return CALLBACK_EVENTS;
48
- }
49
-
50
- /* ═════════════════════════════════════════════════════════════════
51
- * FLUENT API
52
- * ═════════════════════════════════════════════════════════════════ */
53
-
54
- // ✅ Inherited from BaseComponent:
55
- // - style(), class()
56
- // - bind(), sync(), renderTo()
57
- // - addClass(), removeClass(), toggleClass()
58
- // - visible(), show(), hide(), toggleVisibility()
59
- // - attr(), attrs(), removeAttr()
60
- // - disabled(), enable(), disable()
61
- // - loading(), focus(), blur(), remove()
62
-
63
- content(value: string): this {
64
- this.state.content = value;
65
- return this;
66
- }
67
-
68
- type(value: 'info' | 'success' | 'warning' | 'error'): this {
69
- this.state.type = value;
70
- return this;
71
- }
72
-
73
- dismissible(value: boolean): this {
74
- this.state.dismissible = value;
75
- return this;
76
- }
77
-
78
- icon(value: string): this {
79
- this.state.icon = value;
80
- return this;
81
- }
82
-
83
- /* ═════════════════════════════════════════════════════════════════
84
- * RENDER
85
- * ═════════════════════════════════════════════════════════════════ */
86
-
87
- render(targetId?: string): this {
88
- const container = this._setupContainer(targetId);
89
-
90
- const { content, type, dismissible, icon, style, class: className } = this.state;
91
- const hasVisibleSync = this._syncBindings.some(b => b.property === 'visible');
92
-
93
- // Build alert element
94
- const alert = document.createElement('div');
95
- alert.className = `jux-alert jux-alert-${type}`;
96
- alert.id = this._id;
97
- if (className) alert.className += ` ${className}`;
98
- if (style) alert.setAttribute('style', style);
99
-
100
- if (icon) {
101
- const iconEl = document.createElement('span');
102
- iconEl.className = 'jux-alert-icon';
103
- iconEl.appendChild(renderIcon(icon));
104
- alert.appendChild(iconEl);
105
- }
106
-
107
- const contentEl = document.createElement('div');
108
- contentEl.className = 'jux-alert-content';
109
- contentEl.textContent = content;
110
- alert.appendChild(contentEl);
111
-
112
- if (dismissible) {
113
- const closeBtn = document.createElement('button');
114
- closeBtn.className = 'jux-alert-close';
115
- closeBtn.innerHTML = '×';
116
- alert.appendChild(closeBtn);
117
- }
118
-
119
- // Default dismiss behavior (only if NOT using sync)
120
- if (!hasVisibleSync && dismissible) {
121
- const closeBtn = alert.querySelector('.jux-alert-close');
122
- closeBtn?.addEventListener('click', () => {
123
- // 🎯 Fire the dismiss callback event
124
- this._triggerCallback('dismiss');
125
- alert.remove();
126
- });
127
- }
128
-
129
- // Wire events using inherited method
130
- this._wireStandardEvents(alert);
131
-
132
- // Wire sync bindings
133
- this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
134
- if (property === 'content') {
135
- const transform = toComponent || ((v: any) => String(v));
136
-
137
- stateObj.subscribe((val: any) => {
138
- const transformed = transform(val);
139
- contentEl.textContent = transformed;
140
- this.state.content = transformed;
141
- });
142
- }
143
- else if (property === 'type') {
144
- const transform = toComponent || ((v: any) => String(v));
145
-
146
- stateObj.subscribe((val: any) => {
147
- const transformed = transform(val);
148
- alert.className = `jux-alert jux-alert-${transformed}`;
149
- if (className) alert.className += ` ${className}`;
150
- this.state.type = transformed;
151
- });
152
- }
153
- else if (property === 'visible') {
154
- const transformToState = toState || ((v: any) => Boolean(v));
155
- const transform = toComponent || ((v: any) => Boolean(v));
156
-
157
- let isUpdating = false;
158
-
159
- // State → Component
160
- stateObj.subscribe((val: any) => {
161
- if (isUpdating) return;
162
- const transformed = transform(val);
163
- alert.style.display = transformed ? 'flex' : 'none';
164
- });
165
-
166
- // Component → State (close button)
167
- if (dismissible) {
168
- const closeBtn = alert.querySelector('.jux-alert-close');
169
- closeBtn?.addEventListener('click', () => {
170
- if (isUpdating) return;
171
- isUpdating = true;
172
-
173
- alert.style.display = 'none';
174
- stateObj.set(transformToState(false));
175
-
176
- // 🎯 Fire the dismiss callback event
177
- this._triggerCallback('dismiss');
178
-
179
- setTimeout(() => { isUpdating = false; }, 0);
180
- });
181
- }
182
- }
183
- });
184
-
185
- container.appendChild(alert);
186
- this._alert = alert;
187
-
188
- requestAnimationFrame(() => {
189
- if ((window as any).lucide) {
190
- (window as any).lucide.createIcons();
191
- }
192
- });
193
-
194
- return this;
195
- }
196
- }
197
-
198
- export function alert(id: string, options: AlertOptions = {}): Alert {
199
- return new Alert(id, options);
200
- }