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.
- package/README.md +292 -0
- package/bin/cli.js +149 -0
- package/lib/adapters/base-adapter.js +35 -0
- package/lib/adapters/index.js +33 -0
- package/lib/adapters/mysql-adapter.js +65 -0
- package/lib/adapters/postgres-adapter.js +70 -0
- package/lib/adapters/sqlite-adapter.js +56 -0
- package/lib/components/app.ts +124 -0
- package/lib/components/button.ts +136 -0
- package/lib/components/card.ts +205 -0
- package/lib/components/chart.ts +125 -0
- package/lib/components/code.ts +242 -0
- package/lib/components/container.ts +282 -0
- package/lib/components/data.ts +105 -0
- package/lib/components/docs-data.json +1211 -0
- package/lib/components/error-handler.ts +285 -0
- package/lib/components/footer.ts +146 -0
- package/lib/components/header.ts +167 -0
- package/lib/components/hero.ts +170 -0
- package/lib/components/import.ts +430 -0
- package/lib/components/input.ts +175 -0
- package/lib/components/layout.ts +113 -0
- package/lib/components/list.ts +392 -0
- package/lib/components/main.ts +111 -0
- package/lib/components/menu.ts +170 -0
- package/lib/components/modal.ts +216 -0
- package/lib/components/nav.ts +136 -0
- package/lib/components/node.ts +200 -0
- package/lib/components/reactivity.js +104 -0
- package/lib/components/script.ts +152 -0
- package/lib/components/sidebar.ts +168 -0
- package/lib/components/style.ts +129 -0
- package/lib/components/table.ts +279 -0
- package/lib/components/tabs.ts +191 -0
- package/lib/components/theme.ts +97 -0
- package/lib/components/view.ts +174 -0
- package/lib/jux.ts +203 -0
- package/lib/layouts/default.css +260 -0
- package/lib/layouts/default.jux +8 -0
- package/lib/layouts/figma.css +334 -0
- package/lib/layouts/figma.jux +0 -0
- package/lib/layouts/notion.css +258 -0
- package/lib/styles/base-theme.css +186 -0
- package/lib/styles/dark-theme.css +144 -0
- package/lib/styles/global.css +1131 -0
- package/lib/styles/light-theme.css +144 -0
- package/lib/styles/tokens/dark.css +86 -0
- package/lib/styles/tokens/light.css +86 -0
- package/lib/themes/dark.css +86 -0
- package/lib/themes/light.css +86 -0
- package/lib/utils/path-resolver.js +23 -0
- package/machinery/compiler.js +262 -0
- package/machinery/doc-generator.js +160 -0
- package/machinery/generators/css.js +128 -0
- package/machinery/generators/html.js +108 -0
- package/machinery/imports.js +155 -0
- package/machinery/server.js +185 -0
- package/machinery/validators/file-validator.js +123 -0
- package/machinery/watcher.js +148 -0
- package/package.json +58 -0
- 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
|
+
}
|