juxscript 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.
Files changed (61) hide show
  1. package/README.md +292 -0
  2. package/bin/cli.js +149 -0
  3. package/lib/adapters/base-adapter.js +35 -0
  4. package/lib/adapters/index.js +33 -0
  5. package/lib/adapters/mysql-adapter.js +65 -0
  6. package/lib/adapters/postgres-adapter.js +70 -0
  7. package/lib/adapters/sqlite-adapter.js +56 -0
  8. package/lib/components/app.ts +124 -0
  9. package/lib/components/button.ts +136 -0
  10. package/lib/components/card.ts +205 -0
  11. package/lib/components/chart.ts +125 -0
  12. package/lib/components/code.ts +242 -0
  13. package/lib/components/container.ts +282 -0
  14. package/lib/components/data.ts +105 -0
  15. package/lib/components/docs-data.json +1211 -0
  16. package/lib/components/error-handler.ts +285 -0
  17. package/lib/components/footer.ts +146 -0
  18. package/lib/components/header.ts +167 -0
  19. package/lib/components/hero.ts +170 -0
  20. package/lib/components/import.ts +430 -0
  21. package/lib/components/input.ts +175 -0
  22. package/lib/components/layout.ts +113 -0
  23. package/lib/components/list.ts +392 -0
  24. package/lib/components/main.ts +111 -0
  25. package/lib/components/menu.ts +170 -0
  26. package/lib/components/modal.ts +216 -0
  27. package/lib/components/nav.ts +136 -0
  28. package/lib/components/node.ts +200 -0
  29. package/lib/components/reactivity.js +104 -0
  30. package/lib/components/script.ts +152 -0
  31. package/lib/components/sidebar.ts +168 -0
  32. package/lib/components/style.ts +129 -0
  33. package/lib/components/table.ts +279 -0
  34. package/lib/components/tabs.ts +191 -0
  35. package/lib/components/theme.ts +97 -0
  36. package/lib/components/view.ts +174 -0
  37. package/lib/jux.ts +203 -0
  38. package/lib/layouts/default.css +260 -0
  39. package/lib/layouts/default.jux +8 -0
  40. package/lib/layouts/figma.css +334 -0
  41. package/lib/layouts/figma.jux +0 -0
  42. package/lib/layouts/notion.css +258 -0
  43. package/lib/styles/base-theme.css +186 -0
  44. package/lib/styles/dark-theme.css +144 -0
  45. package/lib/styles/global.css +1131 -0
  46. package/lib/styles/light-theme.css +144 -0
  47. package/lib/styles/tokens/dark.css +86 -0
  48. package/lib/styles/tokens/light.css +86 -0
  49. package/lib/themes/dark.css +86 -0
  50. package/lib/themes/light.css +86 -0
  51. package/lib/utils/path-resolver.js +23 -0
  52. package/machinery/compiler.js +262 -0
  53. package/machinery/doc-generator.js +160 -0
  54. package/machinery/generators/css.js +128 -0
  55. package/machinery/generators/html.js +108 -0
  56. package/machinery/imports.js +155 -0
  57. package/machinery/server.js +185 -0
  58. package/machinery/validators/file-validator.js +123 -0
  59. package/machinery/watcher.js +148 -0
  60. package/package.json +58 -0
  61. package/types/globals.d.ts +16 -0
