juxscript 1.0.20 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -1
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/lib/components/tabs.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
|
|
3
|
+
// Event definitions
|
|
4
|
+
const TRIGGER_EVENTS = [] as const;
|
|
5
|
+
const CALLBACK_EVENTS = ['tabChange'] as const;
|
|
3
6
|
|
|
4
7
|
export interface Tab {
|
|
5
8
|
id: string;
|
|
@@ -24,37 +27,30 @@ type TabsState = {
|
|
|
24
27
|
class: string;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
export class Tabs {
|
|
28
|
-
state: TabsState;
|
|
29
|
-
container: HTMLElement | null = null;
|
|
30
|
-
_id: string;
|
|
31
|
-
id: string;
|
|
32
|
-
|
|
33
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
34
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
35
|
-
private _syncBindings: Array<{
|
|
36
|
-
property: string,
|
|
37
|
-
stateObj: State<any>,
|
|
38
|
-
toState?: Function,
|
|
39
|
-
toComponent?: Function
|
|
40
|
-
}> = [];
|
|
41
|
-
|
|
30
|
+
export class Tabs extends BaseComponent<TabsState> {
|
|
42
31
|
constructor(id: string, options: TabsOptions = {}) {
|
|
43
|
-
|
|
44
|
-
this.id = id;
|
|
45
|
-
|
|
46
|
-
this.state = {
|
|
32
|
+
super(id, {
|
|
47
33
|
tabs: options.tabs ?? [],
|
|
48
34
|
activeTab: options.activeTab ?? (options.tabs?.[0]?.id ?? ''),
|
|
49
35
|
variant: options.variant ?? 'default',
|
|
50
36
|
style: options.style ?? '',
|
|
51
37
|
class: options.class ?? ''
|
|
52
|
-
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected getTriggerEvents(): readonly string[] {
|
|
42
|
+
return TRIGGER_EVENTS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected getCallbackEvents(): readonly string[] {
|
|
46
|
+
return CALLBACK_EVENTS;
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
/*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
49
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
50
|
+
* FLUENT API
|
|
51
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
52
|
+
|
|
53
|
+
// ✅ Inherited from BaseComponent
|
|
58
54
|
|
|
59
55
|
tabs(value: Tab[]): this {
|
|
60
56
|
this.state.tabs = value;
|
|
@@ -77,64 +73,32 @@ export class Tabs {
|
|
|
77
73
|
return this;
|
|
78
74
|
}
|
|
79
75
|
|
|
80
|
-
style(value: string): this {
|
|
81
|
-
this.state.style = value;
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
class(value: string): this {
|
|
86
|
-
this.state.class = value;
|
|
87
|
-
return this;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
bind(event: string, handler: Function): this {
|
|
91
|
-
this._bindings.push({ event, handler });
|
|
92
|
-
return this;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
96
|
-
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
97
|
-
throw new Error(`Tabs.sync: Expected a State object for property "${property}"`);
|
|
98
|
-
}
|
|
99
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
76
|
private _updateActiveTab(): void {
|
|
104
77
|
if (!this.container) return;
|
|
105
78
|
const wrapper = this.container.querySelector(`#${this._id}`);
|
|
106
79
|
if (!wrapper) return;
|
|
107
80
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
btn.classList.toggle('active', btn.getAttribute('data-tab-id') === this.state.activeTab);
|
|
81
|
+
wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
82
|
+
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === this.state.activeTab);
|
|
111
83
|
});
|
|
112
84
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
panel.classList.toggle('active',
|
|
85
|
+
wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
86
|
+
const isActive = panel.getAttribute('data-tab') === this.state.activeTab;
|
|
87
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
88
|
+
(panel as HTMLElement).style.display = isActive ? 'block' : 'none';
|
|
116
89
|
});
|
|
117
90
|
}
|
|
118
91
|
|
|
92
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
93
|
+
* RENDER
|
|
94
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
95
|
+
|
|
119
96
|
render(targetId?: string): this {
|
|
120
|
-
|
|
121
|
-
let container: HTMLElement;
|
|
122
|
-
if (targetId) {
|
|
123
|
-
const target = document.querySelector(targetId);
|
|
124
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
125
|
-
throw new Error(`Tabs: Target "${targetId}" not found`);
|
|
126
|
-
}
|
|
127
|
-
container = target;
|
|
128
|
-
} else {
|
|
129
|
-
container = getOrCreateContainer(this._id);
|
|
130
|
-
}
|
|
131
|
-
this.container = container;
|
|
97
|
+
const container = this._setupContainer(targetId);
|
|
132
98
|
|
|
133
|
-
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
134
99
|
const { tabs, activeTab, variant, style, class: className } = this.state;
|
|
135
100
|
const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
|
|
136
101
|
|
|
137
|
-
// === 3. BUILD: Create DOM elements ===
|
|
138
102
|
const wrapper = document.createElement('div');
|
|
139
103
|
wrapper.className = `jux-tabs jux-tabs-${variant}`;
|
|
140
104
|
wrapper.id = this._id;
|
|
@@ -147,8 +111,7 @@ export class Tabs {
|
|
|
147
111
|
const tabPanels = document.createElement('div');
|
|
148
112
|
tabPanels.className = 'jux-tabs-panels';
|
|
149
113
|
|
|
150
|
-
tabs.forEach(
|
|
151
|
-
// Tab button
|
|
114
|
+
tabs.forEach(tab => {
|
|
152
115
|
const tabButton = document.createElement('button');
|
|
153
116
|
tabButton.className = 'jux-tabs-button';
|
|
154
117
|
tabButton.setAttribute('data-tab', tab.id);
|
|
@@ -156,7 +119,6 @@ export class Tabs {
|
|
|
156
119
|
tabButton.textContent = tab.label;
|
|
157
120
|
tabList.appendChild(tabButton);
|
|
158
121
|
|
|
159
|
-
// Tab panel
|
|
160
122
|
const tabPanel = document.createElement('div');
|
|
161
123
|
tabPanel.className = 'jux-tabs-panel';
|
|
162
124
|
tabPanel.setAttribute('data-tab', tab.id);
|
|
@@ -176,9 +138,6 @@ export class Tabs {
|
|
|
176
138
|
wrapper.appendChild(tabList);
|
|
177
139
|
wrapper.appendChild(tabPanels);
|
|
178
140
|
|
|
179
|
-
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
180
|
-
|
|
181
|
-
// Default tab switching behavior (only if NOT using sync)
|
|
182
141
|
if (!hasActiveTabSync) {
|
|
183
142
|
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
184
143
|
button.addEventListener('click', () => {
|
|
@@ -189,12 +148,8 @@ export class Tabs {
|
|
|
189
148
|
});
|
|
190
149
|
}
|
|
191
150
|
|
|
192
|
-
|
|
193
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
194
|
-
wrapper.addEventListener(event, handler as EventListener);
|
|
195
|
-
});
|
|
151
|
+
this._wireStandardEvents(wrapper);
|
|
196
152
|
|
|
197
|
-
// Wire sync bindings from .sync() calls
|
|
198
153
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
199
154
|
if (property === 'activeTab') {
|
|
200
155
|
const transformToState = toState || ((v: any) => String(v));
|
|
@@ -202,7 +157,6 @@ export class Tabs {
|
|
|
202
157
|
|
|
203
158
|
let isUpdating = false;
|
|
204
159
|
|
|
205
|
-
// State → Component
|
|
206
160
|
stateObj.subscribe((val: any) => {
|
|
207
161
|
if (isUpdating) return;
|
|
208
162
|
const transformed = transformToComponent(val);
|
|
@@ -210,7 +164,6 @@ export class Tabs {
|
|
|
210
164
|
this._switchTab(transformed, wrapper);
|
|
211
165
|
});
|
|
212
166
|
|
|
213
|
-
// Component → State (tab clicks)
|
|
214
167
|
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
215
168
|
button.addEventListener('click', () => {
|
|
216
169
|
if (isUpdating) return;
|
|
@@ -228,13 +181,12 @@ export class Tabs {
|
|
|
228
181
|
});
|
|
229
182
|
}
|
|
230
183
|
else if (property === 'tabs') {
|
|
231
|
-
const
|
|
184
|
+
const transform = toComponent || ((v: any) => v);
|
|
232
185
|
|
|
233
186
|
stateObj.subscribe((val: any) => {
|
|
234
|
-
const transformed =
|
|
187
|
+
const transformed = transform(val);
|
|
235
188
|
this.state.tabs = transformed;
|
|
236
189
|
|
|
237
|
-
// Re-render tabs
|
|
238
190
|
tabList.innerHTML = '';
|
|
239
191
|
tabPanels.innerHTML = '';
|
|
240
192
|
|
|
@@ -262,7 +214,6 @@ export class Tabs {
|
|
|
262
214
|
tabPanels.appendChild(tabPanel);
|
|
263
215
|
});
|
|
264
216
|
|
|
265
|
-
// Re-wire click handlers
|
|
266
217
|
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
267
218
|
button.addEventListener('click', () => {
|
|
268
219
|
const tabId = button.getAttribute('data-tab')!;
|
|
@@ -274,38 +225,23 @@ export class Tabs {
|
|
|
274
225
|
}
|
|
275
226
|
});
|
|
276
227
|
|
|
277
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
278
228
|
container.appendChild(wrapper);
|
|
279
229
|
return this;
|
|
280
230
|
}
|
|
281
231
|
|
|
282
232
|
private _switchTab(tabId: string, wrapper: HTMLElement): void {
|
|
283
|
-
// Update buttons
|
|
284
233
|
wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
285
|
-
|
|
286
|
-
btn.classList.add('jux-tabs-button-active');
|
|
287
|
-
} else {
|
|
288
|
-
btn.classList.remove('jux-tabs-button-active');
|
|
289
|
-
}
|
|
234
|
+
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
|
|
290
235
|
});
|
|
291
236
|
|
|
292
|
-
// Update panels
|
|
293
237
|
wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
} else {
|
|
298
|
-
panel.classList.remove('jux-tabs-panel-active');
|
|
299
|
-
(panel as HTMLElement).style.display = 'none';
|
|
300
|
-
}
|
|
238
|
+
const isActive = panel.getAttribute('data-tab') === tabId;
|
|
239
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
240
|
+
(panel as HTMLElement).style.display = isActive ? 'block' : 'none';
|
|
301
241
|
});
|
|
302
|
-
}
|
|
303
242
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
throw new Error('Tabs.renderTo: Invalid component');
|
|
307
|
-
}
|
|
308
|
-
return this.render(`#${juxComponent._id}`);
|
|
243
|
+
// 🎯 Fire the tabChange callback event
|
|
244
|
+
this._triggerCallback('tabChange', tabId);
|
|
309
245
|
}
|
|
310
246
|
}
|
|
311
247
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
2
|
import { ErrorHandler } from './error-handler.js';
|
|
3
3
|
import { renderIcon } from './icons.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Event definitions
|
|
6
|
+
const TRIGGER_EVENTS = [] as const;
|
|
7
|
+
const CALLBACK_EVENTS = ['themeChange'] as const;
|
|
8
|
+
|
|
8
9
|
export interface Theme {
|
|
9
10
|
id: string;
|
|
10
11
|
label: string;
|
|
11
12
|
icon?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* ThemeToggle component options
|
|
16
|
-
*/
|
|
17
15
|
export interface ThemeToggleOptions {
|
|
18
16
|
themes?: Theme[];
|
|
19
17
|
defaultTheme?: string;
|
|
@@ -24,9 +22,6 @@ export interface ThemeToggleOptions {
|
|
|
24
22
|
class?: string;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
/**
|
|
28
|
-
* ThemeToggle component state
|
|
29
|
-
*/
|
|
30
25
|
type ThemeToggleState = {
|
|
31
26
|
themes: Theme[];
|
|
32
27
|
currentTheme: string;
|
|
@@ -37,71 +32,48 @@ type ThemeToggleState = {
|
|
|
37
32
|
class: string;
|
|
38
33
|
};
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
* ThemeToggle component - Manage and switch between themes
|
|
42
|
-
*
|
|
43
|
-
* Usage:
|
|
44
|
-
* // Simple light/dark toggle
|
|
45
|
-
* jux.themeToggle('myToggle').render('#appheader-actions');
|
|
46
|
-
*
|
|
47
|
-
* // Custom themes
|
|
48
|
-
* jux.themeToggle('myToggle', {
|
|
49
|
-
* themes: [
|
|
50
|
-
* { id: 'light', label: 'Light', icon: '☀️' },
|
|
51
|
-
* { id: 'dark', label: 'Dark', icon: '🌙' },
|
|
52
|
-
* { id: 'auto', label: 'Auto', icon: '🌓' }
|
|
53
|
-
* ],
|
|
54
|
-
* variant: 'dropdown'
|
|
55
|
-
* }).render('#appheader-actions');
|
|
56
|
-
*/
|
|
57
|
-
export class ThemeToggle {
|
|
58
|
-
state: ThemeToggleState;
|
|
59
|
-
container: HTMLElement | null = null;
|
|
60
|
-
_id: string;
|
|
61
|
-
id: string;
|
|
62
|
-
|
|
35
|
+
export class ThemeToggle extends BaseComponent<ThemeToggleState> {
|
|
63
36
|
constructor(id: string, options: ThemeToggleOptions = {}) {
|
|
64
|
-
this._id = id;
|
|
65
|
-
this.id = id;
|
|
66
|
-
|
|
67
37
|
const defaultThemes: Theme[] = [
|
|
68
38
|
{ id: 'light', label: 'Light', icon: '☀️' },
|
|
69
39
|
{ id: 'dark', label: 'Dark', icon: '🌙' }
|
|
70
40
|
];
|
|
71
41
|
|
|
72
|
-
|
|
42
|
+
super(id, {
|
|
73
43
|
themes: options.themes ?? defaultThemes,
|
|
74
|
-
currentTheme: options.defaultTheme ??
|
|
44
|
+
currentTheme: options.defaultTheme ?? ThemeToggle.detectTheme(options.storageKey),
|
|
75
45
|
storageKey: options.storageKey ?? 'jux-theme',
|
|
76
46
|
showLabel: options.showLabel ?? false,
|
|
77
47
|
variant: options.variant ?? 'button',
|
|
78
48
|
style: options.style ?? '',
|
|
79
49
|
class: options.class ?? ''
|
|
80
|
-
};
|
|
50
|
+
});
|
|
81
51
|
|
|
82
52
|
// Apply theme on initialization
|
|
83
53
|
this.applyTheme(this.state.currentTheme);
|
|
84
54
|
}
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
56
|
+
protected getTriggerEvents(): readonly string[] {
|
|
57
|
+
return TRIGGER_EVENTS;
|
|
58
|
+
}
|
|
89
59
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
60
|
+
protected getCallbackEvents(): readonly string[] {
|
|
61
|
+
return CALLBACK_EVENTS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
65
|
+
* STATIC METHODS
|
|
66
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
67
|
+
|
|
68
|
+
private static detectTheme(storageKey = 'jux-theme'): string {
|
|
94
69
|
if (typeof window === 'undefined') return 'light';
|
|
95
70
|
|
|
96
|
-
// Check localStorage first
|
|
97
71
|
const stored = localStorage.getItem(storageKey);
|
|
98
72
|
if (stored) return stored;
|
|
99
73
|
|
|
100
|
-
// Check current document attribute
|
|
101
74
|
const current = document.body.getAttribute('data-theme');
|
|
102
75
|
if (current) return current;
|
|
103
76
|
|
|
104
|
-
// Check system preference
|
|
105
77
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
106
78
|
return 'dark';
|
|
107
79
|
}
|
|
@@ -109,58 +81,27 @@ export class ThemeToggle {
|
|
|
109
81
|
return 'light';
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
*/
|
|
115
|
-
private applyTheme(themeId: string): void {
|
|
116
|
-
if (typeof document === 'undefined') return;
|
|
84
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
85
|
+
* FLUENT API
|
|
86
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
117
87
|
|
|
118
|
-
|
|
119
|
-
// Update data-theme attribute on body
|
|
120
|
-
document.body.setAttribute('data-theme', themeId);
|
|
88
|
+
// ✅ Inherited from BaseComponent
|
|
121
89
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Store in localStorage
|
|
126
|
-
localStorage.setItem(this.state.storageKey, themeId);
|
|
127
|
-
|
|
128
|
-
// Dispatch custom event for other components to listen
|
|
129
|
-
window.dispatchEvent(new CustomEvent('themechange', {
|
|
130
|
-
detail: { theme: themeId }
|
|
131
|
-
}));
|
|
132
|
-
|
|
133
|
-
console.log(`🎨 Theme applied: ${themeId}`);
|
|
134
|
-
} catch (error: any) {
|
|
135
|
-
ErrorHandler.captureError({
|
|
136
|
-
component: 'ThemeToggle',
|
|
137
|
-
method: 'applyTheme',
|
|
138
|
-
message: `Failed to apply theme: ${error.message}`,
|
|
139
|
-
stack: error.stack,
|
|
140
|
-
timestamp: new Date(),
|
|
141
|
-
context: { themeId }
|
|
142
|
-
});
|
|
143
|
-
}
|
|
90
|
+
themes(value: Theme[]): this {
|
|
91
|
+
this.state.themes = value;
|
|
92
|
+
return this;
|
|
144
93
|
}
|
|
145
94
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
private cycleTheme(): void {
|
|
150
|
-
const currentIndex = this.state.themes.findIndex(t => t.id === this.state.currentTheme);
|
|
151
|
-
const nextIndex = (currentIndex + 1) % this.state.themes.length;
|
|
152
|
-
const nextTheme = this.state.themes[nextIndex];
|
|
153
|
-
|
|
154
|
-
this.setTheme(nextTheme.id);
|
|
95
|
+
variant(value: 'button' | 'dropdown' | 'cycle'): this {
|
|
96
|
+
this.state.variant = value;
|
|
97
|
+
return this;
|
|
155
98
|
}
|
|
156
99
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
100
|
+
showLabel(value: boolean): this {
|
|
101
|
+
this.state.showLabel = value;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
160
104
|
|
|
161
|
-
/**
|
|
162
|
-
* Set current theme
|
|
163
|
-
*/
|
|
164
105
|
setTheme(themeId: string): this {
|
|
165
106
|
const theme = this.state.themes.find(t => t.id === themeId);
|
|
166
107
|
|
|
@@ -176,16 +117,10 @@ export class ThemeToggle {
|
|
|
176
117
|
return this;
|
|
177
118
|
}
|
|
178
119
|
|
|
179
|
-
/**
|
|
180
|
-
* Get current theme
|
|
181
|
-
*/
|
|
182
120
|
getTheme(): string {
|
|
183
121
|
return this.state.currentTheme;
|
|
184
122
|
}
|
|
185
123
|
|
|
186
|
-
/**
|
|
187
|
-
* Add a theme
|
|
188
|
-
*/
|
|
189
124
|
addTheme(theme: Theme): this {
|
|
190
125
|
if (!this.state.themes.find(t => t.id === theme.id)) {
|
|
191
126
|
this.state.themes.push(theme);
|
|
@@ -193,38 +128,45 @@ export class ThemeToggle {
|
|
|
193
128
|
return this;
|
|
194
129
|
}
|
|
195
130
|
|
|
196
|
-
/*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
131
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
132
|
+
* PRIVATE METHODS
|
|
133
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
199
134
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return this;
|
|
203
|
-
}
|
|
135
|
+
private applyTheme(themeId: string): void {
|
|
136
|
+
if (typeof document === 'undefined') return;
|
|
204
137
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
138
|
+
try {
|
|
139
|
+
document.body.setAttribute('data-theme', themeId);
|
|
140
|
+
document.documentElement.setAttribute('data-theme', themeId);
|
|
141
|
+
localStorage.setItem(this.state.storageKey, themeId);
|
|
209
142
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
143
|
+
window.dispatchEvent(new CustomEvent('themechange', {
|
|
144
|
+
detail: { theme: themeId }
|
|
145
|
+
}));
|
|
214
146
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return this;
|
|
218
|
-
}
|
|
147
|
+
// 🎯 Fire the themeChange callback event
|
|
148
|
+
this._triggerCallback('themeChange', themeId);
|
|
219
149
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
150
|
+
console.log(`🎨 Theme applied: ${themeId}`);
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
ErrorHandler.captureError({
|
|
153
|
+
component: 'ThemeToggle',
|
|
154
|
+
method: 'applyTheme',
|
|
155
|
+
message: `Failed to apply theme: ${error.message}`,
|
|
156
|
+
stack: error.stack,
|
|
157
|
+
timestamp: new Date(),
|
|
158
|
+
context: { themeId }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
223
161
|
}
|
|
224
162
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
163
|
+
private cycleTheme(): void {
|
|
164
|
+
const currentIndex = this.state.themes.findIndex(t => t.id === this.state.currentTheme);
|
|
165
|
+
const nextIndex = (currentIndex + 1) % this.state.themes.length;
|
|
166
|
+
const nextTheme = this.state.themes[nextIndex];
|
|
167
|
+
|
|
168
|
+
this.setTheme(nextTheme.id);
|
|
169
|
+
}
|
|
228
170
|
|
|
229
171
|
private _updateDOM(): void {
|
|
230
172
|
if (!this.container) return;
|
|
@@ -235,7 +177,6 @@ export class ThemeToggle {
|
|
|
235
177
|
const currentTheme = this.state.themes.find(t => t.id === this.state.currentTheme);
|
|
236
178
|
if (!currentTheme) return;
|
|
237
179
|
|
|
238
|
-
// Update button variant
|
|
239
180
|
if (this.state.variant === 'button' || this.state.variant === 'cycle') {
|
|
240
181
|
const button = toggle.querySelector('button');
|
|
241
182
|
if (button && currentTheme.icon) {
|
|
@@ -249,7 +190,6 @@ export class ThemeToggle {
|
|
|
249
190
|
button.appendChild(labelSpan);
|
|
250
191
|
}
|
|
251
192
|
|
|
252
|
-
// Trigger Lucide rendering
|
|
253
193
|
requestAnimationFrame(() => {
|
|
254
194
|
if ((window as any).lucide) {
|
|
255
195
|
(window as any).lucide.createIcons();
|
|
@@ -258,7 +198,6 @@ export class ThemeToggle {
|
|
|
258
198
|
}
|
|
259
199
|
}
|
|
260
200
|
|
|
261
|
-
// Update dropdown variant
|
|
262
201
|
if (this.state.variant === 'dropdown') {
|
|
263
202
|
const select = toggle.querySelector('select') as HTMLSelectElement;
|
|
264
203
|
if (select) {
|
|
@@ -267,24 +206,13 @@ export class ThemeToggle {
|
|
|
267
206
|
}
|
|
268
207
|
}
|
|
269
208
|
|
|
270
|
-
/*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
209
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
210
|
+
* RENDER
|
|
211
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
273
212
|
|
|
274
213
|
render(targetId?: string): this {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (targetId) {
|
|
278
|
-
const target = document.querySelector(targetId);
|
|
279
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
280
|
-
throw new Error(`ThemeToggle: Target element "${targetId}" not found`);
|
|
281
|
-
}
|
|
282
|
-
container = target;
|
|
283
|
-
} else {
|
|
284
|
-
container = getOrCreateContainer(this._id);
|
|
285
|
-
}
|
|
214
|
+
const container = this._setupContainer(targetId);
|
|
286
215
|
|
|
287
|
-
this.container = container;
|
|
288
216
|
const { themes, currentTheme, showLabel, variant, style, class: className } = this.state;
|
|
289
217
|
|
|
290
218
|
const wrapper = document.createElement('div');
|
|
@@ -301,9 +229,7 @@ export class ThemeToggle {
|
|
|
301
229
|
|
|
302
230
|
const theme = themes.find(t => t.id === currentTheme);
|
|
303
231
|
|
|
304
|
-
// Render based on variant
|
|
305
232
|
if (variant === 'button' || variant === 'cycle') {
|
|
306
|
-
// Single button that cycles through themes
|
|
307
233
|
const button = document.createElement('button');
|
|
308
234
|
button.className = 'jux-theme-toggle-button';
|
|
309
235
|
button.type = 'button';
|
|
@@ -329,7 +255,6 @@ export class ThemeToggle {
|
|
|
329
255
|
wrapper.appendChild(button);
|
|
330
256
|
|
|
331
257
|
} else if (variant === 'dropdown') {
|
|
332
|
-
// Dropdown select with all themes
|
|
333
258
|
const select = document.createElement('select');
|
|
334
259
|
select.className = 'jux-theme-toggle-select';
|
|
335
260
|
select.setAttribute('aria-label', 'Select theme');
|
|
@@ -356,9 +281,10 @@ export class ThemeToggle {
|
|
|
356
281
|
wrapper.appendChild(select);
|
|
357
282
|
}
|
|
358
283
|
|
|
284
|
+
this._wireStandardEvents(wrapper);
|
|
285
|
+
|
|
359
286
|
container.appendChild(wrapper);
|
|
360
287
|
|
|
361
|
-
// Trigger Lucide icon rendering
|
|
362
288
|
requestAnimationFrame(() => {
|
|
363
289
|
if ((window as any).lucide) {
|
|
364
290
|
(window as any).lucide.createIcons();
|
|
@@ -367,26 +293,8 @@ export class ThemeToggle {
|
|
|
367
293
|
|
|
368
294
|
return this;
|
|
369
295
|
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Render to another Jux component's container
|
|
373
|
-
*/
|
|
374
|
-
renderTo(juxComponent: any): this {
|
|
375
|
-
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
376
|
-
throw new Error('ThemeToggle.renderTo: Invalid component - not an object');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
380
|
-
throw new Error('ThemeToggle.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return this.render(`#${juxComponent._id}`);
|
|
384
|
-
}
|
|
385
296
|
}
|
|
386
297
|
|
|
387
|
-
/**
|
|
388
|
-
* Factory helper
|
|
389
|
-
*/
|
|
390
298
|
export function themeToggle(id: string, options: ThemeToggleOptions = {}): ThemeToggle {
|
|
391
299
|
return new ThemeToggle(id, options);
|
|
392
300
|
}
|