juxscript 1.0.18 → 1.0.20
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/lib/components/alert.ts +124 -128
- package/lib/components/areachart.ts +169 -287
- package/lib/components/areachartsmooth.ts +2 -2
- package/lib/components/badge.ts +63 -72
- package/lib/components/barchart.ts +120 -48
- package/lib/components/button.ts +99 -101
- package/lib/components/card.ts +97 -121
- package/lib/components/chart-types.ts +159 -0
- package/lib/components/chart-utils.ts +160 -0
- package/lib/components/chart.ts +628 -48
- package/lib/components/checkbox.ts +137 -51
- package/lib/components/code.ts +89 -75
- package/lib/components/container.ts +1 -1
- package/lib/components/datepicker.ts +93 -78
- package/lib/components/dialog.ts +163 -130
- package/lib/components/divider.ts +111 -193
- package/lib/components/docs-data.json +711 -264
- package/lib/components/doughnutchart.ts +125 -57
- package/lib/components/dropdown.ts +172 -85
- package/lib/components/element.ts +66 -61
- package/lib/components/fileupload.ts +142 -171
- package/lib/components/heading.ts +64 -21
- package/lib/components/hero.ts +109 -34
- package/lib/components/icon.ts +247 -0
- package/lib/components/icons.ts +174 -0
- package/lib/components/include.ts +77 -2
- package/lib/components/input.ts +174 -125
- package/lib/components/list.ts +120 -79
- package/lib/components/menu.ts +97 -2
- package/lib/components/modal.ts +144 -63
- package/lib/components/nav.ts +153 -52
- package/lib/components/paragraph.ts +78 -28
- package/lib/components/progress.ts +83 -107
- package/lib/components/radio.ts +151 -52
- package/lib/components/select.ts +110 -102
- package/lib/components/sidebar.ts +148 -105
- package/lib/components/switch.ts +124 -125
- package/lib/components/table.ts +214 -137
- package/lib/components/tabs.ts +194 -113
- package/lib/components/theme-toggle.ts +38 -7
- package/lib/components/tooltip.ts +207 -47
- package/lib/jux.ts +24 -5
- package/lib/reactivity/state.ts +13 -299
- package/package.json +1 -2
package/lib/components/modal.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon } from './icons.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Modal component options
|
|
@@ -23,6 +25,7 @@ type ModalState = {
|
|
|
23
25
|
closeOnBackdropClick: boolean;
|
|
24
26
|
size: string;
|
|
25
27
|
isOpen: boolean;
|
|
28
|
+
open: boolean;
|
|
26
29
|
style: string;
|
|
27
30
|
class: string;
|
|
28
31
|
};
|
|
@@ -45,6 +48,15 @@ export class Modal {
|
|
|
45
48
|
_id: string;
|
|
46
49
|
id: string;
|
|
47
50
|
|
|
51
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
52
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
53
|
+
private _syncBindings: Array<{
|
|
54
|
+
property: string,
|
|
55
|
+
stateObj: State<any>,
|
|
56
|
+
toState?: Function,
|
|
57
|
+
toComponent?: Function
|
|
58
|
+
}> = [];
|
|
59
|
+
|
|
48
60
|
constructor(id: string, options: ModalOptions = {}) {
|
|
49
61
|
this._id = id;
|
|
50
62
|
this.id = id;
|
|
@@ -56,6 +68,7 @@ export class Modal {
|
|
|
56
68
|
closeOnBackdropClick: options.closeOnBackdropClick ?? true,
|
|
57
69
|
size: options.size ?? 'medium',
|
|
58
70
|
isOpen: false,
|
|
71
|
+
open: false,
|
|
59
72
|
style: options.style ?? '',
|
|
60
73
|
class: options.class ?? ''
|
|
61
74
|
};
|
|
@@ -130,94 +143,167 @@ export class Modal {
|
|
|
130
143
|
* Render
|
|
131
144
|
* ------------------------- */
|
|
132
145
|
|
|
146
|
+
bind(event: string, handler: Function): this {
|
|
147
|
+
this._bindings.push({ event, handler });
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
152
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
153
|
+
throw new Error(`Modal.sync: Expected a State object for property "${property}"`);
|
|
154
|
+
}
|
|
155
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
render(targetId?: string): this {
|
|
160
|
+
// === 1. SETUP: Get or create container ===
|
|
134
161
|
let container: HTMLElement;
|
|
135
|
-
|
|
136
162
|
if (targetId) {
|
|
137
163
|
const target = document.querySelector(targetId);
|
|
138
164
|
if (!target || !(target instanceof HTMLElement)) {
|
|
139
|
-
throw new Error(`Modal: Target
|
|
165
|
+
throw new Error(`Modal: Target "${targetId}" not found`);
|
|
140
166
|
}
|
|
141
167
|
container = target;
|
|
142
168
|
} else {
|
|
143
169
|
container = getOrCreateContainer(this._id);
|
|
144
170
|
}
|
|
145
|
-
|
|
146
171
|
this.container = container;
|
|
147
|
-
const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen, style, class: className } = this.state;
|
|
148
172
|
|
|
149
|
-
//
|
|
173
|
+
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
174
|
+
const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
|
|
175
|
+
const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
|
|
176
|
+
|
|
177
|
+
// === 3. BUILD: Create DOM elements ===
|
|
178
|
+
const overlay = document.createElement('div');
|
|
179
|
+
overlay.className = 'jux-modal-overlay';
|
|
180
|
+
overlay.id = this._id;
|
|
181
|
+
overlay.style.display = open ? 'flex' : 'none';
|
|
182
|
+
if (className) overlay.className += ` ${className}`;
|
|
183
|
+
if (style) overlay.setAttribute('style', style);
|
|
184
|
+
|
|
150
185
|
const modal = document.createElement('div');
|
|
151
186
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
152
|
-
modal.id = this._id;
|
|
153
187
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
188
|
+
if (showClose) {
|
|
189
|
+
const closeButton = document.createElement('button');
|
|
190
|
+
closeButton.className = 'jux-modal-close';
|
|
191
|
+
closeButton.innerHTML = '×';
|
|
192
|
+
modal.appendChild(closeButton);
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
195
|
+
if (title) {
|
|
196
|
+
const header = document.createElement('div');
|
|
197
|
+
header.className = 'jux-modal-header';
|
|
198
|
+
header.textContent = title;
|
|
199
|
+
modal.appendChild(header);
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
const body = document.createElement('div');
|
|
203
|
+
body.className = 'jux-modal-body';
|
|
204
|
+
body.innerHTML = content;
|
|
205
|
+
modal.appendChild(body);
|
|
165
206
|
|
|
166
|
-
|
|
167
|
-
const dialog = document.createElement('div');
|
|
168
|
-
dialog.className = 'jux-modal-dialog';
|
|
207
|
+
overlay.appendChild(modal);
|
|
169
208
|
|
|
170
|
-
//
|
|
171
|
-
if (title || showCloseButton) {
|
|
172
|
-
const header = document.createElement('div');
|
|
173
|
-
header.className = 'jux-modal-header';
|
|
209
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
174
210
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
211
|
+
// Default close behavior (only if NOT using sync)
|
|
212
|
+
if (!hasOpenSync) {
|
|
213
|
+
if (showClose) {
|
|
214
|
+
const closeButton = modal.querySelector('.jux-modal-close');
|
|
215
|
+
closeButton?.addEventListener('click', () => {
|
|
216
|
+
this.state.open = false;
|
|
217
|
+
overlay.style.display = 'none';
|
|
218
|
+
});
|
|
180
219
|
}
|
|
181
220
|
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
closeBtn.addEventListener('click', () => this.close());
|
|
221
|
+
if (closeOnBackdrop) {
|
|
222
|
+
overlay.addEventListener('click', (e) => {
|
|
223
|
+
if (e.target === overlay) {
|
|
224
|
+
this.state.open = false;
|
|
225
|
+
overlay.style.display = 'none';
|
|
226
|
+
}
|
|
227
|
+
});
|
|
190
228
|
}
|
|
191
|
-
|
|
192
|
-
dialog.appendChild(header);
|
|
193
229
|
}
|
|
194
230
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
// Wire custom bindings from .bind() calls
|
|
232
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
233
|
+
overlay.addEventListener(event, handler as EventListener);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Wire sync bindings from .sync() calls
|
|
237
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
238
|
+
if (property === 'open') {
|
|
239
|
+
const transformToState = toState || ((v: any) => Boolean(v));
|
|
240
|
+
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
241
|
+
|
|
242
|
+
let isUpdating = false;
|
|
243
|
+
|
|
244
|
+
// State → Component
|
|
245
|
+
stateObj.subscribe((val: any) => {
|
|
246
|
+
if (isUpdating) return;
|
|
247
|
+
const transformed = transformToComponent(val);
|
|
248
|
+
this.state.open = transformed;
|
|
249
|
+
overlay.style.display = transformed ? 'flex' : 'none';
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Component → State (close button, backdrop)
|
|
253
|
+
if (showClose) {
|
|
254
|
+
const closeButton = modal.querySelector('.jux-modal-close');
|
|
255
|
+
closeButton?.addEventListener('click', () => {
|
|
256
|
+
if (isUpdating) return;
|
|
257
|
+
isUpdating = true;
|
|
258
|
+
|
|
259
|
+
this.state.open = false;
|
|
260
|
+
overlay.style.display = 'none';
|
|
261
|
+
stateObj.set(transformToState(false));
|
|
262
|
+
|
|
263
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
200
266
|
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
if (closeOnBackdrop) {
|
|
268
|
+
overlay.addEventListener('click', (e) => {
|
|
269
|
+
if (e.target === overlay) {
|
|
270
|
+
if (isUpdating) return;
|
|
271
|
+
isUpdating = true;
|
|
203
272
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (e.target === modal) {
|
|
208
|
-
this.close();
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
273
|
+
this.state.open = false;
|
|
274
|
+
overlay.style.display = 'none';
|
|
275
|
+
stateObj.set(transformToState(false));
|
|
212
276
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
277
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
217
281
|
}
|
|
218
|
-
|
|
219
|
-
|
|
282
|
+
else if (property === 'content') {
|
|
283
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
284
|
+
|
|
285
|
+
stateObj.subscribe((val: any) => {
|
|
286
|
+
const transformed = transformToComponent(val);
|
|
287
|
+
body.innerHTML = transformed;
|
|
288
|
+
this.state.content = transformed;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (property === 'title') {
|
|
292
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
293
|
+
|
|
294
|
+
stateObj.subscribe((val: any) => {
|
|
295
|
+
const transformed = transformToComponent(val);
|
|
296
|
+
const header = modal.querySelector('.jux-modal-header');
|
|
297
|
+
if (header) {
|
|
298
|
+
header.textContent = transformed;
|
|
299
|
+
}
|
|
300
|
+
this.state.title = transformed;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
220
304
|
|
|
305
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
306
|
+
container.appendChild(overlay);
|
|
221
307
|
return this;
|
|
222
308
|
}
|
|
223
309
|
|
|
@@ -225,14 +311,9 @@ export class Modal {
|
|
|
225
311
|
* Render to another Jux component's container
|
|
226
312
|
*/
|
|
227
313
|
renderTo(juxComponent: any): this {
|
|
228
|
-
if (!juxComponent
|
|
229
|
-
throw new Error('Modal.renderTo: Invalid component
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
233
|
-
throw new Error('Modal.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
314
|
+
if (!juxComponent?._id) {
|
|
315
|
+
throw new Error('Modal.renderTo: Invalid component');
|
|
234
316
|
}
|
|
235
|
-
|
|
236
317
|
return this.render(`#${juxComponent._id}`);
|
|
237
318
|
}
|
|
238
319
|
}
|
package/lib/components/nav.ts
CHANGED
|
@@ -1,61 +1,62 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon } from './icons.js';
|
|
2
4
|
import { req } from './req.js';
|
|
3
5
|
|
|
4
|
-
/**
|
|
5
|
-
* Nav item configuration
|
|
6
|
-
*/
|
|
7
6
|
export interface NavItem {
|
|
8
7
|
label: string;
|
|
9
8
|
href: string;
|
|
10
9
|
active?: boolean;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
export interface NavBrand {
|
|
13
|
+
text?: string;
|
|
14
|
+
href?: string;
|
|
15
|
+
icon?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
16
18
|
export interface NavOptions {
|
|
17
19
|
items?: NavItem[];
|
|
20
|
+
brand?: NavBrand;
|
|
18
21
|
variant?: 'default' | 'pills' | 'tabs';
|
|
22
|
+
sticky?: boolean;
|
|
19
23
|
style?: string;
|
|
20
24
|
class?: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
/**
|
|
24
|
-
* Nav component state
|
|
25
|
-
*/
|
|
26
27
|
type NavState = {
|
|
27
28
|
items: NavItem[];
|
|
29
|
+
brand?: NavBrand;
|
|
28
30
|
variant: string;
|
|
31
|
+
sticky: boolean;
|
|
29
32
|
style: string;
|
|
30
33
|
class: string;
|
|
31
34
|
};
|
|
32
35
|
|
|
33
|
-
/**
|
|
34
|
-
* Nav component
|
|
35
|
-
*
|
|
36
|
-
* Usage:
|
|
37
|
-
* const nav = jux.nav('myNav', {
|
|
38
|
-
* variant: 'pills',
|
|
39
|
-
* items: [
|
|
40
|
-
* { label: 'Home', href: '/', active: true },
|
|
41
|
-
* { label: 'About', href: '/about' }
|
|
42
|
-
* ]
|
|
43
|
-
* });
|
|
44
|
-
* nav.render();
|
|
45
|
-
*/
|
|
46
36
|
export class Nav {
|
|
47
37
|
state: NavState;
|
|
48
38
|
container: HTMLElement | null = null;
|
|
49
39
|
_id: string;
|
|
50
40
|
id: string;
|
|
51
41
|
|
|
42
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
43
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
44
|
+
private _syncBindings: Array<{
|
|
45
|
+
property: string,
|
|
46
|
+
stateObj: State<any>,
|
|
47
|
+
toState?: Function,
|
|
48
|
+
toComponent?: Function
|
|
49
|
+
}> = [];
|
|
50
|
+
|
|
52
51
|
constructor(id: string, options: NavOptions = {}) {
|
|
53
52
|
this._id = id;
|
|
54
53
|
this.id = id;
|
|
55
54
|
|
|
56
55
|
this.state = {
|
|
57
56
|
items: options.items ?? [],
|
|
57
|
+
brand: options.brand,
|
|
58
58
|
variant: options.variant ?? 'default',
|
|
59
|
+
sticky: options.sticky ?? false,
|
|
59
60
|
style: options.style ?? '',
|
|
60
61
|
class: options.class ?? ''
|
|
61
62
|
};
|
|
@@ -90,11 +91,21 @@ export class Nav {
|
|
|
90
91
|
return this;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
brand(value: NavBrand): this {
|
|
95
|
+
this.state.brand = value;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
variant(value: string): this {
|
|
94
100
|
this.state.variant = value;
|
|
95
101
|
return this;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
sticky(value: boolean): this {
|
|
105
|
+
this.state.sticky = value;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
style(value: string): this {
|
|
99
110
|
this.state.style = value;
|
|
100
111
|
return this;
|
|
@@ -105,74 +116,164 @@ export class Nav {
|
|
|
105
116
|
return this;
|
|
106
117
|
}
|
|
107
118
|
|
|
119
|
+
bind(event: string, handler: Function): this {
|
|
120
|
+
this._bindings.push({ event, handler });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
125
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
126
|
+
throw new Error(`Nav.sync: Expected a State object for property "${property}"`);
|
|
127
|
+
}
|
|
128
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
/* -------------------------
|
|
109
|
-
* Render
|
|
133
|
+
* Render (5-Step Pattern)
|
|
110
134
|
* ------------------------- */
|
|
111
135
|
|
|
112
136
|
render(targetId?: string): this {
|
|
137
|
+
// === 1. SETUP: Get or create container ===
|
|
113
138
|
let container: HTMLElement;
|
|
114
|
-
|
|
115
139
|
if (targetId) {
|
|
116
140
|
const target = document.querySelector(targetId);
|
|
117
141
|
if (!target || !(target instanceof HTMLElement)) {
|
|
118
|
-
throw new Error(`Nav: Target
|
|
142
|
+
throw new Error(`Nav: Target "${targetId}" not found`);
|
|
119
143
|
}
|
|
120
144
|
container = target;
|
|
121
145
|
} else {
|
|
122
146
|
container = getOrCreateContainer(this._id);
|
|
123
147
|
}
|
|
124
|
-
|
|
125
148
|
this.container = container;
|
|
126
|
-
const { items, variant, style, class: className } = this.state;
|
|
127
149
|
|
|
150
|
+
// === 2. PREPARE: Destructure state ===
|
|
151
|
+
const { brand, items, variant, sticky, style, class: className } = this.state;
|
|
152
|
+
|
|
153
|
+
// === 3. BUILD: Create DOM elements ===
|
|
128
154
|
const nav = document.createElement('nav');
|
|
129
155
|
nav.className = `jux-nav jux-nav-${variant}`;
|
|
130
156
|
nav.id = this._id;
|
|
157
|
+
if (sticky) nav.classList.add('jux-nav-sticky');
|
|
158
|
+
if (className) nav.className += ` ${className}`;
|
|
159
|
+
if (style) nav.setAttribute('style', style);
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
// Brand/Logo
|
|
162
|
+
if (brand) {
|
|
163
|
+
const brandEl = document.createElement('div');
|
|
164
|
+
brandEl.className = 'jux-nav-brand';
|
|
135
165
|
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
if (brand.href) {
|
|
167
|
+
const link = document.createElement('a');
|
|
168
|
+
link.href = brand.href;
|
|
169
|
+
if (brand.icon) {
|
|
170
|
+
const icon = document.createElement('span');
|
|
171
|
+
icon.appendChild(renderIcon(brand.icon));
|
|
172
|
+
link.appendChild(icon);
|
|
173
|
+
}
|
|
174
|
+
if (brand.text) {
|
|
175
|
+
const text = document.createElement('span');
|
|
176
|
+
text.textContent = brand.text;
|
|
177
|
+
link.appendChild(text);
|
|
178
|
+
}
|
|
179
|
+
brandEl.appendChild(link);
|
|
180
|
+
} else {
|
|
181
|
+
if (brand.icon) {
|
|
182
|
+
const icon = document.createElement('span');
|
|
183
|
+
icon.appendChild(renderIcon(brand.icon));
|
|
184
|
+
brandEl.appendChild(icon);
|
|
185
|
+
}
|
|
186
|
+
if (brand.text) {
|
|
187
|
+
const text = document.createElement('span');
|
|
188
|
+
text.textContent = brand.text;
|
|
189
|
+
brandEl.appendChild(text);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
nav.appendChild(brandEl);
|
|
138
194
|
}
|
|
139
195
|
|
|
196
|
+
// Nav items
|
|
197
|
+
const itemsContainer = document.createElement('div');
|
|
198
|
+
itemsContainer.className = 'jux-nav-items';
|
|
199
|
+
|
|
140
200
|
items.forEach(item => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
const navItem = document.createElement('a');
|
|
202
|
+
navItem.className = 'jux-nav-item';
|
|
203
|
+
navItem.href = item.href;
|
|
204
|
+
navItem.textContent = item.label;
|
|
205
|
+
if (item.active) navItem.classList.add('jux-nav-item-active');
|
|
206
|
+
itemsContainer.appendChild(navItem);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
nav.appendChild(itemsContainer);
|
|
210
|
+
|
|
211
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
212
|
+
|
|
213
|
+
// Wire custom bindings from .bind() calls
|
|
214
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
215
|
+
nav.addEventListener(event, handler as EventListener);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Wire sync bindings from .sync() calls
|
|
219
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
220
|
+
if (property === 'items') {
|
|
221
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
222
|
+
|
|
223
|
+
stateObj.subscribe((val: any) => {
|
|
224
|
+
const transformed = transformToComponent(val);
|
|
225
|
+
this.state.items = transformed;
|
|
226
|
+
this._setActiveStates();
|
|
145
227
|
|
|
146
|
-
|
|
147
|
-
|
|
228
|
+
// Re-render items
|
|
229
|
+
itemsContainer.innerHTML = '';
|
|
230
|
+
this.state.items.forEach((item: any) => {
|
|
231
|
+
const navItem = document.createElement('a');
|
|
232
|
+
navItem.className = 'jux-nav-item';
|
|
233
|
+
navItem.href = item.href;
|
|
234
|
+
navItem.textContent = item.label;
|
|
235
|
+
if (item.active) navItem.classList.add('jux-nav-item-active');
|
|
236
|
+
itemsContainer.appendChild(navItem);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
148
239
|
}
|
|
240
|
+
else if (property === 'brand') {
|
|
241
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
149
242
|
|
|
150
|
-
|
|
243
|
+
stateObj.subscribe((val: any) => {
|
|
244
|
+
const transformed = transformToComponent(val);
|
|
245
|
+
const brandEl = nav.querySelector('.jux-nav-brand');
|
|
246
|
+
if (brandEl && transformed.text) {
|
|
247
|
+
const textEl = brandEl.querySelector('span:last-child');
|
|
248
|
+
if (textEl) {
|
|
249
|
+
textEl.textContent = transformed.text;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.state.brand = transformed;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
151
255
|
});
|
|
152
256
|
|
|
257
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
153
258
|
container.appendChild(nav);
|
|
259
|
+
|
|
260
|
+
requestAnimationFrame(() => {
|
|
261
|
+
if ((window as any).lucide) {
|
|
262
|
+
(window as any).lucide.createIcons();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
154
266
|
return this;
|
|
155
267
|
}
|
|
156
268
|
|
|
157
|
-
/**
|
|
158
|
-
* Render to another Jux component's container
|
|
159
|
-
*/
|
|
160
269
|
renderTo(juxComponent: any): this {
|
|
161
|
-
if (!juxComponent
|
|
162
|
-
throw new Error('Nav.renderTo: Invalid component
|
|
270
|
+
if (!juxComponent?._id) {
|
|
271
|
+
throw new Error('Nav.renderTo: Invalid component');
|
|
163
272
|
}
|
|
164
|
-
|
|
165
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
166
|
-
throw new Error('Nav.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
273
|
return this.render(`#${juxComponent._id}`);
|
|
170
274
|
}
|
|
171
275
|
}
|
|
172
276
|
|
|
173
|
-
/**
|
|
174
|
-
* Factory helper
|
|
175
|
-
*/
|
|
176
277
|
export function nav(id: string, options: NavOptions = {}): Nav {
|
|
177
278
|
return new Nav(id, options);
|
|
178
279
|
}
|