juxscript 1.0.2 → 1.0.4
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/README.md +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +228 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1697 -388
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +166 -78
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +247 -0
- package/lib/components/sidebar.ts +86 -36
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +172 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { ErrorHandler } from './error-handler.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Theme configuration
|
|
6
|
+
*/
|
|
7
|
+
export interface Theme {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ThemeToggle component options
|
|
15
|
+
*/
|
|
16
|
+
export interface ThemeToggleOptions {
|
|
17
|
+
themes?: Theme[];
|
|
18
|
+
defaultTheme?: string;
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
showLabel?: boolean;
|
|
21
|
+
variant?: 'button' | 'dropdown' | 'cycle';
|
|
22
|
+
style?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ThemeToggle component state
|
|
28
|
+
*/
|
|
29
|
+
type ThemeToggleState = {
|
|
30
|
+
themes: Theme[];
|
|
31
|
+
currentTheme: string;
|
|
32
|
+
storageKey: string;
|
|
33
|
+
showLabel: boolean;
|
|
34
|
+
variant: string;
|
|
35
|
+
style: string;
|
|
36
|
+
class: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* ThemeToggle component - Manage and switch between themes
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* // Simple light/dark toggle
|
|
44
|
+
* jux.themeToggle('myToggle').render('#appheader-actions');
|
|
45
|
+
*
|
|
46
|
+
* // Custom themes
|
|
47
|
+
* jux.themeToggle('myToggle', {
|
|
48
|
+
* themes: [
|
|
49
|
+
* { id: 'light', label: 'Light', icon: '☀️' },
|
|
50
|
+
* { id: 'dark', label: 'Dark', icon: '🌙' },
|
|
51
|
+
* { id: 'auto', label: 'Auto', icon: '🌓' }
|
|
52
|
+
* ],
|
|
53
|
+
* variant: 'dropdown'
|
|
54
|
+
* }).render('#appheader-actions');
|
|
55
|
+
*/
|
|
56
|
+
export class ThemeToggle {
|
|
57
|
+
state: ThemeToggleState;
|
|
58
|
+
container: HTMLElement | null = null;
|
|
59
|
+
_id: string;
|
|
60
|
+
id: string;
|
|
61
|
+
|
|
62
|
+
constructor(id: string, options: ThemeToggleOptions = {}) {
|
|
63
|
+
this._id = id;
|
|
64
|
+
this.id = id;
|
|
65
|
+
|
|
66
|
+
const defaultThemes: Theme[] = [
|
|
67
|
+
{ id: 'light', label: 'Light', icon: '☀️' },
|
|
68
|
+
{ id: 'dark', label: 'Dark', icon: '🌙' }
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
this.state = {
|
|
72
|
+
themes: options.themes ?? defaultThemes,
|
|
73
|
+
currentTheme: options.defaultTheme ?? this.detectTheme(options.storageKey),
|
|
74
|
+
storageKey: options.storageKey ?? 'jux-theme',
|
|
75
|
+
showLabel: options.showLabel ?? false,
|
|
76
|
+
variant: options.variant ?? 'button',
|
|
77
|
+
style: options.style ?? '',
|
|
78
|
+
class: options.class ?? ''
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Apply theme on initialization
|
|
82
|
+
this.applyTheme(this.state.currentTheme);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* -------------------------
|
|
86
|
+
* Theme Detection & Management
|
|
87
|
+
* ------------------------- */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect theme from localStorage or system preference
|
|
91
|
+
*/
|
|
92
|
+
private detectTheme(storageKey = 'jux-theme'): string {
|
|
93
|
+
if (typeof window === 'undefined') return 'light';
|
|
94
|
+
|
|
95
|
+
// Check localStorage first
|
|
96
|
+
const stored = localStorage.getItem(storageKey);
|
|
97
|
+
if (stored) return stored;
|
|
98
|
+
|
|
99
|
+
// Check current document attribute
|
|
100
|
+
const current = document.body.getAttribute('data-theme');
|
|
101
|
+
if (current) return current;
|
|
102
|
+
|
|
103
|
+
// Check system preference
|
|
104
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
105
|
+
return 'dark';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return 'light';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Apply theme to document
|
|
113
|
+
*/
|
|
114
|
+
private applyTheme(themeId: string): void {
|
|
115
|
+
if (typeof document === 'undefined') return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Update data-theme attribute on body
|
|
119
|
+
document.body.setAttribute('data-theme', themeId);
|
|
120
|
+
|
|
121
|
+
// Also update on html element for broader CSS support
|
|
122
|
+
document.documentElement.setAttribute('data-theme', themeId);
|
|
123
|
+
|
|
124
|
+
// Store in localStorage
|
|
125
|
+
localStorage.setItem(this.state.storageKey, themeId);
|
|
126
|
+
|
|
127
|
+
// Dispatch custom event for other components to listen
|
|
128
|
+
window.dispatchEvent(new CustomEvent('themechange', {
|
|
129
|
+
detail: { theme: themeId }
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
console.log(`🎨 Theme applied: ${themeId}`);
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
ErrorHandler.captureError({
|
|
135
|
+
component: 'ThemeToggle',
|
|
136
|
+
method: 'applyTheme',
|
|
137
|
+
message: `Failed to apply theme: ${error.message}`,
|
|
138
|
+
stack: error.stack,
|
|
139
|
+
timestamp: new Date(),
|
|
140
|
+
context: { themeId }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cycle to next theme
|
|
147
|
+
*/
|
|
148
|
+
private cycleTheme(): void {
|
|
149
|
+
const currentIndex = this.state.themes.findIndex(t => t.id === this.state.currentTheme);
|
|
150
|
+
const nextIndex = (currentIndex + 1) % this.state.themes.length;
|
|
151
|
+
const nextTheme = this.state.themes[nextIndex];
|
|
152
|
+
|
|
153
|
+
this.setTheme(nextTheme.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* -------------------------
|
|
157
|
+
* Public API
|
|
158
|
+
* ------------------------- */
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set current theme
|
|
162
|
+
*/
|
|
163
|
+
setTheme(themeId: string): this {
|
|
164
|
+
const theme = this.state.themes.find(t => t.id === themeId);
|
|
165
|
+
|
|
166
|
+
if (!theme) {
|
|
167
|
+
console.warn(`Theme "${themeId}" not found in available themes`);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.state.currentTheme = themeId;
|
|
172
|
+
this.applyTheme(themeId);
|
|
173
|
+
this._updateDOM();
|
|
174
|
+
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get current theme
|
|
180
|
+
*/
|
|
181
|
+
getTheme(): string {
|
|
182
|
+
return this.state.currentTheme;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Add a theme
|
|
187
|
+
*/
|
|
188
|
+
addTheme(theme: Theme): this {
|
|
189
|
+
if (!this.state.themes.find(t => t.id === theme.id)) {
|
|
190
|
+
this.state.themes.push(theme);
|
|
191
|
+
}
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* -------------------------
|
|
196
|
+
* Fluent API
|
|
197
|
+
* ------------------------- */
|
|
198
|
+
|
|
199
|
+
themes(value: Theme[]): this {
|
|
200
|
+
this.state.themes = value;
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
variant(value: 'button' | 'dropdown' | 'cycle'): this {
|
|
205
|
+
this.state.variant = value;
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
showLabel(value: boolean): this {
|
|
210
|
+
this.state.showLabel = value;
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
style(value: string): this {
|
|
215
|
+
this.state.style = value;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class(value: string): this {
|
|
220
|
+
this.state.class = value;
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* -------------------------
|
|
225
|
+
* DOM Update
|
|
226
|
+
* ------------------------- */
|
|
227
|
+
|
|
228
|
+
private _updateDOM(): void {
|
|
229
|
+
if (!this.container) return;
|
|
230
|
+
|
|
231
|
+
const toggle = this.container.querySelector(`#${this._id}`);
|
|
232
|
+
if (!toggle) return;
|
|
233
|
+
|
|
234
|
+
const currentTheme = this.state.themes.find(t => t.id === this.state.currentTheme);
|
|
235
|
+
if (!currentTheme) return;
|
|
236
|
+
|
|
237
|
+
// Update button variant
|
|
238
|
+
if (this.state.variant === 'button' || this.state.variant === 'cycle') {
|
|
239
|
+
const button = toggle.querySelector('button');
|
|
240
|
+
if (button) {
|
|
241
|
+
button.innerHTML = this.state.showLabel
|
|
242
|
+
? `${currentTheme.icon || ''} ${currentTheme.label}`.trim()
|
|
243
|
+
: currentTheme.icon || currentTheme.label;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Update dropdown variant
|
|
248
|
+
if (this.state.variant === 'dropdown') {
|
|
249
|
+
const select = toggle.querySelector('select') as HTMLSelectElement;
|
|
250
|
+
if (select) {
|
|
251
|
+
select.value = this.state.currentTheme;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* -------------------------
|
|
257
|
+
* Render
|
|
258
|
+
* ------------------------- */
|
|
259
|
+
|
|
260
|
+
render(targetId?: string): this {
|
|
261
|
+
let container: HTMLElement;
|
|
262
|
+
|
|
263
|
+
if (targetId) {
|
|
264
|
+
const target = document.querySelector(targetId);
|
|
265
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
266
|
+
throw new Error(`ThemeToggle: Target element "${targetId}" not found`);
|
|
267
|
+
}
|
|
268
|
+
container = target;
|
|
269
|
+
} else {
|
|
270
|
+
container = getOrCreateContainer(this._id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.container = container;
|
|
274
|
+
const { themes, currentTheme, showLabel, variant, style, class: className } = this.state;
|
|
275
|
+
|
|
276
|
+
const wrapper = document.createElement('div');
|
|
277
|
+
wrapper.className = `jux-theme-toggle jux-theme-toggle-${variant}`;
|
|
278
|
+
wrapper.id = this._id;
|
|
279
|
+
|
|
280
|
+
if (className) {
|
|
281
|
+
wrapper.className += ` ${className}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (style) {
|
|
285
|
+
wrapper.setAttribute('style', style);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const theme = themes.find(t => t.id === currentTheme);
|
|
289
|
+
|
|
290
|
+
// Render based on variant
|
|
291
|
+
if (variant === 'button' || variant === 'cycle') {
|
|
292
|
+
// Single button that cycles through themes
|
|
293
|
+
const button = document.createElement('button');
|
|
294
|
+
button.className = 'jux-theme-toggle-button';
|
|
295
|
+
button.type = 'button';
|
|
296
|
+
button.setAttribute('aria-label', 'Toggle theme');
|
|
297
|
+
|
|
298
|
+
button.innerHTML = showLabel
|
|
299
|
+
? `${theme?.icon || ''} ${theme?.label || currentTheme}`.trim()
|
|
300
|
+
: theme?.icon || theme?.label || currentTheme;
|
|
301
|
+
|
|
302
|
+
button.addEventListener('click', () => {
|
|
303
|
+
this.cycleTheme();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
wrapper.appendChild(button);
|
|
307
|
+
|
|
308
|
+
} else if (variant === 'dropdown') {
|
|
309
|
+
// Dropdown select with all themes
|
|
310
|
+
const select = document.createElement('select');
|
|
311
|
+
select.className = 'jux-theme-toggle-select';
|
|
312
|
+
select.setAttribute('aria-label', 'Select theme');
|
|
313
|
+
|
|
314
|
+
themes.forEach(t => {
|
|
315
|
+
const option = document.createElement('option');
|
|
316
|
+
option.value = t.id;
|
|
317
|
+
option.textContent = showLabel
|
|
318
|
+
? `${t.icon || ''} ${t.label}`.trim()
|
|
319
|
+
: t.icon || t.label;
|
|
320
|
+
|
|
321
|
+
if (t.id === currentTheme) {
|
|
322
|
+
option.selected = true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
select.appendChild(option);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
select.addEventListener('change', (e) => {
|
|
329
|
+
const target = e.target as HTMLSelectElement;
|
|
330
|
+
this.setTheme(target.value);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
wrapper.appendChild(select);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
container.appendChild(wrapper);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Render to another Jux component's container
|
|
342
|
+
*/
|
|
343
|
+
renderTo(juxComponent: any): this {
|
|
344
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
345
|
+
throw new Error('ThemeToggle.renderTo: Invalid component - not an object');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
349
|
+
throw new Error('ThemeToggle.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return this.render(`#${juxComponent._id}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Factory helper
|
|
358
|
+
*/
|
|
359
|
+
export function themeToggle(id: string, options: ThemeToggleOptions = {}): ThemeToggle {
|
|
360
|
+
return new ThemeToggle(id, options);
|
|
361
|
+
}
|