@lancar/lxui 1.0.0 → 1.0.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 (90) hide show
  1. package/CHANGELOG.md +222 -55
  2. package/LICENSE +1 -1
  3. package/README.md +224 -68
  4. package/fonts/accessibility/accessibility.css +34 -0
  5. package/fonts/accessibility/accessibility.min.css +2 -0
  6. package/fonts/accessibility/atkinson-hyperlegible/AtkinsonHyperlegible-400.woff2 +0 -0
  7. package/fonts/accessibility/atkinson-hyperlegible/AtkinsonHyperlegible-400Italic.woff2 +0 -0
  8. package/fonts/accessibility/atkinson-hyperlegible/AtkinsonHyperlegible-700.woff2 +0 -0
  9. package/fonts/accessibility/atkinson-hyperlegible/atkinson-hyperlegible.css +30 -0
  10. package/fonts/accessibility/atkinson-hyperlegible/atkinson-hyperlegible.min.css +2 -0
  11. package/fonts/fonts.css +357 -0
  12. package/fonts/fonts.min.css +2 -0
  13. package/fonts/mono/fira-code/FiraCode-400.woff2 +0 -0
  14. package/fonts/mono/fira-code/FiraCode-500.woff2 +0 -0
  15. package/fonts/mono/fira-code/FiraCode-700.woff2 +0 -0
  16. package/fonts/mono/fira-code/fira-code.css +30 -0
  17. package/fonts/mono/fira-code/fira-code.min.css +2 -0
  18. package/fonts/mono/jetbrains-mono/JetBrainsMono-400.woff2 +0 -0
  19. package/fonts/mono/jetbrains-mono/JetBrainsMono-500.woff2 +0 -0
  20. package/fonts/mono/jetbrains-mono/JetBrainsMono-700.woff2 +0 -0
  21. package/fonts/mono/jetbrains-mono/jetbrains-mono.css +30 -0
  22. package/fonts/mono/jetbrains-mono/jetbrains-mono.min.css +2 -0
  23. package/fonts/mono/mono.css +61 -0
  24. package/fonts/mono/mono.min.css +2 -0
  25. package/fonts/sans/ibm-plex-sans/IBMPlexSans-400.woff2 +0 -0
  26. package/fonts/sans/ibm-plex-sans/IBMPlexSans-400Italic.woff2 +0 -0
  27. package/fonts/sans/ibm-plex-sans/IBMPlexSans-500.woff2 +0 -0
  28. package/fonts/sans/ibm-plex-sans/IBMPlexSans-600.woff2 +0 -0
  29. package/fonts/sans/ibm-plex-sans/IBMPlexSans-700.woff2 +0 -0
  30. package/fonts/sans/ibm-plex-sans/ibm-plex-sans.css +48 -0
  31. package/fonts/sans/ibm-plex-sans/ibm-plex-sans.min.css +2 -0
  32. package/fonts/sans/inter/Inter-400.woff2 +0 -0
  33. package/fonts/sans/inter/Inter-500.woff2 +0 -0
  34. package/fonts/sans/inter/Inter-600.woff2 +0 -0
  35. package/fonts/sans/inter/Inter-700.woff2 +0 -0
  36. package/fonts/sans/inter/inter.css +39 -0
  37. package/fonts/sans/inter/inter.min.css +2 -0
  38. package/fonts/sans/manrope/Manrope-400.woff2 +0 -0
  39. package/fonts/sans/manrope/Manrope-500.woff2 +0 -0
  40. package/fonts/sans/manrope/Manrope-600.woff2 +0 -0
  41. package/fonts/sans/manrope/Manrope-700.woff2 +0 -0
  42. package/fonts/sans/manrope/manrope.css +39 -0
  43. package/fonts/sans/manrope/manrope.min.css +2 -0
  44. package/fonts/sans/plus-jakarta-sans/PlusJakartaSans-400.woff2 +0 -0
  45. package/fonts/sans/plus-jakarta-sans/PlusJakartaSans-400Italic.woff2 +0 -0
  46. package/fonts/sans/plus-jakarta-sans/PlusJakartaSans-500.woff2 +0 -0
  47. package/fonts/sans/plus-jakarta-sans/PlusJakartaSans-600.woff2 +0 -0
  48. package/fonts/sans/plus-jakarta-sans/PlusJakartaSans-700.woff2 +0 -0
  49. package/fonts/sans/plus-jakarta-sans/plus-jakarta-sans.css +48 -0
  50. package/fonts/sans/plus-jakarta-sans/plus-jakarta-sans.min.css +2 -0
  51. package/fonts/sans/sans.css +169 -0
  52. package/fonts/sans/sans.min.css +2 -0
  53. package/fonts/serif/literata/Literata-400.woff2 +0 -0
  54. package/fonts/serif/literata/Literata-400Italic.woff2 +0 -0
  55. package/fonts/serif/literata/Literata-600.woff2 +0 -0
  56. package/fonts/serif/literata/Literata-700.woff2 +0 -0
  57. package/fonts/serif/literata/literata.css +39 -0
  58. package/fonts/serif/literata/literata.min.css +2 -0
  59. package/fonts/serif/merriweather/Merriweather-400.woff2 +0 -0
  60. package/fonts/serif/merriweather/Merriweather-400Italic.woff2 +0 -0
  61. package/fonts/serif/merriweather/Merriweather-700.woff2 +0 -0
  62. package/fonts/serif/merriweather/merriweather.css +30 -0
  63. package/fonts/serif/merriweather/merriweather.min.css +2 -0
  64. package/fonts/serif/serif.css +106 -0
  65. package/fonts/serif/serif.min.css +2 -0
  66. package/fonts/serif/source-serif-4/SourceSerif4-400.woff2 +0 -0
  67. package/fonts/serif/source-serif-4/SourceSerif4-400Italic.woff2 +0 -0
  68. package/fonts/serif/source-serif-4/SourceSerif4-600.woff2 +0 -0
  69. package/fonts/serif/source-serif-4/SourceSerif4-700.woff2 +0 -0
  70. package/fonts/serif/source-serif-4/source-serif-4.css +39 -0
  71. package/fonts/serif/source-serif-4/source-serif-4.min.css +2 -0
  72. package/lx-grid.min.css +2 -2
  73. package/lx-utilities.min.css +2 -2
  74. package/lxeditor.min.css +2 -2
  75. package/lxfonts.min.css +2 -2
  76. package/lxicons.min.css +2 -2
  77. package/lxthemes.min.css +2 -2
  78. package/lxui.bundle.min.js +5 -5
  79. package/lxui.esm.min.js +3 -3
  80. package/lxui.js +3 -3
  81. package/lxui.min.css +1 -1
  82. package/lxui.min.js +3 -3
  83. package/lxui.rtl.min.css +1 -1
  84. package/package.json +4 -9
  85. package/types/index.d.ts +1 -1
  86. package/lxui.bundle.js +0 -540
  87. package/lxui.css +0 -2163
  88. package/lxui.esm.js +0 -669
  89. package/lxui.rtl.css +0 -2466
  90. package/marked.min.js +0 -69
