millas 0.2.12-beta → 0.2.12-beta-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -0,0 +1,1044 @@
1
+ /**
2
+ * ui.js — Millas Admin UI Utilities
3
+ *
4
+ * Zero dependencies. No framework. No third-party libraries.
5
+ * Minify-safe: no dynamic property names, no eval, no template magic.
6
+ *
7
+ * What this solves:
8
+ * The classic CSS stacking context trap — position:fixed breaks inside any
9
+ * ancestor that has transform, filter, perspective, or will-change set.
10
+ * position:absolute is clipped by overflow:hidden parents.
11
+ * Both issues kill dropdowns and modals in complex layouts.
12
+ *
13
+ * The solution — Portal rendering:
14
+ * Every floating element (dropdown, modal, tooltip, drawer) is moved into
15
+ * document.body via a dedicated #ui-portal container that sits at the very
16
+ * top of the DOM stacking order. Coordinates are calculated from the anchor
17
+ * element using getBoundingClientRect() and applied as fixed pixel positions.
18
+ * Scroll and resize listeners keep them in sync.
19
+ *
20
+ * Exports (on window.UI):
21
+ * Portal — low-level portal mount/unmount
22
+ * Dropdown — anchor-attached floating panel (FK select, action menus)
23
+ * Modal — centered overlay dialog
24
+ * Drawer — slide-in side panel
25
+ * Tooltip — hover label
26
+ * Toast — ephemeral status notification
27
+ * Confirm — promise-based confirm dialog
28
+ * FocusTrap — keyboard focus containment for modals/drawers
29
+ * ScrollLock — body scroll lock / unlock
30
+ */
31
+
32
+ (function (root) {
33
+ 'use strict';
34
+
35
+ // ── Constants ───────────────────────────────────────────────────────────────
36
+
37
+ var PORTAL_ID = 'ui-portal';
38
+ var Z_DROPDOWN = 1100;
39
+ var Z_MODAL = 1200;
40
+ var Z_DRAWER = 1200;
41
+ var Z_TOOLTIP = 1300;
42
+ var Z_TOAST = 1400;
43
+ var EASE_OUT = 'cubic-bezier(0.16,1,0.3,1)';
44
+ var EASE_IN = 'cubic-bezier(0.4,0,1,1)';
45
+
46
+ // ── Portal ──────────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Portal — appends floating elements to document.body, bypassing all
50
+ * stacking context traps from transformed or overflow:hidden ancestors.
51
+ *
52
+ * Usage:
53
+ * var el = Portal.mount('<div class="my-panel">...</div>');
54
+ * Portal.unmount(el);
55
+ */
56
+ var Portal = (function () {
57
+ function _container() {
58
+ var c = document.getElementById(PORTAL_ID);
59
+ if (!c) {
60
+ c = document.createElement('div');
61
+ c.id = PORTAL_ID;
62
+ c.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:' + Z_DROPDOWN + ';pointer-events:none';
63
+ document.body.appendChild(c);
64
+ }
65
+ return c;
66
+ }
67
+
68
+ /**
69
+ * Mount an element or HTML string into the portal container.
70
+ * Returns the mounted element.
71
+ */
72
+ function mount(elOrHtml) {
73
+ var el;
74
+ if (typeof elOrHtml === 'string') {
75
+ var wrap = document.createElement('div');
76
+ wrap.innerHTML = elOrHtml;
77
+ el = wrap.firstElementChild;
78
+ } else {
79
+ el = elOrHtml;
80
+ }
81
+ el.style.pointerEvents = 'auto';
82
+ _container().appendChild(el);
83
+ return el;
84
+ }
85
+
86
+ /**
87
+ * Remove an element from the portal container.
88
+ */
89
+ function unmount(el) {
90
+ if (el && el.parentNode && el.parentNode.id === PORTAL_ID) {
91
+ el.parentNode.removeChild(el);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Remove all portal children (nuclear reset).
97
+ */
98
+ function clear() {
99
+ var c = document.getElementById(PORTAL_ID);
100
+ if (c) c.innerHTML = '';
101
+ }
102
+
103
+ return { mount: mount, unmount: unmount, clear: clear };
104
+ })();
105
+
106
+ // ── ScrollLock ──────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * ScrollLock — locks body scroll when modals/drawers are open.
110
+ * Uses a reference count so nested lock/unlock calls are safe.
111
+ */
112
+ var ScrollLock = (function () {
113
+ var _count = 0;
114
+ var _scrollY = 0;
115
+ var _origStyle = '';
116
+
117
+ function lock() {
118
+ _count++;
119
+ if (_count > 1) return;
120
+ _scrollY = window.pageYOffset || document.documentElement.scrollTop;
121
+ _origStyle = document.body.style.cssText;
122
+ document.body.style.cssText = _origStyle +
123
+ ';overflow:hidden;position:fixed;top:-' + _scrollY + 'px;left:0;right:0;';
124
+ }
125
+
126
+ function unlock() {
127
+ _count = Math.max(0, _count - 1);
128
+ if (_count > 0) return;
129
+ document.body.style.cssText = _origStyle;
130
+ window.scrollTo(0, _scrollY);
131
+ }
132
+
133
+ return { lock: lock, unlock: unlock };
134
+ })();
135
+
136
+ // ── FocusTrap ───────────────────────────────────────────────────────────────
137
+
138
+ /**
139
+ * FocusTrap — keeps keyboard focus inside a container (modal, drawer).
140
+ * Call activate(el) to trap, deactivate() to release.
141
+ */
142
+ var FocusTrap = (function () {
143
+ var _el = null;
144
+ var _prevFocus = null;
145
+ var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
146
+
147
+ function _onKeyDown(e) {
148
+ if (!_el || e.key !== 'Tab') return;
149
+ var focusable = Array.from(_el.querySelectorAll(FOCUSABLE)).filter(function (n) {
150
+ return !n.offsetParent === false; // visible only
151
+ });
152
+ if (!focusable.length) { e.preventDefault(); return; }
153
+ var first = focusable[0];
154
+ var last = focusable[focusable.length - 1];
155
+ if (e.shiftKey) {
156
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
157
+ } else {
158
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
159
+ }
160
+ }
161
+
162
+ function activate(el) {
163
+ _el = el;
164
+ _prevFocus = document.activeElement;
165
+ document.addEventListener('keydown', _onKeyDown);
166
+ // Focus first focusable element
167
+ var first = el.querySelector(FOCUSABLE);
168
+ if (first) {
169
+ requestAnimationFrame(function () { first.focus(); });
170
+ }
171
+ }
172
+
173
+ function deactivate() {
174
+ document.removeEventListener('keydown', _onKeyDown);
175
+ _el = null;
176
+ if (_prevFocus && _prevFocus.focus) {
177
+ requestAnimationFrame(function () { _prevFocus.focus(); });
178
+ }
179
+ }
180
+
181
+ return { activate: activate, deactivate: deactivate };
182
+ })();
183
+
184
+ // ── Dropdown ─────────────────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Dropdown — an anchor-attached floating panel rendered in the portal.
188
+ * Handles position, scroll, resize, outside-click, and keyboard dismiss.
189
+ *
190
+ * Usage:
191
+ * var dd = Dropdown.create({
192
+ * anchor: document.getElementById('my-btn'),
193
+ * content: '<div class="my-menu">...</div>',
194
+ * placement: 'bottom-start', // 'bottom-start'|'bottom-end'|'top-start'|'top-end'
195
+ * offset: 4, // gap between anchor and panel (px)
196
+ * onClose: function() {},
197
+ * className: 'my-dropdown-panel',
198
+ * });
199
+ * dd.open();
200
+ * dd.close();
201
+ * dd.destroy();
202
+ */
203
+ var Dropdown = (function () {
204
+
205
+ function create(opts) {
206
+ var anchor = opts.anchor;
207
+ var placement = opts.placement || 'bottom-start';
208
+ var offset = opts.offset !== undefined ? opts.offset : 4;
209
+ var onClose = opts.onClose || null;
210
+ var className = opts.className || 'ui-dropdown-panel';
211
+ var minWidth = opts.minWidth || null;
212
+ var maxHeight = opts.maxHeight || 300;
213
+
214
+ // Use the provided HTMLElement directly to avoid double-wrapping.
215
+ // For string content, create a wrapper div.
216
+ var panel;
217
+ if (opts.content instanceof HTMLElement) {
218
+ panel = opts.content;
219
+ if (className && !panel.className) panel.className = className;
220
+ } else {
221
+ panel = document.createElement('div');
222
+ panel.className = className;
223
+ if (typeof opts.content === 'string') panel.innerHTML = opts.content;
224
+ }
225
+
226
+ // Own only the positioning properties — leave class-based styles
227
+ // (border, shadow, radius) untouched.
228
+ panel.style.position = 'fixed';
229
+ panel.style.zIndex = Z_DROPDOWN;
230
+ panel.style.opacity = '0';
231
+ panel.style.transform = 'translateY(-4px)';
232
+ panel.style.transition = 'opacity .15s,transform .15s ' + EASE_OUT;
233
+ panel.style.pointerEvents = 'none';
234
+ panel.style.maxHeight = maxHeight + 'px';
235
+ panel.style.overflowY = 'auto'; // scroll if content exceeds maxHeight
236
+ panel.style.display = 'none';
237
+
238
+ // Transparent backdrop — sits just below the panel, covers the whole
239
+ // viewport. Clicking it closes the dropdown. Gives the user a clear
240
+ // "everything else is off limits" signal without any visible overlay.
241
+ var backdrop = document.createElement('div');
242
+ backdrop.style.cssText =
243
+ 'position:fixed;inset:0;z-index:' + (Z_DROPDOWN - 1) + ';' +
244
+ 'background:transparent;cursor:default;display:none;';
245
+ backdrop.addEventListener('mousedown', function (e) {
246
+ e.preventDefault(); // don't steal focus from the trigger
247
+ close();
248
+ });
249
+
250
+ var _isOpen = false;
251
+ var _closeTimer = null;
252
+ var _scrollParents = [];
253
+
254
+ function _onEscape(e) {
255
+ if (e.key === 'Escape') close();
256
+ }
257
+
258
+ function _getScrollParents() {
259
+ var parents = [];
260
+ var node = anchor.parentNode;
261
+ while (node && node !== document.body) {
262
+ var s = getComputedStyle(node);
263
+ if (/auto|scroll/.test(s.overflow + s.overflowX + s.overflowY)) {
264
+ parents.push(node);
265
+ }
266
+ node = node.parentNode;
267
+ }
268
+ parents.push(window);
269
+ return parents;
270
+ }
271
+
272
+ function _position() {
273
+ var rect = anchor.getBoundingClientRect();
274
+ var vw = window.innerWidth;
275
+ var vh = window.innerHeight;
276
+ var pw = panel.offsetWidth || 220;
277
+ var ph = panel.offsetHeight || 120;
278
+
279
+ var preferTop = placement.startsWith('top');
280
+ var goUp = preferTop
281
+ ? true
282
+ : (vh - rect.bottom < ph + offset && rect.top > vh - rect.bottom);
283
+
284
+ var top = goUp ? rect.top - ph - offset : rect.bottom + offset;
285
+ var left = placement.endsWith('end') ? rect.right - pw : rect.left;
286
+
287
+ left = Math.max(8, Math.min(left, vw - pw - 8));
288
+ top = Math.max(8, Math.min(top, vh - ph - 8));
289
+
290
+ panel.style.top = top + 'px';
291
+ panel.style.left = left + 'px';
292
+
293
+ if (minWidth === true) panel.style.minWidth = rect.width + 'px';
294
+ else if (minWidth) panel.style.minWidth = minWidth + 'px';
295
+ }
296
+
297
+ function open() {
298
+ if (_isOpen) return;
299
+
300
+ // Cancel any in-flight unmount for this panel
301
+ if (_closeTimer) { clearTimeout(_closeTimer); _closeTimer = null; }
302
+
303
+ _isOpen = true;
304
+
305
+ backdrop.style.display = '';
306
+ panel.style.display = '';
307
+ panel.style.opacity = '0';
308
+ panel.style.transform = 'translateY(-4px)';
309
+ panel.style.pointerEvents = 'none';
310
+
311
+ Portal.mount(backdrop);
312
+ Portal.mount(panel);
313
+ panel.getBoundingClientRect(); // force layout so dimensions are real
314
+ _position();
315
+
316
+ panel.style.opacity = '1';
317
+ panel.style.transform = 'translateY(0)';
318
+ panel.style.pointerEvents = 'auto';
319
+
320
+ // Per-instance listeners — added on open, removed on close
321
+ _scrollParents = _getScrollParents();
322
+ _scrollParents.forEach(function (p) {
323
+ p.addEventListener('scroll', _position, { passive: true });
324
+ });
325
+ window.addEventListener('resize', _position, { passive: true });
326
+ document.addEventListener('keydown', _onEscape);
327
+ }
328
+
329
+ function close() {
330
+ if (!_isOpen) return;
331
+ _isOpen = false;
332
+
333
+ // Remove all listeners this instance added
334
+ _scrollParents.forEach(function (p) {
335
+ p.removeEventListener('scroll', _position);
336
+ });
337
+ window.removeEventListener('resize', _position);
338
+ document.removeEventListener('keydown', _onEscape);
339
+ _scrollParents = [];
340
+
341
+ // Hide backdrop immediately — no animation needed
342
+ backdrop.style.display = 'none';
343
+ Portal.unmount(backdrop);
344
+
345
+ panel.style.opacity = '0';
346
+ panel.style.transform = 'translateY(-4px)';
347
+ panel.style.pointerEvents = 'none';
348
+
349
+ _closeTimer = setTimeout(function () {
350
+ _closeTimer = null;
351
+ panel.style.display = 'none';
352
+ Portal.unmount(panel);
353
+ if (onClose) onClose();
354
+ }, 160);
355
+ }
356
+
357
+ function toggle() { _isOpen ? close() : open(); }
358
+
359
+ function destroy() {
360
+ if (_closeTimer) clearTimeout(_closeTimer);
361
+ if (_isOpen) close();
362
+ if (backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
363
+ if (panel.parentNode) panel.parentNode.removeChild(panel);
364
+ }
365
+
366
+ function reposition() { requestAnimationFrame(_position); }
367
+
368
+ var instance = {
369
+ _el: panel,
370
+ _anchor: anchor,
371
+ open: open,
372
+ close: close,
373
+ toggle: toggle,
374
+ destroy: destroy,
375
+ isOpen: function () { return _isOpen; },
376
+ reposition: reposition,
377
+ };
378
+
379
+ return instance;
380
+ }
381
+
382
+ return { create: create };
383
+ })();
384
+
385
+ // ── Modal ───────────────────────────────────────────────────────────────────
386
+
387
+ /**
388
+ * Modal — centered overlay dialog rendered in the portal.
389
+ * Traps focus, locks scroll, handles Escape.
390
+ *
391
+ * Usage:
392
+ * var m = Modal.create({
393
+ * title: 'Confirm Delete',
394
+ * content: '<p>Are you sure?</p>',
395
+ * size: 'sm', // 'sm'|'md'|'lg'|'xl' — default 'md'
396
+ * footer: '<button>OK</button>',
397
+ * onClose: function() {},
398
+ * closeable: true, // show ✕ and allow Escape/backdrop close
399
+ * });
400
+ * m.open();
401
+ * m.close();
402
+ *
403
+ * // Or use the shorthand:
404
+ * Modal.open({ title: 'Hello', content: '<p>World</p>' });
405
+ */
406
+ var Modal = (function () {
407
+ var _stack = []; // support nested modals
408
+
409
+ var SIZES = { sm: '400px', md: '520px', lg: '700px', xl: '900px' };
410
+
411
+ // Escape closes topmost modal
412
+ document.addEventListener('keydown', function (e) {
413
+ if (e.key === 'Escape' && _stack.length) {
414
+ var top = _stack[_stack.length - 1];
415
+ if (top.closeable !== false) top.close();
416
+ }
417
+ });
418
+
419
+ function create(opts) {
420
+ var title = opts.title || '';
421
+ var content = opts.content || '';
422
+ var footer = opts.footer || null;
423
+ var size = opts.size || 'md';
424
+ var closeable = opts.closeable !== false;
425
+ var onClose = opts.onClose || null;
426
+ var onOpen = opts.onOpen || null;
427
+ var className = opts.className || '';
428
+
429
+ var maxW = SIZES[size] || SIZES.md;
430
+
431
+ // Build overlay + dialog
432
+ var overlay = document.createElement('div');
433
+ overlay.style.cssText =
434
+ 'position:fixed;inset:0;' +
435
+ 'background:rgba(17,24,39,.55);' +
436
+ 'backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);' +
437
+ 'display:flex;align-items:center;justify-content:center;' +
438
+ 'padding:24px;z-index:' + Z_MODAL + ';' +
439
+ 'opacity:0;transition:opacity .18s;pointer-events:none;';
440
+ overlay.setAttribute('role', 'dialog');
441
+ overlay.setAttribute('aria-modal', 'true');
442
+ if (title) overlay.setAttribute('aria-label', title);
443
+
444
+ var dialog = document.createElement('div');
445
+ dialog.style.cssText =
446
+ 'background:var(--surface,#fff);' +
447
+ 'border:1px solid var(--border,#e3e6ec);' +
448
+ 'border-radius:var(--radius-lg,12px);' +
449
+ 'width:100%;max-width:' + maxW + ';' +
450
+ 'max-height:90vh;overflow-y:auto;' +
451
+ 'transform:translateY(12px) scale(.98);' +
452
+ 'transition:transform .22s ' + EASE_OUT + ';' +
453
+ 'box-shadow:0 20px 60px rgba(0,0,0,.18);' +
454
+ (className ? '' : '');
455
+ if (className) dialog.className = className;
456
+
457
+ // Header
458
+ var header = document.createElement('div');
459
+ header.style.cssText =
460
+ 'padding:18px 22px;border-bottom:1px solid var(--border-soft,#edf0f5);' +
461
+ 'display:flex;justify-content:space-between;align-items:center;' +
462
+ 'position:sticky;top:0;background:var(--surface,#fff);z-index:1;';
463
+
464
+ var titleEl = document.createElement('span');
465
+ titleEl.style.cssText = 'font-size:15px;font-weight:600;color:var(--text,#111827)';
466
+ titleEl.textContent = title;
467
+ header.appendChild(titleEl);
468
+
469
+ if (closeable) {
470
+ var closeBtn = document.createElement('button');
471
+ closeBtn.type = 'button';
472
+ closeBtn.style.cssText =
473
+ 'background:none;border:none;cursor:pointer;color:var(--text-muted,#6b7280);' +
474
+ 'padding:4px;border-radius:4px;display:flex;align-items:center;' +
475
+ 'transition:background .1s,color .1s;';
476
+ closeBtn.innerHTML =
477
+ '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
478
+ '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
479
+ closeBtn.addEventListener('mouseenter', function () {
480
+ closeBtn.style.background = 'var(--surface3,#eef0f4)';
481
+ closeBtn.style.color = 'var(--text,#111827)';
482
+ });
483
+ closeBtn.addEventListener('mouseleave', function () {
484
+ closeBtn.style.background = '';
485
+ closeBtn.style.color = '';
486
+ });
487
+ closeBtn.addEventListener('click', function () { instance.close(); });
488
+ header.appendChild(closeBtn);
489
+ }
490
+
491
+ // Body
492
+ var body = document.createElement('div');
493
+ body.style.cssText = 'padding:20px 22px;';
494
+ if (typeof content === 'string') {
495
+ body.innerHTML = content;
496
+ } else if (content instanceof HTMLElement) {
497
+ body.appendChild(content);
498
+ }
499
+
500
+ // Footer
501
+ var footerEl = null;
502
+ if (footer) {
503
+ footerEl = document.createElement('div');
504
+ footerEl.style.cssText =
505
+ 'padding:14px 22px;border-top:1px solid var(--border-soft,#edf0f5);' +
506
+ 'display:flex;justify-content:flex-end;gap:8px;' +
507
+ 'position:sticky;bottom:0;background:var(--surface,#fff);';
508
+ if (typeof footer === 'string') {
509
+ footerEl.innerHTML = footer;
510
+ } else if (footer instanceof HTMLElement) {
511
+ footerEl.appendChild(footer);
512
+ }
513
+ }
514
+
515
+ dialog.appendChild(header);
516
+ dialog.appendChild(body);
517
+ if (footerEl) dialog.appendChild(footerEl);
518
+ overlay.appendChild(dialog);
519
+
520
+ // Backdrop click to close
521
+ if (closeable) {
522
+ overlay.addEventListener('mousedown', function (e) {
523
+ if (e.target === overlay) instance.close();
524
+ });
525
+ }
526
+
527
+ var _isOpen = false;
528
+
529
+ function open() {
530
+ if (_isOpen) return;
531
+ _isOpen = true;
532
+ _stack.push(instance);
533
+
534
+ Portal.mount(overlay);
535
+ ScrollLock.lock();
536
+ FocusTrap.activate(dialog);
537
+
538
+ // Animate in
539
+ overlay.getBoundingClientRect(); // force layout
540
+ overlay.style.opacity = '1';
541
+ overlay.style.pointerEvents = 'auto';
542
+ dialog.style.transform = 'translateY(0) scale(1)';
543
+
544
+ if (onOpen) onOpen(instance);
545
+ }
546
+
547
+ function close() {
548
+ if (!_isOpen) return;
549
+ _isOpen = false;
550
+ _stack = _stack.filter(function (m) { return m !== instance; });
551
+
552
+ overlay.style.opacity = '0';
553
+ overlay.style.pointerEvents = 'none';
554
+ dialog.style.transform = 'translateY(12px) scale(.98)';
555
+
556
+ FocusTrap.deactivate();
557
+ ScrollLock.unlock();
558
+
559
+ setTimeout(function () {
560
+ Portal.unmount(overlay);
561
+ if (onClose) onClose(instance);
562
+ }, 200);
563
+ }
564
+
565
+ // Expose body and footer for content updates
566
+ function setContent(html) {
567
+ if (typeof html === 'string') body.innerHTML = html;
568
+ else if (html instanceof HTMLElement) { body.innerHTML = ''; body.appendChild(html); }
569
+ }
570
+
571
+ function setTitle(t) {
572
+ titleEl.textContent = t;
573
+ }
574
+
575
+ var instance = {
576
+ overlay: overlay,
577
+ dialog: dialog,
578
+ body: body,
579
+ closeable: closeable,
580
+ open: open,
581
+ close: close,
582
+ setContent: setContent,
583
+ setTitle: setTitle,
584
+ };
585
+
586
+ return instance;
587
+ }
588
+
589
+ // Shorthand — create + immediately open
590
+ function open(opts) {
591
+ var m = create(opts);
592
+ m.open();
593
+ return m;
594
+ }
595
+
596
+ return { create: create, open: open };
597
+ })();
598
+
599
+ // ── Drawer ──────────────────────────────────────────────────────────────────
600
+
601
+ /**
602
+ * Drawer — slide-in panel from a screen edge.
603
+ *
604
+ * Usage:
605
+ * var d = Drawer.create({
606
+ * title: 'Filters',
607
+ * content: '<p>Filter controls here</p>',
608
+ * side: 'right', // 'right'|'left'|'bottom' — default 'right'
609
+ * width: '400px', // for left/right drawers
610
+ * height: '50vh', // for bottom drawer
611
+ * onClose: function() {},
612
+ * });
613
+ * d.open();
614
+ */
615
+ var Drawer = (function () {
616
+ function create(opts) {
617
+ var title = opts.title || '';
618
+ var content = opts.content || '';
619
+ var side = opts.side || 'right';
620
+ var width = opts.width || '420px';
621
+ var height = opts.height || '50vh';
622
+ var closeable = opts.closeable !== false;
623
+ var onClose = opts.onClose || null;
624
+
625
+ // Backdrop
626
+ var backdrop = document.createElement('div');
627
+ backdrop.style.cssText =
628
+ 'position:fixed;inset:0;background:rgba(17,24,39,.4);' +
629
+ 'z-index:' + Z_DRAWER + ';opacity:0;pointer-events:none;' +
630
+ 'transition:opacity .22s;';
631
+
632
+ // Panel
633
+ var panel = document.createElement('div');
634
+ var isHoriz = side === 'left' || side === 'right';
635
+ var panelBase =
636
+ 'position:fixed;z-index:' + (Z_DRAWER + 1) + ';' +
637
+ 'background:var(--surface,#fff);' +
638
+ 'box-shadow:0 0 40px rgba(0,0,0,.18);' +
639
+ 'display:flex;flex-direction:column;' +
640
+ 'transition:transform .28s ' + EASE_OUT + ';';
641
+
642
+ if (side === 'right') {
643
+ panel.style.cssText = panelBase + 'top:0;right:0;bottom:0;width:' + width + ';max-width:95vw;transform:translateX(100%);';
644
+ } else if (side === 'left') {
645
+ panel.style.cssText = panelBase + 'top:0;left:0;bottom:0;width:' + width + ';max-width:95vw;transform:translateX(-100%);';
646
+ } else {
647
+ // bottom
648
+ panel.style.cssText = panelBase + 'left:0;right:0;bottom:0;height:' + height + ';border-radius:12px 12px 0 0;transform:translateY(100%);';
649
+ }
650
+
651
+ // Header
652
+ var header = document.createElement('div');
653
+ header.style.cssText =
654
+ 'padding:18px 20px;border-bottom:1px solid var(--border,#e3e6ec);' +
655
+ 'display:flex;align-items:center;justify-content:space-between;flex-shrink:0;';
656
+
657
+ var titleEl = document.createElement('span');
658
+ titleEl.style.cssText = 'font-size:15px;font-weight:600;color:var(--text,#111827)';
659
+ titleEl.textContent = title;
660
+ header.appendChild(titleEl);
661
+
662
+ if (closeable) {
663
+ var closeBtn = document.createElement('button');
664
+ closeBtn.type = 'button';
665
+ closeBtn.style.cssText = 'background:none;border:none;cursor:pointer;color:var(--text-muted,#6b7280);padding:4px;border-radius:4px;display:flex;';
666
+ closeBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
667
+ closeBtn.addEventListener('click', function () { instance.close(); });
668
+ header.appendChild(closeBtn);
669
+ }
670
+
671
+ // Body
672
+ var body = document.createElement('div');
673
+ body.style.cssText = 'flex:1;overflow-y:auto;padding:20px;';
674
+ if (typeof content === 'string') body.innerHTML = content;
675
+ else if (content instanceof HTMLElement) body.appendChild(content);
676
+
677
+ panel.appendChild(header);
678
+ panel.appendChild(body);
679
+
680
+ if (closeable) {
681
+ backdrop.addEventListener('click', function () { instance.close(); });
682
+ }
683
+
684
+ // Escape
685
+ function _onEsc(e) { if (e.key === 'Escape' && closeable) instance.close(); }
686
+
687
+ var _isOpen = false;
688
+
689
+ function open() {
690
+ if (_isOpen) return;
691
+ _isOpen = true;
692
+
693
+ Portal.mount(backdrop);
694
+ Portal.mount(panel);
695
+ ScrollLock.lock();
696
+ FocusTrap.activate(panel);
697
+
698
+ backdrop.getBoundingClientRect();
699
+ backdrop.style.opacity = '1';
700
+ backdrop.style.pointerEvents = 'auto';
701
+ panel.style.transform = 'translate(0,0)';
702
+
703
+ document.addEventListener('keydown', _onEsc);
704
+ }
705
+
706
+ function close() {
707
+ if (!_isOpen) return;
708
+ _isOpen = false;
709
+
710
+ backdrop.style.opacity = '0';
711
+ backdrop.style.pointerEvents = 'none';
712
+ if (side === 'right') panel.style.transform = 'translateX(100%)';
713
+ else if (side === 'left') panel.style.transform = 'translateX(-100%)';
714
+ else panel.style.transform = 'translateY(100%)';
715
+
716
+ FocusTrap.deactivate();
717
+ ScrollLock.unlock();
718
+ document.removeEventListener('keydown', _onEsc);
719
+
720
+ setTimeout(function () {
721
+ Portal.unmount(backdrop);
722
+ Portal.unmount(panel);
723
+ if (onClose) onClose(instance);
724
+ }, 280);
725
+ }
726
+
727
+ function setContent(html) {
728
+ if (typeof html === 'string') body.innerHTML = html;
729
+ else if (html instanceof HTMLElement) { body.innerHTML = ''; body.appendChild(html); }
730
+ }
731
+
732
+ var instance = {
733
+ panel: panel,
734
+ body: body,
735
+ open: open,
736
+ close: close,
737
+ setContent: setContent,
738
+ };
739
+
740
+ return instance;
741
+ }
742
+
743
+ function open(opts) {
744
+ var d = create(opts);
745
+ d.open();
746
+ return d;
747
+ }
748
+
749
+ return { create: create, open: open };
750
+ })();
751
+
752
+ // ── Tooltip ─────────────────────────────────────────────────────────────────
753
+
754
+ /**
755
+ * Tooltip — lightweight hover label anchored to any element.
756
+ *
757
+ * Usage:
758
+ * // Declarative — add data-tooltip="text" to any element, auto-initialised
759
+ * <button data-tooltip="Save changes">Save</button>
760
+ *
761
+ * // Programmatic
762
+ * var t = Tooltip.create({ anchor: btn, text: 'Save changes', placement: 'top' });
763
+ * t.show(); t.hide();
764
+ */
765
+ var Tooltip = (function () {
766
+ var _tip = null; // single shared tooltip element, re-used for all
767
+
768
+ function _ensureTip() {
769
+ if (!_tip) {
770
+ _tip = document.createElement('div');
771
+ _tip.style.cssText =
772
+ 'position:fixed;z-index:' + Z_TOOLTIP + ';' +
773
+ 'background:var(--text,#111827);color:#fff;' +
774
+ 'font-size:12px;font-family:inherit;' +
775
+ 'padding:5px 9px;border-radius:5px;' +
776
+ 'pointer-events:none;white-space:nowrap;' +
777
+ 'opacity:0;transition:opacity .12s;' +
778
+ 'box-shadow:0 2px 8px rgba(0,0,0,.2);';
779
+ Portal.mount(_tip);
780
+ }
781
+ return _tip;
782
+ }
783
+
784
+ function _position(anchor, placement) {
785
+ var tip = _ensureTip();
786
+ var rect = anchor.getBoundingClientRect();
787
+ var tw = tip.offsetWidth;
788
+ var th = tip.offsetHeight;
789
+ var gap = 6;
790
+ var top, left;
791
+
792
+ placement = placement || 'top';
793
+
794
+ if (placement === 'top') {
795
+ top = rect.top - th - gap;
796
+ left = rect.left + rect.width / 2 - tw / 2;
797
+ } else if (placement === 'bottom') {
798
+ top = rect.bottom + gap;
799
+ left = rect.left + rect.width / 2 - tw / 2;
800
+ } else if (placement === 'left') {
801
+ top = rect.top + rect.height / 2 - th / 2;
802
+ left = rect.left - tw - gap;
803
+ } else {
804
+ top = rect.top + rect.height / 2 - th / 2;
805
+ left = rect.right + gap;
806
+ }
807
+
808
+ // Clamp
809
+ var vw = window.innerWidth, vh = window.innerHeight;
810
+ left = Math.max(8, Math.min(left, vw - tw - 8));
811
+ top = Math.max(8, Math.min(top, vh - th - 8));
812
+
813
+ tip.style.top = top + 'px';
814
+ tip.style.left = left + 'px';
815
+ }
816
+
817
+ function create(opts) {
818
+ var anchor = opts.anchor;
819
+ var text = opts.text || '';
820
+ var placement = opts.placement || 'top';
821
+
822
+ function show() {
823
+ var tip = _ensureTip();
824
+ tip.textContent = text;
825
+ _position(anchor, placement);
826
+ tip.style.opacity = '1';
827
+ }
828
+
829
+ function hide() {
830
+ var tip = _ensureTip();
831
+ tip.style.opacity = '0';
832
+ }
833
+
834
+ anchor.addEventListener('mouseenter', show);
835
+ anchor.addEventListener('mouseleave', hide);
836
+ anchor.addEventListener('focus', show);
837
+ anchor.addEventListener('blur', hide);
838
+
839
+ return { show: show, hide: hide };
840
+ }
841
+
842
+ // Auto-init all [data-tooltip] elements
843
+ function init(root) {
844
+ var scope = root || document;
845
+ scope.querySelectorAll('[data-tooltip]').forEach(function (el) {
846
+ create({
847
+ anchor: el,
848
+ text: el.dataset.tooltip,
849
+ placement: el.dataset.tooltipPlacement || 'top',
850
+ });
851
+ });
852
+ }
853
+
854
+ return { create: create, init: init };
855
+ })();
856
+
857
+ // ── Toast ───────────────────────────────────────────────────────────────────
858
+
859
+ /**
860
+ * Toast — ephemeral notification shown bottom-right.
861
+ *
862
+ * Usage:
863
+ * Toast.show('Record saved');
864
+ * Toast.show('Something went wrong', 'error');
865
+ * Toast.show('Copied!', 'info', 2000);
866
+ */
867
+ var Toast = (function () {
868
+ var _container = null;
869
+
870
+ function _getContainer() {
871
+ if (!_container) {
872
+ _container = document.createElement('div');
873
+ _container.style.cssText =
874
+ 'position:fixed;bottom:22px;right:22px;' +
875
+ 'display:flex;flex-direction:column-reverse;gap:8px;' +
876
+ 'z-index:' + Z_TOAST + ';pointer-events:none;';
877
+ Portal.mount(_container);
878
+ }
879
+ return _container;
880
+ }
881
+
882
+ var ICONS = {
883
+ success: '<polyline points="20 6 9 17 4 12"/>',
884
+ error: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
885
+ warning: '<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
886
+ info: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>',
887
+ };
888
+
889
+ var DOT_COLORS = {
890
+ success: '#4ade80',
891
+ error: '#f87171',
892
+ warning: '#fbbf24',
893
+ info: '#60a5fa',
894
+ };
895
+
896
+ function show(message, type, duration) {
897
+ type = type || 'success';
898
+ duration = duration !== undefined ? duration : 3500;
899
+
900
+ var c = _getContainer();
901
+ var el = document.createElement('div');
902
+ var dot = DOT_COLORS[type] || DOT_COLORS.success;
903
+
904
+ el.style.cssText =
905
+ 'background:var(--text,#111827);color:#fff;' +
906
+ 'border-radius:var(--radius,8px);' +
907
+ 'padding:11px 16px;font-size:13px;font-family:inherit;' +
908
+ 'max-width:320px;box-shadow:0 8px 24px rgba(0,0,0,.18);' +
909
+ 'pointer-events:auto;' +
910
+ 'display:flex;align-items:center;gap:9px;' +
911
+ 'transform:translateX(16px);opacity:0;' +
912
+ 'transition:transform .2s ' + EASE_OUT + ',opacity .2s;';
913
+
914
+ el.innerHTML =
915
+ '<span style="flex-shrink:0;color:' + dot + '">' +
916
+ '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
917
+ (ICONS[type] || ICONS.success) +
918
+ '</svg></span>' +
919
+ '<span>' + message + '</span>';
920
+
921
+ c.appendChild(el);
922
+
923
+ // Animate in
924
+ requestAnimationFrame(function () {
925
+ el.style.transform = 'translateX(0)';
926
+ el.style.opacity = '1';
927
+ });
928
+
929
+ if (duration > 0) {
930
+ setTimeout(function () {
931
+ el.style.transform = 'translateX(16px)';
932
+ el.style.opacity = '0';
933
+ setTimeout(function () { el.remove(); }, 220);
934
+ }, duration);
935
+ }
936
+
937
+ return el;
938
+ }
939
+
940
+ return { show: show };
941
+ })();
942
+
943
+ // ── Confirm ─────────────────────────────────────────────────────────────────
944
+
945
+ /**
946
+ * Confirm — promise-based confirmation dialog.
947
+ * Resolves true on confirm, false on cancel.
948
+ *
949
+ * Usage:
950
+ * Confirm.show({
951
+ * title: 'Delete record',
952
+ * message: 'This cannot be undone.',
953
+ * confirm: 'Delete',
954
+ * cancel: 'Cancel',
955
+ * danger: true,
956
+ * }).then(function(ok) {
957
+ * if (ok) doDelete();
958
+ * });
959
+ *
960
+ * // async/await friendly
961
+ * if (await Confirm.show({ title: 'Sure?' })) doIt();
962
+ */
963
+ var Confirm = (function () {
964
+ function show(opts) {
965
+ opts = opts || {};
966
+ var title = opts.title || 'Are you sure?';
967
+ var message = opts.message || '';
968
+ var confirmTxt = opts.confirm || 'Confirm';
969
+ var cancelTxt = opts.cancel || 'Cancel';
970
+ var danger = opts.danger !== false;
971
+
972
+ return new Promise(function (resolve) {
973
+ var confirmBtnStyle =
974
+ 'padding:7px 16px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;border:1px solid transparent;font-family:inherit;transition:all .12s;' +
975
+ (danger
976
+ ? 'background:var(--danger,#dc2626);color:#fff;border-color:var(--danger,#dc2626);'
977
+ : 'background:var(--primary,#2563eb);color:#fff;border-color:var(--primary,#2563eb);');
978
+
979
+ var bodyContent = message
980
+ ? '<p style="color:var(--text-soft,#374151);line-height:1.6;font-size:13.5px">' + message + '</p>'
981
+ : '';
982
+
983
+ var footerHtml =
984
+ '<button id="_confirm-cancel" style="padding:7px 16px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;background:transparent;border:1px solid var(--border,#e3e6ec);color:var(--text-soft,#374151);font-family:inherit;transition:all .12s">' +
985
+ cancelTxt + '</button>' +
986
+ '<button id="_confirm-ok" style="' + confirmBtnStyle + '">' + confirmTxt + '</button>';
987
+
988
+ var m = Modal.create({
989
+ title: title,
990
+ content: bodyContent,
991
+ footer: footerHtml,
992
+ size: 'sm',
993
+ closeable: true,
994
+ onClose: function () { resolve(false); },
995
+ });
996
+
997
+ m.open();
998
+
999
+ // Wire buttons after DOM is ready
1000
+ requestAnimationFrame(function () {
1001
+ var okBtn = m.dialog.querySelector('#_confirm-ok');
1002
+ var cancelBtn = m.dialog.querySelector('#_confirm-cancel');
1003
+
1004
+ if (okBtn) {
1005
+ okBtn.addEventListener('click', function () {
1006
+ m.close();
1007
+ resolve(true);
1008
+ });
1009
+ }
1010
+ if (cancelBtn) {
1011
+ cancelBtn.addEventListener('click', function () {
1012
+ m.close();
1013
+ resolve(false);
1014
+ });
1015
+ }
1016
+ });
1017
+ });
1018
+ }
1019
+
1020
+ return { show: show };
1021
+ })();
1022
+
1023
+ // ── Auto-init ────────────────────────────────────────────────────────────────
1024
+
1025
+ document.addEventListener('DOMContentLoaded', function () {
1026
+ // Init declarative tooltips
1027
+ Tooltip.init(document);
1028
+ });
1029
+
1030
+ // ── Public API ───────────────────────────────────────────────────────────────
1031
+
1032
+ root.UI = {
1033
+ Portal: Portal,
1034
+ Dropdown: Dropdown,
1035
+ Modal: Modal,
1036
+ Drawer: Drawer,
1037
+ Tooltip: Tooltip,
1038
+ Toast: Toast,
1039
+ Confirm: Confirm,
1040
+ FocusTrap: FocusTrap,
1041
+ ScrollLock: ScrollLock,
1042
+ };
1043
+
1044
+ }(typeof window !== 'undefined' ? window : this));