juxscript 1.0.19 → 1.0.21

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 (77) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +212 -165
  3. package/lib/components/badge.ts +93 -103
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +63 -122
  7. package/lib/components/card.ts +109 -155
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/charts/lib/chart-types.ts +159 -0
  13. package/lib/components/charts/lib/chart-utils.ts +160 -0
  14. package/lib/components/charts/lib/chart.ts +707 -0
  15. package/lib/components/checkbox.ts +264 -127
  16. package/lib/components/code.ts +75 -108
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +195 -147
  20. package/lib/components/dialog.ts +187 -157
  21. package/lib/components/divider.ts +85 -191
  22. package/lib/components/docs-data.json +544 -2027
  23. package/lib/components/dropdown.ts +178 -136
  24. package/lib/components/element.ts +227 -171
  25. package/lib/components/fileupload.ts +285 -228
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +46 -69
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +107 -95
  30. package/lib/components/icon.ts +160 -0
  31. package/lib/components/icons.ts +175 -0
  32. package/lib/components/include.ts +153 -5
  33. package/lib/components/input.ts +174 -374
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +378 -240
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +103 -97
  38. package/lib/components/modal.ts +138 -144
  39. package/lib/components/nav.ts +169 -90
  40. package/lib/components/paragraph.ts +49 -150
  41. package/lib/components/progress.ts +118 -200
  42. package/lib/components/radio.ts +297 -149
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +184 -186
  45. package/lib/components/sidebar.ts +152 -140
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +258 -188
  48. package/lib/components/table.ts +1117 -170
  49. package/lib/components/tabs.ts +162 -145
  50. package/lib/components/theme-toggle.ts +108 -169
  51. package/lib/components/tooltip.ts +86 -157
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +86 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -2
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1246
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1250
  69. package/lib/components/chart.ts +0 -127
  70. package/lib/components/doughnutchart.ts +0 -1191
  71. package/lib/components/footer.ts +0 -165
  72. package/lib/components/header.ts +0 -187
  73. package/lib/components/layout.ts +0 -239
  74. package/lib/components/main.ts +0 -137
  75. package/lib/layouts/default.jux +0 -8
  76. package/lib/layouts/figma.jux +0 -0
  77. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -1,9 +1,11 @@
1
- import { getOrCreateContainer } from './helpers.js';
1
+ import { BaseComponent } from './base/BaseComponent.js';
2
+ import { renderIcon } from './icons.js';
2
3
  import { req } from './req.js';
3
4
 
