juxscript 1.1.60 → 1.1.63

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,55 @@ 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
+ * Check if modal is currently open
70
+ */
71
+ isOpen(): boolean;
72
+ /**
73
+ * Check if modal is currently closed
74
+ */
75
+ isClosed(): boolean;
33
76
  /**
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());
77
+ * Add content to modal (supports strings and BaseComponents)
40
78
  */
41
- bodyId(): string;
79
+ addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this;
42
80
  /**
43
- * Get the modal body element (only available after render)
44
- * @returns The modal body element or null if not rendered
81
+ * Clear all content from modal
45
82
  */
46
- getBodyElement(): HTMLElement | null;
83
+ clearContent(): this;
84
+ /**
85
+ * Get the modal content element ID for rendering child components
86
+ */
87
+ contentId(): string;
88
+ /**
89
+ * Get the modal content element (only available after render)
90
+ */
91
+ getContentElement(): HTMLElement | null;
47
92
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
48
- update(prop: string, value: any): void;
49
93
  }
50
94
  export declare function modal(id: string, options?: ModalOptions): Modal;
51
95
  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,255 @@ 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);
41
99
  }
100
+ iconEl.innerHTML = '';
101
+ iconEl.appendChild(renderIcon(value.icon, value.collection));
102
+ }
103
+ else if (iconEl) {
104
+ iconEl.remove();
42
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);
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;
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
+ * Check if modal is currently open
214
+ */
215
+ isOpen() {
216
+ return this.state.open;
217
+ }
218
+ /**
219
+ * Check if modal is currently closed
220
+ */
221
+ isClosed() {
222
+ return !this.state.open;
223
+ }
224
+ /* ═════════════════════════════════════════════════════════════════
225
+ * DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
226
+ * ═════════════════════════════════════════════════════════════════ */
227
+ /**
228
+ * Add content to modal (supports strings and BaseComponents)
229
+ */
230
+ addContent(content) {
231
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
232
+ if (!contentEl) {
233
+ console.warn('[Modal] Content element not found');
234
+ return this;
235
+ }
236
+ const items = Array.isArray(content) ? content : [content];
237
+ items.forEach(item => {
238
+ if (typeof item === 'string') {
239
+ const tempDiv = document.createElement('div');
240
+ tempDiv.innerHTML = item;
241
+ while (tempDiv.firstChild) {
242
+ contentEl.appendChild(tempDiv.firstChild);
243
+ }
244
+ }
245
+ else if (item instanceof BaseComponent) {
246
+ item.render(contentEl);
247
+ }
248
+ });
249
+ return this;
250
+ }
251
+ /**
252
+ * Clear all content from modal
253
+ */
254
+ clearContent() {
255
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
256
+ if (contentEl) {
257
+ contentEl.innerHTML = '';
86
258
  }
87
- // 🎯 Fire the close callback event
88
- this._triggerCallback('close');
89
259
  return this;
90
260
  }
91
261
  /**
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());
262
+ * Get the modal content element ID for rendering child components
98
263
  */
