juxscript 1.1.59 → 1.1.62

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.
@@ -1,20 +1,38 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
+ export type EmojiIconType = {
3
+ icon: string;
4
+ collection?: 'lucide' | 'heroicons' | 'material-symbols' | 'material' | 'icon-park' | string;
5
+ };
2
6
  export interface ModalOptions {
3
7
  title?: string;
4
- content?: string;
5
- showCloseButton?: boolean;
6
- closeOnBackdropClick?: boolean;
8
+ content?: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
9
+ icon?: EmojiIconType | undefined;
10
+ close?: boolean;
11
+ backdropClose?: boolean;
12
+ position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
7
13
  size?: 'small' | 'medium' | 'large';
14
+ actions?: Array<{
15
+ label: string;
16
+ variant?: string;
17
+ click?: () => void;
18
+ } | Array<BaseComponent<any>>>;
8
19
  style?: string;
9
20
  class?: string;
10
21
  }
11
22
  type ModalState = {
12
23
  title: string;
13
- content: string;
14
- showCloseButton: boolean;
15
- closeOnBackdropClick: boolean;
24
+ content: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
25
+ icon: EmojiIconType | undefined;
26
+ close: boolean;
27
+ backdropClose: boolean;
28
+ position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
16
29
  size: string;
17
30
  open: boolean;
31
+ actions: Array<{
32
+ label: string;
33
+ variant?: string;
34
+ click?: () => void;
35
+ } | Array<BaseComponent<any>>>;
18
36
  style: string;
19
37
  class: string;
20
38
  };
@@ -23,29 +41,47 @@ export declare class Modal extends BaseComponent<ModalState> {
23
41
  constructor(id: string, options?: ModalOptions);
24
42
  protected getTriggerEvents(): readonly string[];
25
43
  protected getCallbackEvents(): readonly string[];
44
+ update(prop: string, value: any): void;
45
+ private _updateTitle;
46
+ private _updateIcon;
47
+ private _updateContent;
48
+ private _rebuildActions;
26
49
  title(value: string): this;
27
- content(value: string): this;
28
- showCloseButton(value: boolean): this;
29
- closeOnBackdropClick(value: boolean): this;
50
+ content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
51
+ icon(value: EmojiIconType): this;
52
+ close(value: boolean): this;
53
+ backdropClose(value: boolean): this;
30
54
  size(value: 'small' | 'medium' | 'large'): this;
55
+ actions(value: Array<{
56
+ label: string;
57
+ variant?: string;
58
+ click?: () => void;
59
+ }>): this;
60
+ addAction(action: {
61
+ label: string;
62
+ variant?: string;
63
+ click?: () => void;
64
+ }): this;
31
65
  open(): this;
32
- close(): this;
66
+ closeModal(): this;
67
+ toggle(): this;
68
+ /**
69
+ * Add content to modal (supports strings and BaseComponents)
70
+ */
71
+ addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
33
72
  /**
34
- * Get the modal body element ID for rendering child components
35
- * @returns The ID of the modal body element (e.g., 'mymodal-body')
36
- *
37
- * Usage:
38
- * const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
39
- * jux.table('files-table', {...}).render(modal.bodyId());
73
+ * Clear all content from modal
40
74
  */
41
- bodyId(): string;
75
+ clearContent(): this;
42
76
  /**
43
- * Get the modal body element (only available after render)
44
- * @returns The modal body element or null if not rendered
77
+ * Get the modal content element ID for rendering child components
45
78
  */
46
- getBodyElement(): HTMLElement | null;
79
+ contentId(): string;
80
+ /**
81
+ * Get the modal content element (only available after render)
82
+ */
83
+ getContentElement(): HTMLElement | null;
47
84
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
48
- update(prop: string, value: any): void;
49
85
  }
50
86
  export declare function modal(id: string, options?: ModalOptions): Modal;