4
- /**
5
- * Menu item configuration
6
- */
5
+ // Event definitions
6
+ const TRIGGER_EVENTS = [] as const;
7
+ const CALLBACK_EVENTS = ['itemClick'] as const; // ✅ Fire when menu item is clicked
8
+
7
9
  export interface MenuItem {
8
10
  label: string;
9
11
  href?: string;
@@ -11,11 +13,9 @@ export interface MenuItem {
11
13
  icon?: string;
12
14
  items?: MenuItem[];
13
15
  active?: boolean;
16
+ itemClass?: string;
14
17
  }
15
18
 
16
- /**
17
- * Menu component options
18
- */
19
19
  export interface MenuOptions {
20
20
  items?: MenuItem[];
21
21
  orientation?: 'vertical' | 'horizontal';
@@ -23,9 +23,6 @@ export interface MenuOptions {
23
23
  class?: string;
24
24
  }
25
25
 
26
- /**
27
- * Menu component state
28
- */
29
26
  type MenuState = {
30
27
  items: MenuItem[];
31
28
  orientation: string;
@@ -33,50 +30,30 @@ type MenuState = {
33
30
  class: string;
34
31
  };
35
32
 
36
- /**
37
- * Menu component
38
- *
39
- * Usage:
40
- * const menu = jux.menu('myMenu', {
41
- * orientation: 'vertical',
42
- * items: [
43
- * { label: 'Home', href: '/' },
44
- * { label: 'About', href: '/about' }
45
- * ]
46
- * });
47
- * menu.render();
48
- *
49
- * Active states are automatically set based on current URL
50
- */
51
- export class Menu {
52
- state: MenuState;
53
- container: HTMLElement | null = null;
54
- _id: string;
55
- id: string;
56
-
33
+ export class Menu extends BaseComponent<MenuState> {
57
34
  constructor(id: string, options: MenuOptions = {}) {
58
- this._id = id;
59
- this.id = id;
60
-
61
- this.state = {
35
+ super(id, {
62
36
  items: options.items ?? [],
63
37
  orientation: options.orientation ?? 'vertical',
64
38
  style: options.style ?? '',
65
39
  class: options.class ?? ''
66
- };
40
+ });
67
41
 
68
- // Auto-set active state based on current path
69
42
  this._setActiveStates();
70
43
  }
71
44
 
72
- /**
73
- * Set active state on items based on current request path
74
- */
45
+ protected getTriggerEvents(): readonly string[] {
46
+ return TRIGGER_EVENTS;
47
+ }
48
+
49
+ protected getCallbackEvents(): readonly string[] {
50
+ return CALLBACK_EVENTS;
51
+ }
52
+
75
53
  private _setActiveStates(): void {
76
54
  this.state.items = this.state.items.map(item => ({
77
55
  ...item,
78
56
  active: item.href ? req.isActiveNavItem(item.href) : false,
79
- // Recursively set active for subitems
80
57
  items: item.items?.map(subItem => ({
81
58
  ...subItem,
82
59
  active: subItem.href ? req.isActiveNavItem(subItem.href) : false
@@ -84,9 +61,11 @@ export class Menu {
84
61
  }));
85
62
  }
86
63
 
87
- /* -------------------------
88
- * Fluent API
89
- * ------------------------- */
64
+ /* ═════════════════════════════════════════════════════════════════
65
+ * FLUENT API
66
+ * ═════════════════════════════════════════════════════════════════ */
67
+
68
+ // ✅ Inherited from BaseComponent
90
69
 
91
70
  items(value: MenuItem[]): this {
92
71
  this.state.items = value;
@@ -100,30 +79,35 @@ export class Menu {
100
79
  return this;
101
80
  }
102
81
 
103
- orientation(value: 'vertical' | 'horizontal'): this {
104
- this.state.orientation = value;
105
- return this;
106
- }
107
-
108
- style(value: string): this {
109
- this.state.style = value;
82
+ itemClass(className: string): this {
83
+ this.state.items = this.state.items.map(item => ({
84
+ ...item,
85
+ itemClass: className,
86
+ items: item.items?.map(subItem => ({
87
+ ...subItem,
88
+ itemClass: className
89
+ }))
90
+ }));
110
91
  return this;
111
92
  }
112
93
 
113
- class(value: string): this {
114
- this.state.class = value;
94
+ orientation(value: 'vertical' | 'horizontal'): this {
95
+ this.state.orientation = value;
115
96
  return this;
116
97
  }
117
98
 
118
- /* -------------------------
119
- * Helpers
120
- * ------------------------- */
99
+ /* ═════════════════════════════════════════════════════════════════
100
+ * HELPERS
101
+ * ═════════════════════════════════════════════════════════════════ */
121
102
 
122
103
  private _renderMenuItem(item: MenuItem): HTMLElement {
123
104
  const menuItem = document.createElement('div');
124
105
  menuItem.className = 'jux-menu-item';
125
106
 
126
- // Add active class if item is active
107
+ if (item.itemClass) {
108
+ menuItem.className += ` ${item.itemClass}`;
109
+ }
110
+
127
111
  if (item.active) {
128
112
  menuItem.classList.add('jux-menu-item-active');
129
113
  }
@@ -133,7 +117,6 @@ export class Menu {
133
117
  link.className = 'jux-menu-link';
134
118
  link.href = item.href;
135
119
 
136
- // Add active class to link if item is active
137
120
  if (item.active) {
138
121
  link.classList.add('jux-menu-link-active');
139
122
  }
@@ -141,7 +124,7 @@ export class Menu {
141
124
  if (item.icon) {
142
125
  const icon = document.createElement('span');
143
126
  icon.className = 'jux-menu-icon';
144
- icon.textContent = item.icon;
127
+ icon.appendChild(renderIcon(item.icon));
145
128
  link.appendChild(icon);
146
129
  }
147
130
 
@@ -150,6 +133,11 @@ export class Menu {
150
133
  label.textContent = item.label;
151
134
  link.appendChild(label);
152
135
 
136
+ // ✅ Fire itemClick callback when link is clicked
137
+ link.addEventListener('click', (e) => {
138
+ this._triggerCallback('itemClick', { item, event: e });
139
+ });
140
+
153
141
  menuItem.appendChild(link);
154
142
  } else {
155
143
  const button = document.createElement('button');
@@ -158,7 +146,7 @@ export class Menu {
158
146
  if (item.icon) {
159
147
  const icon = document.createElement('span');
160
148
  icon.className = 'jux-menu-icon';
161
- icon.textContent = item.icon;
149
+ icon.appendChild(renderIcon(item.icon));
162
150
  button.appendChild(icon);
163
151
  }
164
152
 
@@ -169,13 +157,15 @@ export class Menu {
169
157
 
170
158
  menuItem.appendChild(button);
171
159
 
172
- // Event binding - click
173
- if (item.click) {
174
- button.addEventListener('click', item.click);
175
- }
160
+ // Fire both the item's click handler AND the callback event
161
+ button.addEventListener('click', (e) => {
162
+ if (item.click) {
163
+ item.click();
164
+ }
165
+ this._triggerCallback('itemClick', { item, event: e });
166
+ });
176
167
  }
177
168
 
178
- // Nested items (submenu)
179
169
  if (item.items && item.items.length > 0) {
180
170
  const submenu = document.createElement('div');
181
171
  submenu.className = 'jux-menu-submenu';
@@ -190,24 +180,13 @@ export class Menu {
190
180
  return menuItem;
191
181
  }
192
182
 
193
- /* -------------------------
194
- * Render
195
- * ------------------------- */
183
+ /* ═════════════════════════════════════════════════════════════════
184
+ * RENDER
185
+ * ═════════════════════════════════════════════════════════════════ */
196
186
 
197
187
  render(targetId?: string): this {
198
- let container: HTMLElement;
199
-
200
- if (targetId) {
201
- const target = document.querySelector(targetId);
202
- if (!target || !(target instanceof HTMLElement)) {
203
- throw new Error(`Menu: Target element "${targetId}" not found`);
204
- }
205
- container = target;
206
- } else {
207
- container = getOrCreateContainer(this._id);
208
- }
188
+ const container = this._setupContainer(targetId);
209
189
 
210
- this.container = container;
211
190
  const { items, orientation, style, class: className } = this.state;
212
191
 
213
192
  const menu = document.createElement('nav');
@@ -226,29 +205,56 @@ export class Menu {
226
205
  menu.appendChild(this._renderMenuItem(item));
227
206
  });
228
207
 
229
- container.appendChild(menu);
230
- return this;
231
- }
208
+ this._wireStandardEvents(menu);
232
209
 
233
- /**
234
- * Render to another Jux component's container
235
- */
236
- renderTo(juxComponent: any): this {
237
- if (!juxComponent || typeof juxComponent !== 'object') {
238
- throw new Error('Menu.renderTo: Invalid component - not an object');
239
- }
210
+ // Wire sync bindings
211
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
212
+ if (property === 'items') {
213
+ const transform = toComponent || ((v: any) => v);
240
214
 
241
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
242
- throw new Error('Menu.renderTo: Invalid component - missing _id (not a Jux component)');
243
- }
215
+ stateObj.subscribe((val: any) => {
216
+ const transformed = transform(val);
217
+ this.state.items = transformed;
218
+ this._setActiveStates();
244
219
 
245
- return this.render(`#${juxComponent._id}`);
220
+ const existingItems = menu.querySelectorAll('.jux-menu-item');
221
+ existingItems.forEach(item => item.remove());
222
+
223
+ this.state.items.forEach(item => {
224
+ menu.appendChild(this._renderMenuItem(item));
225
+ });
226
+
227
+ requestAnimationFrame(() => {
228
+ if ((window as any).lucide) {
229
+ (window as any).lucide.createIcons();
230
+ }
231
+ });
232
+ });
233
+ }
234
+ else if (property === 'orientation') {
235
+ const transform = toComponent || ((v: any) => String(v));
236
+
237
+ stateObj.subscribe((val: any) => {
238
+ const transformed = transform(val);
239
+ menu.classList.remove(`jux-menu-${this.state.orientation}`);
240
+ this.state.orientation = transformed;
241
+ menu.classList.add(`jux-menu-${transformed}`);
242
+ });
243
+ }
244
+ });
245
+
246
+ container.appendChild(menu);
247
+
248
+ requestAnimationFrame(() => {
249
+ if ((window as any).lucide) {
250
+ (window as any).lucide.createIcons();
251
+ }
252
+ });
253
+
254
+ return this;
246
255
  }
247
256
  }
248
257
 
249
- /**
250
- * Factory helper
251
- */
252
258
  export function menu(id: string, options: MenuOptions = {}): Menu {
253
259
  return new Menu(id, options);
254
260
  }
@@ -1,8 +1,9 @@
1
- import { getOrCreateContainer } from './helpers.js';
1
+ import { BaseComponent } from './base/BaseComponent.js';
2
+
3
+ // Event definitions
4
+ const TRIGGER_EVENTS = [] as const;
5
+ const CALLBACK_EVENTS = ['open', 'close'] as const;
2
6
 
3
- /**
4
- * Modal component options
5
- */
6
7
  export interface ModalOptions {
7
8
  title?: string;
8
9
  content?: string;
@@ -13,57 +14,46 @@ export interface ModalOptions {
13
14
  class?: string;
14
15
  }
15
16
 
16
- /**
17
- * Modal component state
18
- */
19
17
  type ModalState = {
20
18
  title: string;
21
19
  content: string;
22
20
  showCloseButton: boolean;
23
21
  closeOnBackdropClick: boolean;
24
22
  size: string;
25
- isOpen: boolean;
23
+ open: boolean;
26
24
  style: string;
27
25
  class: string;
28
26
  };
29
27
 
30
- /**
31
- * Modal component
32
- *
33
- * Usage:
34
- * const modal = jux.modal('myModal', {
35
- * title: 'Confirmation',
36
- * content: 'Are you sure?',
37
- * size: 'medium'
38
- * });
39
- * modal.render();
40
- * modal.open();
41
- */
42
- export class Modal {
43
- state: ModalState;
44
- container: HTMLElement | null = null;
45
- _id: string;
46
- id: string;
28
+ export class Modal extends BaseComponent<ModalState> {
29
+ private _overlay: HTMLElement | null = null;
47
30
 
48
31
  constructor(id: string, options: ModalOptions = {}) {
49
- this._id = id;
50
- this.id = id;
51
-
52
- this.state = {
32
+ super(id, {
53
33
  title: options.title ?? '',
54
34
  content: options.content ?? '',
55
35
  showCloseButton: options.showCloseButton ?? true,
56
36
  closeOnBackdropClick: options.closeOnBackdropClick ?? true,
57
37
  size: options.size ?? 'medium',
58
- isOpen: false,
38
+ open: false,
59
39
  style: options.style ?? '',
60
40
  class: options.class ?? ''
61
- };
41
+ });
42
+ }
43
+
44
+ protected getTriggerEvents(): readonly string[] {
45
+ return TRIGGER_EVENTS;
46
+ }
47
+
48
+ protected getCallbackEvents(): readonly string[] {
49
+ return CALLBACK_EVENTS;
62
50
  }
63
51
 
64
- /* -------------------------
65
- * Fluent API
66
- * ------------------------- */
52
+ /* ═════════════════════════════════════════════════════════════════
53
+ * FLUENT API
54
+ * ═════════════════════════════════════════════════════════════════ */
55
+
56
+ // ✅ Inherited from BaseComponent
67
57
 
68
58
  title(value: string): this {
69
59
  this.state.title = value;
@@ -90,156 +80,160 @@ export class Modal {
90
80
  return this;
91
81
  }
92
82
 
93
- style(value: string): this {
94
- this.state.style = value;
95
- return this;
96
- }
97
-
98
- class(value: string): this {
99
- this.state.class = value;
100
- return this;
101
- }
102
-
103
- /* -------------------------
104
- * Modal controls
105
- * ------------------------- */
106
-
107
83
  open(): this {
108
- this.state.isOpen = true;
109
- if (this.container) {
110
- const modalEl = this.container.querySelector('.jux-modal');
111
- if (modalEl) {
112
- modalEl.classList.add('jux-modal-open');
113
- }
84
+ this.state.open = true;
85
+ if (this._overlay) {
86
+ this._overlay.style.display = 'flex';
114
87
  }
88
+ // 🎯 Fire the open callback event
89
+ this._triggerCallback('open');
115
90
  return this;
116
91
  }
117
92
 
118
93
  close(): this {
119
- this.state.isOpen = false;
120
- if (this.container) {
121
- const modalEl = this.container.querySelector('.jux-modal');
122
- if (modalEl) {
123
- modalEl.classList.remove('jux-modal-open');
124
- }
94
+ this.state.open = false;
95
+ if (this._overlay) {
96
+ this._overlay.style.display = 'none';
125
97
  }
98
+ // 🎯 Fire the close callback event
99
+ this._triggerCallback('close');
126
100
  return this;
127
101
  }
128
102
 
129
- /* -------------------------
130
- * Render
131
- * ------------------------- */
103
+ /* ═════════════════════════════════════════════════════════════════
104
+ * RENDER
105
+ * ═════════════════════════════════════════════════════════════════ */
132
106
 
133
107
  render(targetId?: string): this {
134
- let container: HTMLElement;
108
+ const container = this._setupContainer(targetId);
135
109
 
136
- if (targetId) {
137
- const target = document.querySelector(targetId);
138
- if (!target || !(target instanceof HTMLElement)) {
139
- throw new Error(`Modal: Target element "${targetId}" not found`);
140
- }
141
- container = target;
142
- } else {
143
- container = getOrCreateContainer(this._id);
144
- }
110
+ const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
111
+ const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
145
112
 
146
- this.container = container;
147
- const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen, style, class: className } = this.state;
113
+ const overlay = document.createElement('div');
114
+ overlay.className = 'jux-modal-overlay';
115
+ overlay.id = this._id;
116
+ overlay.style.display = open ? 'flex' : 'none';
117
+ if (className) overlay.className += ` ${className}`;
118
+ if (style) overlay.setAttribute('style', style);
119
+ this._overlay = overlay;
148
120
 
149
- // Modal backdrop
150
121
  const modal = document.createElement('div');
151
122
  modal.className = `jux-modal jux-modal-${size}`;
152
- modal.id = this._id;
153
123
 
154
- if (className) {
155
- modal.className += ` ${className}`;
124
+ if (showClose) {
125
+ const closeButton = document.createElement('button');
126
+ closeButton.className = 'jux-modal-close';
127
+ closeButton.innerHTML = '×';
128
+ modal.appendChild(closeButton);
156
129
  }
157
130
 
158
- if (style) {
159
- modal.setAttribute('style', style);
131
+ if (title) {
132
+ const header = document.createElement('div');
133
+ header.className = 'jux-modal-header';
134
+ header.textContent = title;
135
+ modal.appendChild(header);
160
136
  }
161
137
 
162
- if (isOpen) {
163
- modal.classList.add('jux-modal-open');
164
- }
138
+ const body = document.createElement('div');
139
+ body.className = 'jux-modal-body';
140
+ body.innerHTML = content;
141
+ modal.appendChild(body);
165
142
 
166
- // Modal dialog
167
- const dialog = document.createElement('div');
168
- dialog.className = 'jux-modal-dialog';
143
+ overlay.appendChild(modal);
169
144
 
170
- // Modal header
171
- if (title || showCloseButton) {
172
- const header = document.createElement('div');
173
- header.className = 'jux-modal-header';
145
+ if (!hasOpenSync) {
146
+ if (showClose) {
147
+ const closeButton = modal.querySelector('.jux-modal-close');
148
+ closeButton?.addEventListener('click', () => {
149
+ this.state.open = false;
150
+ overlay.style.display = 'none';
151
+ });
152
+ }
174
153
 
175
- if (title) {
176
- const titleEl = document.createElement('h3');
177
- titleEl.className = 'jux-modal-title';
178
- titleEl.textContent = title;
179
- header.appendChild(titleEl);
154
+ if (closeOnBackdrop) {
155
+ overlay.addEventListener('click', (e) => {
156
+ if (e.target === overlay) {
157
+ this.state.open = false;
158
+ overlay.style.display = 'none';
159
+ }
160
+ });
180
161
  }
162
+ }
181
163
 
182
- if (showCloseButton) {
183
- const closeBtn = document.createElement('button');
184
- closeBtn.className = 'jux-modal-close';
185
- closeBtn.innerHTML = '&times;';
186
- header.appendChild(closeBtn);
164
+ this._wireStandardEvents(overlay);
187
165
 
188
- // Event binding - close button
189
- closeBtn.addEventListener('click', () => this.close());
190
- }
166
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
167
+ if (property === 'open') {
168
+ const transformToState = toState || ((v: any) => Boolean(v));
169
+ const transformToComponent = toComponent || ((v: any) => Boolean(v));
191
170
 
192
- dialog.appendChild(header);
193
- }
171
+ let isUpdating = false;
194
172
 
195
- // Modal body
196
- const body = document.createElement('div');
197
- body.className = 'jux-modal-body';
198
- body.innerHTML = content;
199
- dialog.appendChild(body);
173
+ stateObj.subscribe((val: any) => {
174
+ if (isUpdating) return;
175
+ const transformed = transformToComponent(val);
176
+ this.state.open = transformed;
177
+ overlay.style.display = transformed ? 'flex' : 'none';
178
+ });
200
179
 
201
- modal.appendChild(dialog);
202
- container.appendChild(modal);
180
+ if (showClose) {
181
+ const closeButton = modal.querySelector('.jux-modal-close');
182
+ closeButton?.addEventListener('click', () => {
183
+ if (isUpdating) return;
184
+ isUpdating = true;
203
185
 
204
- // Event binding - backdrop click
205
- if (closeOnBackdropClick) {
206
- modal.addEventListener('click', (e) => {
207
- if (e.target === modal) {
208
- this.close();
209
- }
210
- });
211
- }
186
+ this.state.open = false;
187
+ overlay.style.display = 'none';
188
+ stateObj.set(transformToState(false));
212
189
 
213
- // Event binding - escape key
214
- const escapeHandler = (e: KeyboardEvent) => {
215
- if (e.key === 'Escape' && this.state.isOpen) {
216
- this.close();
217
- }
218
- };
219
- document.addEventListener('keydown', escapeHandler);
190
+ setTimeout(() => { isUpdating = false; }, 0);
191
+ });
192
+ }
220
193
 
221
- return this;
222
- }
194
+ if (closeOnBackdrop) {
195
+ overlay.addEventListener('click', (e) => {
196
+ if (e.target === overlay) {
197
+ if (isUpdating) return;
198
+ isUpdating = true;
223
199
 
224
- /**
225
- * Render to another Jux component's container
226
- */
227
- renderTo(juxComponent: any): this {
228
- if (!juxComponent || typeof juxComponent !== 'object') {
229
- throw new Error('Modal.renderTo: Invalid component - not an object');
230
- }
200
+ this.state.open = false;
201
+ overlay.style.display = 'none';
202
+ stateObj.set(transformToState(false));
231
203
 
232
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
233
- throw new Error('Modal.renderTo: Invalid component - missing _id (not a Jux component)');
234
- }
204
+ setTimeout(() => { isUpdating = false; }, 0);
205
+ }
206
+ });
207
+ }
208
+ }
209
+ else if (property === 'content') {
210
+ const transform = toComponent || ((v: any) => String(v));
211
+
212
+ stateObj.subscribe((val: any) => {
213
+ const transformed = transform(val);
214
+ body.innerHTML = transformed;
215
+ this.state.content = transformed;
216
+ });
217
+ }
218
+ else if (property === 'title') {
219
+ const transform = toComponent || ((v: any) => String(v));
220
+
221
+ stateObj.subscribe((val: any) => {
222
+ const transformed = transform(val);
223
+ const header = modal.querySelector('.jux-modal-header');
224
+ if (header) {
225
+ header.textContent = transformed;
226
+ }
227
+ this.state.title = transformed;
228
+ });
229
+ }
230
+ });
235
231
 
236
- return this.render(`#${juxComponent._id}`);
232
+ container.appendChild(overlay);
233
+ return this;
237
234
  }
238
235
  }
239
236
 
240
- /**
241
- * Factory helper
242
- */
243
237
  export function modal(id: string, options: ModalOptions = {}): Modal {
244
238
  return new Modal(id, options);
245
239
  }