@myxo-victor/chexjs 6.0.0

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.
@@ -0,0 +1,617 @@
1
+ /* ==========================================================
2
+ FLEXMODAL: Self-Styling Zero-Dependency Modal Engine
3
+ ========================================================== */
4
+ class FlexModal {
5
+ constructor() {
6
+ this.backdrop = null;
7
+ this.container = null;
8
+ this.focusableElements = [];
9
+ this.previouslyFocusedElement = null;
10
+ this.resolvePromise = null;
11
+
12
+ this._boundKeyDown = this._handleKeyDown.bind(this);
13
+ this._boundOverlayClick = this._handleOverlayClick.bind(this);
14
+
15
+ // Dynamic self-styling injection
16
+ this._injectStyles();
17
+ }
18
+
19
+ // Pure CSS Stylesheet Injector (Zero CSS dependencies)
20
+ _injectStyles() {
21
+ const STYLE_ID = 'flexmodal-embedded-styles';
22
+ if (document.getElementById(STYLE_ID)) return;
23
+
24
+ const stylesheet = document.createElement('style');
25
+ stylesheet.id = STYLE_ID;
26
+ stylesheet.textContent = `
27
+ .flexmodal-backdrop {
28
+ position: fixed;
29
+ top: 0;
30
+ left: 0;
31
+ width: 100%;
32
+ height: 100%;
33
+ background-color: rgba(15, 23, 42, 0.6);
34
+ backdrop-filter: blur(4px);
35
+ -webkit-backdrop-filter: blur(4px);
36
+ z-index: 99999;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ padding: 16px;
41
+ opacity: 0;
42
+ transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
43
+ box-sizing: border-box;
44
+ }
45
+ .flexmodal-backdrop.is-active {
46
+ opacity: 1;
47
+ }
48
+ .flexmodal-container {
49
+ background: #ffffff;
50
+ border-radius: 16px;
51
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
52
+ width: 100%;
53
+ max-width: 500px;
54
+ max-height: 90vh;
55
+ overflow-y: auto;
56
+ border: 1px solid #e2e8f0;
57
+ transform: scale(0.95) translateY(12px);
58
+ opacity: 0;
59
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.25s ease;
60
+ box-sizing: border-box;
61
+ }
62
+ .flexmodal-backdrop.is-active .flexmodal-container {
63
+ transform: scale(1) translateY(0);
64
+ opacity: 1;
65
+ }
66
+
67
+ /* Slide-Over Right Panel variation */
68
+ .flexmodal-backdrop.slide-over {
69
+ justify-content: flex-end;
70
+ padding: 0;
71
+ }
72
+ .flexmodal-backdrop.slide-over .flexmodal-container {
73
+ height: 100%;
74
+ max-height: 100%;
75
+ border-radius: 0;
76
+ border-left: 1px solid #e2e8f0;
77
+ border-top: none;
78
+ border-bottom: none;
79
+ width: 100%;
80
+ max-width: 400px;
81
+ transform: translateX(100%);
82
+ transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
83
+ }
84
+ .flexmodal-backdrop.slide-over.is-active .flexmodal-container {
85
+ transform: translateX(0);
86
+ }
87
+
88
+ /* Inner UI Styles */
89
+ .flexmodal-body {
90
+ padding: 24px;
91
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
92
+ color: #1e293b;
93
+ line-height: 1.5;
94
+ box-sizing: border-box;
95
+ }
96
+ .flexmodal-row {
97
+ display: flex;
98
+ gap: 16px;
99
+ align-items: flex-start;
100
+ }
101
+ .flexmodal-column {
102
+ flex: 1;
103
+ }
104
+ .flexmodal-title {
105
+ font-size: 18px;
106
+ font-weight: 700;
107
+ margin: 0 0 6px 0;
108
+ color: #0f172a;
109
+ }
110
+ .flexmodal-subtitle {
111
+ font-size: 14px;
112
+ color: #64748b;
113
+ margin: 0;
114
+ }
115
+
116
+ /* Icons */
117
+ .flexmodal-icon-box {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ width: 48px;
122
+ height: 48px;
123
+ border-radius: 50%;
124
+ flex-shrink: 0;
125
+ }
126
+ .flexmodal-icon-box.informational {
127
+ background-color: #e0f2fe;
128
+ color: #0369a1;
129
+ }
130
+ .flexmodal-icon-box.promise {
131
+ background-color: #dcfce7;
132
+ color: #15803d;
133
+ }
134
+
135
+ /* Header & Close Action */
136
+ .flexmodal-head-row {
137
+ display: flex;
138
+ justify-content: space-between;
139
+ align-items: center;
140
+ border-bottom: 1px solid #f1f5f9;
141
+ padding-bottom: 16px;
142
+ margin-bottom: 16px;
143
+ }
144
+ .flexmodal-head-title {
145
+ font-size: 18px;
146
+ font-weight: 700;
147
+ margin: 0;
148
+ color: #0f172a;
149
+ }
150
+ .flexmodal-close-icon {
151
+ background: transparent;
152
+ border: none;
153
+ padding: 4px;
154
+ border-radius: 6px;
155
+ cursor: pointer;
156
+ color: #94a3b8;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ transition: background 0.15s, color 0.15s;
161
+ }
162
+ .flexmodal-close-icon:hover {
163
+ background-color: #f1f5f9;
164
+ color: #334155;
165
+ }
166
+
167
+ /* Buttons & Footer */
168
+ .flexmodal-footer {
169
+ display: flex;
170
+ justify-content: flex-end;
171
+ gap: 12px;
172
+ margin-top: 24px;
173
+ padding-top: 16px;
174
+ border-top: 1px solid #f1f5f9;
175
+ }
176
+ .flexmodal-btn {
177
+ font-family: inherit;
178
+ font-size: 14px;
179
+ font-weight: 600;
180
+ padding: 10px 18px;
181
+ border-radius: 8px;
182
+ cursor: pointer;
183
+ transition: background 0.15s, border-color 0.15s;
184
+ border: none;
185
+ outline: none;
186
+ }
187
+ .flexmodal-btn:focus-visible {
188
+ outline: 2px solid #4f46e5;
189
+ outline-offset: 2px;
190
+ }
191
+ .flexmodal-btn-primary {
192
+ background-color: #4f46e5;
193
+ color: #ffffff;
194
+ }
195
+ .flexmodal-btn-primary:hover {
196
+ background-color: #4338ca;
197
+ }
198
+ .flexmodal-btn-success {
199
+ background-color: #16a34a;
200
+ color: #ffffff;
201
+ }
202
+ .flexmodal-btn-success:hover {
203
+ background-color: #15803d;
204
+ }
205
+ .flexmodal-btn-secondary {
206
+ background-color: #ffffff;
207
+ border: 1px solid #cbd5e1;
208
+ color: #475569;
209
+ }
210
+ .flexmodal-btn-secondary:hover {
211
+ background-color: #f8fafc;
212
+ border-color: #94a3b8;
213
+ }
214
+ .flexmodal-btn-full {
215
+ width: 100%;
216
+ }
217
+
218
+ /* Containers for tabs */
219
+ .flexmodal-tabs-container {
220
+ margin-top: 16px;
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 10px;
224
+ }
225
+ `;
226
+ document.head.appendChild(stylesheet);
227
+ }
228
+
229
+ // Static entry shortcut
230
+ static open(options) {
231
+ const modal = new FlexModal();
232
+ return modal.open(options);
233
+ }
234
+
235
+ open(options = {}) {
236
+ this.options = Object.assign({
237
+ type: 'informational', // informational, promise, slide-over, dynamic
238
+ title: 'Alert Notification',
239
+ subtitle: '',
240
+ button1: 'OK',
241
+ button2: 'Cancel',
242
+ tabs: null, // HTML string OR HTMLElement node
243
+ html: '', // Raw HTML for dynamic mode
244
+ closeOnOverlayClick: true,
245
+ closeOnEscape: true
246
+ }, options);
247
+
248
+ this.previouslyFocusedElement = document.activeElement;
249
+
250
+ // Build the dynamic elements
251
+ this._prepareDOM();
252
+
253
+ // Attach keyboard / overlay binders
254
+ if (this.options.closeOnOverlayClick) {
255
+ this.backdrop.addEventListener('click', this._boundOverlayClick);
256
+ }
257
+ if (this.options.closeOnEscape) {
258
+ document.addEventListener('keydown', this._boundKeyDown);
259
+ }
260
+
261
+ // Trigger visual transitions
262
+ requestAnimationFrame(() => {
263
+ this.backdrop.classList.add('is-active');
264
+ });
265
+
266
+ // Capture focus loop
267
+ this._updateFocusableElements();
268
+ if (this.focusableElements.length > 0) {
269
+ this.focusableElements[0].focus();
270
+ }
271
+
272
+ // Auto update any lucide-icons if present in host app
273
+ if (window.lucide) {
274
+ window.lucide.createIcons();
275
+ }
276
+
277
+ return new Promise((resolve) => {
278
+ this.resolvePromise = resolve;
279
+ });
280
+ }
281
+
282
+ _prepareDOM() {
283
+ this.backdrop = document.createElement('div');
284
+ this.backdrop.className = `flexmodal-backdrop ${this.options.type === 'slide-over' ? 'slide-over' : ''}`;
285
+ this.backdrop.setAttribute('role', 'dialog');
286
+ this.backdrop.setAttribute('aria-modal', 'true');
287
+
288
+ this.container = document.createElement('div');
289
+ this.container.className = 'flexmodal-container';
290
+ this.backdrop.appendChild(this.container);
291
+
292
+ // Assemble the body markup with pure CSS styles
293
+ const bodyWrapper = document.createElement('div');
294
+ bodyWrapper.className = 'flexmodal-body';
295
+
296
+ if (this.options.type === 'slide-over') {
297
+ // Right Slide Drawer Panel
298
+ const headRow = document.createElement('div');
299
+ headRow.className = 'flexmodal-head-row';
300
+
301
+ const headTitle = document.createElement('h3');
302
+ headTitle.className = 'flexmodal-head-title';
303
+ headTitle.textContent = this.options.title;
304
+
305
+ const closeBtn = document.createElement('button');
306
+ closeBtn.className = 'flexmodal-close-icon';
307
+ closeBtn.onclick = () => this.close(false);
308
+ closeBtn.appendChild(this._svgCloseIcon());
309
+
310
+ headRow.appendChild(headTitle);
311
+ headRow.appendChild(closeBtn);
312
+
313
+ const contentBox = document.createElement('div');
314
+ contentBox.className = 'flexmodal-column';
315
+
316
+ if (this.options.subtitle) {
317
+ const subtitle = document.createElement('p');
318
+ subtitle.className = 'flexmodal-subtitle';
319
+ subtitle.textContent = this.options.subtitle;
320
+ contentBox.appendChild(subtitle);
321
+ }
322
+
323
+ const tabsTarget = document.createElement('div');
324
+ tabsTarget.id = 'flexmodal-tabs-target';
325
+ tabsTarget.className = 'flexmodal-tabs-container';
326
+ contentBox.appendChild(tabsTarget);
327
+
328
+ const footer = document.createElement('div');
329
+ footer.className = 'flexmodal-footer';
330
+
331
+ const footerBtn = document.createElement('button');
332
+ footerBtn.className = 'flexmodal-btn flexmodal-btn-primary flexmodal-btn-full';
333
+ footerBtn.onclick = () => this.close(true);
334
+ footerBtn.textContent = this.options.button1;
335
+ footer.appendChild(footerBtn);
336
+
337
+ bodyWrapper.appendChild(headRow);
338
+ bodyWrapper.appendChild(contentBox);
339
+ bodyWrapper.appendChild(footer);
340
+
341
+ } else if (this.options.type === 'dynamic') {
342
+ // Custom Dynamic Injected Canvas
343
+ const headRow = document.createElement('div');
344
+ headRow.className = 'flexmodal-head-row';
345
+
346
+ const headTitle = document.createElement('h3');
347
+ headTitle.className = 'flexmodal-head-title';
348
+ headTitle.textContent = this.options.title;
349
+
350
+ const closeBtn = document.createElement('button');
351
+ closeBtn.className = 'flexmodal-close-icon';
352
+ closeBtn.onclick = () => this.close(false);
353
+ closeBtn.appendChild(this._svgCloseIcon());
354
+
355
+ headRow.appendChild(headTitle);
356
+ headRow.appendChild(closeBtn);
357
+
358
+ const dynamicContentWrapper = document.createElement('div');
359
+ dynamicContentWrapper.style.margin = '16px 0';
360
+ if (typeof this.options.html === 'string') {
361
+ dynamicContentWrapper.innerHTML = this.options.html;
362
+ } else if (this.options.html instanceof HTMLElement) {
363
+ dynamicContentWrapper.appendChild(this.options.html);
364
+ }
365
+
366
+ const footer = document.createElement('div');
367
+ footer.className = 'flexmodal-footer';
368
+
369
+ const footerBtn = document.createElement('button');
370
+ footerBtn.className = 'flexmodal-btn flexmodal-btn-primary';
371
+ footerBtn.onclick = () => this.close(true);
372
+ footerBtn.textContent = this.options.button1;
373
+ footer.appendChild(footerBtn);
374
+
375
+ bodyWrapper.appendChild(headRow);
376
+ bodyWrapper.appendChild(dynamicContentWrapper);
377
+ bodyWrapper.appendChild(footer);
378
+
379
+ } else {
380
+ // Informational & Promise Standard Dialog Box
381
+ const isPromise = this.options.type === 'promise';
382
+ const iconClass = isPromise ? 'promise' : 'informational';
383
+
384
+ const row = document.createElement('div');
385
+ row.className = 'flexmodal-row';
386
+
387
+ const iconBox = document.createElement('div');
388
+ iconBox.className = `flexmodal-icon-box ${iconClass}`;
389
+ iconBox.appendChild(isPromise ? this._svgPromiseIcon() : this._svgAlertIcon());
390
+
391
+ const column = document.createElement('div');
392
+ column.className = 'flexmodal-column';
393
+
394
+ const title = document.createElement('h3');
395
+ title.className = 'flexmodal-title';
396
+ title.textContent = this.options.title;
397
+ column.appendChild(title);
398
+
399
+ if (this.options.subtitle) {
400
+ const subtitle = document.createElement('p');
401
+ subtitle.className = 'flexmodal-subtitle';
402
+ subtitle.textContent = this.options.subtitle;
403
+ column.appendChild(subtitle);
404
+ }
405
+
406
+ row.appendChild(iconBox);
407
+ row.appendChild(column);
408
+
409
+ const footer = document.createElement('div');
410
+ footer.className = 'flexmodal-footer';
411
+
412
+ if (isPromise) {
413
+ const btnSecondary = document.createElement('button');
414
+ btnSecondary.className = 'flexmodal-btn flexmodal-btn-secondary';
415
+ btnSecondary.onclick = () => this.close(false);
416
+ btnSecondary.textContent = this.options.button2;
417
+ footer.appendChild(btnSecondary);
418
+ }
419
+
420
+ const btnPrimary = document.createElement('button');
421
+ btnPrimary.className = `flexmodal-btn ${isPromise ? 'flexmodal-btn-success' : 'flexmodal-btn-primary'}`;
422
+ btnPrimary.onclick = () => this.close(true);
423
+ btnPrimary.textContent = this.options.button1;
424
+ footer.appendChild(btnPrimary);
425
+
426
+ bodyWrapper.appendChild(row);
427
+ bodyWrapper.appendChild(footer);
428
+ }
429
+
430
+ this.container.appendChild(bodyWrapper);
431
+
432
+ // Inject tabs element node if applicable
433
+ if (this.options.type === 'slide-over' && this.options.tabs) {
434
+ const target = this.container.querySelector('#flexmodal-tabs-target');
435
+ if (target) {
436
+ if (this.options.tabs instanceof HTMLElement) {
437
+ target.appendChild(this.options.tabs);
438
+ } else {
439
+ target.innerHTML = this.options.tabs;
440
+ }
441
+ }
442
+ }
443
+
444
+ document.body.appendChild(this.backdrop);
445
+ }
446
+
447
+ /* Internal SVGs to ensure zero-dependencies */
448
+ _svgCloseIcon() {
449
+ const wrapper = document.createElement('div');
450
+ wrapper.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
451
+ return wrapper.firstChild;
452
+ }
453
+
454
+ _svgAlertIcon() {
455
+ const wrapper = document.createElement('div');
456
+ wrapper.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`;
457
+ return wrapper.firstChild;
458
+ }
459
+
460
+ _svgPromiseIcon() {
461
+ const wrapper = document.createElement('div');
462
+ wrapper.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`;
463
+ return wrapper.firstChild;
464
+ }
465
+
466
+ close(resultVal = false) {
467
+ if (!this.backdrop) return;
468
+
469
+ this.backdrop.classList.remove('is-active');
470
+
471
+ // Tear down elements after transitions
472
+ setTimeout(() => {
473
+ this._destroy(resultVal);
474
+ }, 250);
475
+ }
476
+
477
+ _destroy(resultVal) {
478
+ if (this.options.closeOnOverlayClick && this.backdrop) {
479
+ this.backdrop.removeEventListener('click', this._boundOverlayClick);
480
+ }
481
+ if (this.options.closeOnEscape) {
482
+ document.removeEventListener('keydown', this._boundKeyDown);
483
+ }
484
+
485
+ if (this.backdrop && this.backdrop.parentNode) {
486
+ this.backdrop.parentNode.removeChild(this.backdrop);
487
+ }
488
+
489
+ if (this.previouslyFocusedElement) {
490
+ this.previouslyFocusedElement.focus();
491
+ }
492
+
493
+ if (this.resolvePromise) {
494
+ this.resolvePromise(resultVal);
495
+ }
496
+ }
497
+
498
+ _handleOverlayClick(e) {
499
+ if (e.target === this.backdrop) {
500
+ this.close(false);
501
+ }
502
+ }
503
+
504
+ _handleKeyDown(e) {
505
+ if (e.key === 'Escape') {
506
+ this.close(false);
507
+ return;
508
+ }
509
+
510
+ if (e.key === 'Tab') {
511
+ this._updateFocusableElements();
512
+ if (this.focusableElements.length === 0) {
513
+ e.preventDefault();
514
+ return;
515
+ }
516
+
517
+ const first = this.focusableElements[0];
518
+ const last = this.focusableElements[this.focusableElements.length - 1];
519
+
520
+ if (e.shiftKey) { // Back Tab
521
+ if (document.activeElement === first) {
522
+ last.focus();
523
+ e.preventDefault();
524
+ }
525
+ } else { // Forward Tab
526
+ if (document.activeElement === last) {
527
+ first.focus();
528
+ e.preventDefault();
529
+ }
530
+ }
531
+ }
532
+ }
533
+
534
+ _updateFocusableElements() {
535
+ const query = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
536
+ this.focusableElements = Array.from(this.container.querySelectorAll(query))
537
+ .filter(el => !el.hasAttribute('disabled') && el.offsetParent !== null);
538
+ }
539
+ }
540
+
541
+ // Global hook injection
542
+ window.modal = FlexModal;
543
+
544
+
545
+ /*****************************
546
+ * How to use this library: Use anyone of your choice below to open a modal. The modal returns a Promise that resolves to true or false depending on the user's action.
547
+ *****************************
548
+ */
549
+
550
+ //For informational modal:
551
+ //Use this when you want to show a clean message box to the user with a single action button.
552
+ /* modal.open({
553
+ type: 'informational',
554
+ title: 'Welcome to My App',
555
+ subtitle: 'In this app you can enjoy free gifts, I hope this finds you well!',
556
+ button1: 'Awesome, got it!'
557
+ }).then((dismissed) => {
558
+ console.log("Modal closed:", dismissed); // Returns true when button1 is clicked
559
+ });
560
+ */
561
+
562
+ // For promise modal:
563
+ //Use this for dual-option operations (such as destructive actions). It returns a native JavaScript Promise resolving to true (primary button clicked) or false (secondary button clicked, backdrop clicked, or Escape pressed).
564
+ /*
565
+ modal.open({
566
+ type: 'promise',
567
+ title: 'Confirm Critical Settings',
568
+ subtitle: 'Are you sure you want to deploy these configuration files to production?',
569
+ button1: 'Deploy Now',
570
+ button2: 'Review Changes'
571
+ }).then((confirmed) => {
572
+ if (confirmed) {
573
+ console.log("Proceed with deployment...");
574
+ } else {
575
+ console.log("User cancelled execution.");
576
+ }
577
+ });
578
+
579
+ */
580
+
581
+
582
+ //For slide-over modal:
583
+ //Use this for a right-side slide-over panel with optional tabbed content. It returns a native JavaScript Promise resolving to true (primary button clicked) or false (secondary button clicked, backdrop clicked, or Escape pressed).
584
+ /*
585
+ modal.open({
586
+ type: 'slide-over',
587
+ title: 'Settings Panel',
588
+ subtitle: 'Adjust your preferences below.',
589
+ button1: 'Save Changes',
590
+ button2: 'Cancel',
591
+ tabs: '<div>Tab 1 Content</div><div>Tab 2 Content</div>'
592
+ }).then((confirmed) => {
593
+ if (confirmed) {
594
+ console.log("Changes saved.");
595
+ } else {
596
+ console.log("User cancelled changes.");
597
+ }
598
+ });
599
+ */
600
+
601
+ //For dynamic modal:
602
+ //Use this for a fully custom modal with your own HTML content. It returns a native JavaScript Promise resolving to true (primary button clicked) or false (secondary button clicked, backdrop clicked, or Escape pressed).
603
+ /*
604
+ modal.open({
605
+ type: 'dynamic',
606
+ title: 'Custom Modal',
607
+ html: '<p>This is a custom modal with your own HTML content.</p><input type="text" placeholder="Enter something...">',
608
+ button1: 'Submit'
609
+ }).then((confirmed) => {
610
+ if (confirmed) {
611
+ console.log("User submitted the form.");
612
+ } else {
613
+ console.log("User closed the modal without submitting.");
614
+ }
615
+ });
616
+ */
617
+