@@ -0,0 +1,129 @@
1
+ import { ErrorHandler } from './error-handler.js';
2
+
3
+ export class Style {
4
+ private _content: string = '';
5
+ private _url: string = '';
6
+ private _isUrl: boolean = false;
7
+ private _element: HTMLLinkElement | HTMLStyleElement | null = null;
8
+
9
+ constructor(contentOrUrl: string = '') {
10
+ // Detect if it's a URL or inline CSS
11
+ if (contentOrUrl.trim().startsWith('http') ||
12
+ contentOrUrl.endsWith('.css') ||
13
+ contentOrUrl.includes('/')) {
14
+ this._url = contentOrUrl;
15
+ this._isUrl = true;
16
+ } else {
17
+ this._content = contentOrUrl;
18
+ this._isUrl = false;
19
+ }
20
+
21
+ // Auto-render if content/url provided
22
+ // render() has its own error handling, so no need for try-catch here
23
+ if (contentOrUrl) {
24
+ this.render();
25
+ }
26
+ }
27
+
28
+ content(css: string): this {
29
+ this._content = css;
30
+ this._isUrl = false;
31
+ this._url = '';
32
+ this.render();
33
+ return this;
34
+ }
35
+
36
+ url(href: string): this {
37
+ this._url = href;
38
+ this._isUrl = true;
39
+ this._content = '';
40
+ this.render();
41
+ return this;
42
+ }
43
+
44
+ append(css: string): this {
45
+ if (this._isUrl) {
46
+ console.warn('Cannot append to external stylesheet. Use content() to switch to inline styles.');
47
+ return this;
48
+ }
49
+ this._content += '\n' + css;
50
+ this.render();
51
+ return this;
52
+ }
53
+
54
+ prepend(css: string): this {
55
+ if (this._isUrl) {
56
+ console.warn('Cannot prepend to external stylesheet. Use content() to switch to inline styles.');
57
+ return this;
58
+ }
59
+ this._content = css + '\n' + this._content;
60
+ this.render();
61
+ return this;
62
+ }
63
+
64
+ render(): this {
65
+ if (typeof document === 'undefined') {
66
+ return this;
67
+ }
68
+
69
+ try {
70
+ // Remove existing element if it exists
71
+ this.remove();
72
+
73
+ if (this._isUrl) {
74
+ // Create <link> element for external stylesheet
75
+ const link = document.createElement('link');
76
+ link.rel = 'stylesheet';
77
+ link.href = this._url;
78
+
79
+ // Add error handler for failed loads (404, network, etc.)
80
+ link.onerror = () => {
81
+ ErrorHandler.captureError({
82
+ component: 'Style',
83
+ method: 'render',
84
+ message: `Failed to load stylesheet: ${this._url}`,
85
+ timestamp: new Date(),
86
+ context: { url: this._url, type: 'external', error: 'load_failed' }
87
+ });
88
+ };
89
+
90
+ link.onload = () => {
91
+ console.log(`✓ Stylesheet loaded: ${this._url}`);
92
+ };
93
+
94
+ document.head.appendChild(link);
95
+ this._element = link;
96
+ } else {
97
+ // Create <style> element for inline CSS
98
+ const style = document.createElement('style');
99
+ style.textContent = this._content;
100
+ document.head.appendChild(style);
101
+ this._element = style;
102
+ }
103
+ } catch (error: any) {
104
+ // Catch DOM manipulation errors, permission errors, etc.
105
+ ErrorHandler.captureError({
106
+ component: 'Style',
107
+ method: 'render',
108
+ message: error.message,
109
+ stack: error.stack,
110
+ timestamp: new Date(),
111
+ context: {
112
+ isUrl: this._isUrl,
113
+ url: this._url,
114
+ error: 'runtime_exception'
115
+ }
116
+ });
117
+ }
118
+
119
+ return this;
120
+ }
121
+
122
+ remove(): this {
123
+ if (this._element && this._element.parentNode) {
124
+ this._element.parentNode.removeChild(this._element);
125
+ this._element = null;
126
+ }
127
+ return this;
128
+ }
129
+ }
@@ -0,0 +1,279 @@
1
+ import { Reactive, getOrCreateContainer } from './reactivity.js';
2
+
3
+ /**
4
+ * Table column configuration
5
+ */
6
+ export interface TableColumn {
7
+ key: string;
8
+ label: string;
9
+ width?: string;
10
+ align?: 'left' | 'center' | 'right';
11
+ renderCell?: (value: any, row: any) => string | HTMLElement;
12
+ }
13
+
14
+ /**
15
+ * Table component options
16
+ */
17
+ export interface TableOptions {
18
+ columns?: TableColumn[];
19
+ data?: any[];
20
+ striped?: boolean;
21
+ hoverable?: boolean;
22
+ bordered?: boolean;
23
+ allowHtml?: boolean; // Enable HTML rendering in cells
24
+ }
25
+
26
+ /**
27
+ * Table component state
28
+ */
29
+ type TableState = {
30
+ columns: TableColumn[];
31
+ data: any[];
32
+ striped: boolean;
33
+ hoverable: boolean;
34
+ bordered: boolean;
35
+ allowHtml: boolean;
36
+ };
37
+
38
+ /**
39
+ * Table component
40
+ *
41
+ * Usage:
42
+ * // Auto-generate columns from data
43
+ * const table = jux.table('myTable', {
44
+ * data: [
45
+ * { name: 'Alice', age: 30 },
46
+ * { name: 'Bob', age: 25 }
47
+ * ],
48
+ * striped: true,
49
+ * allowHtml: true // Enable HTML rendering
50
+ * });
51
+ * table.render();
52
+ *
53
+ * // Or specify columns explicitly
54
+ * const table = jux.table('myTable', {
55
+ * columns: [
56
+ * { key: 'name', label: 'Name' },
57
+ * { key: 'age', label: 'Age', align: 'center' }
58
+ * ],
59
+ * data: [
60
+ * { name: 'Alice', age: 30 },
61
+ * { name: 'Bob', age: 25 }
62
+ * ]
63
+ * });
64
+ * table.render();
65
+ */
66
+ export class Table extends Reactive {
67
+ state!: TableState;
68
+ container: HTMLElement | null = null;
69
+
70
+ constructor(componentId: string, options: TableOptions = {}) {
71
+ super();
72
+ this._setComponentId(componentId);
73
+
74
+ this.state = this._createReactiveState({
75
+ columns: options.columns ?? [],
76
+ data: options.data ?? [],
77
+ striped: options.striped ?? false,
78
+ hoverable: options.hoverable ?? true,
79
+ bordered: options.bordered ?? false,
80
+ allowHtml: options.allowHtml ?? true // Default to true for flexibility
81
+ }) as TableState;
82
+ }
83
+
84
+ /* -------------------------
85
+ * Fluent API
86
+ * ------------------------- */
87
+
88
+ columns(value: TableColumn[]): this {
89
+ this.state.columns = value;
90
+ return this;
91
+ }
92
+
93
+ data(value: any[]): this {
94
+ this.state.data = value;
95
+ return this;
96
+ }
97
+
98
+ striped(value: boolean): this {
99
+ this.state.striped = value;
100
+ return this;
101
+ }
102
+
103
+ hoverable(value: boolean): this {
104
+ this.state.hoverable = value;
105
+ return this;
106
+ }
107
+
108
+ bordered(value: boolean): this {
109
+ this.state.bordered = value;
110
+ return this;
111
+ }
112
+
113
+ allowHtml(value: boolean): this {
114
+ this.state.allowHtml = value;
115
+ return this;
116
+ }
117
+
118
+ /* -------------------------
119
+ * Helpers
120
+ * ------------------------- */
121
+
122
+ /**
123
+ * Auto-generate columns from data
124
+ */
125
+ private _autoGenerateColumns(data: any[]): TableColumn[] {
126
+ if (!data || data.length === 0) {
127
+ return [];
128
+ }
129
+
130
+ const firstRow = data[0];
131
+ const keys = Object.keys(firstRow);
132
+
133
+ return keys.map(key => ({
134
+ key,
135
+ label: this._formatLabel(key)
136
+ }));
137
+ }
138
+
139
+ /**
140
+ * Format column label from key
141
+ * Examples:
142
+ * 'name' -> 'Name'
143
+ * 'firstName' -> 'First Name'
144
+ * 'user_id' -> 'User Id'
145
+ */
146
+ private _formatLabel(key: string): string {
147
+ // Split on camelCase or snake_case
148
+ const words = key
149
+ .replace(/([A-Z])/g, ' $1') // camelCase -> camel Case
150
+ .replace(/_/g, ' ') // snake_case -> snake case
151
+ .trim()
152
+ .split(/\s+/);
153
+
154
+ // Capitalize first letter of each word
155
+ return words
156
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
157
+ .join(' ');
158
+ }
159
+
160
+ /* -------------------------
161
+ * Render
162
+ * ------------------------- */
163
+
164
+ render(targetId?: string): this {
165
+ let container: HTMLElement;
166
+
167
+ if (targetId) {
168
+ const target = document.querySelector(targetId);
169
+ if (!target || !(target instanceof HTMLElement)) {
170
+ throw new Error(`Table: Target element "${targetId}" not found`);
171
+ }
172
+ container = target;
173
+ } else {
174
+ container = getOrCreateContainer(this._componentId) as HTMLElement;
175
+ }
176
+
177
+ this.container = container;
178
+ let { columns, data, striped, hoverable, bordered, allowHtml } = this.state;
179
+
180
+ // Auto-generate columns if not provided
181
+ if (columns.length === 0 && data.length > 0) {
182
+ columns = this._autoGenerateColumns(data);
183
+ }
184
+
185
+ const wrapper = document.createElement('div');
186
+ wrapper.className = 'jux-table-wrapper';
187
+ wrapper.id = this._componentId;
188
+
189
+ const table = document.createElement('table');
190
+ table.className = 'jux-table';
191
+
192
+ if (striped) table.classList.add('jux-table-striped');
193
+ if (hoverable) table.classList.add('jux-table-hoverable');
194
+ if (bordered) table.classList.add('jux-table-bordered');
195
+
196
+ // Table header
197
+ const thead = document.createElement('thead');
198
+ const headerRow = document.createElement('tr');
199
+
200
+ columns.forEach(col => {
201
+ const th = document.createElement('th');
202
+ th.textContent = col.label;
203
+ if (col.width) th.style.width = col.width;
204
+ if (col.align) th.style.textAlign = col.align;
205
+ headerRow.appendChild(th);
206
+ });
207
+
208
+ thead.appendChild(headerRow);
209
+ table.appendChild(thead);
210
+
211
+ // Table body
212
+ const tbody = document.createElement('tbody');
213
+
214
+ data.forEach(row => {
215
+ const tr = document.createElement('tr');
216
+
217
+ columns.forEach(col => {
218
+ const td = document.createElement('td');
219
+ if (col.align) td.style.textAlign = col.align;
220
+
221
+ // Get cell value
222
+ const cellValue = row[col.key] ?? '';
223
+
224
+ // Custom render function takes precedence
225
+ if (col.renderCell && typeof col.renderCell === 'function') {
226
+ const rendered = col.renderCell(cellValue, row);
227
+ if (rendered instanceof HTMLElement) {
228
+ td.appendChild(rendered);
229
+ } else if (typeof rendered === 'string') {
230
+ if (allowHtml) {
231
+ td.innerHTML = rendered;
232
+ } else {
233
+ td.textContent = rendered;
234
+ }
235
+ }
236
+ } else {
237
+ // Default rendering
238
+ if (allowHtml && typeof cellValue === 'string' && cellValue.includes('<')) {
239
+ td.innerHTML = cellValue;
240
+ } else {
241
+ td.textContent = String(cellValue);
242
+ }
243
+ }
244
+
245
+ tr.appendChild(td);
246
+ });
247
+
248
+ tbody.appendChild(tr);
249
+ });
250
+
251
+ table.appendChild(tbody);
252
+ wrapper.appendChild(table);
253
+ container.appendChild(wrapper);
254
+
255
+ return this;
256
+ }
257
+
258
+ /**
259
+ * Render to another Jux component's container
260
+ */
261
+ renderTo(juxComponent: any): this {
262
+ if (!juxComponent || typeof juxComponent !== 'object') {
263
+ throw new Error('Table.renderTo: Invalid component - not an object');
264
+ }
265
+
266
+ if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
267
+ throw new Error('Table.renderTo: Invalid component - missing _componentId (not a Jux component)');
268
+ }
269
+
270
+ return this.render(`#${juxComponent._componentId}`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Factory helper
276
+ */
277
+ export function table(componentId: string, options: TableOptions = {}): Table {
278
+ return new Table(componentId, options);
279
+ }
@@ -0,0 +1,191 @@
1
+ import { Reactive, getOrCreateContainer } from './reactivity.js';
2
+
3
+ /**
4
+ * Tab configuration
5
+ */
6
+ export interface Tab {
7
+ id: string;
8
+ label: string;
9
+ content: string | (() => string);
10
+ }
11
+
12
+ /**
13
+ * Tabs component options
14
+ */
15
+ export interface TabsOptions {
16
+ tabs?: Tab[];
17
+ activeTab?: string;
18
+ }
19
+
20
+ /**
21
+ * Tabs component state
22
+ */
23
+ type TabsState = {
24
+ tabs: Tab[];
25
+ activeTab: string;
26
+ };
27
+
28
+ /**
29
+ * Tabs component
30
+ *
31
+ * Usage:
32
+ * const tabs = jux.tabs('myTabs', {
33
+ * tabs: [
34
+ * { id: 'tab1', label: 'Tab 1', content: 'Content 1' },
35
+ * { id: 'tab2', label: 'Tab 2', content: 'Content 2' }
36
+ * ]
37
+ * });
38
+ * await tabs.render();
39
+ */
40
+ export class Tabs extends Reactive {
41
+ state!: TabsState;
42
+ container: HTMLElement | null = null;
43
+
44
+ constructor(componentId: string, options: TabsOptions = {}) {
45
+ super();
46
+ this._setComponentId(componentId);
47
+
48
+ const tabs = options.tabs ?? [];
49
+ const activeTab = options.activeTab ?? (tabs.length > 0 ? tabs[0].id : '');
50
+
51
+ this.state = this._createReactiveState({
52
+ tabs,
53
+ activeTab
54
+ }) as TabsState;
55
+ }
56
+
57
+ /* -------------------------
58
+ * Fluent API
59
+ * ------------------------- */
60
+
61
+ tabs(value: Tab[]): this {
62
+ this.state.tabs = value;
63
+ return this;
64
+ }
65
+
66
+ addTab(tab: Tab): this {
67
+ this.state.tabs.push(tab);
68
+ return this;
69
+ }
70
+
71
+ activeTab(value: string): this {
72
+ this.state.activeTab = value;
73
+ return this;
74
+ }
75
+
76
+ /* -------------------------
77
+ * Render
78
+ * ------------------------- */
79
+
80
+ async render(targetId?: string): Promise<this> {
81
+ let container: HTMLElement;
82
+
83
+ if (targetId) {
84
+ const target = document.querySelector(targetId);
85
+ if (!target || !(target instanceof HTMLElement)) {
86
+ throw new Error(`Tabs: Target element "${targetId}" not found`);
87
+ }
88
+ container = target;
89
+ } else {
90
+ container = getOrCreateContainer(this._componentId) as HTMLElement;
91
+ }
92
+
93
+ this.container = container;
94
+ const { tabs, activeTab } = this.state;
95
+
96
+ const wrapper = document.createElement('div');
97
+ wrapper.className = 'jux-tabs';
98
+ wrapper.id = this._componentId;
99
+
100
+ // Tab headers
101
+ const tabHeaders = document.createElement('div');
102
+ tabHeaders.className = 'jux-tabs-header';
103
+
104
+ tabs.forEach(tab => {
105
+ const tabBtn = document.createElement('button');
106
+ tabBtn.className = 'jux-tab-button';
107
+ tabBtn.textContent = tab.label;
108
+
109
+ if (tab.id === activeTab) {
110
+ tabBtn.classList.add('jux-tab-button-active');
111
+ }
112
+
113
+ tabHeaders.appendChild(tabBtn);
114
+
115
+ // Event binding - tab click
116
+ tabBtn.addEventListener('click', () => {
117
+ this.state.activeTab = tab.id;
118
+
119
+ // Update active states
120
+ tabHeaders.querySelectorAll('.jux-tab-button').forEach(btn => {
121
+ btn.classList.remove('jux-tab-button-active');
122
+ });
123
+ tabBtn.classList.add('jux-tab-button-active');
124
+
125
+ // Update content
126
+ tabPanels.querySelectorAll('.jux-tab-panel').forEach(panel => {
127
+ panel.classList.remove('jux-tab-panel-active');
128
+ });
129
+ const activePanel = tabPanels.querySelector(`[data-tab-id="${tab.id}"]`);
130
+ if (activePanel) {
131
+ activePanel.classList.add('jux-tab-panel-active');
132
+ }
133
+
134
+ this.emit('tabChange', { tabId: tab.id });
135
+ });
136
+ });
137
+
138
+ wrapper.appendChild(tabHeaders);
139
+
140
+ // Tab panels
141
+ const tabPanels = document.createElement('div');
142
+ tabPanels.className = 'jux-tabs-panels';
143
+
144
+ for (const tab of tabs) {
145
+ const panel = document.createElement('div');
146
+ panel.className = 'jux-tab-panel';
147
+ panel.setAttribute('data-tab-id', tab.id);
148
+
149
+ if (tab.id === activeTab) {
150
+ panel.classList.add('jux-tab-panel-active');
151
+ }
152
+
153
+ // Render content
154
+ if (typeof tab.content === 'function') {
155
+ panel.innerHTML = tab.content();
156
+ } else {
157
+ panel.innerHTML = tab.content;
158
+ }
159
+
160
+ tabPanels.appendChild(panel);
161
+ }
162
+
163
+ wrapper.appendChild(tabPanels);
164
+ container.appendChild(wrapper);
165
+
166
+ this.emit('rendered');
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Render to another Jux component's container
172
+ */
173
+ async renderTo(juxComponent: any): Promise<this> {
174
+ if (!juxComponent || typeof juxComponent !== 'object') {
175
+ throw new Error('Tabs.renderTo: Invalid component - not an object');
176
+ }
177
+
178
+ if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
179
+ throw new Error('Tabs.renderTo: Invalid component - missing _componentId (not a Jux component)');
180
+ }
181
+
182
+ return this.render(`#${juxComponent._componentId}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Factory helper
188
+ */
189
+ export function tabs(componentId: string, options: TabsOptions = {}): Tabs {
190
+ return new Tabs(componentId, options);
191
+ }
@@ -0,0 +1,97 @@
1
+ import { ErrorHandler } from './error-handler.js';
2
+
3
+ /**
4
+ * Theme - Load and apply CSS themes
5
+ *
6
+ * Usage:
7
+ * jux.theme('dark'); // Load vendor theme from lib/themes/
8
+ * jux.theme('./my-theme.css'); // Load custom theme (fully qualified path)
9
+ * jux.theme('/themes/custom.css'); // Load custom theme (absolute path)
10
+ */
11
+
12
+ interface ThemeConfig {
13
+ dir: string;
14
+ }
15
+
16
+ export class Theme {
17
+ private currentTheme: string | null = null;
18
+ private themeLink: HTMLLinkElement | null = null;
19
+ private config: ThemeConfig;
20
+
21
+ constructor(config?: ThemeConfig) {
22
+ // Default config if not provided
23
+ this.config = config || {
24
+ dir: './lib/themes/'
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Load a theme
30
+ *
31
+ * @param name - Theme name (e.g., 'dark') or full path to CSS file (e.g., './custom.css')
32
+ */
33
+ load(name: string): this {
34
+ if (typeof document === 'undefined') {
35
+ return this;
36
+ }
37
+
38
+ let themePath: string;
39
+ let themeName: string;
40
+
41
+ // Check if it's a path (contains / or .)
42
+ if (name.includes('/') || name.includes('.')) {
43
+ // Custom theme - use as-is
44
+ themePath = name;
45
+ themeName = name.split('/').pop()?.replace('.css', '') || 'custom';
46
+ } else {
47
+ // Vendor theme - look in config directory
48
+ themePath = `${this.config.dir}${name}.css`;
49
+ themeName = name;
50
+ }
51
+
52
+ // Remove existing theme link if present
53
+ if (this.themeLink) {
54
+ this.themeLink.remove();
55
+ }
56
+
57
+ // Create new link element
58
+ this.themeLink = document.createElement('link');
59
+ this.themeLink.rel = 'stylesheet';
60
+ this.themeLink.href = themePath;
61
+ this.themeLink.dataset.juxTheme = themeName;
62
+
63
+ // Handle load errors
64
+ this.themeLink.onerror = () => {
65
+ ErrorHandler.captureError({
66
+ component: 'Theme',
67
+ method: 'load',
68
+ message: `Failed to load theme: ${themePath}`,
69
+ timestamp: new Date(),
70
+ context: { themePath, themeName }
71
+ });
72
+ };
73
+
74
+ // Insert in head
75
+ document.head.appendChild(this.themeLink);
76
+
77
+ // Update current theme
78
+ this.currentTheme = themeName;
79
+
80
+ // Update body data attribute for CSS targeting
81
+ document.body.dataset.theme = themeName;
82
+
83
+ console.log(`🎨 Theme loaded: ${themeName}`);
84
+
85
+ return this;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Factory helper
91
+ *
92
+ * @param name - Theme name or path
93
+ */
94
+ export function theme(name: string): Theme {
95
+ const themeInstance = new Theme();
96
+ return themeInstance.load(name);
97
+ }