ui-optago 1.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.
package/ui-optago.js ADDED
@@ -0,0 +1,446 @@
1
+ /* ============================================================================
2
+ UI ÓPTAGO — Design System v1.0 (JavaScript)
3
+ Sem dependências de build. Carregue no <head> SEM defer para evitar
4
+ flash de tema errado (FOUC):
5
+
6
+ <link rel="stylesheet" href="ui-optago.css">
7
+ <script src="ui-optago.js"></script>
8
+
9
+ O script injeta automaticamente as fontes (Google Fonts) e os ícones
10
+ Phosphor, aplica o tema salvo e liga todos os comportamentos via
11
+ atributos data-op-*:
12
+
13
+ data-op-theme-toggle → alterna tema claro/escuro
14
+ data-op-sidebar-toggle → colapsa/expande a sidebar (desktop)
15
+ data-op-sidebar-mobile → abre/fecha a sidebar (mobile drawer)
16
+ data-op-modal-open="#id" → abre o modal
17
+ data-op-modal-close → fecha o modal (dentro dele)
18
+ data-op-tab="nome" → botão de aba (com data-op-tab-group no pai)
19
+ data-op-tab-panel="nome" → painel correspondente
20
+ data-op-dropdown-toggle → abre/fecha o .op-dropdown pai
21
+ data-op-dismiss → remove o elemento pai .op-alert
22
+ data-op-copy="#seletor" → copia o HTML do elemento alvo
23
+ data-op-code="#seletor" → mostra/esconde o código-fonte do alvo
24
+
25
+ API global: OptagoUI.toast(msg, tipo, ms) · OptagoUI.openModal(sel)
26
+ OptagoUI.closeModal(sel) · OptagoUI.setTheme('dark'|'light')
27
+ ============================================================================ */
28
+ (function () {
29
+ 'use strict';
30
+
31
+ var OptagoUI = {};
32
+
33
+ /* --------------------------------------------------------------------------
34
+ 1. TEMA — aplicado imediatamente (antes do primeiro paint)
35
+ -------------------------------------------------------------------------- */
36
+ var THEME_KEY = 'op-theme';
37
+
38
+ function currentTheme() {
39
+ var saved = localStorage.getItem(THEME_KEY);
40
+ if (saved === 'light' || saved === 'dark') return saved;
41
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
42
+ }
43
+
44
+ OptagoUI.setTheme = function (theme) {
45
+ document.documentElement.classList.toggle('light', theme === 'light');
46
+ localStorage.setItem(THEME_KEY, theme);
47
+ document.querySelectorAll('[data-op-theme-toggle]').forEach(function (btn) {
48
+ var moon = btn.querySelector('.op-icon-moon');
49
+ var sun = btn.querySelector('.op-icon-sun');
50
+ if (moon) moon.style.display = theme === 'dark' ? 'none' : '';
51
+ if (sun) sun.style.display = theme === 'dark' ? '' : 'none';
52
+ });
53
+ };
54
+
55
+ OptagoUI.toggleTheme = function () {
56
+ OptagoUI.setTheme(document.documentElement.classList.contains('light') ? 'dark' : 'light');
57
+ };
58
+
59
+ // Aplica já, sem esperar o DOM (evita FOUC)
60
+ document.documentElement.classList.toggle('light', currentTheme() === 'light');
61
+
62
+ /* --------------------------------------------------------------------------
63
+ 2. INJEÇÃO DE DEPENDÊNCIAS (Google Fonts + Phosphor Icons)
64
+ -------------------------------------------------------------------------- */
65
+ function ensureAssets() {
66
+ if (!document.getElementById('op-fonts')) {
67
+ var pre1 = document.createElement('link');
68
+ pre1.rel = 'preconnect'; pre1.href = 'https://fonts.googleapis.com';
69
+ var pre2 = document.createElement('link');
70
+ pre2.rel = 'preconnect'; pre2.href = 'https://fonts.gstatic.com'; pre2.crossOrigin = '';
71
+ var fonts = document.createElement('link');
72
+ fonts.id = 'op-fonts';
73
+ fonts.rel = 'stylesheet';
74
+ fonts.href = 'https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap';
75
+ document.head.append(pre1, pre2, fonts);
76
+ }
77
+ if (!document.getElementById('op-icons') && !window.PhosphorIcons) {
78
+ var icons = document.createElement('script');
79
+ icons.id = 'op-icons';
80
+ icons.src = 'https://unpkg.com/@phosphor-icons/web';
81
+ document.head.appendChild(icons);
82
+ }
83
+ }
84
+ ensureAssets();
85
+
86
+ /* --------------------------------------------------------------------------
87
+ 3. SIDEBAR (colapsar no desktop / drawer no mobile)
88
+ -------------------------------------------------------------------------- */
89
+ var SIDEBAR_KEY = 'op-sidebar-collapsed';
90
+
91
+ function getSidebar() { return document.querySelector('.op-sidebar'); }
92
+ function getOverlay() {
93
+ var ov = document.querySelector('.op-overlay');
94
+ if (!ov) {
95
+ ov = document.createElement('div');
96
+ ov.className = 'op-overlay';
97
+ ov.addEventListener('click', OptagoUI.closeSidebarMobile);
98
+ document.body.appendChild(ov);
99
+ }
100
+ return ov;
101
+ }
102
+
103
+ OptagoUI.toggleSidebar = function () {
104
+ var sb = getSidebar();
105
+ if (!sb) return;
106
+ if (window.innerWidth < 1024) { OptagoUI.toggleSidebarMobile(); return; }
107
+ sb.classList.toggle('is-collapsed');
108
+ localStorage.setItem(SIDEBAR_KEY, sb.classList.contains('is-collapsed') ? '1' : '0');
109
+ };
110
+
111
+ OptagoUI.toggleSidebarMobile = function () {
112
+ var sb = getSidebar();
113
+ if (!sb) return;
114
+ var open = sb.classList.toggle('is-open');
115
+ getOverlay().classList.toggle('is-open', open);
116
+ };
117
+
118
+ OptagoUI.closeSidebarMobile = function () {
119
+ var sb = getSidebar();
120
+ if (sb) sb.classList.remove('is-open');
121
+ var ov = document.querySelector('.op-overlay');
122
+ if (ov) ov.classList.remove('is-open');
123
+ };
124
+
125
+ /* --------------------------------------------------------------------------
126
+ 4. TOASTS — OptagoUI.toast('Mensagem', 'success'|'error'|'warning'|'info')
127
+ -------------------------------------------------------------------------- */
128
+ var TOAST_ICONS = {
129
+ success: 'ph-check-circle',
130
+ error: 'ph-x-circle',
131
+ danger: 'ph-x-circle',
132
+ warning: 'ph-warning',
133
+ info: 'ph-info'
134
+ };
135
+
136
+ OptagoUI.toast = function (message, type, duration) {
137
+ type = type || 'info';
138
+ if (type === 'error') type = 'danger';
139
+ duration = duration || 4000;
140
+
141
+ var container = document.querySelector('.op-toast-container');
142
+ if (!container) {
143
+ container = document.createElement('div');
144
+ container.className = 'op-toast-container';
145
+ document.body.appendChild(container);
146
+ }
147
+
148
+ var toast = document.createElement('div');
149
+ toast.className = 'op-toast op-toast-' + type;
150
+ toast.setAttribute('role', 'status');
151
+ toast.innerHTML =
152
+ '<div class="op-toast-icon"><i class="ph ' + (TOAST_ICONS[type] || TOAST_ICONS.info) + '"></i></div>' +
153
+ '<span></span>' +
154
+ '<button class="op-toast-close" aria-label="Fechar"><i class="ph ph-x"></i></button>';
155
+ toast.querySelector('span').textContent = message;
156
+ container.appendChild(toast);
157
+
158
+ var removed = false;
159
+ var dismiss = function () {
160
+ if (removed) return;
161
+ removed = true;
162
+ toast.classList.remove('is-visible');
163
+ setTimeout(function () { toast.remove(); }, 320);
164
+ };
165
+
166
+ toast.querySelector('.op-toast-close').addEventListener('click', dismiss);
167
+
168
+ requestAnimationFrame(function () {
169
+ requestAnimationFrame(function () { toast.classList.add('is-visible'); });
170
+ });
171
+
172
+ setTimeout(dismiss, duration);
173
+
174
+ return toast;
175
+ };
176
+
177
+ /* --------------------------------------------------------------------------
178
+ 5. MODAIS
179
+ -------------------------------------------------------------------------- */
180
+ OptagoUI.openModal = function (selector) {
181
+ var modal = typeof selector === 'string' ? document.querySelector(selector) : selector;
182
+ if (!modal) return;
183
+ modal.classList.remove('is-closing');
184
+ modal.classList.add('is-open');
185
+ };
186
+
187
+ OptagoUI.closeModal = function (selector) {
188
+ var modal = typeof selector === 'string' ? document.querySelector(selector) : selector;
189
+ if (!modal || !modal.classList.contains('is-open')) return;
190
+ modal.classList.add('is-closing');
191
+ setTimeout(function () {
192
+ modal.classList.remove('is-open', 'is-closing');
193
+ }, 200);
194
+ };
195
+
196
+ /* --------------------------------------------------------------------------
197
+ 5b. TABULATOR — tabela avançada com tema Óptago
198
+ Uso: OptagoUI.tabulator('#minhaTabela', { data: [...], columns: [...] })
199
+ Carrega a biblioteca (CDN) sob demanda; o CSS base do Tabulator é
200
+ inserido ANTES do ui-optago.css para que o tema personalizado
201
+ (.op-tabulator) vença na cascata.
202
+ -------------------------------------------------------------------------- */
203
+ var TABULATOR_JS = 'https://unpkg.com/tabulator-tables@6.3/dist/js/tabulator.min.js';
204
+ var TABULATOR_CSS = 'https://unpkg.com/tabulator-tables@6.3/dist/css/tabulator.min.css';
205
+
206
+ OptagoUI.tabulator = function (selector, options) {
207
+ return new Promise(function (resolve, reject) {
208
+ function build() {
209
+ var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
210
+ if (!el) { reject(new Error('Tabulator: elemento não encontrado: ' + selector)); return; }
211
+ el.classList.add('op-tabulator');
212
+ resolve(new window.Tabulator(el, options || {}));
213
+ }
214
+
215
+ if (window.Tabulator) { build(); return; }
216
+
217
+ if (!document.getElementById('op-tabulator-css')) {
218
+ var css = document.createElement('link');
219
+ css.id = 'op-tabulator-css';
220
+ css.rel = 'stylesheet';
221
+ css.href = TABULATOR_CSS;
222
+ var own = document.querySelector('link[href*="ui-optago.css"]');
223
+ if (own && own.parentNode) {
224
+ own.parentNode.insertBefore(css, own);
225
+ } else {
226
+ document.head.prepend(css);
227
+ }
228
+ }
229
+
230
+ var script = document.getElementById('op-tabulator-js');
231
+ if (!script) {
232
+ script = document.createElement('script');
233
+ script.id = 'op-tabulator-js';
234
+ script.src = TABULATOR_JS;
235
+ document.head.appendChild(script);
236
+ }
237
+ script.addEventListener('load', build);
238
+ script.addEventListener('error', function () {
239
+ reject(new Error('Tabulator: falha ao carregar a biblioteca via CDN.'));
240
+ });
241
+ });
242
+ };
243
+
244
+ /* --------------------------------------------------------------------------
245
+ 6. HELPERS DO SHOWCASE — copiar HTML / ver código
246
+ -------------------------------------------------------------------------- */
247
+ function dedentHTML(el) {
248
+ var html = el.innerHTML.replace(/^\n+|\s+$/g, '');
249
+ var lines = html.split('\n');
250
+ var indent = Infinity;
251
+ lines.forEach(function (line) {
252
+ if (!line.trim()) return;
253
+ var m = line.match(/^\s*/);
254
+ indent = Math.min(indent, m[0].length);
255
+ });
256
+ if (!isFinite(indent)) indent = 0;
257
+ return lines.map(function (line) { return line.slice(indent); }).join('\n');
258
+ }
259
+
260
+ OptagoUI.copyHTML = function (targetSelector, feedbackBtn) {
261
+ var target = document.querySelector(targetSelector);
262
+ if (!target) return;
263
+ var html = dedentHTML(target);
264
+
265
+ var done = function () {
266
+ if (feedbackBtn) {
267
+ var original = feedbackBtn.innerHTML;
268
+ feedbackBtn.innerHTML = '<i class="ph ph-check"></i> Copiado!';
269
+ setTimeout(function () { feedbackBtn.innerHTML = original; }, 2000);
270
+ }
271
+ };
272
+
273
+ if (navigator.clipboard && navigator.clipboard.writeText) {
274
+ navigator.clipboard.writeText(html).then(done);
275
+ } else {
276
+ var ta = document.createElement('textarea');
277
+ ta.value = html;
278
+ ta.style.position = 'fixed';
279
+ ta.style.left = '-9999px';
280
+ document.body.appendChild(ta);
281
+ ta.select();
282
+ document.execCommand('copy');
283
+ ta.remove();
284
+ done();
285
+ }
286
+ };
287
+
288
+ OptagoUI.toggleCode = function (targetSelector, btn) {
289
+ var target = document.querySelector(targetSelector);
290
+ if (!target) return;
291
+ var demo = btn.closest('.op-demo') || target.parentElement;
292
+ var pre = demo.querySelector('.op-demo-code');
293
+ if (!pre) {
294
+ pre = document.createElement('pre');
295
+ pre.className = 'op-demo-code';
296
+ demo.appendChild(pre);
297
+ }
298
+ if (!pre.dataset.filled) {
299
+ pre.textContent = dedentHTML(target);
300
+ pre.dataset.filled = '1';
301
+ }
302
+ pre.classList.toggle('is-visible');
303
+ };
304
+
305
+ /* --------------------------------------------------------------------------
306
+ 7. BINDING GLOBAL via data-attributes (event delegation)
307
+ -------------------------------------------------------------------------- */
308
+ document.addEventListener('click', function (e) {
309
+ var t;
310
+
311
+ // Tema
312
+ if (e.target.closest('[data-op-theme-toggle]')) { OptagoUI.toggleTheme(); return; }
313
+
314
+ // Sidebar
315
+ if (e.target.closest('[data-op-sidebar-toggle]')) { OptagoUI.toggleSidebar(); return; }
316
+ if (e.target.closest('[data-op-sidebar-mobile]')) { OptagoUI.toggleSidebarMobile(); return; }
317
+
318
+ // Fecha drawer mobile ao clicar em link da sidebar
319
+ if (e.target.closest('.op-sidebar .op-nav-item') && window.innerWidth < 1024) {
320
+ OptagoUI.closeSidebarMobile();
321
+ }
322
+
323
+ // Modal
324
+ t = e.target.closest('[data-op-modal-open]');
325
+ if (t) { OptagoUI.openModal(t.getAttribute('data-op-modal-open')); return; }
326
+ if (e.target.closest('[data-op-modal-close]')) {
327
+ OptagoUI.closeModal(e.target.closest('.op-modal'));
328
+ return;
329
+ }
330
+ if (e.target.classList && e.target.classList.contains('op-modal')) {
331
+ OptagoUI.closeModal(e.target); // clique no backdrop
332
+ return;
333
+ }
334
+
335
+ // Tabs
336
+ t = e.target.closest('[data-op-tab]');
337
+ if (t) {
338
+ var name = t.getAttribute('data-op-tab');
339
+ var group = t.closest('[data-op-tab-group]');
340
+ var scope = group ? '[data-op-tab-group="' + group.getAttribute('data-op-tab-group') + '"]' : '';
341
+ document.querySelectorAll(scope + ' [data-op-tab]').forEach(function (tab) {
342
+ tab.classList.toggle('is-active', tab === t);
343
+ });
344
+ document.querySelectorAll(scope + ' [data-op-tab-panel]').forEach(function (panel) {
345
+ panel.classList.toggle('is-active', panel.getAttribute('data-op-tab-panel') === name);
346
+ });
347
+ return;
348
+ }
349
+
350
+ // Dropdown
351
+ t = e.target.closest('[data-op-dropdown-toggle]');
352
+ if (t) {
353
+ var dd = t.closest('.op-dropdown');
354
+ var wasOpen = dd.classList.contains('is-open');
355
+ document.querySelectorAll('.op-dropdown.is-open').forEach(function (d) { d.classList.remove('is-open'); });
356
+ if (!wasOpen) dd.classList.add('is-open');
357
+ return;
358
+ }
359
+ // Fecha dropdowns em clique fora
360
+ if (!e.target.closest('.op-dropdown')) {
361
+ document.querySelectorAll('.op-dropdown.is-open').forEach(function (d) { d.classList.remove('is-open'); });
362
+ }
363
+
364
+ // Alert dismiss
365
+ t = e.target.closest('[data-op-dismiss]');
366
+ if (t) {
367
+ var box = t.closest('.op-alert') || t.parentElement;
368
+ box.style.transition = 'opacity .25s, transform .25s';
369
+ box.style.opacity = '0';
370
+ box.style.transform = 'translateX(8px)';
371
+ setTimeout(function () { box.remove(); }, 250);
372
+ return;
373
+ }
374
+
375
+ // Copiar HTML
376
+ t = e.target.closest('[data-op-copy]');
377
+ if (t) { OptagoUI.copyHTML(t.getAttribute('data-op-copy'), t); return; }
378
+
379
+ // Ver código
380
+ t = e.target.closest('[data-op-code]');
381
+ if (t) { OptagoUI.toggleCode(t.getAttribute('data-op-code'), t); return; }
382
+ });
383
+
384
+ // Tooltip da sidebar colapsada: o overflow-y da nav cortaria um tooltip
385
+ // com position:absolute, então ele usa position:fixed e o top/right é
386
+ // calculado aqui a cada hover.
387
+ document.addEventListener('mouseover', function (e) {
388
+ var item = e.target.closest('.op-sidebar.is-collapsed .op-nav-item');
389
+ if (!item) return;
390
+ var tip = item.querySelector('.op-nav-tooltip');
391
+ if (!tip) return;
392
+ var r = item.getBoundingClientRect();
393
+ tip.style.top = (r.top + r.height / 2) + 'px';
394
+ tip.style.right = (window.innerWidth - r.left + 14) + 'px';
395
+ tip.style.left = 'auto';
396
+ });
397
+
398
+ // ESC fecha modais e dropdowns
399
+ document.addEventListener('keydown', function (e) {
400
+ if (e.key !== 'Escape') return;
401
+ document.querySelectorAll('.op-modal.is-open').forEach(function (m) { OptagoUI.closeModal(m); });
402
+ document.querySelectorAll('.op-dropdown.is-open').forEach(function (d) { d.classList.remove('is-open'); });
403
+ OptagoUI.closeSidebarMobile();
404
+ });
405
+
406
+ /* --------------------------------------------------------------------------
407
+ 8. INICIALIZAÇÃO no DOM pronto
408
+ -------------------------------------------------------------------------- */
409
+ function init() {
410
+ // Restaura estado da sidebar (desktop)
411
+ var sb = getSidebar();
412
+ if (sb && window.innerWidth >= 1024 && localStorage.getItem(SIDEBAR_KEY) === '1') {
413
+ sb.classList.add('is-collapsed');
414
+ }
415
+
416
+ // Ajusta ícones do botão de tema
417
+ OptagoUI.setTheme(document.documentElement.classList.contains('light') ? 'light' : 'dark');
418
+
419
+ // Scrollspy simples: marca .op-nav-item ativo conforme a seção visível
420
+ var main = document.querySelector('.op-main');
421
+ var links = Array.prototype.slice.call(document.querySelectorAll('.op-sidebar .op-nav-item[href^="#"]'));
422
+ if (main && links.length) {
423
+ var sections = links
424
+ .map(function (l) { return document.querySelector(l.getAttribute('href')); })
425
+ .filter(Boolean);
426
+ var onScroll = function () {
427
+ var top = main.scrollTop + 120;
428
+ var active = sections[0];
429
+ sections.forEach(function (s) { if (s.offsetTop <= top) active = s; });
430
+ links.forEach(function (l) {
431
+ l.classList.toggle('is-active', active && l.getAttribute('href') === '#' + active.id);
432
+ });
433
+ };
434
+ main.addEventListener('scroll', onScroll, { passive: true });
435
+ onScroll();
436
+ }
437
+ }
438
+
439
+ if (document.readyState === 'loading') {
440
+ document.addEventListener('DOMContentLoaded', init);
441
+ } else {
442
+ init();
443
+ }
444
+
445
+ window.OptagoUI = OptagoUI;
446
+ })();