99
- bodyId() {
100
- return `${this._id}-body`;
264
+ contentId() {
265
+ return `${this._id}-content`;
101
266
  }
102
267
  /**
103
- * Get the modal body element (only available after render)
104
- * @returns The modal body element or null if not rendered
268
+ * Get the modal content element (only available after render)
105
269
  */
106
- getBodyElement() {
107
- return document.getElementById(this.bodyId());
270
+ getContentElement() {
271
+ return document.getElementById(this.contentId());
108
272
  }
109
273
  /* ═════════════════════════════════════════════════════════════════
110
274
  * RENDER
111
275
  * ═════════════════════════════════════════════════════════════════ */
112
276
  render(targetId) {
113
277
  const container = this._setupContainer(targetId);
114
- const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
278
+ const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
115
279
  const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
116
280
  const overlay = document.createElement('div');
117
281
  overlay.className = 'jux-modal-overlay';
@@ -124,42 +288,96 @@ export class Modal extends BaseComponent {
124
288
  this._overlay = overlay;
125
289
  const modal = document.createElement('div');
126
290
  modal.className = `jux-modal jux-modal-${size}`;
127
- if (showClose) {
291
+ if (close) {
128
292
  const closeButton = document.createElement('button');
129
293
  closeButton.className = 'jux-modal-close';
130
294
  closeButton.innerHTML = '×';
295
+ closeButton.type = 'button';
131
296
  modal.appendChild(closeButton);
132
297
  }
133
- if (title) {
298
+ if (title || icon) {
134
299
  const header = document.createElement('div');
135
300
  header.className = 'jux-modal-header';
136
- header.textContent = title;
301
+ if (icon) {
302
+ const iconEl = document.createElement('span');
303
+ iconEl.className = 'jux-modal-header-icon';
304
+ iconEl.appendChild(renderIcon(icon.icon, icon.collection));
305
+ header.appendChild(iconEl);
306
+ }
307
+ if (title) {
308
+ const titleEl = document.createElement('span');
309
+ titleEl.className = 'jux-modal-header-title';
310
+ titleEl.textContent = title;
311
+ header.appendChild(titleEl);
312
+ }
137
313
  modal.appendChild(header);
138
314
  }
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);
315
+ const contentElement = document.createElement('div');
316
+ contentElement.className = 'jux-modal-content';
317
+ contentElement.id = this.contentId();
318
+ modal.appendChild(contentElement);
319
+ // Render initial content
320
+ if (content) {
321
+ const items = Array.isArray(content) ? content : [content];
322
+ items.forEach(item => {
323
+ if (typeof item === 'string') {
324
+ const tempDiv = document.createElement('div');
325
+ tempDiv.innerHTML = item;
326
+ while (tempDiv.firstChild) {
327
+ contentElement.appendChild(tempDiv.firstChild);
328
+ }
329
+ }
330
+ else if (item instanceof BaseComponent) {
331
+ item.render(contentElement);
332
+ }
333
+ });
334
+ }
335
+ if (actions.length > 0) {
336
+ const footer = document.createElement('div');
337
+ footer.className = 'jux-modal-footer';
338
+ actions.forEach(action => {
339
+ // Check if it's an array of BaseComponents
340
+ if (Array.isArray(action)) {
341
+ action.forEach(component => {
342
+ if (component instanceof BaseComponent) {
343
+ component.render(footer);
344
+ }
345
+ });
346
+ }
347
+ // Otherwise it's an action object with label property
348
+ else if ('label' in action) {
349
+ const btn = document.createElement('button');
350
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
351
+ btn.textContent = action.label;
352
+ btn.type = 'button';
353
+ btn.addEventListener('click', () => {
354
+ if (action.click)
355
+ action.click();
356
+ });
357
+ footer.appendChild(btn);
358
+ }
359
+ });
360
+ modal.appendChild(footer);
361
+ }
144
362
  overlay.appendChild(modal);
363
+ // Default dismiss behavior (only if NOT using sync)
145
364
  if (!hasOpenSync) {
146
- if (showClose) {
365
+ if (close) {
147
366
  const closeButton = modal.querySelector('.jux-modal-close');
148
367
  closeButton?.addEventListener('click', () => {
149
368
  this.state.open = false;
150
- overlay.style.display = 'none';
151
369
  });
152
370
  }
153
- if (closeOnBackdrop) {
371
+ if (backdropClose) {
154
372
  overlay.addEventListener('click', (e) => {
155
373
  if (e.target === overlay) {
156
374
  this.state.open = false;
157
- overlay.style.display = 'none';
158
375
  }
159
376
  });
160
377
  }
161
378
  }
162
379
  this._wireStandardEvents(overlay);
380
+ // Wire sync bindings
163
381
  this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
164
382
  if (property === 'open') {
165
383
  const transformToState = toState || ((v) => Boolean(v));
@@ -170,28 +388,25 @@ export class Modal extends BaseComponent {
170
388
  return;
171
389
  const transformed = transformToComponent(val);
172
390
  this.state.open = transformed;
173
- overlay.style.display = transformed ? 'flex' : 'none';
174
391
  });
175
- if (showClose) {
392
+ if (close) {
176
393
  const closeButton = modal.querySelector('.jux-modal-close');
177
394
  closeButton?.addEventListener('click', () => {
178
395
  if (isUpdating)
179
396
  return;
180
397
  isUpdating = true;
181
398
  this.state.open = false;
182
- overlay.style.display = 'none';
183
399
  stateObj.set(transformToState(false));
184
400
  setTimeout(() => { isUpdating = false; }, 0);
185
401
  });
186
402
  }
187
- if (closeOnBackdrop) {
403
+ if (backdropClose) {
188
404
  overlay.addEventListener('click', (e) => {
189
405
  if (e.target === overlay) {
190
406
  if (isUpdating)
191
407
  return;
192
408
  isUpdating = true;
193
409
  this.state.open = false;
194
- overlay.style.display = 'none';
195
410
  stateObj.set(transformToState(false));
196
411
  setTimeout(() => { isUpdating = false; }, 0);
197
412
  }
@@ -202,7 +417,6 @@ export class Modal extends BaseComponent {
202
417
  const transform = toComponent || ((v) => String(v));
203
418
  stateObj.subscribe((val) => {
204
419
  const transformed = transform(val);
205
- body.innerHTML = transformed;
206
420
  this.state.content = transformed;
207
421
  });
208
422
  }
@@ -210,20 +424,18 @@ export class Modal extends BaseComponent {
210
424
  const transform = toComponent || ((v) => String(v));
211
425
  stateObj.subscribe((val) => {
212
426
  const transformed = transform(val);
213
- const header = modal.querySelector('.jux-modal-header');
214
- if (header) {
215
- header.textContent = transformed;
216
- }
217
427
  this.state.title = transformed;
218
428
  });
219
429
  }
220
430
  });
221
431
  container.appendChild(overlay);
432
+ requestAnimationFrame(() => {
433
+ if (window.Iconify) {
434
+ window.Iconify.scan();
435
+ }
436
+ });
222
437
  return this;
223
438
  }
224
- update(prop, value) {
225
- // No reactive updates needed
226
- }
227
439
  }
228
440
  export function modal(id, options = {}) {
229
441
  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,289 @@ 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;
189
+ }
190
+
191
+ if (!footerEl) {
192
+ footerEl = document.createElement('div');
193
+ footerEl.className = 'jux-modal-footer';
194
+ modal.appendChild(footerEl);
82
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;
230
+ return this;
231
+ }
232
+
233
+ content(value: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
234
+ this.state.content = value;
235
+ return this;
236
+ }
237
+
238
+ icon(value: EmojiIconType): this {
239
+ this.state.icon = value;
83
240
  return this;
84
241
  }
85
242
 
86
- showCloseButton(value: boolean): this {
87
- this.state.showCloseButton = value;
243
+ close(value: boolean): this {
244
+ this.state.close = value;
88
245
  return this;
89
246
  }
90
247
 
91
- closeOnBackdropClick(value: boolean): this {
92
- this.state.closeOnBackdropClick = value;
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
+ * Check if modal is currently open
285
+ */
286
+ isOpen(): boolean {
287
+ return this.state.open;
288
+ }
289
+
290
+ /**
291
+ * Check if modal is currently closed
292
+ */
293
+ isClosed(): boolean {
294
+ return !this.state.open;
295
+ }
296
+
297
+ /* ═════════════════════════════════════════════════════════════════
298
+ * DYNAMIC CONTENT ADDITION (Like tabs.addTabContent)
299
+ * ═════════════════════════════════════════════════════════════════ */
300
+
301
+ /**
302
+ * Add content to modal (supports strings and BaseComponents)
303
+ */
304
+ addContent(content: string | BaseComponent<any> | Array<string | BaseComponent<any>>): this {
305
+ const contentEl = this._overlay?.querySelector('.jux-modal-content') as HTMLElement;
306
+ if (!contentEl) {
307
+ console.warn('[Modal] Content element not found');
308
+ return this;
121
309
  }
122
- // 🎯 Fire the close callback event
123
- this._triggerCallback('close');
310
+
311
+ const items = Array.isArray(content) ? content : [content];
312
+
313
+ items.forEach(item => {
314
+ if (typeof item === 'string') {
315
+ const tempDiv = document.createElement('div');
316
+ tempDiv.innerHTML = item;
317
+ while (tempDiv.firstChild) {
318
+ contentEl.appendChild(tempDiv.firstChild);
319
+ }
320
+ } else if (item instanceof BaseComponent) {
321
+ item.render(contentEl);
322
+ }
323
+ });
324
+
124
325
  return this;
125
326
  }
126
327
 
127
328
  /**
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());
329
+ * Clear all content from modal
134
330
  */
135
- bodyId(): string {
136
- return `${this._id}-body`;
331
+ clearContent(): this {
332
+ const contentEl = this._overlay?.querySelector('.jux-modal-content');
333
+ if (contentEl) {
334
+ contentEl.innerHTML = '';
335
+ }
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Get the modal content element ID for rendering child components
341
+ */
342
+ contentId(): string {
343
+ return `${this._id}-content`;
137
344
  }
138
345
 
139
346
  /**
140
- * Get the modal body element (only available after render)
141
- * @returns The modal body element or null if not rendered
347
+ * Get the modal content element (only available after render)
142
348
  */
143
- getBodyElement(): HTMLElement | null {
144
- return document.getElementById(this.bodyId());
349
+ getContentElement(): HTMLElement | null {
350
+ return document.getElementById(this.contentId());
145
351
  }
146
352
 
147
353
  /* ═════════════════════════════════════════════════════════════════
@@ -151,7 +357,7 @@ export class Modal extends BaseComponent<ModalState> {
151
357
  render(targetId?: string | HTMLElement | BaseComponent<any>): this {
152
358
  const container = this._setupContainer(targetId);
153
359
 
154
- const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
360
+ const { open, title, content, icon, size, close, backdropClose, actions, style, class: className } = this.state;
155
361
  const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
156
362
 
157
363
  const overlay = document.createElement('div');
@@ -165,42 +371,102 @@ export class Modal extends BaseComponent<ModalState> {
165
371
  const modal = document.createElement('div');
166
372
  modal.className = `jux-modal jux-modal-${size}`;
167
373
 
168
- if (showClose) {
374
+ if (close) {
169
375
  const closeButton = document.createElement('button');
170
376
  closeButton.className = 'jux-modal-close';
171
377
  closeButton.innerHTML = '×';
378
+ closeButton.type = 'button';
172
379
  modal.appendChild(closeButton);
173
380
  }
174
381
 
175
- if (title) {
382
+ if (title || icon) {
176
383
  const header = document.createElement('div');
177
384
  header.className = 'jux-modal-header';
178
- header.textContent = title;
385
+
386
+ if (icon) {
387
+ const iconEl = document.createElement('span');
388
+ iconEl.className = 'jux-modal-header-icon';
389
+ iconEl.appendChild(renderIcon(icon.icon, icon.collection));
390
+ header.appendChild(iconEl);
391
+ }
392
+
393
+ if (title) {
394
+ const titleEl = document.createElement('span');
395
+ titleEl.className = 'jux-modal-header-title';
396
+ titleEl.textContent = title;
397
+ header.appendChild(titleEl);
398
+ }
399
+
179
400
  modal.appendChild(header);
180
401
  }
181
402
 
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);
403
+ const contentElement = document.createElement('div');
404
+ contentElement.className = 'jux-modal-content';
405
+ contentElement.id = this.contentId();
406
+ modal.appendChild(contentElement);
407
+
408
+ // Render initial content
409
+ if (content) {
410
+ const items = Array.isArray(content) ? content : [content];
411
+ items.forEach(item => {
412
+ if (typeof item === 'string') {
413
+ const tempDiv = document.createElement('div');
414
+ tempDiv.innerHTML = item;
415
+ while (tempDiv.firstChild) {
416
+ contentElement.appendChild(tempDiv.firstChild);
417
+ }
418
+ } else if (item instanceof BaseComponent) {
419
+ item.render(contentElement);
420
+ }
421
+ });
422
+ }
423
+
424
+ if (actions.length > 0) {
425
+ const footer = document.createElement('div');
426
+ footer.className = 'jux-modal-footer';
427
+
428
+ actions.forEach(action => {
429
+ // Check if it's an array of BaseComponents
430
+ if (Array.isArray(action)) {
431
+ action.forEach(component => {
432
+ if (component instanceof BaseComponent) {
433
+ component.render(footer);
434
+ }
435
+ });
436
+ }
437
+ // Otherwise it's an action object with label property
438
+ else if ('label' in action) {
439
+ const btn = document.createElement('button');
440
+ btn.className = `jux-modal-action jux-modal-action-${action.variant || 'default'}`;
441
+ btn.textContent = action.label;
442
+ btn.type = 'button';
443
+
444
+ btn.addEventListener('click', () => {
445
+ if (action.click) action.click();
446
+ });
447
+
448
+ footer.appendChild(btn);
449
+ }
450
+ });
451
+
452
+ modal.appendChild(footer);
453
+ }
187
454
 
188
455
  overlay.appendChild(modal);
189
456
 
457
+ // Default dismiss behavior (only if NOT using sync)
190
458
  if (!hasOpenSync) {
191
- if (showClose) {
459
+ if (close) {
192
460
  const closeButton = modal.querySelector('.jux-modal-close');
193
461
  closeButton?.addEventListener('click', () => {
194
462
  this.state.open = false;
195
- overlay.style.display = 'none';
196
463
  });
197
464
  }
198
465
 
199
- if (closeOnBackdrop) {
466
+ if (backdropClose) {
200
467
  overlay.addEventListener('click', (e) => {
201
468
  if (e.target === overlay) {
202
469
  this.state.open = false;
203
- overlay.style.display = 'none';
204
470
  }
205
471
  });
206
472
  }
@@ -208,6 +474,7 @@ export class Modal extends BaseComponent<ModalState> {
208
474
 
209
475
  this._wireStandardEvents(overlay);
210
476
 
477
+ // Wire sync bindings
211
478
  this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
212
479
  if (property === 'open') {
213
480
  const transformToState = toState || ((v: any) => Boolean(v));
@@ -219,31 +486,28 @@ export class Modal extends BaseComponent<ModalState> {
219
486
  if (isUpdating) return;
220
487
  const transformed = transformToComponent(val);
221
488
  this.state.open = transformed;
222
- overlay.style.display = transformed ? 'flex' : 'none';
223
489
  });
224
490
 
225
- if (showClose) {
491
+ if (close) {
226
492
  const closeButton = modal.querySelector('.jux-modal-close');
227
493
  closeButton?.addEventListener('click', () => {
228
494
  if (isUpdating) return;
229
495
  isUpdating = true;
230
496
 
231
497
  this.state.open = false;
232
- overlay.style.display = 'none';
233
498
  stateObj.set(transformToState(false));
234
499
 
235
500
  setTimeout(() => { isUpdating = false; }, 0);
236
501
  });
237
502
  }
238
503
 
239
- if (closeOnBackdrop) {
504
+ if (backdropClose) {
240
505
  overlay.addEventListener('click', (e) => {
241
506
  if (e.target === overlay) {
242
507
  if (isUpdating) return;
243
508
  isUpdating = true;
244
509
 
245
510
  this.state.open = false;
246
- overlay.style.display = 'none';
247
511
  stateObj.set(transformToState(false));
248
512
 
249
513
  setTimeout(() => { isUpdating = false; }, 0);
@@ -256,7 +520,6 @@ export class Modal extends BaseComponent<ModalState> {
256
520
 
257
521
  stateObj.subscribe((val: any) => {
258
522
  const transformed = transform(val);
259
- body.innerHTML = transformed;
260
523
  this.state.content = transformed;
261
524
  });
262
525
  }
@@ -265,21 +528,20 @@ export class Modal extends BaseComponent<ModalState> {
265
528
 
266
529
  stateObj.subscribe((val: any) => {
267
530
  const transformed = transform(val);
268
- const header = modal.querySelector('.jux-modal-header');
269
- if (header) {
270
- header.textContent = transformed;
271
- }
272
531
  this.state.title = transformed;
273
532
  });
274
533
  }
275
534
  });
276
535
 
277
536
  container.appendChild(overlay);
278
- return this;
279
- }
280
537
 
281
- update(prop: string, value: any): void {
282
- // No reactive updates needed
538
+ requestAnimationFrame(() => {
539
+ if ((window as any).Iconify) {
540
+ (window as any).Iconify.scan();
541
+ }
542
+ });
543
+
544
+ return this;
283
545
  }
284
546
  }
285
547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.60",
3
+ "version": "1.1.63",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",