minora 0.1.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.
package/dist/minora.js ADDED
@@ -0,0 +1,779 @@
1
+ /*! Minora UI Kit v0.1.0 — https://github.com/username/minora */
2
+
3
+ 'use strict';
4
+
5
+ /* ─── src\js\C:\DEV\minora\src\js\modal.js ─── */
6
+ /**
7
+ * Minora — Modal Manager
8
+ * ──────────────────────
9
+ * Full-featured modal system with focus trap, body scroll lock,
10
+ * nested stacking, and backdrop/escape close.
11
+ *
12
+ * Usage:
13
+ * ModalManager.open('modal-id');
14
+ * ModalManager.close('modal-id');
15
+ */
16
+ (function() {
17
+ 'use strict';
18
+
19
+ var ModalManager = (function() {
20
+ var openModals = [];
21
+ var previouslyFocused = [];
22
+ var scrollBarWidth = 0;
23
+
24
+ function getScrollBarWidth() {
25
+ var outer = document.createElement('div');
26
+ outer.style.cssText = 'position:absolute;top:-9999px;width:50px;height:50px;overflow:hidden;';
27
+ document.body.appendChild(outer);
28
+ var wNoScroll = outer.offsetWidth;
29
+ outer.style.overflow = 'scroll';
30
+ var inner = document.createElement('div');
31
+ inner.style.cssText = 'width:100%;height:50px;';
32
+ outer.appendChild(inner);
33
+ var wScroll = inner.offsetWidth;
34
+ document.body.removeChild(outer);
35
+ return wNoScroll - wScroll;
36
+ }
37
+
38
+ function lockBodyScroll() {
39
+ if (openModals.length === 0) {
40
+ scrollBarWidth = getScrollBarWidth();
41
+ document.body.style.paddingRight = scrollBarWidth + 'px';
42
+ document.body.classList.add('is-modal-open');
43
+ }
44
+ }
45
+
46
+ function unlockBodyScroll() {
47
+ if (openModals.length === 0) {
48
+ document.body.style.paddingRight = '';
49
+ document.body.classList.remove('is-modal-open');
50
+ }
51
+ }
52
+
53
+ function getFocusable(modal) {
54
+ var selectors = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
55
+ return Array.from(modal.querySelectorAll(selectors)).filter(function(el) {
56
+ return el.offsetParent !== null;
57
+ });
58
+ }
59
+
60
+ function trapFocus(e, overlay) {
61
+ if (e.key !== 'Tab') return;
62
+ var focusable = getFocusable(overlay);
63
+ if (focusable.length === 0) return;
64
+
65
+ var first = focusable[0];
66
+ var last = focusable[focusable.length - 1];
67
+
68
+ if (e.shiftKey) {
69
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
70
+ } else {
71
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
72
+ }
73
+ }
74
+
75
+ function open(id) {
76
+ var overlay = document.getElementById(id);
77
+ if (!overlay) return;
78
+
79
+ previouslyFocused.push(document.activeElement);
80
+ lockBodyScroll();
81
+ openModals.push(overlay);
82
+
83
+ overlay.classList.add('is-open');
84
+ overlay.setAttribute('aria-hidden', 'false');
85
+
86
+ overlay._focusHandler = function(e) { trapFocus(e, overlay); };
87
+ overlay.addEventListener('keydown', overlay._focusHandler);
88
+
89
+ overlay._backdropHandler = function(e) {
90
+ if (e.target === overlay) close(id);
91
+ };
92
+ overlay.addEventListener('click', overlay._backdropHandler);
93
+
94
+ overlay._escapeHandler = function(e) {
95
+ if (e.key === 'Escape' && openModals[openModals.length - 1] === overlay) close(id);
96
+ };
97
+ document.addEventListener('keydown', overlay._escapeHandler);
98
+
99
+ requestAnimationFrame(function() {
100
+ var focusable = getFocusable(overlay);
101
+ if (focusable.length > 0) focusable[0].focus();
102
+ });
103
+ }
104
+
105
+ function close(id) {
106
+ var overlay = document.getElementById(id);
107
+ if (!overlay) return;
108
+
109
+ overlay.classList.remove('is-open');
110
+ overlay.setAttribute('aria-hidden', 'true');
111
+
112
+ if (overlay._focusHandler) overlay.removeEventListener('keydown', overlay._focusHandler);
113
+ if (overlay._backdropHandler) overlay.removeEventListener('click', overlay._backdropHandler);
114
+ if (overlay._escapeHandler) document.removeEventListener('keydown', overlay._escapeHandler);
115
+
116
+ openModals = openModals.filter(function(m) { return m !== overlay; });
117
+ unlockBodyScroll();
118
+
119
+ var prev = previouslyFocused.pop();
120
+ if (prev && prev.focus) setTimeout(function() { prev.focus(); }, 50);
121
+ }
122
+
123
+ return { open: open, close: close };
124
+ })();
125
+
126
+ window.ModalManager = ModalManager;
127
+
128
+ // Initialize all overlays as hidden
129
+ document.querySelectorAll('.modal-overlay').forEach(function(el) {
130
+ el.setAttribute('aria-hidden', 'true');
131
+ });
132
+ })();
133
+
134
+
135
+ /* ─── src\js\C:\DEV\minora\src\js\select.js ─── */
136
+ /**
137
+ * Minora — Select & Multiselect Manager
138
+ * ─────────────────────────────────────
139
+ * Custom dropdowns with search, keyboard navigation,
140
+ * grouped options, and tag-based multiselect.
141
+ *
142
+ * Usage:
143
+ * <div class="select select-md" data-select>
144
+ * <div class="select-trigger" role="combobox" tabindex="0">
145
+ * <span class="select-placeholder">Choose…</span>
146
+ * <span class="select-value" hidden></span>
147
+ * <span class="select-chevron"><svg>…</svg></span>
148
+ * </div>
149
+ * <select name="field" hidden>
150
+ * <option value="a">Option A</option>
151
+ * </select>
152
+ * <div class="select-dropdown">
153
+ * <div class="select-search-wrapper"><input class="select-search-input" /></div>
154
+ * <div class="select-options">
155
+ * <div class="select-option" data-value="a"><span>Option A</span><svg class="select-check">…</svg></div>
156
+ * </div>
157
+ * </div>
158
+ * </div>
159
+ */
160
+ (function() {
161
+ 'use strict';
162
+
163
+ /* ─── Utility: Close all open selects ─── */
164
+ function closeAllSelects(except) {
165
+ document.querySelectorAll('.select-open').forEach(function(el) {
166
+ if (el !== except) {
167
+ el.classList.remove('select-open');
168
+ var trigger = el.querySelector('.select-trigger');
169
+ if (trigger) trigger.setAttribute('aria-expanded', 'false');
170
+ el.classList.remove('select-flip-up');
171
+ }
172
+ });
173
+ }
174
+
175
+ /* ─── SINGLE SELECT ─── */
176
+ function initSingleSelect(selectEl) {
177
+ if (selectEl.classList.contains('select-disabled')) return;
178
+
179
+ var trigger = selectEl.querySelector('.select-trigger');
180
+ var dropdown = selectEl.querySelector('.select-dropdown');
181
+ var nativeSelect = selectEl.querySelector('select');
182
+ var searchInput = selectEl.querySelector('.select-search-input');
183
+ var options = selectEl.querySelectorAll('.select-option');
184
+ var placeholder = selectEl.querySelector('.select-placeholder');
185
+ var valueEl = selectEl.querySelector('.select-value');
186
+
187
+ if (!trigger || !dropdown || !nativeSelect) return;
188
+
189
+ if (nativeSelect.value) {
190
+ var initialOption = nativeSelect.options[nativeSelect.selectedIndex];
191
+ if (initialOption && initialOption.value) {
192
+ if (placeholder) placeholder.hidden = true;
193
+ if (valueEl) { valueEl.hidden = false; valueEl.textContent = initialOption.text; }
194
+ var matchingOpt = selectEl.querySelector('.select-option[data-value="' + initialOption.value + '"]');
195
+ if (matchingOpt) matchingOpt.classList.add('is-selected');
196
+ }
197
+ }
198
+
199
+ trigger.addEventListener('click', function(e) {
200
+ e.stopPropagation();
201
+ var isOpen = selectEl.classList.contains('select-open');
202
+ if (isOpen) {
203
+ selectEl.classList.remove('select-open');
204
+ trigger.setAttribute('aria-expanded', 'false');
205
+ selectEl.classList.remove('select-flip-up');
206
+ } else {
207
+ closeAllSelects();
208
+ selectEl.classList.add('select-open');
209
+ trigger.setAttribute('aria-expanded', 'true');
210
+ if (searchInput) setTimeout(function() { searchInput.focus(); }, 50);
211
+ requestAnimationFrame(function() {
212
+ var rect = dropdown.getBoundingClientRect();
213
+ if (rect.bottom > window.innerHeight - 16) selectEl.classList.add('select-flip-up');
214
+ });
215
+ }
216
+ });
217
+
218
+ options.forEach(function(opt) {
219
+ opt.addEventListener('click', function(e) {
220
+ e.stopPropagation();
221
+ if (opt.classList.contains('is-disabled')) return;
222
+ var val = opt.getAttribute('data-value');
223
+ var text = opt.querySelector('span').textContent;
224
+ options.forEach(function(o) { o.classList.remove('is-selected'); });
225
+ opt.classList.add('is-selected');
226
+ nativeSelect.value = val;
227
+ if (placeholder) placeholder.hidden = true;
228
+ if (valueEl) { valueEl.hidden = false; valueEl.textContent = text; }
229
+ selectEl.classList.remove('select-open');
230
+ trigger.setAttribute('aria-expanded', 'false');
231
+ selectEl.classList.remove('select-flip-up');
232
+ if (searchInput) { searchInput.value = ''; options.forEach(function(o) { o.classList.remove('is-hidden'); }); }
233
+ nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
234
+ });
235
+ });
236
+
237
+ if (searchInput) {
238
+ searchInput.addEventListener('input', function(e) {
239
+ e.stopPropagation();
240
+ var query = searchInput.value.toLowerCase().trim();
241
+ options.forEach(function(opt) {
242
+ var text = opt.querySelector('span').textContent.toLowerCase();
243
+ opt.classList.toggle('is-hidden', query && text.indexOf(query) === -1);
244
+ });
245
+ selectEl.querySelectorAll('.select-group-label').forEach(function(label) {
246
+ var next = label.nextElementSibling, hasVisible = false;
247
+ while (next && !next.classList.contains('select-group-label')) {
248
+ if (next.classList.contains('select-option') && !next.classList.contains('is-hidden')) { hasVisible = true; break; }
249
+ next = next.nextElementSibling;
250
+ }
251
+ label.style.display = hasVisible ? '' : 'none';
252
+ });
253
+ options.forEach(function(o) { o.classList.remove('is-focused'); });
254
+ });
255
+ searchInput.addEventListener('click', function(e) { e.stopPropagation(); });
256
+ }
257
+
258
+ trigger.addEventListener('keydown', function(e) {
259
+ var isOpen = selectEl.classList.contains('select-open');
260
+ var visibleOptions = Array.from(options).filter(function(o) { return !o.classList.contains('is-hidden') && !o.classList.contains('is-disabled'); });
261
+ if (!isOpen && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown')) { e.preventDefault(); trigger.click(); return; }
262
+ if (isOpen) {
263
+ var focusedOpt = selectEl.querySelector('.select-option.is-focused');
264
+ var focusedIdx = visibleOptions.indexOf(focusedOpt);
265
+ if (e.key === 'ArrowDown') { e.preventDefault(); if (focusedOpt) focusedOpt.classList.remove('is-focused'); var n = focusedIdx < visibleOptions.length - 1 ? focusedIdx + 1 : 0; visibleOptions[n].classList.add('is-focused'); visibleOptions[n].scrollIntoView({ block: 'nearest' }); }
266
+ else if (e.key === 'ArrowUp') { e.preventDefault(); if (focusedOpt) focusedOpt.classList.remove('is-focused'); var p = focusedIdx > 0 ? focusedIdx - 1 : visibleOptions.length - 1; visibleOptions[p].classList.add('is-focused'); visibleOptions[p].scrollIntoView({ block: 'nearest' }); }
267
+ else if (e.key === 'Enter') { e.preventDefault(); if (focusedOpt) focusedOpt.click(); }
268
+ else if (e.key === 'Escape') { e.preventDefault(); selectEl.classList.remove('select-open'); trigger.setAttribute('aria-expanded', 'false'); selectEl.classList.remove('select-flip-up'); }
269
+ }
270
+ });
271
+
272
+ document.addEventListener('click', function(e) {
273
+ if (!selectEl.contains(e.target)) {
274
+ selectEl.classList.remove('select-open');
275
+ trigger.setAttribute('aria-expanded', 'false');
276
+ selectEl.classList.remove('select-flip-up');
277
+ if (searchInput) { searchInput.value = ''; options.forEach(function(o) { o.classList.remove('is-hidden'); }); }
278
+ }
279
+ });
280
+ }
281
+
282
+ /* ─── MULTISELECT ─── */
283
+ function initMultiselect(multiEl) {
284
+ if (multiEl.classList.contains('select-disabled')) return;
285
+
286
+ var trigger = multiEl.querySelector('.select-trigger');
287
+ var dropdown = multiEl.querySelector('.select-dropdown');
288
+ var nativeSelect = multiEl.querySelector('select');
289
+ var options = multiEl.querySelectorAll('.select-option');
290
+ var tagsContainer = multiEl.querySelector('.multiselect-tags');
291
+ var searchInput = multiEl.querySelector('.multiselect-search-input');
292
+ var placeholder = multiEl.querySelector('.multiselect-tag-placeholder');
293
+ var MAX_VISIBLE = 2;
294
+ var selectedValues = [];
295
+
296
+ if (!trigger || !dropdown || !nativeSelect || !tagsContainer) return;
297
+
298
+ Array.from(nativeSelect.selectedOptions).forEach(function(opt) { selectedValues.push(opt.value); });
299
+
300
+ function renderTags() {
301
+ tagsContainer.querySelectorAll('.multiselect-tag, .multiselect-counter').forEach(function(el) { el.remove(); });
302
+ if (selectedValues.length === 0) { if (placeholder) placeholder.style.display = ''; return; }
303
+ if (placeholder) placeholder.style.display = 'none';
304
+ var visibleCount = Math.min(selectedValues.length, MAX_VISIBLE);
305
+ for (var i = 0; i < visibleCount; i++) {
306
+ var val = selectedValues[i];
307
+ var optEl = multiEl.querySelector('.select-option[data-value="' + val + '"]');
308
+ var text = optEl ? optEl.querySelector('span').textContent : val;
309
+ var tag = document.createElement('span');
310
+ tag.className = 'multiselect-tag';
311
+ tag.setAttribute('data-value', val);
312
+ tag.innerHTML = '<span class="multiselect-tag-label">' + text + '</span>' +
313
+ '<button class="multiselect-tag-remove" type="button" aria-label="Remove">' +
314
+ '<svg 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><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>';
315
+ tagsContainer.insertBefore(tag, searchInput);
316
+ tag.querySelector('.multiselect-tag-remove').addEventListener('click', function(e) { e.stopPropagation(); removeValue(val); });
317
+ }
318
+ var remaining = selectedValues.length - MAX_VISIBLE;
319
+ if (remaining > 0) {
320
+ var counter = document.createElement('span');
321
+ counter.className = 'multiselect-counter';
322
+ counter.textContent = '+' + remaining;
323
+ tagsContainer.insertBefore(counter, searchInput);
324
+ }
325
+ }
326
+
327
+ function removeValue(val) { selectedValues = selectedValues.filter(function(v) { return v !== val; }); updateNativeSelect(); renderTags(); var o = multiEl.querySelector('.select-option[data-value="' + val + '"]'); if (o) o.classList.remove('is-selected'); }
328
+ function addValue(val) { if (selectedValues.indexOf(val) === -1) selectedValues.push(val); updateNativeSelect(); renderTags(); var o = multiEl.querySelector('.select-option[data-value="' + val + '"]'); if (o) o.classList.add('is-selected'); }
329
+ function updateNativeSelect() { Array.from(nativeSelect.options).forEach(function(opt) { opt.selected = selectedValues.indexOf(opt.value) !== -1; }); nativeSelect.dispatchEvent(new Event('change', { bubbles: true })); }
330
+
331
+ function openDropdown() {
332
+ closeAllSelects();
333
+ multiEl.classList.add('select-open');
334
+ trigger.setAttribute('aria-expanded', 'true');
335
+ if (searchInput) { searchInput.style.display = ''; setTimeout(function() { searchInput.focus(); }, 50); }
336
+ requestAnimationFrame(function() {
337
+ var rect = dropdown.getBoundingClientRect();
338
+ if (rect.bottom > window.innerHeight - 16) multiEl.classList.add('select-flip-up');
339
+ });
340
+ }
341
+
342
+ function closeDropdown() {
343
+ multiEl.classList.remove('select-open');
344
+ trigger.setAttribute('aria-expanded', 'false');
345
+ multiEl.classList.remove('select-flip-up');
346
+ if (selectedValues.length === 0 && searchInput) searchInput.style.display = 'none';
347
+ if (searchInput) { searchInput.value = ''; options.forEach(function(o) { o.classList.remove('is-hidden'); }); }
348
+ }
349
+
350
+ trigger.addEventListener('click', function(e) { e.stopPropagation(); multiEl.classList.contains('select-open') ? closeDropdown() : openDropdown(); });
351
+
352
+ options.forEach(function(opt) {
353
+ opt.addEventListener('click', function(e) {
354
+ e.stopPropagation();
355
+ if (opt.classList.contains('is-disabled')) return;
356
+ var val = opt.getAttribute('data-value');
357
+ opt.classList.contains('is-selected') ? removeValue(val) : addValue(val);
358
+ });
359
+ });
360
+
361
+ if (searchInput) {
362
+ searchInput.addEventListener('input', function(e) {
363
+ e.stopPropagation();
364
+ var query = searchInput.value.toLowerCase().trim();
365
+ options.forEach(function(opt) { var t = opt.querySelector('span').textContent.toLowerCase(); opt.classList.toggle('is-hidden', query && t.indexOf(query) === -1); });
366
+ multiEl.querySelectorAll('.select-group-label').forEach(function(label) {
367
+ var next = label.nextElementSibling, hasVisible = false;
368
+ while (next && !next.classList.contains('select-group-label')) { if (next.classList.contains('select-option') && !next.classList.contains('is-hidden')) { hasVisible = true; break; } next = next.nextElementSibling; }
369
+ label.style.display = hasVisible ? '' : 'none';
370
+ });
371
+ options.forEach(function(o) { o.classList.remove('is-focused'); });
372
+ });
373
+ searchInput.addEventListener('click', function(e) { e.stopPropagation(); });
374
+ }
375
+
376
+ var selectAllBtn = multiEl.querySelector('.multiselect-select-all');
377
+ if (selectAllBtn) selectAllBtn.addEventListener('click', function(e) { e.stopPropagation(); options.forEach(function(opt) { if (!opt.classList.contains('is-disabled')) { addValue(opt.getAttribute('data-value')); opt.classList.add('is-selected'); } }); });
378
+
379
+ var clearAllBtn = multiEl.querySelector('.multiselect-clear-all');
380
+ if (clearAllBtn) clearAllBtn.addEventListener('click', function(e) { e.stopPropagation(); selectedValues = []; updateNativeSelect(); renderTags(); options.forEach(function(opt) { opt.classList.remove('is-selected'); }); });
381
+
382
+ trigger.addEventListener('keydown', function(e) {
383
+ var isOpen = multiEl.classList.contains('select-open');
384
+ var visibleOptions = Array.from(options).filter(function(o) { return !o.classList.contains('is-hidden') && !o.classList.contains('is-disabled'); });
385
+ if (!isOpen && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown')) { e.preventDefault(); openDropdown(); return; }
386
+ if (isOpen) {
387
+ var focusedOpt = multiEl.querySelector('.select-option.is-focused');
388
+ var focusedIdx = visibleOptions.indexOf(focusedOpt);
389
+ if (e.key === 'ArrowDown') { e.preventDefault(); if (focusedOpt) focusedOpt.classList.remove('is-focused'); var n = focusedIdx < visibleOptions.length - 1 ? focusedIdx + 1 : 0; visibleOptions[n].classList.add('is-focused'); visibleOptions[n].scrollIntoView({ block: 'nearest' }); }
390
+ else if (e.key === 'ArrowUp') { e.preventDefault(); if (focusedOpt) focusedOpt.classList.remove('is-focused'); var p = focusedIdx > 0 ? focusedIdx - 1 : visibleOptions.length - 1; visibleOptions[p].classList.add('is-focused'); visibleOptions[p].scrollIntoView({ block: 'nearest' }); }
391
+ else if (e.key === 'Enter') { e.preventDefault(); if (focusedOpt) focusedOpt.click(); }
392
+ else if (e.key === 'Escape') { e.preventDefault(); closeDropdown(); }
393
+ }
394
+ });
395
+
396
+ renderTags();
397
+ document.addEventListener('click', function(e) { if (!multiEl.contains(e.target)) closeDropdown(); });
398
+ }
399
+
400
+ /* ─── Init on DOM ready ─── */
401
+ function init() {
402
+ document.querySelectorAll('[data-select]').forEach(initSingleSelect);
403
+ document.querySelectorAll('[data-multiselect]').forEach(initMultiselect);
404
+ }
405
+
406
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }
407
+ })();
408
+
409
+
410
+ /* ─── src\js\C:\DEV\minora\src\js\toast.js ─── */
411
+ /**
412
+ * Minora — Toast Manager
413
+ * ──────────────────────
414
+ * Queue-based toast system with position control,
415
+ * auto-dismiss, progress bar, and action buttons.
416
+ *
417
+ * Usage:
418
+ * ToastManager.show({ type: 'success', message: 'Saved!' });
419
+ * ToastManager.show({ type: 'error', message: 'Failed', duration: 6000 });
420
+ * ToastManager.show({ type: 'info', message: 'Hint', action: { label: 'Undo', onClick: fn } });
421
+ * ToastManager.setPosition('top-left');
422
+ * ToastManager.clearAll();
423
+ */
424
+ (function() {
425
+ 'use strict';
426
+
427
+ var ToastManager = (function() {
428
+ var queue = [];
429
+ var active = [];
430
+ var stackEl = null;
431
+ var position = 'bottom-right';
432
+ var MAX_VISIBLE = 5;
433
+ var DEFAULT_DURATION = 4000;
434
+
435
+ /* SVG icons by type */
436
+ var ICONS = {
437
+ success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>',
438
+ error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>',
439
+ warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>',
440
+ info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
441
+ neutral: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>',
442
+ loading: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" stroke-dasharray="20" stroke-dashoffset="15"></circle></svg>'
443
+ };
444
+
445
+ function init() {
446
+ if (!stackEl) {
447
+ stackEl = document.createElement('div');
448
+ stackEl.className = 'toast-stack toast-stack-' + position;
449
+ stackEl.id = 'toast-stack';
450
+ document.body.appendChild(stackEl);
451
+ }
452
+ }
453
+
454
+ function show(opts) {
455
+ opts = opts || {};
456
+ var type = opts.type || 'neutral';
457
+ var message = opts.message || '';
458
+ var duration = opts.duration || DEFAULT_DURATION;
459
+ var action = opts.action || null;
460
+ var item = { type: type, message: message, duration: duration, action: action };
461
+
462
+ if (active.length < MAX_VISIBLE) {
463
+ renderToast(item);
464
+ } else {
465
+ queue.push(item);
466
+ }
467
+ }
468
+
469
+ function renderToast(item) {
470
+ init();
471
+
472
+ var toast = document.createElement('div');
473
+ toast.className = 'toast toast-' + item.type + ' toast-enter';
474
+ toast.setAttribute('role', 'alert');
475
+ toast.setAttribute('aria-live', 'assertive');
476
+
477
+ var actionHTML = item.action
478
+ ? '<button class="toast-action" data-action="1">' + item.action.label + '</button>'
479
+ : '';
480
+
481
+ toast.innerHTML =
482
+ '<span class="toast-icon">' + (ICONS[item.type] || ICONS.neutral) + '</span>' +
483
+ '<div class="toast-body">' +
484
+ '<span class="toast-message">' + item.message + '</span>' +
485
+ actionHTML +
486
+ '</div>' +
487
+ '<button class="toast-close" aria-label="Close">' +
488
+ '<svg 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><line x1="6" y1="6" x2="18" y2="18"></line></svg>' +
489
+ '</button>' +
490
+ (item.type !== 'loading' ? '<div class="toast-progress running" style="animation-duration:' + item.duration + 'ms"></div>' : '<div class="toast-progress"></div>');
491
+
492
+ var closeBtn = toast.querySelector('.toast-close');
493
+ closeBtn.addEventListener('click', function() { dismissToast(toast); });
494
+
495
+ var actionBtn = toast.querySelector('.toast-action');
496
+ if (actionBtn && item.action && item.action.onClick) {
497
+ actionBtn.addEventListener('click', function() { dismissToast(toast); item.action.onClick(); });
498
+ }
499
+
500
+ var progressEl = toast.querySelector('.toast-progress.running');
501
+ if (progressEl) {
502
+ toast.addEventListener('mouseenter', function() { progressEl.style.animationPlayState = 'paused'; });
503
+ toast.addEventListener('mouseleave', function() { progressEl.style.animationPlayState = 'running'; });
504
+ }
505
+
506
+ stackEl.appendChild(toast);
507
+ active.push(toast);
508
+
509
+ if (item.type !== 'loading') {
510
+ toast._dismissTimer = setTimeout(function() { dismissToast(toast); }, item.duration + 200);
511
+ }
512
+ }
513
+
514
+ function dismissToast(toast) {
515
+ clearTimeout(toast._dismissTimer);
516
+ toast.classList.remove('toast-enter');
517
+ toast.classList.add('toast-exit');
518
+ toast.addEventListener('animationend', function handler() {
519
+ toast.removeEventListener('animationend', handler);
520
+ if (toast.parentNode) toast.parentNode.removeChild(toast);
521
+ var idx = active.indexOf(toast);
522
+ if (idx !== -1) active.splice(idx, 1);
523
+ processQueue();
524
+ });
525
+ }
526
+
527
+ function processQueue() {
528
+ while (queue.length > 0 && active.length < MAX_VISIBLE) {
529
+ renderToast(queue.shift());
530
+ }
531
+ }
532
+
533
+ function clearAll() {
534
+ active.slice().forEach(function(t) { dismissToast(t); });
535
+ queue = [];
536
+ }
537
+
538
+ function setPosition(pos) {
539
+ position = pos;
540
+ if (stackEl) {
541
+ stackEl.className = 'toast-stack';
542
+ stackEl.classList.add('toast-stack-' + position);
543
+ }
544
+ }
545
+
546
+ return { show: show, clearAll: clearAll, setPosition: setPosition };
547
+ })();
548
+
549
+ window.ToastManager = ToastManager;
550
+ })();
551
+
552
+
553
+ /* ─── src\js\C:\DEV\minora\src\js\tooltip.js ─── */
554
+ /**
555
+ * Minora — Tooltip & Popover Manager
556
+ * ───────────────────────────────────
557
+ * Tooltips with 9 positions, auto-flip, rich content,
558
+ * interactive variants, and click-triggered popovers.
559
+ *
560
+ * Usage (tooltip):
561
+ * data-tooltip="text"
562
+ * data-tooltip-title="Title" data-tooltip-body="Body text"
563
+ * data-tooltip-position="top|bottom|left|right|top-start|..."
564
+ * data-tooltip-variant="light"
565
+ * data-tooltip-trigger="hover|focus|click|hover-focus"
566
+ * data-tooltip-interactive
567
+ *
568
+ * Usage (popover):
569
+ * data-popover="popover-id" data-popover-position="bottom|top|bottom-end|..."
570
+ */
571
+ (function() {
572
+ 'use strict';
573
+
574
+ var ICONS = {
575
+ info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
576
+ star: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>',
577
+ check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>',
578
+ alert: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>'
579
+ };
580
+
581
+ /* ─── TooltipManager ─── */
582
+ var TooltipManager = (function() {
583
+ var tooltips = [];
584
+
585
+ function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
586
+
587
+ function buildContent(trigger) {
588
+ var text = trigger.getAttribute('data-tooltip');
589
+ var title = trigger.getAttribute('data-tooltip-title');
590
+ var body = trigger.getAttribute('data-tooltip-body');
591
+ var icon = trigger.getAttribute('data-tooltip-icon');
592
+ var isInteractive = trigger.hasAttribute('data-tooltip-interactive');
593
+ var html = '<span class="tooltip-arrow"></span><div>';
594
+ if (title || body) {
595
+ html += '<span class="tooltip-rich">';
596
+ if (title) html += '<span class="tooltip-title">' + escapeHtml(title) + '</span>';
597
+ if (body) html += '<span class="tooltip-body">' + escapeHtml(body) + '</span>';
598
+ html += '</span>';
599
+ } else if (icon) {
600
+ html += '<span class="tooltip-with-icon"><span class="tooltip-icon-img">' + (ICONS[icon] || ICONS.info) + '</span><span>' + escapeHtml(text) + '</span></span>';
601
+ } else if (text) { html += escapeHtml(text); }
602
+ if (isInteractive) {
603
+ html += '<div class="tooltip-actions"><button class="tooltip-action-btn" data-tooltip-action>Learn More</button><button class="tooltip-action-btn" data-tooltip-dismiss>Dismiss</button></div>';
604
+ }
605
+ return html + '</div>';
606
+ }
607
+
608
+ function positionTooltip(trigger, tooltip, position) {
609
+ var triggerRect = trigger.getBoundingClientRect();
610
+ var tooltipRect = tooltip.getBoundingClientRect();
611
+ var gap = 10;
612
+ var basePos = position.replace(/-(start|end)$/, '');
613
+ var align = position.indexOf('-start') !== -1 ? 'start' : position.indexOf('-end') !== -1 ? 'end' : 'center';
614
+ var top, left;
615
+
616
+ switch (basePos) {
617
+ case 'top': top = triggerRect.top - gap - tooltipRect.height; left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2; break;
618
+ case 'bottom': top = triggerRect.bottom + gap; left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2; break;
619
+ case 'left': top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2; left = triggerRect.left - gap - tooltipRect.width; break;
620
+ case 'right': top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2; left = triggerRect.right + gap; break;
621
+ }
622
+ if (align === 'start') { if (basePos === 'top' || basePos === 'bottom') left = triggerRect.left; else top = triggerRect.top; }
623
+ else if (align === 'end') { if (basePos === 'top' || basePos === 'bottom') left = triggerRect.right - tooltipRect.width; else top = triggerRect.bottom - tooltipRect.height; }
624
+
625
+ if (top < 4 && basePos === 'top') { basePos = 'bottom'; top = triggerRect.bottom + gap; }
626
+ if (top + tooltipRect.height > window.innerHeight - 4 && basePos === 'bottom') { basePos = 'top'; top = triggerRect.top - gap - tooltipRect.height; }
627
+ if (left < 4 && basePos === 'left') { basePos = 'right'; left = triggerRect.right + gap; }
628
+ if (left + tooltipRect.width > window.innerWidth - 4 && basePos === 'right') { basePos = 'left'; left = triggerRect.left - gap - tooltipRect.width; }
629
+
630
+ left = Math.max(4, Math.min(left, window.innerWidth - tooltipRect.width - 4));
631
+ top = Math.max(4, Math.min(top, window.innerHeight - tooltipRect.height - 4));
632
+ tooltip.style.left = left + 'px';
633
+ tooltip.style.top = top + 'px';
634
+
635
+ var arrow = tooltip.querySelector('.tooltip-arrow');
636
+ if (arrow) {
637
+ if (basePos === 'top') { arrow.style.bottom = 'calc(var(--tooltip-arrow-size) * -1 + 1px)'; arrow.style.top = ''; var c = (triggerRect.left + triggerRect.width / 2) - left; arrow.style.left = Math.max(8, Math.min(c - 3, tooltipRect.width - 16)) + 'px'; arrow.style.right = ''; }
638
+ else if (basePos === 'bottom') { arrow.style.top = 'calc(var(--tooltip-arrow-size) * -1 + 1px)'; arrow.style.bottom = ''; var c2 = (triggerRect.left + triggerRect.width / 2) - left; arrow.style.left = Math.max(8, Math.min(c2 - 3, tooltipRect.width - 16)) + 'px'; arrow.style.right = ''; }
639
+ else if (basePos === 'left') { arrow.style.right = 'calc(var(--tooltip-arrow-size) * -1 + 1px)'; arrow.style.left = ''; var c3 = (triggerRect.top + triggerRect.height / 2) - top; arrow.style.top = Math.max(8, Math.min(c3 - 3, tooltipRect.height - 16)) + 'px'; arrow.style.bottom = ''; }
640
+ else if (basePos === 'right') { arrow.style.left = 'calc(var(--tooltip-arrow-size) * -1 + 1px)'; arrow.style.right = ''; var c4 = (triggerRect.top + triggerRect.height / 2) - top; arrow.style.top = Math.max(8, Math.min(c4 - 3, tooltipRect.height - 16)) + 'px'; arrow.style.bottom = ''; }
641
+ }
642
+ }
643
+
644
+ function show(trigger) {
645
+ hide(trigger);
646
+ var position = trigger.getAttribute('data-tooltip-position') || 'top';
647
+ var variant = trigger.getAttribute('data-tooltip-variant') || '';
648
+ var isInteractive = trigger.hasAttribute('data-tooltip-interactive');
649
+ var tooltip = document.createElement('div');
650
+ tooltip.className = 'tooltip tooltip-' + position;
651
+ if (variant === 'light') tooltip.classList.add('tooltip-light');
652
+ if (isInteractive) tooltip.classList.add('tooltip-interactive');
653
+ tooltip.setAttribute('role', 'tooltip');
654
+ tooltip.innerHTML = buildContent(trigger);
655
+ document.body.appendChild(tooltip);
656
+ positionTooltip(trigger, tooltip, position);
657
+ tooltip._showTimer = setTimeout(function() { tooltip.classList.add('is-visible'); }, 100);
658
+ tooltips.push({ trigger: trigger, tooltip: tooltip });
659
+
660
+ if (isInteractive) {
661
+ tooltip.addEventListener('mouseenter', function() { clearTimeout(tooltip._hideTimer); });
662
+ tooltip.addEventListener('mouseleave', function() { tooltip._hideTimer = setTimeout(function() { hide(trigger); }, 100); });
663
+ tooltip.addEventListener('click', function(e) {
664
+ if (e.target.hasAttribute('data-tooltip-dismiss')) hide(trigger);
665
+ if (e.target.hasAttribute('data-tooltip-action')) { hide(trigger); if (window.ToastManager) ToastManager.show({ type: 'info', message: 'Action clicked!' }); }
666
+ });
667
+ }
668
+ }
669
+
670
+ function hide(trigger) {
671
+ var entry = tooltips.find(function(t) { return t.trigger === trigger; });
672
+ if (!entry) return;
673
+ clearTimeout(entry.tooltip._showTimer);
674
+ entry.tooltip.classList.remove('is-visible');
675
+ entry.tooltip._hideTimer = setTimeout(function() {
676
+ if (entry.tooltip.parentNode) entry.tooltip.parentNode.removeChild(entry.tooltip);
677
+ tooltips = tooltips.filter(function(t) { return t !== entry; });
678
+ }, 100);
679
+ }
680
+
681
+ function init() {
682
+ document.querySelectorAll('[data-tooltip]').forEach(function(trigger) {
683
+ var triggerType = trigger.getAttribute('data-tooltip-trigger') || 'hover';
684
+ if (triggerType === 'hover' || triggerType === 'hover-focus') {
685
+ trigger.addEventListener('mouseenter', function() { show(trigger); });
686
+ trigger.addEventListener('mouseleave', function() { hide(trigger); });
687
+ }
688
+ if (triggerType === 'focus' || triggerType === 'hover-focus') {
689
+ trigger.addEventListener('focus', function() { show(trigger); });
690
+ trigger.addEventListener('blur', function() { hide(trigger); });
691
+ }
692
+ if (triggerType === 'click') {
693
+ trigger.addEventListener('click', function(e) {
694
+ e.stopPropagation();
695
+ var entry = tooltips.find(function(t) { return t.trigger === trigger; });
696
+ entry ? hide(trigger) : show(trigger);
697
+ });
698
+ }
699
+ });
700
+ var scrollTimer;
701
+ window.addEventListener('scroll', function() {
702
+ clearTimeout(scrollTimer);
703
+ scrollTimer = setTimeout(function() { tooltips.slice().forEach(function(entry) { hide(entry.trigger); }); }, 50);
704
+ }, { passive: true });
705
+ }
706
+
707
+ return { init: init, show: show, hide: hide };
708
+ })();
709
+
710
+ /* ─── PopoverManager ─── */
711
+ var PopoverManager = (function() {
712
+ var openPopovers = [];
713
+
714
+ function positionPopover(trigger, popover, position) {
715
+ document.body.appendChild(popover);
716
+ var triggerRect = trigger.getBoundingClientRect();
717
+ var popoverRect = popover.getBoundingClientRect();
718
+ var gap = 8;
719
+ var pos = position || 'bottom';
720
+ var align = pos.indexOf('-end') !== -1 ? 'end' : pos.indexOf('-start') !== -1 ? 'start' : 'center';
721
+ var base = pos.replace(/-(start|end)$/, '');
722
+ var top, left;
723
+
724
+ if (base === 'bottom') { top = triggerRect.bottom + gap; if (align === 'end') left = triggerRect.right - popoverRect.width; else if (align === 'start') left = triggerRect.left; else left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2; }
725
+ else if (base === 'top') { top = triggerRect.top - gap - popoverRect.height; if (align === 'end') left = triggerRect.right - popoverRect.width; else if (align === 'start') left = triggerRect.left; else left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2; }
726
+
727
+ left = Math.max(8, Math.min(left, window.innerWidth - popoverRect.width - 8));
728
+ top = Math.max(8, Math.min(top, window.innerHeight - popoverRect.height - 8));
729
+ popover.style.left = left + 'px';
730
+ popover.style.top = top + 'px';
731
+ }
732
+
733
+ function show(trigger, popoverId, position) {
734
+ var popover = document.getElementById(popoverId);
735
+ if (!popover) return;
736
+ closeAll();
737
+ popover.classList.add('is-visible');
738
+ popover.classList.remove('is-hidden');
739
+ openPopovers.push(popover);
740
+ positionPopover(trigger, popover, position);
741
+ popover._closeHandler = function(e) { if (!popover.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) close(popoverId); };
742
+ setTimeout(function() { document.addEventListener('click', popover._closeHandler); }, 0);
743
+ popover.querySelectorAll('[data-popover-close]').forEach(function(btn) { btn.addEventListener('click', function() { close(popoverId); }); });
744
+ }
745
+
746
+ function close(popoverId) {
747
+ var popover = document.getElementById(popoverId);
748
+ if (!popover) return;
749
+ popover.classList.remove('is-visible');
750
+ popover.classList.add('is-hidden');
751
+ if (popover._closeHandler) document.removeEventListener('click', popover._closeHandler);
752
+ openPopovers = openPopovers.filter(function(p) { return p !== popover; });
753
+ setTimeout(function() { popover.classList.remove('is-hidden'); }, 200);
754
+ }
755
+
756
+ function closeAll() { openPopovers.slice().forEach(function(popover) { close(popover.id); }); }
757
+
758
+ function init() {
759
+ document.querySelectorAll('[data-popover]').forEach(function(trigger) {
760
+ var popoverId = trigger.getAttribute('data-popover');
761
+ var position = trigger.getAttribute('data-popover-position') || 'bottom';
762
+ trigger.addEventListener('click', function(e) {
763
+ e.stopPropagation();
764
+ var popover = document.getElementById(popoverId);
765
+ if (popover && popover.classList.contains('is-visible')) close(popoverId); else show(trigger, popoverId, position);
766
+ });
767
+ });
768
+ }
769
+
770
+ return { init: init, close: close, closeAll: closeAll };
771
+ })();
772
+
773
+ window.TooltipManager = TooltipManager;
774
+ window.PopoverManager = PopoverManager;
775
+
776
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { TooltipManager.init(); PopoverManager.init(); }); } else { TooltipManager.init(); PopoverManager.init(); }
777
+ })();
778
+
779
+