package/lxui.bundle.js DELETED
@@ -1,540 +0,0 @@
1
- /*!
2
- * LxUI v1.0.0 Bundle — Modern CSS Framework
3
- * Includes: LxUI core + all components
4
- * https://ui.lancar.id | https://cdn.lancar.id/ui/lxui.bundle.js
5
- * Copyright (c) 2025 lancar.id — MIT License
6
- */
7
- /*!
8
- * LxUI v1.0.0 — Modern CSS Framework
9
- * https://ui.lancar.id | https://cdn.lancar.id/ui/lxui.js
10
- * Copyright (c) 2025 lancar.id
11
- * Licensed under MIT
12
- */
13
- (function (global, factory) {
14
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
15
- typeof define === 'function' && define.amd ? define(factory) :
16
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.LxUI = factory());
17
- })(this, function () {
18
- 'use strict';
19
-
20
- /* ── Helpers ── */
21
- function qs(sel, ctx) { return (ctx || document).querySelector(sel); }
22
- function qsa(sel, ctx) { return Array.from((ctx || document).querySelectorAll(sel)); }
23
- function on(el, ev, fn) { el && el.addEventListener(ev, fn); }
24
- function off(el, ev, fn) { el && el.removeEventListener(ev, fn); }
25
- function emit(el, name, detail) {
26
- el.dispatchEvent(new CustomEvent('lx.' + name, { bubbles: true, cancelable: true, detail }));
27
- }
28
- function trap(el) {
29
- const focusable = qsa('button,a,[href],[tabindex]:not([tabindex="-1"]),input,select,textarea', el)
30
- .filter(e => !e.disabled && e.offsetParent !== null);
31
- if (!focusable.length) return;
32
- const first = focusable[0], last = focusable[focusable.length - 1];
33
- on(el, 'keydown', e => {
34
- if (e.key !== 'Tab') return;
35
- if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } }
36
- else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } }
37
- });
38
- }
39
-
40
- /* ================================================================
41
- * Modal
42
- * ================================================================ */
43
- class Modal {
44
- constructor(sel, opts = {}) {
45
- this.el = typeof sel === 'string' ? qs(sel) : sel;
46
- if (!this.el) return;
47
- this.opts = Object.assign({ backdrop: true, keyboard: true, focus: true }, opts);
48
- this._backdrop = null;
49
- this._prevFocus = null;
50
- }
51
- show() {
52
- if (!this.el) return;
53
- emit(this.el, 'modal.show');
54
- this._prevFocus = document.activeElement;
55
- this._backdrop = document.createElement('div');
56
- this._backdrop.className = 'lx-modal-backdrop lx-fade lx-show';
57
- document.body.appendChild(this._backdrop);
58
- document.body.classList.add('lx-modal-open');
59
- this.el.classList.add('lx-show');
60
- this.el.style.display = 'flex';
61
- this.el.removeAttribute('aria-hidden');
62
- if (this.opts.focus) trap(this.el);
63
- if (this.opts.keyboard) {
64
- this._keyHandler = e => { if (e.key === 'Escape') this.hide(); };
65
- on(document, 'keydown', this._keyHandler);
66
- }
67
- if (this.opts.backdrop) on(this._backdrop, 'click', () => this.hide());
68
- requestAnimationFrame(() => emit(this.el, 'modal.shown'));
69
- }
70
- hide() {
71
- if (!this.el) return;
72
- emit(this.el, 'modal.hide');
73
- this.el.classList.remove('lx-show');
74
- this.el.style.display = '';
75
- this.el.setAttribute('aria-hidden', 'true');
76
- if (this._backdrop) { this._backdrop.remove(); this._backdrop = null; }
77
- document.body.classList.remove('lx-modal-open');
78
- if (this._keyHandler) off(document, 'keydown', this._keyHandler);
79
- if (this._prevFocus) this._prevFocus.focus();
80
- emit(this.el, 'modal.hidden');
81
- }
82
- toggle() { this.el.classList.contains('lx-show') ? this.hide() : this.show(); }
83
- }
84
-
85
- /* ================================================================
86
- * Toast
87
- * ================================================================ */
88
- const Toast = {
89
- _container(pos) {
90
- const id = 'lx-toast-' + pos;
91
- let c = document.getElementById(id);
92
- if (!c) {
93
- c = document.createElement('div');
94
- c.id = id;
95
- const posMap = {
96
- 'top-right':'top:1rem;right:1rem', 'top-left':'top:1rem;left:1rem',
97
- 'bottom-right':'bottom:1rem;right:1rem', 'bottom-left':'bottom:1rem;left:1rem',
98
- 'top-center':'top:1rem;left:50%;transform:translateX(-50%)'
99
- };
100
- c.setAttribute('style', `position:fixed;z-index:1090;display:flex;flex-direction:column;gap:.5rem;${posMap[pos]||posMap['top-right']}`);
101
- document.body.appendChild(c);
102
- }
103
- return c;
104
- },
105
- show(opts = {}) {
106
- const { message = '', type = 'default', duration = 4000, position = 'top-right', title = '' } = opts;
107
- const colors = { success:'#22c55e', warning:'#f59e0b', danger:'#ef4444', info:'#06b6d4', default:'#3b82f6' };
108
- const t = document.createElement('div');
109
- t.setAttribute('role','alert');
110
- t.setAttribute('aria-live','assertive');
111
- t.setAttribute('aria-atomic','true');
112
- t.style.cssText = `background:var(--surface,#fff);border:1px solid var(--border-default,#e5e7eb);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:.875rem 1rem;min-width:280px;max-width:380px;display:flex;align-items:flex-start;gap:.75rem;animation:lxSlideIn .2s ease`;
113
- const dot = `<span style="width:8px;height:8px;border-radius:50%;background:${colors[type]||colors.default};flex-shrink:0;margin-top:4px"></span>`;
114
- const body = title ? `<div><strong style="display:block;margin-bottom:2px;font-size:.875rem">${title}</strong><span style="font-size:.875rem;color:var(--text-secondary,#6b7280)">${message}</span></div>` : `<span style="font-size:.875rem;color:var(--text-primary,#111827);flex:1">${message}</span>`;
115
- const close = `<button onclick="this.closest('[role=alert]').remove()" style="background:none;border:none;cursor:pointer;padding:0;color:var(--text-muted,#9ca3af);font-size:1.125rem;line-height:1;margin-left:auto" aria-label="Tutup">×</button>`;
116
- t.innerHTML = dot + body + close;
117
- this._container(position).appendChild(t);
118
- if (!document.getElementById('lx-toast-style')) {
119
- const s = document.createElement('style');
120
- s.id = 'lx-toast-style';
121
- s.textContent = '@keyframes lxSlideIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}';
122
- document.head.appendChild(s);
123
- }
124
- if (duration > 0) setTimeout(() => { t.style.opacity='0'; t.style.transition='opacity .2s'; setTimeout(()=>t.remove(), 200); }, duration);
125
- },
126
- success(msg, opts={}) { this.show({...opts, message:msg, type:'success'}); },
127
- error(msg, opts={}) { this.show({...opts, message:msg, type:'danger'}); },
128
- warning(msg, opts={}) { this.show({...opts, message:msg, type:'warning'}); },
129
- info(msg, opts={}) { this.show({...opts, message:msg, type:'info'}); },
130
- };
131
-
132
- /* ================================================================
133
- * Collapse / Accordion
134
- * ================================================================ */
135
- class Collapse {
136
- constructor(sel) {
137
- this.el = typeof sel === 'string' ? qs(sel) : sel;
138
- }
139
- show() {
140
- if (!this.el) return;
141
- emit(this.el, 'collapse.show');
142
- this.el.style.height = '0px';
143
- this.el.classList.add('lx-collapsing');
144
- this.el.classList.remove('lx-collapse', 'lx-show');
145
- const h = this.el.scrollHeight;
146
- this.el.style.height = h + 'px';
147
- this.el.addEventListener('transitionend', () => {
148
- this.el.classList.remove('lx-collapsing');
149
- this.el.classList.add('lx-collapse', 'lx-show');
150
- this.el.style.height = '';
151
- emit(this.el, 'collapse.shown');
152
- }, { once: true });
153
- }
154
- hide() {
155
- if (!this.el) return;
156
- emit(this.el, 'collapse.hide');
157
- this.el.style.height = this.el.scrollHeight + 'px';
158
- this.el.classList.add('lx-collapsing');
159
- this.el.classList.remove('lx-collapse', 'lx-show');
160
- requestAnimationFrame(() => { this.el.style.height = '0px'; });
161
- this.el.addEventListener('transitionend', () => {
162
- this.el.classList.remove('lx-collapsing');
163
- this.el.classList.add('lx-collapse');
164
- this.el.style.height = '';
165
- emit(this.el, 'collapse.hidden');
166
- }, { once: true });
167
- }
168
- toggle() { this.el && this.el.classList.contains('lx-show') ? this.hide() : this.show(); }
169
- }
170
-
171
- /* ================================================================
172
- * Dropdown
173
- * ================================================================ */
174
- class Dropdown {
175
- constructor(trigger) {
176
- this.trigger = typeof trigger === 'string' ? qs(trigger) : trigger;
177
- this.menu = this.trigger && this.trigger.nextElementSibling;
178
- this._outside = e => { if (!this.trigger.closest('.lx-dropdown').contains(e.target)) this.hide(); };
179
- }
180
- show() {
181
- if (!this.menu) return;
182
- emit(this.trigger, 'dropdown.show');
183
- this.menu.classList.add('lx-show');
184
- this.trigger.setAttribute('aria-expanded', 'true');
185
- setTimeout(() => on(document, 'click', this._outside), 0);
186
- emit(this.trigger, 'dropdown.shown');
187
- }
188
- hide() {
189
- if (!this.menu) return;
190
- emit(this.trigger, 'dropdown.hide');
191
- this.menu.classList.remove('lx-show');
192
- this.trigger.setAttribute('aria-expanded', 'false');
193
- off(document, 'click', this._outside);
194
- emit(this.trigger, 'dropdown.hidden');
195
- }
196
- toggle() { this.menu && this.menu.classList.contains('lx-show') ? this.hide() : this.show(); }
197
- }
198
-
199
- /* ================================================================
200
- * Offcanvas / Drawer
201
- * ================================================================ */
202
- class Offcanvas {
203
- constructor(sel) {
204
- this.el = typeof sel === 'string' ? qs(sel) : sel;
205
- this._backdrop = null;
206
- }
207
- show() {
208
- if (!this.el) return;
209
- this._backdrop = document.createElement('div');
210
- this._backdrop.className = 'lx-offcanvas-backdrop';
211
- this._backdrop.style.cssText = 'position:fixed;inset:0;z-index:1039;background:rgba(0,0,0,.5)';
212
- document.body.appendChild(this._backdrop);
213
- document.body.style.overflow = 'hidden';
214
- this.el.classList.add('lx-show');
215
- this.el.removeAttribute('aria-hidden');
216
- trap(this.el);
217
- on(this._backdrop, 'click', () => this.hide());
218
- on(document, 'keydown', this._esc = e => { if (e.key === 'Escape') this.hide(); });
219
- }
220
- hide() {
221
- if (!this.el) return;
222
- this.el.classList.remove('lx-show');
223
- this.el.setAttribute('aria-hidden', 'true');
224
- if (this._backdrop) { this._backdrop.remove(); this._backdrop = null; }
225
- document.body.style.overflow = '';
226
- off(document, 'keydown', this._esc);
227
- }
228
- toggle() { this.el && this.el.classList.contains('lx-show') ? this.hide() : this.show(); }
229
- }
230
-
231
- /* ================================================================
232
- * Tooltip
233
- * ================================================================ */
234
- class Tooltip {
235
- constructor(el, opts = {}) {
236
- this.el = typeof el === 'string' ? qs(el) : el;
237
- if (!this.el) return;
238
- this.opts = Object.assign({ placement: 'top', trigger: 'hover', html: false, delay: { show: 0, hide: 0 } }, opts);
239
- this._tip = null;
240
- this._attach();
241
- }
242
- _attach() {
243
- const show = () => { clearTimeout(this._hideTimer); this._showTimer = setTimeout(() => this._show(), this.opts.delay.show); };
244
- const hide = () => { clearTimeout(this._showTimer); this._hideTimer = setTimeout(() => this._hide(), this.opts.delay.hide); };
245
- if (this.opts.trigger.includes('hover')) { on(this.el, 'mouseenter', show); on(this.el, 'mouseleave', hide); }
246
- if (this.opts.trigger.includes('focus')) { on(this.el, 'focusin', show); on(this.el, 'focusout', hide); }
247
- }
248
- _show() {
249
- const text = this.el.getAttribute('title') || this.el.dataset.lxOrigTitle || '';
250
- if (!text) return;
251
- if (this.el.getAttribute('title')) { this.el.dataset.lxOrigTitle = text; this.el.removeAttribute('title'); }
252
- this._tip = document.createElement('div');
253
- this._tip.className = 'lx-tooltip lx-tooltip-' + this.opts.placement;
254
- this._tip.setAttribute('role','tooltip');
255
- this._tip.style.cssText = 'position:absolute;z-index:1080;padding:4px 8px;background:rgba(0,0,0,.85);color:#fff;border-radius:4px;font-size:.75rem;white-space:nowrap;pointer-events:none';
256
- this._tip.textContent = text;
257
- document.body.appendChild(this._tip);
258
- const r = this.el.getBoundingClientRect(), t = this._tip.getBoundingClientRect(), s = window.scrollY;
259
- const p = this.opts.placement;
260
- let top = 0, left = 0;
261
- if (p === 'top') { top = r.top + s - t.height - 6; left = r.left + (r.width - t.width) / 2; }
262
- else if (p === 'bottom') { top = r.bottom + s + 6; left = r.left + (r.width - t.width) / 2; }
263
- else if (p === 'left') { top = r.top + s + (r.height - t.height) / 2; left = r.left - t.width - 6; }
264
- else { top = r.top + s + (r.height - t.height) / 2; left = r.right + 6; }
265
- this._tip.style.top = top + 'px';
266
- this._tip.style.left = left + 'px';
267
- }
268
- _hide() { if (this._tip) { this._tip.remove(); this._tip = null; } }
269
- }
270
-
271
- /* ================================================================
272
- * Popover
273
- * ================================================================ */
274
- class Popover {
275
- constructor(el, opts = {}) {
276
- this.el = typeof el === 'string' ? qs(el) : el;
277
- if (!this.el) return;
278
- this.opts = Object.assign({ placement: 'top', trigger: 'click', html: false }, opts);
279
- this._pop = null;
280
- const toggle = () => this._pop ? this._hide() : this._show();
281
- if (this.opts.trigger.includes('click')) on(this.el, 'click', toggle);
282
- }
283
- _show() {
284
- const title = this.el.dataset.lxTitle || '';
285
- const content = this.el.dataset.lxContent || '';
286
- this._pop = document.createElement('div');
287
- this._pop.className = 'lx-popover';
288
- this._pop.setAttribute('role','tooltip');
289
- this._pop.style.cssText = 'position:absolute;z-index:1070;background:var(--surface,#fff);border:1px solid var(--border-default,#e5e7eb);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:.75rem 1rem;min-width:200px;max-width:300px';
290
- this._pop.innerHTML = (title ? `<strong style="display:block;margin-bottom:.5rem;font-size:.875rem">${title}</strong>` : '') + `<p style="margin:0;font-size:.875rem;color:var(--text-secondary,#6b7280)">${content}</p>`;
291
- document.body.appendChild(this._pop);
292
- const r = this.el.getBoundingClientRect(), s = window.scrollY;
293
- this._pop.style.top = (r.bottom + s + 8) + 'px';
294
- this._pop.style.left = r.left + 'px';
295
- setTimeout(() => on(document, 'click', this._outside = e => { if (!this.el.contains(e.target) && !this._pop?.contains(e.target)) this._hide(); }), 0);
296
- }
297
- _hide() { if (this._pop) { this._pop.remove(); this._pop = null; off(document, 'click', this._outside); } }
298
- }
299
-
300
- /* ================================================================
301
- * Scrollspy
302
- * ================================================================ */
303
- class Scrollspy {
304
- constructor(el, opts = {}) {
305
- this.el = typeof el === 'string' ? qs(el) : el;
306
- if (!this.el) return;
307
- this.opts = Object.assign({ offset: 70 }, opts);
308
- const targetSel = this.el.dataset.lxTarget;
309
- this.nav = targetSel ? qs(targetSel) : null;
310
- this._update = this._update.bind(this);
311
- on(this.el === document.body ? window : this.el, 'scroll', this._update);
312
- this._update();
313
- }
314
- _update() {
315
- if (!this.nav) return;
316
- const links = qsa('a[href^="#"]', this.nav);
317
- const scrollTop = (this.el === document.body ? window.scrollY : this.el.scrollTop) + this.opts.offset;
318
- let active = null;
319
- links.forEach(a => {
320
- const target = qs(a.getAttribute('href'));
321
- if (target && target.offsetTop <= scrollTop) active = a;
322
- });
323
- links.forEach(a => a.classList.remove('lx-active'));
324
- if (active) active.classList.add('lx-active');
325
- }
326
- }
327
-
328
- /* ================================================================
329
- * Tabs
330
- * ================================================================ */
331
- class Tab {
332
- constructor(trigger) {
333
- this.trigger = typeof trigger === 'string' ? qs(trigger) : trigger;
334
- }
335
- show() {
336
- if (!this.trigger) return;
337
- const targetSel = this.trigger.dataset.lxTarget;
338
- const target = targetSel ? qs(targetSel) : null;
339
- if (!target) return;
340
- const tablist = this.trigger.closest('[role="tablist"]') || this.trigger.parentElement.parentElement;
341
- qsa('.lx-nav-link, .lx-tab-item', tablist).forEach(t => { t.classList.remove('lx-active'); t.setAttribute('aria-selected','false'); });
342
- qsa('.lx-tab-pane', target.parentElement).forEach(p => p.classList.remove('lx-active', 'lx-show'));
343
- this.trigger.classList.add('lx-active');
344
- this.trigger.setAttribute('aria-selected','true');
345
- target.classList.add('lx-active', 'lx-show');
346
- }
347
- }
348
-
349
- /* ================================================================
350
- * Carousel
351
- * ================================================================ */
352
- class Carousel {
353
- constructor(sel, opts = {}) {
354
- this.el = typeof sel === 'string' ? qs(sel) : sel;
355
- if (!this.el) return;
356
- this.opts = Object.assign({ interval: 5000, pause: 'hover', wrap: true, touch: true }, opts);
357
- this.items = qsa('.lx-carousel-item', this.el);
358
- this.current = this.items.findIndex(i => i.classList.contains('lx-active'));
359
- if (this.current < 0) { this.current = 0; this.items[0]?.classList.add('lx-active'); }
360
- this._timer = null;
361
- if (this.opts.interval) this.cycle();
362
- if (this.opts.pause === 'hover') { on(this.el, 'mouseenter', () => this.pause()); on(this.el, 'mouseleave', () => this.cycle()); }
363
- if (this.opts.touch) this._initTouch();
364
- this._updateIndicators();
365
- }
366
- _go(idx) {
367
- const n = this.items.length;
368
- if (!this.opts.wrap && (idx < 0 || idx >= n)) return;
369
- this.current = ((idx % n) + n) % n;
370
- this.items.forEach((item, i) => item.classList.toggle('lx-active', i === this.current));
371
- this._updateIndicators();
372
- }
373
- _updateIndicators() {
374
- qsa('.lx-carousel-indicators button, .lx-carousel-indicators [data-lx-slide-to]', this.el).forEach((btn, i) => {
375
- btn.classList.toggle('lx-active', i === this.current);
376
- btn.setAttribute('aria-current', i === this.current ? 'true' : 'false');
377
- });
378
- }
379
- next() { this._go(this.current + 1); }
380
- prev() { this._go(this.current - 1); }
381
- to(idx) { this._go(idx); }
382
- cycle() { this.pause(); if (this.opts.interval) this._timer = setInterval(() => this.next(), this.opts.interval); }
383
- pause() { clearInterval(this._timer); this._timer = null; }
384
- _initTouch() {
385
- let startX = 0;
386
- on(this.el, 'touchstart', e => { startX = e.touches[0].clientX; }, { passive: true });
387
- on(this.el, 'touchend', e => {
388
- const dx = e.changedTouches[0].clientX - startX;
389
- if (Math.abs(dx) > 40) dx > 0 ? this.prev() : this.next();
390
- });
391
- }
392
- }
393
-
394
- /* ================================================================
395
- * Alert dismiss
396
- * ================================================================ */
397
- function initAlerts() {
398
- on(document, 'click', e => {
399
- const btn = e.target.closest('[data-lx-dismiss="alert"]');
400
- if (btn) { const alert = btn.closest('.lx-alert'); if (alert) { alert.style.opacity = '0'; alert.style.transition = 'opacity .15s'; setTimeout(() => alert.remove(), 150); } }
401
- });
402
- }
403
-
404
- /* ================================================================
405
- * Breakpoint utilities
406
- * ================================================================ */
407
- const breakpoint = {
408
- _bp: { xs: 0, sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536 },
409
- current() {
410
- const w = window.innerWidth;
411
- return Object.entries(this._bp).reverse().find(([, v]) => w >= v)?.[0] || 'xs';
412
- },
413
- on(bp, fn) {
414
- const mq = window.matchMedia(`(min-width: ${this._bp[bp]}px)`);
415
- mq.addEventListener('change', e => { if (e.matches) fn(); });
416
- if (mq.matches) fn();
417
- }
418
- };
419
-
420
- /* ================================================================
421
- * Theme switcher
422
- * ================================================================ */
423
- const theme = {
424
- set(t, mode) {
425
- document.documentElement.dataset.theme = t;
426
- if (mode) document.documentElement.dataset.mode = mode;
427
- },
428
- setMode(mode) { document.documentElement.dataset.mode = mode; },
429
- toggle() {
430
- const cur = document.documentElement.dataset.mode || 'light';
431
- this.setMode(cur === 'dark' ? 'light' : 'dark');
432
- },
433
- current() { return { theme: document.documentElement.dataset.theme, mode: document.documentElement.dataset.mode }; },
434
- save(t, mode) { this.set(t, mode); localStorage.setItem('lx-theme', t); if (mode) localStorage.setItem('lx-mode', mode); },
435
- restore() {
436
- const t = localStorage.getItem('lx-theme'); const m = localStorage.getItem('lx-mode');
437
- if (t) this.set(t, m || undefined);
438
- }
439
- };
440
-
441
- /* ================================================================
442
- * Data-attribute bridge (no-JS approach)
443
- * ================================================================ */
444
- function initDataBridge() {
445
- on(document, 'click', e => {
446
- const el = e.target.closest('[data-lx-toggle], [data-lx-dismiss], [data-lx-slide], [data-lx-slide-to]');
447
- if (!el) return;
448
- const toggle = el.dataset.lxToggle;
449
- const dismiss = el.dataset.lxDismiss;
450
- const target = el.dataset.lxTarget ? qs(el.dataset.lxTarget) : null;
451
-
452
- if (dismiss === 'modal') { const m = el.closest('.lx-modal'); if (m) new Modal(m).hide(); }
453
- else if (dismiss === 'offcanvas') { const o = el.closest('.lx-offcanvas'); if (o) new Offcanvas(o).hide(); }
454
- else if (dismiss === 'alert') { const a = el.closest('.lx-alert'); if (a) { a.style.opacity='0'; a.style.transition='opacity .15s'; setTimeout(()=>a.remove(),150); } }
455
- else if (dismiss === 'toast') { const t = el.closest('.lx-toast'); if (t) { t.style.opacity='0'; t.style.transition='opacity .15s'; setTimeout(()=>t.remove(),150); } }
456
-
457
- if (toggle === 'modal' && target) { e.preventDefault(); new Modal(target).toggle(); }
458
- else if (toggle === 'collapse' && target) { e.preventDefault(); new Collapse(target).toggle(); const expanded = target.classList.contains('lx-show'); el.setAttribute('aria-expanded', (!expanded).toString()); }
459
- else if (toggle === 'accordion' && target) {
460
- e.preventDefault();
461
- const isOpen = target.classList.contains('lx-show');
462
- const accordion = el.closest('.lx-accordion');
463
- if (accordion) {
464
- qsa('.lx-accordion-body.lx-show', accordion).forEach(b => { if (b !== target) new Collapse(b).hide(); });
465
- qsa('.lx-accordion-trigger', accordion).forEach(t => { if (t !== el) { t.classList.remove('lx-active'); t.setAttribute('aria-expanded','false'); } });
466
- }
467
- new Collapse(target)[isOpen ? 'hide' : 'show']();
468
- el.classList.toggle('lx-active', !isOpen);
469
- el.setAttribute('aria-expanded', (!isOpen).toString());
470
- }
471
- else if (toggle === 'dropdown') { e.preventDefault(); e.stopPropagation(); new Dropdown(el).toggle(); }
472
- else if (toggle === 'offcanvas' && target) { e.preventDefault(); new Offcanvas(target).toggle(); }
473
- else if (toggle === 'tab' && target) { e.preventDefault(); new Tab(el).show(); }
474
- else if (toggle === 'popover') { /* handled by Popover instance */ }
475
-
476
- const slideTo = el.dataset.lxSlideTo;
477
- const slideDir = el.dataset.lxSlide;
478
- if (slideTo !== undefined || slideDir) {
479
- const carouselEl = qs(el.dataset.lxTarget) || el.closest('.lx-carousel');
480
- if (carouselEl && carouselEl._lxCarousel) {
481
- if (slideDir === 'next') carouselEl._lxCarousel.next();
482
- else if (slideDir === 'prev') carouselEl._lxCarousel.prev();
483
- else if (slideTo !== undefined) carouselEl._lxCarousel.to(parseInt(slideTo));
484
- }
485
- }
486
- });
487
- }
488
-
489
- /* ================================================================
490
- * Scrollspy auto-init
491
- * ================================================================ */
492
- function initScrollspy() {
493
- qsa('[data-lx-spy="scroll"]').forEach(el => new Scrollspy(el));
494
- }
495
-
496
- /* ================================================================
497
- * Tooltip auto-init
498
- * ================================================================ */
499
- function initTooltips() {
500
- qsa('[data-lx-toggle="tooltip"]').forEach(el => new Tooltip(el, {
501
- placement: el.dataset.lxPlacement || 'top',
502
- trigger: 'hover focus',
503
- delay: { show: 100, hide: 0 }
504
- }));
505
- }
506
-
507
- /* ================================================================
508
- * Carousel auto-init
509
- * ================================================================ */
510
- function initCarousels() {
511
- qsa('.lx-carousel').forEach(el => {
512
- const c = new Carousel(el, { interval: parseInt(el.dataset.lxInterval) || 5000 });
513
- el._lxCarousel = c;
514
- });
515
- }
516
-
517
- /* ================================================================
518
- * LxUI.init() — initialize everything
519
- * ================================================================ */
520
- function init() {
521
- initDataBridge();
522
- initAlerts();
523
- initScrollspy();
524
- initTooltips();
525
- initCarousels();
526
- theme.restore();
527
- }
528
-
529
- if (document.readyState === 'loading') { on(document, 'DOMContentLoaded', init); }
530
- else { init(); }
531
-
532
- return {
533
- init,
534
- Modal, Toast, Collapse, Dropdown, Offcanvas, Tooltip, Popover, Tab, Carousel, Scrollspy,
535
- breakpoint, theme,
536
- LxToast: Toast,
537
- version: '1.0.0',
538
- homepage: 'https://ui.lancar.id'
539
- };
540
- });