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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorHandler - Global error handler for JUX components
|
|
3
|
+
* Displays errors visually in the DOM for easy debugging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ComponentError {
|
|
7
|
+
component: string;
|
|
8
|
+
method: string;
|
|
9
|
+
message: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
context?: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ErrorHandler {
|
|
16
|
+
private static errors: ComponentError[] = [];
|
|
17
|
+
private static errorContainer: HTMLDivElement | null = null;
|
|
18
|
+
private static enabled: boolean = true;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize error handler
|
|
22
|
+
*/
|
|
23
|
+
static init() {
|
|
24
|
+
if (typeof document === 'undefined') return;
|
|
25
|
+
|
|
26
|
+
// Create error container
|
|
27
|
+
this.errorContainer = document.createElement('div');
|
|
28
|
+
this.errorContainer.id = 'jux-error-container';
|
|
29
|
+
this.errorContainer.style.cssText = `
|
|
30
|
+
position: fixed;
|
|
31
|
+
bottom: 20px;
|
|
32
|
+
right: 20px;
|
|
33
|
+
max-width: 500px;
|
|
34
|
+
max-height: 600px;
|
|
35
|
+
overflow-y: auto;
|
|
36
|
+
z-index: 999999;
|
|
37
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
document.body.appendChild(this.errorContainer);
|
|
42
|
+
|
|
43
|
+
// Listen for unhandled errors
|
|
44
|
+
window.addEventListener('error', (event) => {
|
|
45
|
+
this.captureError({
|
|
46
|
+
component: 'Global',
|
|
47
|
+
method: 'window.onerror',
|
|
48
|
+
message: event.message,
|
|
49
|
+
stack: event.error?.stack,
|
|
50
|
+
timestamp: new Date(),
|
|
51
|
+
context: { filename: event.filename, lineno: event.lineno, colno: event.colno }
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Listen for unhandled promise rejections
|
|
56
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
57
|
+
this.captureError({
|
|
58
|
+
component: 'Global',
|
|
59
|
+
method: 'unhandledrejection',
|
|
60
|
+
message: event.reason?.message || String(event.reason),
|
|
61
|
+
stack: event.reason?.stack,
|
|
62
|
+
timestamp: new Date(),
|
|
63
|
+
context: { promise: event.promise }
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Capture and display an error
|
|
70
|
+
*/
|
|
71
|
+
static captureError(error: ComponentError) {
|
|
72
|
+
if (!this.enabled) return;
|
|
73
|
+
|
|
74
|
+
this.errors.push(error);
|
|
75
|
+
this.renderError(error);
|
|
76
|
+
console.error(`[JUX Error] ${error.component}.${error.method}:`, error.message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Render an error card in the DOM
|
|
81
|
+
*/
|
|
82
|
+
private static renderError(error: ComponentError) {
|
|
83
|
+
if (!this.errorContainer) return;
|
|
84
|
+
|
|
85
|
+
const errorCard = document.createElement('div');
|
|
86
|
+
errorCard.style.cssText = `
|
|
87
|
+
background: linear-gradient(135deg, #1a1a1a 0%, #2a1a1a 100%);
|
|
88
|
+
border-left: 4px solid #ff4757;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
padding: 16px;
|
|
91
|
+
margin-bottom: 12px;
|
|
92
|
+
box-shadow: 0 4px 12px rgba(255, 71, 87, 0.3);
|
|
93
|
+
animation: slideIn 0.3s ease-out;
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
const time = error.timestamp.toLocaleTimeString();
|
|
97
|
+
|
|
98
|
+
errorCard.innerHTML = `
|
|
99
|
+
<style>
|
|
100
|
+
@keyframes slideIn {
|
|
101
|
+
from {
|
|
102
|
+
transform: translateX(100%);
|
|
103
|
+
opacity: 0;
|
|
104
|
+
}
|
|
105
|
+
to {
|
|
106
|
+
transform: translateX(0);
|
|
107
|
+
opacity: 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
.jux-error-header {
|
|
111
|
+
display: flex;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
align-items: center;
|
|
114
|
+
margin-bottom: 8px;
|
|
115
|
+
}
|
|
116
|
+
.jux-error-title {
|
|
117
|
+
color: #ff4757;
|
|
118
|
+
font-weight: bold;
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
}
|
|
121
|
+
.jux-error-time {
|
|
122
|
+
color: #666;
|
|
123
|
+
font-size: 11px;
|
|
124
|
+
}
|
|
125
|
+
.jux-error-location {
|
|
126
|
+
color: #ffa502;
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
margin-bottom: 8px;
|
|
129
|
+
}
|
|
130
|
+
.jux-error-message {
|
|
131
|
+
color: #e0e0e0;
|
|
132
|
+
margin-bottom: 8px;
|
|
133
|
+
line-height: 1.4;
|
|
134
|
+
}
|
|
135
|
+
.jux-error-stack {
|
|
136
|
+
background: #0a0a0a;
|
|
137
|
+
padding: 8px;
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
color: #888;
|
|
140
|
+
font-size: 10px;
|
|
141
|
+
max-height: 150px;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
margin-bottom: 8px;
|
|
144
|
+
}
|
|
145
|
+
.jux-error-context {
|
|
146
|
+
background: #0a0a0a;
|
|
147
|
+
padding: 8px;
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
color: #888;
|
|
150
|
+
font-size: 10px;
|
|
151
|
+
}
|
|
152
|
+
.jux-error-close {
|
|
153
|
+
background: #ff4757;
|
|
154
|
+
color: white;
|
|
155
|
+
border: none;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
padding: 4px 12px;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
font-size: 11px;
|
|
160
|
+
float: right;
|
|
161
|
+
}
|
|
162
|
+
.jux-error-close:hover {
|
|
163
|
+
background: #ff6b7a;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
166
|
+
<div class="jux-error-header">
|
|
167
|
+
<div class="jux-error-title">⚠️ Component Error</div>
|
|
168
|
+
<div class="jux-error-time">${time}</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="jux-error-location">
|
|
171
|
+
${error.component}.${error.method}()
|
|
172
|
+
</div>
|
|
173
|
+
<div class="jux-error-message">
|
|
174
|
+
${this.escapeHtml(error.message)}
|
|
175
|
+
</div>
|
|
176
|
+
${error.stack ? `
|
|
177
|
+
<details>
|
|
178
|
+
<summary style="color: #888; cursor: pointer; margin-bottom: 8px;">Stack Trace</summary>
|
|
179
|
+
<div class="jux-error-stack">${this.escapeHtml(error.stack)}</div>
|
|
180
|
+
</details>
|
|
181
|
+
` : ''}
|
|
182
|
+
${error.context ? `
|
|
183
|
+
<details>
|
|
184
|
+
<summary style="color: #888; cursor: pointer; margin-bottom: 8px;">Context</summary>
|
|
185
|
+
<div class="jux-error-context">${this.escapeHtml(JSON.stringify(error.context, null, 2))}</div>
|
|
186
|
+
</details>
|
|
187
|
+
` : ''}
|
|
188
|
+
<button class="jux-error-close">Dismiss</button>
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
// Add close button handler
|
|
192
|
+
const closeBtn = errorCard.querySelector('.jux-error-close');
|
|
193
|
+
closeBtn?.addEventListener('click', () => {
|
|
194
|
+
errorCard.style.animation = 'slideOut 0.3s ease-out';
|
|
195
|
+
setTimeout(() => errorCard.remove(), 300);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.errorContainer.appendChild(errorCard);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Escape HTML to prevent XSS
|
|
203
|
+
*/
|
|
204
|
+
private static escapeHtml(str: string): string {
|
|
205
|
+
const div = document.createElement('div');
|
|
206
|
+
div.textContent = str;
|
|
207
|
+
return div.innerHTML;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Clear all errors
|
|
212
|
+
*/
|
|
213
|
+
static clear() {
|
|
214
|
+
this.errors = [];
|
|
215
|
+
if (this.errorContainer) {
|
|
216
|
+
this.errorContainer.innerHTML = '';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Enable/disable error handler
|
|
222
|
+
*/
|
|
223
|
+
static setEnabled(enabled: boolean) {
|
|
224
|
+
this.enabled = enabled;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all captured errors
|
|
229
|
+
*/
|
|
230
|
+
static getErrors(): ComponentError[] {
|
|
231
|
+
return [...this.errors];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Utility function to wrap component methods with error handling
|
|
237
|
+
*/
|
|
238
|
+
export function withErrorHandling<T extends (...args: any[]) => any>(
|
|
239
|
+
component: string,
|
|
240
|
+
method: string,
|
|
241
|
+
fn: T,
|
|
242
|
+
context?: any
|
|
243
|
+
): T {
|
|
244
|
+
return ((...args: any[]) => {
|
|
245
|
+
try {
|
|
246
|
+
const result = fn(...args);
|
|
247
|
+
|
|
248
|
+
// Handle async functions
|
|
249
|
+
if (result instanceof Promise) {
|
|
250
|
+
return result.catch((error) => {
|
|
251
|
+
ErrorHandler.captureError({
|
|
252
|
+
component,
|
|
253
|
+
method,
|
|
254
|
+
message: error.message || String(error),
|
|
255
|
+
stack: error.stack,
|
|
256
|
+
timestamp: new Date(),
|
|
257
|
+
context
|
|
258
|
+
});
|
|
259
|
+
throw error;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
ErrorHandler.captureError({
|
|
266
|
+
component,
|
|
267
|
+
method,
|
|
268
|
+
message: error.message || String(error),
|
|
269
|
+
stack: error.stack,
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
context
|
|
272
|
+
});
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}) as T;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Initialize on import
|
|
279
|
+
if (typeof document !== 'undefined') {
|
|
280
|
+
if (document.readyState === 'loading') {
|
|
281
|
+
document.addEventListener('DOMContentLoaded', () => ErrorHandler.init());
|
|
282
|
+
} else {
|
|
283
|
+
ErrorHandler.init();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Footer component options
|
|
5
|
+
*/
|
|
6
|
+
export interface FooterOptions {
|
|
7
|
+
content?: string;
|
|
8
|
+
copyright?: string;
|
|
9
|
+
links?: Array<{ label: string; href: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Footer component state
|
|
14
|
+
*/
|
|
15
|
+
type FooterState = {
|
|
16
|
+
content: string;
|
|
17
|
+
copyright: string;
|
|
18
|
+
links: Array<{ label: string; href: string }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Footer component
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* const footer = jux.footer('myFooter', {
|
|
26
|
+
* copyright: '© 2025 My Company',
|
|
27
|
+
* links: [
|
|
28
|
+
* { label: 'Privacy', href: '/privacy' },
|
|
29
|
+
* { label: 'Terms', href: '/terms' }
|
|
30
|
+
* ]
|
|
31
|
+
* });
|
|
32
|
+
* footer.render('#appfooter');
|
|
33
|
+
*/
|
|
34
|
+
export class Footer extends Reactive {
|
|
35
|
+
state!: FooterState;
|
|
36
|
+
container: HTMLElement | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(componentId: string, options: FooterOptions = {}) {
|
|
39
|
+
super();
|
|
40
|
+
this._setComponentId(componentId);
|
|
41
|
+
|
|
42
|
+
this.state = this._createReactiveState({
|
|
43
|
+
content: options.content ?? '',
|
|
44
|
+
copyright: options.copyright ?? '',
|
|
45
|
+
links: options.links ?? []
|
|
46
|
+
}) as FooterState;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* -------------------------
|
|
50
|
+
* Fluent API
|
|
51
|
+
* ------------------------- */
|
|
52
|
+
|
|
53
|
+
content(value: string): this {
|
|
54
|
+
this.state.content = value;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
copyright(value: string): this {
|
|
59
|
+
this.state.copyright = value;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
links(value: Array<{ label: string; href: string }>): this {
|
|
64
|
+
this.state.links = value;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* -------------------------
|
|
69
|
+
* Render
|
|
70
|
+
* ------------------------- */
|
|
71
|
+
|
|
72
|
+
render(targetId?: string): this {
|
|
73
|
+
let container: HTMLElement;
|
|
74
|
+
|
|
75
|
+
if (targetId) {
|
|
76
|
+
const target = document.querySelector(targetId);
|
|
77
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
78
|
+
throw new Error(`Footer: Target element "${targetId}" not found`);
|
|
79
|
+
}
|
|
80
|
+
container = target;
|
|
81
|
+
} else {
|
|
82
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.container = container;
|
|
86
|
+
const { content, copyright, links } = this.state;
|
|
87
|
+
|
|
88
|
+
const footer = document.createElement('footer');
|
|
89
|
+
footer.className = 'jux-footer';
|
|
90
|
+
footer.id = this._componentId;
|
|
91
|
+
|
|
92
|
+
if (content) {
|
|
93
|
+
const contentEl = document.createElement('div');
|
|
94
|
+
contentEl.className = 'jux-footer-content';
|
|
95
|
+
contentEl.textContent = content;
|
|
96
|
+
footer.appendChild(contentEl);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (links.length > 0) {
|
|
100
|
+
const linksEl = document.createElement('div');
|
|
101
|
+
linksEl.className = 'jux-footer-links';
|
|
102
|
+
|
|
103
|
+
links.forEach(link => {
|
|
104
|
+
const linkEl = document.createElement('a');
|
|
105
|
+
linkEl.className = 'jux-footer-link';
|
|
106
|
+
linkEl.href = link.href;
|
|
107
|
+
linkEl.textContent = link.label;
|
|
108
|
+
linksEl.appendChild(linkEl);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
footer.appendChild(linksEl);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (copyright) {
|
|
115
|
+
const copyrightEl = document.createElement('div');
|
|
116
|
+
copyrightEl.className = 'jux-footer-copyright';
|
|
117
|
+
copyrightEl.textContent = copyright;
|
|
118
|
+
footer.appendChild(copyrightEl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
container.appendChild(footer);
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render to another Jux component's container
|
|
127
|
+
*/
|
|
128
|
+
renderTo(juxComponent: any): this {
|
|
129
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
130
|
+
throw new Error('Footer.renderTo: Invalid component - not an object');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
134
|
+
throw new Error('Footer.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Factory helper
|
|
143
|
+
*/
|
|
144
|
+
export function footer(componentId: string, options: FooterOptions = {}): Footer {
|
|
145
|
+
return new Footer(componentId, options);
|
|
146
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Header component options
|
|
5
|
+
*/
|
|
6
|
+
export interface HeaderOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
logo?: string;
|
|
9
|
+
navigation?: Array<{ label: string; href: string }>;
|
|
10
|
+
sticky?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Header component state
|
|
15
|
+
*/
|
|
16
|
+
type HeaderState = {
|
|
17
|
+
title: string;
|
|
18
|
+
logo: string;
|
|
19
|
+
navigation: Array<{ label: string; href: string }>;
|
|
20
|
+
sticky: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Header component
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* const header = jux.header('myHeader', {
|
|
28
|
+
* title: 'My App',
|
|
29
|
+
* navigation: [
|
|
30
|
+
* { label: 'Home', href: '/' },
|
|
31
|
+
* { label: 'About', href: '/about' }
|
|
32
|
+
* ]
|
|
33
|
+
* });
|
|
34
|
+
* header.render('#appheader');
|
|
35
|
+
*/
|
|
36
|
+
export class Header extends Reactive {
|
|
37
|
+
state!: HeaderState;
|
|
38
|
+
container: HTMLElement | null = null;
|
|
39
|
+
|
|
40
|
+
constructor(componentId: string, options: HeaderOptions = {}) {
|
|
41
|
+
super();
|
|
42
|
+
this._setComponentId(componentId);
|
|
43
|
+
|
|
44
|
+
this.state = this._createReactiveState({
|
|
45
|
+
title: options.title ?? '',
|
|
46
|
+
logo: options.logo ?? '',
|
|
47
|
+
navigation: options.navigation ?? [],
|
|
48
|
+
sticky: options.sticky ?? true
|
|
49
|
+
}) as HeaderState;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* -------------------------
|
|
53
|
+
* Fluent API
|
|
54
|
+
* ------------------------- */
|
|
55
|
+
|
|
56
|
+
title(value: string): this {
|
|
57
|
+
this.state.title = value;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logo(value: string): this {
|
|
62
|
+
this.state.logo = value;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
navigation(value: Array<{ label: string; href: string }>): this {
|
|
67
|
+
this.state.navigation = value;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sticky(value: boolean): this {
|
|
72
|
+
this.state.sticky = value;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* -------------------------
|
|
77
|
+
* Render
|
|
78
|
+
* ------------------------- */
|
|
79
|
+
|
|
80
|
+
render(targetId?: string): 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(`Header: 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 { title, logo, navigation, sticky } = this.state;
|
|
95
|
+
|
|
96
|
+
const header = document.createElement('header');
|
|
97
|
+
header.className = 'jux-header';
|
|
98
|
+
header.id = this._componentId;
|
|
99
|
+
|
|
100
|
+
if (sticky) {
|
|
101
|
+
header.classList.add('jux-header-sticky');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Logo section
|
|
105
|
+
if (logo || title) {
|
|
106
|
+
const logoSection = document.createElement('div');
|
|
107
|
+
logoSection.className = 'jux-header-logo';
|
|
108
|
+
|
|
109
|
+
if (logo) {
|
|
110
|
+
const logoImg = document.createElement('img');
|
|
111
|
+
logoImg.src = logo;
|
|
112
|
+
logoImg.alt = title || 'Logo';
|
|
113
|
+
logoSection.appendChild(logoImg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (title) {
|
|
117
|
+
const titleEl = document.createElement('span');
|
|
118
|
+
titleEl.className = 'jux-header-title';
|
|
119
|
+
titleEl.textContent = title;
|
|
120
|
+
logoSection.appendChild(titleEl);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
header.appendChild(logoSection);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Navigation
|
|
127
|
+
if (navigation.length > 0) {
|
|
128
|
+
const nav = document.createElement('nav');
|
|
129
|
+
nav.className = 'jux-header-nav';
|
|
130
|
+
|
|
131
|
+
navigation.forEach(item => {
|
|
132
|
+
const link = document.createElement('a');
|
|
133
|
+
link.className = 'jux-header-nav-item';
|
|
134
|
+
link.href = item.href;
|
|
135
|
+
link.textContent = item.label;
|
|
136
|
+
nav.appendChild(link);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
header.appendChild(nav);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
container.appendChild(header);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render to another Jux component's container
|
|
148
|
+
*/
|
|
149
|
+
renderTo(juxComponent: any): this {
|
|
150
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
151
|
+
throw new Error('Header.renderTo: Invalid component - not an object');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
155
|
+
throw new Error('Header.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Factory helper
|
|
164
|
+
*/
|
|
165
|
+
export function header(componentId: string, options: HeaderOptions = {}): Header {
|
|
166
|
+
return new Header(componentId, options);
|
|
167
|
+
}
|