51
87
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAMxD,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,qBAAa,KAAM,SAAQ,aAAa,CAAC,UAAU,CAAC;IAClD,OAAO,CAAC,QAAQ,CAA4B;gBAEhC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;IAalD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAehD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAY1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAW5B,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKrC,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAK1C,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI;IAW/C,IAAI,IAAI,IAAI;IAUZ,KAAK,IAAI,IAAI;IAUb;;;;;;;OAOG;IACH,MAAM,IAAI,MAAM;IAIhB;;;OAGG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAQpC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;IAkIlE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;CAGvC;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,KAAK,CAEnE"}
1
+ {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
+ import { renderIcon } from './icons.js';
2
3
  // Event definitions
3
4
  const TRIGGER_EVENTS = [];
4
5
  const CALLBACK_EVENTS = ['open', 'close'];
@@ -7,10 +8,13 @@ export class Modal extends BaseComponent {
7
8
  super(id, {
8
9
  title: options.title ?? '',
9
10
  content: options.content ?? '',
10
- showCloseButton: options.showCloseButton ?? true,
11
- closeOnBackdropClick: options.closeOnBackdropClick ?? true,
11
+ icon: options.icon ?? undefined,
12
+ close: options.close ?? true,
13
+ backdropClose: options.backdropClose ?? true,
14
+ position: options.position,
12
15
  size: options.size ?? 'medium',
13
16
  open: false,
17
+ actions: options.actions ?? [],
14
18
  style: options.style ?? '',
15
19
  class: options.class ?? ''
16
20
  });
@@ -23,95 +27,243 @@ export class Modal extends BaseComponent {
23
27
  return CALLBACK_EVENTS;
24
28
  }
25
29
  /* ═════════════════════════════════════════════════════════════════
26
- * FLUENT API
30
+ * REACTIVE UPDATE (Precision DOM edits)
27
31
  * ═════════════════════════════════════════════════════════════════ */
28
- // Inherited from BaseComponent:
29
- // - style(), class()
30
- // - bind(), sync(), renderTo()
31
- // - addClass(), removeClass(), toggleClass()
32
- // - visible(), show(), hide()
33
- // - attr(), attrs(), removeAttr()
34
- title(value) {
35
- this.state.title = value;
36
- if (this._overlay) {
32
+ update(prop, value) {
33
+ super.update(prop, value);
34
+ if (!this._overlay)
35
+ return;
36
+ const modal = this._overlay.querySelector('.jux-modal');
37
+ if (!modal)
38
+ return;
39
+ switch (prop) {
40
+ case 'open':
41
+ this._overlay.style.display = value ? 'flex' : 'none';
42
+ if (value) {
43
+ this._triggerCallback('open');
44
+ }
45
+ else {
46
+ this._triggerCallback('close');
47
+ }
48
+ break;
49
+ case 'title':
50
+ this._updateTitle(value);
51
+ break;
52
+ case 'content':
53
+ this._updateContent(value);
54
+ break;
55
+ case 'icon':
56
+ this._updateIcon(value);
57
+ break;
58
+ case 'size':
59
+ modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
60
+ break;
61
+ case 'actions':
62
+ this._rebuildActions();
63
+ break;
64
+ }
65
+ }
66
+ _updateTitle(value) {
67
+ let headerEl = this._overlay.querySelector('.jux-modal-header');
68
+ if (!headerEl && value) {
69
+ // Create header if it doesn't exist
70
+ headerEl = document.createElement('div');
71
+ headerEl.className = 'jux-modal-header';
37
72
  const modal = this._overlay.querySelector('.jux-modal');
38
- const header = modal?.querySelector('.jux-modal-header');
39
- if (header) {
40
- header.textContent = value;
73
+ const body = modal.querySelector('.jux-modal-body');
74
+ modal.insertBefore(headerEl, body);
75
+ }
76
+ if (headerEl) {
77
+ // Preserve icon if exists
78
+ const iconEl = headerEl.querySelector('.jux-modal-header-icon');
79
+ headerEl.innerHTML = '';
80
+ if (iconEl) {
81
+ headerEl.appendChild(iconEl);
82
+ }
83
+ const titleEl = document.createElement('span');
84
+ titleEl.className = 'jux-modal-header-title';
85
+ titleEl.textContent = value;
86
+ headerEl.appendChild(titleEl);
87
+ }
88
+ }
89
+ _updateIcon(value) {
90
+ const headerEl = this._overlay.querySelector('.jux-modal-header');
91
+ if (!headerEl)
92
+ return;
93
+ let iconEl = headerEl.querySelector('.jux-modal-header-icon');
94
+ if (value) {
95
+ if (!iconEl) {
96
+ iconEl = document.createElement('span');
97
+ iconEl.className = 'jux-modal-header-icon';
98
+ headerEl.insertBefore(iconEl, headerEl.firstChild);
99
+ }
100
+ iconEl.innerHTML = '';
101
+ iconEl.appendChild(renderIcon(value.icon, value.collection));
102
+ }
103
+ else if (iconEl) {
104
+ iconEl.remove();
105
+ }
106
+ }
107
+ _updateContent(value) {
108
+ const contentEl = this._overlay.querySelector('.jux-modal-content');
109
+ if (!contentEl)
110
+ return;
111
+ contentEl.innerHTML = '';
112
+ const items = Array.isArray(value) ? value : [value];
113
+ items.forEach(item => {
114
+ if (typeof item === 'string') {
115
+ const tempDiv = document.createElement('div');
116
+ tempDiv.innerHTML = item;
117
+ while (tempDiv.firstChild) {
118
+ contentEl.appendChild(tempDiv.firstChild);
119
+ }
120
+ }
121
+ else if (item instanceof BaseComponent) {
122
+ item.render(contentEl);
41
123
  }
124
+ });
125
+ }
126
+ _rebuildActions() {
127
+ const modal = this._overlay.querySelector('.jux-modal');
128
+ if (!modal)
129
+ return;
130
+ let footerEl = modal.querySelector('.jux-modal-footer');
131
+ if (this.state.actions.length === 0) {
132
+ if (footerEl)
133
+ footerEl.remove();
134
+ return;
42
135
  }
136
+ if (!footerEl) {
137
+ footerEl = document.createElement('div');
138
+ footerEl.className = 'jux-modal-footer';
139
+ modal.appendChild(footerEl);
140
+ }
141
+ footerEl.innerHTML = '';
142
+ this.state.actions.forEach(action => {
143
+ // Check if it's an array of BaseComponents
144
+ if (Array.isArray(action)) {
145
+ action.forEach(component => {
146
+ if (component instanceof BaseComponent) {
147
+ component.render(footerEl);
148
+ }
149
+ });
150
+ }
151
+ // Otherwise it's an action object with label property
152
+ else if ('label' in action) {
153
+ const btn = document.createElement('button');
154
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
155
+ btn.textContent = action.label;
156
+ btn.type = 'button';
157
+ btn.addEventListener('click', () => {
158
+ if (action.click)
159
+ action.click();
160
+ });
161
+ footerEl.appendChild(btn);
162
+ }
163
+ });
164
+ }
165
+ /* ═════════════════════════════════════════════════════════════════
166
+ * FLUENT API (Just set state, update() handles DOM)
167
+ * ═════════════════════════════════════════════════════════════════ */
168
+ title(value) {
169
+ this.state.title = value;
43
170
  return this;
44
171
  }
45
172
  content(value) {
46
173
  this.state.content = value;
47
- if (this._overlay) {
48
- const body = this._overlay.querySelector('.jux-modal-body');
49
- if (body) {
50
- body.innerHTML = value;
51
- }
52
- }
53
174
  return this;
54
175
  }
55
- showCloseButton(value) {
56
- this.state.showCloseButton = value;
176
+ icon(value) {
177
+ this.state.icon = value;
178
+ return this;
179
+ }
180
+ close(value) {
181
+ this.state.close = value;
57
182
  return this;
58
183
  }
59
- closeOnBackdropClick(value) {
60
- this.state.closeOnBackdropClick = value;
184
+ backdropClose(value) {
185
+ this.state.backdropClose = value;
61
186
  return this;
62
187
  }
63
188
  size(value) {
64
189
  this.state.size = value;
65
- if (this._overlay) {
66
- const modal = this._overlay.querySelector('.jux-modal');
67
- if (modal) {
68
- modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
69
- }
70
- }
190
+ return this;
191
+ }
192
+ actions(value) {
193
+ this.state.actions = value;
194
+ return this;
195
+ }
196
+ addAction(action) {
197
+ this.state.actions = [...this.state.actions, action];
71
198
  return this;
72
199
  }
73
200
  open() {
74
201
  this.state.open = true;
75
- if (this._overlay) {
76
- this._overlay.style.display = 'flex';
77
- }
78
- // 🎯 Fire the open callback event
79
- this._triggerCallback('open');
80
202
  return this;
81
203
  }
82
- close() {
204
+ closeModal() {
83
205
  this.state.open = false;
84
- if (this._overlay) {
85
- this._overlay.style.display = 'none';
206
+ return this;
207
+ }
208
+ toggle() {
209
+ this.state.open = !this.state.open;
210
+ return this;
211
+ }
212
+ /* ═════════════════════════════════════════════════════════════════
213
+ * DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
214
+ * ═════════════════════════════════════════════════════════════════ */
215
+ /**
216
+ * Add content to modal (supports strings and BaseComponents)
217
+ */
218
+ addContent(content) {
219
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
220
+ if (!contentEl) {
221
+ console.warn('[Modal] Content element not found');
222
+ return this;
86
223
  }
87
- // 🎯 Fire the close callback event
88
- this._triggerCallback('close');
224
+ const items = Array.isArray(content) ? content : [content];
225
+ items.forEach(item => {
226
+ if (typeof item === 'string') {
227
+ const tempDiv = document.createElement('div');
228
+ tempDiv.innerHTML = item;
229
+ while (tempDiv.firstChild) {
230
+ contentEl.appendChild(tempDiv.firstChild);
231
+ }
232
+ }
233
+ else if (item instanceof BaseComponent) {
234
+ item.render(contentEl);
235
+ }
236
+ });
89
237
  return this;
90
238
  }
91
239
  /**
92
- * Get the modal body element ID for rendering child components
93
- * @returns The ID of the modal body element (e.g., 'mymodal-body')
94
- *
95
- * Usage:
96
- * const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
97
- * jux.table('files-table', {...}).render(modal.bodyId());
240
+ * Clear all content from modal
98
241
  */
99
- bodyId() {
100
- return `${this._id}-body`;
242
+ clearContent() {
243
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
244
+ if (contentEl) {
245
+ contentEl.innerHTML = '';
246
+ }
247
+ return this;
101
248
  }
102
249
  /**
103
- * Get the modal body element (only available after render)
104
- * @returns The modal body element or null if not rendered
250
+ * Get the modal content element ID for rendering child components
105
251
  */
106
- getBodyElement() {
107
- return document.getElementById(this.bodyId());
252
+ contentId() {
253
+ return `${this._id}-content`;
254
+ }
255
+ /**
256
+ * Get the modal content element (only available after render)
257
+ */
258
+ getContentElement() {
259
+ return document.getElementById(this.contentId());
108
260
  }
109
261
  /* ═════════════════════════════════════════════════════════════════
110
262
  * RENDER
111
263
  * ═════════════════════════════════════════════════════════════════ */
112
264
  render(targetId) {
113
265
  const container = this._setupContainer(targetId);
114
- const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
266
+ const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
115
267
  const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
116
268
  const overlay = document.createElement('div');
117
269
  overlay.className = 'jux-modal-overlay';
@@ -124,42 +276,96 @@ export class Modal extends BaseComponent {
124
276
  this._overlay = overlay;
125
277
  const modal = document.createElement('div');
126
278
  modal.className = `jux-modal jux-modal-${size}`;
127
- if (showClose) {
279
+ if (close) {
128
280
  const closeButton = document.createElement('button');
129
281
  closeButton.className = 'jux-modal-close';
130
282
  closeButton.innerHTML = '×';
283
+ closeButton.type = 'button';
131
284
  modal.appendChild(closeButton);
132
285
  }
133
- if (title) {
286
+ if (title || icon) {
134
287
  const header = document.createElement('div');
135
288
  header.className = 'jux-modal-header';
136
- header.textContent = title;
289
+ if (icon) {
290
+ const iconEl = document.createElement('span');
291
+ iconEl.className = 'jux-modal-header-icon';
292
+ iconEl.appendChild(renderIcon(icon.icon, icon.collection));
293
+ header.appendChild(iconEl);
294
+ }
295
+ if (title) {
296
+ const titleEl = document.createElement('span');
297
+ titleEl.className = 'jux-modal-header-title';
298
+ titleEl.textContent = title;
299
+ header.appendChild(titleEl);
300
+ }
137
301
  modal.appendChild(header);
138
302
  }
139
- const body = document.createElement('div');
140
- body.className = 'jux-modal-body';
141
- body.id = this.bodyId(); // Set predictable ID for child component rendering
142
- body.innerHTML = content;
143
- modal.appendChild(body);
303
+ const contentElement = document.createElement('div');
304
+ contentElement.className = 'jux-modal-content';
305
+ contentElement.id = this.contentId();
306
+ modal.appendChild(contentElement);
307
+ // Render initial content
308
+ if (content) {
309
+ const items = Array.isArray(content) ? content : [content];
310
+ items.forEach(item => {
311
+ if (typeof item === 'string') {
312
+ const tempDiv = document.createElement('div');
313
+ tempDiv.innerHTML = item;
314
+ while (tempDiv.firstChild) {
315
+ contentElement.appendChild(tempDiv.firstChild);
316
+ }
317
+ }
318
+ else if (item instanceof BaseComponent) {
319
+ item.render(contentElement);
320
+ }
321
+ });
322
+ }
323
+ if (actions.length > 0) {
324
+ const footer = document.createElement('div');
325
+ footer.className = 'jux-modal-footer';
326
+ actions.forEach(action => {
327
+ // Check if it's an array of BaseComponents
328
+ if (Array.isArray(action)) {
329
+ action.forEach(component => {
330
+ if (component instanceof BaseComponent) {
331
+ component.render(footer);
332
+ }
333
+ });
334
+ }
335
+ // Otherwise it's an action object with label property
336
+ else if ('label' in action) {
337
+ const btn = document.createElement('button');
338
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
339
+ btn.textContent = action.label;
340
+ btn.type = 'button';
341
+ btn.addEventListener('click', () => {
342
+ if (action.click)
343
+ action.click();
344
+ });
345
+ footer.appendChild(btn);
346
+ }
347
+ });
348
+ modal.appendChild(footer);
349
+ }
144
350
  overlay.appendChild(modal);
351
+ // Default dismiss behavior (only if NOT using sync)
145
352
  if (!hasOpenSync) {
146
- if (showClose) {
353
+ if (close) {
147
354
  const closeButton = modal.querySelector('.jux-modal-close');
148
355
  closeButton?.addEventListener('click', () => {
149
356
  this.state.open = false;
150
- overlay.style.display = 'none';
151
357
  });
152
358
  }
153
- if (closeOnBackdrop) {
359
+ if (backdropClose) {
154
360
  overlay.addEventListener('click', (e) => {
155
361
  if (e.target === overlay) {
156
362
  this.state.open = false;
157
- overlay.style.display = 'none';
158
363
  }
159
364
  });
160
365
  }
161
366
  }
162
367
  this._wireStandardEvents(overlay);
368
+ // Wire sync bindings
163
369
  this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
164
370
  if (property === 'open') {
165
371
  const transformToState = toState || ((v) => Boolean(v));
@@ -170,28 +376,25 @@ export class Modal extends BaseComponent {
170
376
  return;
171
377
  const transformed = transformToComponent(val);
172
378
  this.state.open = transformed;
173
- overlay.style.display = transformed ? 'flex' : 'none';
174
379
  });
175
- if (showClose) {
380
+ if (close) {
176
381
  const closeButton = modal.querySelector('.jux-modal-close');
177
382
  closeButton?.addEventListener('click', () => {
178
383
  if (isUpdating)
179
384
  return;
180
385
  isUpdating = true;
181
386
  this.state.open = false;
182
- overlay.style.display = 'none';
183
387
  stateObj.set(transformToState(false));
184
388
  setTimeout(() => { isUpdating = false; }, 0);
185
389
  });
186
390
  }
187
- if (closeOnBackdrop) {
391
+ if (backdropClose) {
188
392
  overlay.addEventListener('click', (e) => {
189
393
  if (e.target === overlay) {
190
394
  if (isUpdating)
191
395
  return;
192
396
  isUpdating = true;
193
397
  this.state.open = false;
194
- overlay.style.display = 'none';
195
398
  stateObj.set(transformToState(false));
196
399
  setTimeout(() => { isUpdating = false; }, 0);
197
400
  }
@@ -202,7 +405,6 @@ export class Modal extends BaseComponent {
202
405
  const transform = toComponent || ((v) => String(v));
203
406
  stateObj.subscribe((val) => {
204
407
  const transformed = transform(val);
205
- body.innerHTML = transformed;
206
408
  this.state.content = transformed;
207
409
  });
208
410
  }
@@ -210,20 +412,18 @@ export class Modal extends BaseComponent {
210
412
  const transform = toComponent || ((v) => String(v));
211
413
  stateObj.subscribe((val) => {
212
414
  const transformed = transform(val);
213
- const header = modal.querySelector('.jux-modal-header');
214
- if (header) {
215
- header.textContent = transformed;
216
- }
217
415
  this.state.title = transformed;
218
416
  });
219
417
  }
220
418
  });
221
419
  container.appendChild(overlay);
420
+ requestAnimationFrame(() => {
421
+ if (window.Iconify) {
422
+ window.Iconify.scan();
423
+ }
424
+ });
222
425
  return this;
223
426
  }
224
- update(prop, value) {
225
- // No reactive updates needed
226
- }
227
427
  }
228
428
  export function modal(id, options = {}) {
229
429
  return new Modal(id, options);
@@ -1,26 +1,38 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
+ import { renderIcon } from './icons.js';
2
3
 
3
4
  // Event definitions
4
5
  const TRIGGER_EVENTS = [] as const;
5
6
  const CALLBACK_EVENTS = ['open', 'close'] as const;
6
7
 
8
+ export type EmojiIconType = {
9
+ // Can be an emoji character or an icon name from a collection
10
+ icon: string;
11
+ collection?: 'lucide' | 'heroicons' | 'material-symbols' | 'material' | 'icon-park' | string;
12
+ }
7
13
  export interface ModalOptions {
8
14
  title?: string;
9
- content?: string;
10
- showCloseButton?: boolean;
11
- closeOnBackdropClick?: boolean;
15
+ content?: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
16
+ icon?: EmojiIconType | undefined;
17
+ close?: boolean;
18
+ backdropClose?: boolean;
19
+ position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
12
20
  size?: 'small' | 'medium' | 'large';
21
+ actions?: Array<{ label: string; variant?: string; click?: () => void } | Array<BaseComponent<any>>>;
13
22
  style?: string;
14
23
  class?: string;
15
24
  }
16
25
 
17
26
  type ModalState = {
18
27
  title: string;
19
- content: string;
20
- showCloseButton: boolean;
21
- closeOnBackdropClick: boolean;
28
+ content: string | BaseComponent<any> | Array<string | BaseComponent<any>>;
29
+ icon: EmojiIconType | undefined;
30
+ close: boolean;
31
+ backdropClose: boolean;
32
+ position?: 'bottom' | 'center' | 'top' | 'fullscreen' | 'popover';
22
33
  size: string;
23
34
  open: boolean;
35
+ actions: Array<{ label: string; variant?: string; click?: () => void } | Array<BaseComponent<any>>>;
24
36
  style: string;
25
37
  class: string;
26
38
  };
@@ -32,10 +44,13 @@ export class Modal extends BaseComponent<ModalState> {
32
44
  super(id, {
33
45
  title: options.title ?? '',
34
46
  content: options.content ?? '',
35
- showCloseButton: options.showCloseButton ?? true,
36
- closeOnBackdropClick: options.closeOnBackdropClick ?? true,
47
+ icon: options.icon ?? undefined,
48
+ close: options.close ?? true,
49
+ backdropClose: options.backdropClose ?? true,
50
+ position: options.position,
37
51
  size: options.size ?? 'medium',
38
52
  open: false,
53
+ actions: options.actions ?? [],
39
54
  style: options.style ?? '',
40
55
  class: options.class ?? ''
41
56
  });
@@ -50,98 +65,275 @@ export class Modal extends BaseComponent<ModalState> {
50
65
  }
51
66
 
52
67
  /* ═════════════════════════════════════════════════════════════════
53
- * FLUENT API
68
+ * REACTIVE UPDATE (Precision DOM edits)
54
69
  * ═════════════════════════════════════════════════════════════════ */
55
70
 
56
- // Inherited from BaseComponent:
57
- // - style(), class()
58
- // - bind(), sync(), renderTo()
59
- // - addClass(), removeClass(), toggleClass()
60
- // - visible(), show(), hide()
61
- // - attr(), attrs(), removeAttr()
71
+ update(prop: string, value: any): void {
72
+ super.update(prop, value);
62
73
 
63
- title(value: string): this {
64
- this.state.title = value;
65
- if (this._overlay) {
66
- const modal = this._overlay.querySelector('.jux-modal');
67
- const header = modal?.querySelector('.jux-modal-header');
68
- if (header) {
69
- header.textContent = value;
74
+ if (!this._overlay) return;
75
+
76
+ const modal = this._overlay.querySelector('.jux-modal');
77
+ if (!modal) return;
78
+
79
+ switch (prop) {
80
+ case 'open':
81
+ this._overlay.style.display = value ? 'flex' : 'none';
82
+ if (value) {
83
+ this._triggerCallback('open');
84
+ } else {
85
+ this._triggerCallback('close');
86
+ }
87
+ break;
88
+
89
+ case 'title':
90
+ this._updateTitle(value);
91
+ break;
92
+
93
+ case 'content':
94
+ this._updateContent(value);
95
+ break;
96
+
97
+ case 'icon':
98
+ this._updateIcon(value);
99
+ break;
100
+
101
+ case 'size':
102
+ modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
103
+ break;
104
+
105
+ case 'actions':
106
+ this._rebuildActions();
107
+ break;
108
+ }
109
+ }
110
+
111
+ private _updateTitle(value: string): void {
112
+ let headerEl = this._overlay!.querySelector('.jux-modal-header');
113
+
114
+ if (!headerEl && value) {
115
+ // Create header if it doesn't exist
116
+ headerEl = document.createElement('div');
117
+ headerEl.className = 'jux-modal-header';
118
+
119
+ const modal = this._overlay!.querySelector('.jux-modal');
120
+ const body = modal!.querySelector('.jux-modal-body');
121
+ modal!.insertBefore(headerEl, body);
122
+ }
123
+
124
+ if (headerEl) {
125
+ // Preserve icon if exists
126
+ const iconEl = headerEl.querySelector('.jux-modal-header-icon');
127
+ headerEl.innerHTML = '';
128
+
129
+ if (iconEl) {
130
+ headerEl.appendChild(iconEl);
70
131
  }
132
+
133
+ const titleEl = document.createElement('span');
134
+ titleEl.className = 'jux-modal-header-title';
135
+ titleEl.textContent = value;
136
+ headerEl.appendChild(titleEl);
71
137
  }
72
- return this;
73
138
  }
74
139
 
75
- content(value: string): this {
76
- this.state.content = value;
77
- if (this._overlay) {
78
- const body = this._overlay.querySelector('.jux-modal-body');
79
- if (body) {
80
- body.innerHTML = value;
140
+ private _updateIcon(value?: EmojiIconType): void {
141
+ const headerEl = this._overlay!.querySelector('.jux-modal-header');
142
+ if (!headerEl) return;
143
+
144
+ let iconEl = headerEl.querySelector('.jux-modal-header-icon');
145
+
146
+ if (value) {
147
+ if (!iconEl) {
148
+ iconEl = document.createElement('span');
149
+ iconEl.className = 'jux-modal-header-icon';
150
+ headerEl.insertBefore(iconEl, headerEl.firstChild);
151
+ }
152
+ iconEl.innerHTML = '';
153
+ iconEl.appendChild(renderIcon(value.icon, value.collection));
154
+ } else if (iconEl) {
155
+ iconEl.remove();
156
+ }
157
+ }
158
+
159
+ private _updateContent(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): void {
160
+ const contentEl = this._overlay!.querySelector('.jux-modal-content') as HTMLElement;
161
+ if (!contentEl) return;
162
+
163
+ contentEl.innerHTML = '';
164
+
165
+ const items = Array.isArray(value) ? value : [value];
166
+
167
+ items.forEach(item => {
168
+ if (typeof item === 'string') {
169
+ const tempDiv = document.createElement('div');
170
+ tempDiv.innerHTML = item;
171
+ while (tempDiv.firstChild) {
172
+ contentEl.appendChild(tempDiv.firstChild);
173
+ }
174
+ } else if (item instanceof BaseComponent) {
175
+ item.render(contentEl);
81
176
  }
177
+ });
178
+ }
179
+
180
+ private _rebuildActions(): void {
181
+ const modal = this._overlay!.querySelector('.jux-modal');
182
+ if (!modal) return;
183
+
184
+ let footerEl = modal.querySelector('.jux-modal-footer') as HTMLElement | null;
185
+
186
+ if (this.state.actions.length === 0) {
187
+ if (footerEl) footerEl.remove();
188
+ return;
82
189
  }
190
+
191
+ if (!footerEl) {
192
+ footerEl = document.createElement('div');
193
+ footerEl.className = 'jux-modal-footer';
194
+ modal.appendChild(footerEl);
195
+ }
196
+
197
+ footerEl.innerHTML = '';
198
+
199
+ this.state.actions.forEach(action => {
200
+ // Check if it's an array of BaseComponents
201
+ if (Array.isArray(action)) {
202
+ action.forEach(component => {
203
+ if (component instanceof BaseComponent) {
204
+ component.render(footerEl!);
205
+ }
206
+ });
207
+ }
208
+ // Otherwise it's an action object with label property
209
+ else if ('label' in action) {
210
+ const btn = document.createElement('button');
211
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
212
+ btn.textContent = action.label;
213
+ btn.type = 'button';
214
+
215
+ btn.addEventListener('click', () => {
216
+ if (action.click) action.click();
217
+ });
218
+
219
+ footerEl!.appendChild(btn);
220
+ }
221
+ });
222
+ }
223
+
224
+ /* ═════════════════════════════════════════════════════════════════
225
+ * FLUENT API (Just set state, update() handles DOM)
226
+ * ═════════════════════════════════════════════════════════════════ */
227
+
228
+ title(value: string): this {
229
+ this.state.title = value;
83
230
  return this;
84
231
  }
85
232
 
86
- showCloseButton(value: boolean): this {
87
- this.state.showCloseButton = value;
233
+ content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
234
+ this.state.content = value;
88
235
  return this;
89
236
  }
90
237
 
91
- closeOnBackdropClick(value: boolean): this {
92
- this.state.closeOnBackdropClick = value;
238
+ icon(value: EmojiIconType): this {
239
+ this.state.icon = value;
240
+ return this;
241
+ }
242
+
243
+ close(value: boolean): this {
244
+ this.state.close = value;
245
+ return this;
246
+ }
247
+
248
+ backdropClose(value: boolean): this {
249
+ this.state.backdropClose = value;
93
250
  return this;
94
251
  }
95
252
 
96
253
  size(value: 'small' | 'medium' | 'large'): this {
97
254
  this.state.size = value;
98
- if (this._overlay) {
99
- const modal = this._overlay.querySelector('.jux-modal');
100
- if (modal) {
101
- modal.className = modal.className.replace(/jux-modal-(small|medium|large)/, `jux-modal-${value}`);
102
- }
103
- }
255
+ return this;
256
+ }
257
+
258
+ actions(value: Array<{ label: string; variant?: string; click?: () => void }>): this {
259
+ this.state.actions = value;
260
+ return this;
261
+ }
262
+
263
+ addAction(action: { label: string; variant?: string; click?: () => void }): this {
264
+ this.state.actions = [...this.state.actions, action];
104
265
  return this;
105
266
  }
106
267
 
107
268
  open(): this {
108
269
  this.state.open = true;
109
- if (this._overlay) {
110
- this._overlay.style.display = 'flex';
111
- }
112
- // 🎯 Fire the open callback event
113
- this._triggerCallback('open');
114
270
  return this;
115
271
  }
116
272
 
117
- close(): this {
273
+ closeModal(): this {
118
274
  this.state.open = false;
119
- if (this._overlay) {
120
- this._overlay.style.display = 'none';
275
+ return this;
276
+ }
277
+
278
+ toggle(): this {
279
+ this.state.open = !this.state.open;
280
+ return this;
281
+ }
282
+
283
+ /* ═════════════════════════════════════════════════════════════════
284
+ * DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
285
+ * ═════════════════════════════════════════════════════════════════ */
286
+
287
+ /**
288
+ * Add content to modal (supports strings and BaseComponents)
289
+ */
290
+ addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
291
+ const contentEl = this._overlay?.querySelector('.jux-modal-content') as HTMLElement;
292
+ if (!contentEl) {
293
+ console.warn('[Modal] Content element not found');
294
+ return this;
121
295
  }
122
- // 🎯 Fire the close callback event
123
- this._triggerCallback('close');
296
+
297
+ const items = Array.isArray(content) ? content : [content];
298
+
299
+ items.forEach(item => {
300
+ if (typeof item === 'string') {
301
+ const tempDiv = document.createElement('div');
302
+ tempDiv.innerHTML = item;
303
+ while (tempDiv.firstChild) {
304
+ contentEl.appendChild(tempDiv.firstChild);
305
+ }
306
+ } else if (item instanceof BaseComponent) {
307
+ item.render(contentEl);
308
+ }
309
+ });
310
+
124
311
  return this;
125
312
  }
126
313
 
127
314
  /**
128
- * Get the modal body element ID for rendering child components
129
- * @returns The ID of the modal body element (e.g., 'mymodal-body')
130
- *
131
- * Usage:
132
- * const modal = jux.modal('files-modal', { title: 'Files' }).render('body').open();
133
- * jux.table('files-table', {...}).render(modal.bodyId());
315
+ * Clear all content from modal
134
316
  */
135
- bodyId(): string {
136
- return `${this._id}-body`;
317
+ clearContent(): this {
318
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
319
+ if (contentEl) {
320
+ contentEl.innerHTML = '';
321
+ }
322
+ return this;
137
323
  }
138
324
 
139
325
  /**
140
- * Get the modal body element (only available after render)
141
- * @returns The modal body element or null if not rendered
326
+ * Get the modal content element ID for rendering child components
142
327
  */
143
- getBodyElement(): HTMLElement | null {
144
- return document.getElementById(this.bodyId());
328
+ contentId(): string {
329
+ return `${this._id}-content`;
330
+ }
331
+
332
+ /**
333
+ * Get the modal content element (only available after render)
334
+ */
335
+ getContentElement(): HTMLElement | null {
336
+ return document.getElementById(this.contentId());
145
337
  }
146
338
 
147
339
  /* ═════════════════════════════════════════════════════════════════
@@ -151,7 +343,7 @@ export class Modal extends BaseComponent<ModalState> {
151
343
  render(targetId?: string | HTMLElement | BaseComponent<any>): this {
152
344
  const container = this._setupContainer(targetId);
153
345
 
154
- const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
346
+ const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
155
347
  const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
156
348
 
157
349
  const overlay = document.createElement('div');
@@ -165,42 +357,102 @@ export class Modal extends BaseComponent<ModalState> {
165
357
  const modal = document.createElement('div');
166
358
  modal.className = `jux-modal jux-modal-${size}`;
167
359
 
168
- if (showClose) {
360
+ if (close) {
169
361
  const closeButton = document.createElement('button');
170
362
  closeButton.className = 'jux-modal-close';
171
363
  closeButton.innerHTML = '×';
364
+ closeButton.type = 'button';
172
365
  modal.appendChild(closeButton);
173
366
  }
174
367
 
175
- if (title) {
368
+ if (title || icon) {
176
369
  const header = document.createElement('div');
177
370
  header.className = 'jux-modal-header';
178
- header.textContent = title;
371
+
372
+ if (icon) {
373
+ const iconEl = document.createElement('span');
374
+ iconEl.className = 'jux-modal-header-icon';
375
+ iconEl.appendChild(renderIcon(icon.icon, icon.collection));
376
+ header.appendChild(iconEl);
377
+ }
378
+
379
+ if (title) {
380
+ const titleEl = document.createElement('span');
381
+ titleEl.className = 'jux-modal-header-title';
382
+ titleEl.textContent = title;
383
+ header.appendChild(titleEl);
384
+ }
385
+
179
386
  modal.appendChild(header);
180
387
  }
181
388
 
182
- const body = document.createElement('div');
183
- body.className = 'jux-modal-body';
184
- body.id = this.bodyId(); // Set predictable ID for child component rendering
185
- body.innerHTML = content;
186
- modal.appendChild(body);
389
+ const contentElement = document.createElement('div');
390
+ contentElement.className = 'jux-modal-content';
391
+ contentElement.id = this.contentId();
392
+ modal.appendChild(contentElement);
393
+
394
+ // Render initial content
395
+ if (content) {
396
+ const items = Array.isArray(content) ? content : [content];
397
+ items.forEach(item => {
398
+ if (typeof item === 'string') {
399
+ const tempDiv = document.createElement('div');
400
+ tempDiv.innerHTML = item;
401
+ while (tempDiv.firstChild) {
402
+ contentElement.appendChild(tempDiv.firstChild);
403
+ }
404
+ } else if (item instanceof BaseComponent) {
405
+ item.render(contentElement);
406
+ }
407
+ });
408
+ }
409
+
410
+ if (actions.length > 0) {
411
+ const footer = document.createElement('div');
412
+ footer.className = 'jux-modal-footer';
413
+
414
+ actions.forEach(action => {
415
+ // Check if it's an array of BaseComponents
416
+ if (Array.isArray(action)) {
417
+ action.forEach(component => {
418
+ if (component instanceof BaseComponent) {
419
+ component.render(footer);
420
+ }
421
+ });
422
+ }
423
+ // Otherwise it's an action object with label property
424
+ else if ('label' in action) {
425
+ const btn = document.createElement('button');
426
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
427
+ btn.textContent = action.label;
428
+ btn.type = 'button';
429
+
430
+ btn.addEventListener('click', () => {
431
+ if (action.click) action.click();
432
+ });
433
+
434
+ footer.appendChild(btn);
435
+ }
436
+ });
437
+
438
+ modal.appendChild(footer);
439
+ }
187
440
 
188
441
  overlay.appendChild(modal);
189
442
 
443
+ // Default dismiss behavior (only if NOT using sync)
190
444
  if (!hasOpenSync) {
191
- if (showClose) {
445
+ if (close) {
192
446
  const closeButton = modal.querySelector('.jux-modal-close');
193
447
  closeButton?.addEventListener('click', () => {
194
448
  this.state.open = false;
195
- overlay.style.display = 'none';
196
449
  });
197
450
  }
198
451
 
199
- if (closeOnBackdrop) {
452
+ if (backdropClose) {
200
453
  overlay.addEventListener('click', (e) => {
201
454
  if (e.target === overlay) {
202
455
  this.state.open = false;
203
- overlay.style.display = 'none';
204
456
  }
205
457
  });
206
458
  }
@@ -208,6 +460,7 @@ export class Modal extends BaseComponent<ModalState> {
208
460
 
209
461
  this._wireStandardEvents(overlay);
210
462
 
463
+ // Wire sync bindings
211
464
  this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
212
465
  if (property === 'open') {
213
466
  const transformToState = toState || ((v: any) => Boolean(v));
@@ -219,31 +472,28 @@ export class Modal extends BaseComponent<ModalState> {
219
472
  if (isUpdating) return;
220
473
  const transformed = transformToComponent(val);
221
474
  this.state.open = transformed;
222
- overlay.style.display = transformed ? 'flex' : 'none';
223
475
  });
224
476
 
225
- if (showClose) {
477
+ if (close) {
226
478
  const closeButton = modal.querySelector('.jux-modal-close');
227
479
  closeButton?.addEventListener('click', () => {
228
480
  if (isUpdating) return;
229
481
  isUpdating = true;
230
482
 
231
483
  this.state.open = false;
232
- overlay.style.display = 'none';
233
484
  stateObj.set(transformToState(false));
234
485
 
235
486
  setTimeout(() => { isUpdating = false; }, 0);
236
487
  });
237
488
  }
238
489
 
239
- if (closeOnBackdrop) {
490
+ if (backdropClose) {
240
491
  overlay.addEventListener('click', (e) => {
241
492
  if (e.target === overlay) {
242
493
  if (isUpdating) return;
243
494
  isUpdating = true;
244
495
 
245
496
  this.state.open = false;
246
- overlay.style.display = 'none';
247
497
  stateObj.set(transformToState(false));
248
498
 
249
499
  setTimeout(() => { isUpdating = false; }, 0);
@@ -256,7 +506,6 @@ export class Modal extends BaseComponent<ModalState> {
256
506
 
257
507
  stateObj.subscribe((val: any) => {
258
508
  const transformed = transform(val);
259
- body.innerHTML = transformed;
260
509
  this.state.content = transformed;
261
510
  });
262
511
  }
@@ -265,21 +514,20 @@ export class Modal extends BaseComponent<ModalState> {
265
514
 
266
515
  stateObj.subscribe((val: any) => {
267
516
  const transformed = transform(val);
268
- const header = modal.querySelector('.jux-modal-header');
269
- if (header) {
270
- header.textContent = transformed;
271
- }
272
517
  this.state.title = transformed;
273
518
  });
274
519
  }
275
520
  });
276
521
 
277
522
  container.appendChild(overlay);
278
- return this;
279
- }
280
523
 
281
- update(prop: string, value: any): void {
282
- // No reactive updates needed
524
+ requestAnimationFrame(() => {
525
+ if ((window as any).Iconify) {
526
+ (window as any).Iconify.scan();
527
+ }
528
+ });
529
+
530
+ return this;
283
531
  }
284
532
  }
285
533
 
@@ -306,7 +306,7 @@ export function getSyntaxHighlightCSS() {
306
306
  return `
307
307
  /* Semantic Code Highlighting */
308
308
  .jux-code {
309
- font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
309
+ font-family: 'Consolas', 'Monaco', monospace;
310
310
  font-size: 14px;
311
311
  line-height: 1.6;
312
312
  background: #1e1e1e;
@@ -332,7 +332,7 @@ export function getSyntaxHighlightCSS(): string {
332
332
  return `
333
333
  /* Semantic Code Highlighting */
334
334
  .jux-code {
335
- font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
335
+ font-family: 'Consolas', 'Monaco', monospace;
336
336
  font-size: 14px;
337
337
  line-height: 1.6;
338
338
  background: #1e1e1e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.59",
3
+ "version": "1.1.62",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",