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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Heading options
|
|
@@ -33,6 +34,15 @@ export class Heading {
|
|
|
33
34
|
_id: string;
|
|
34
35
|
id: string;
|
|
35
36
|
|
|
37
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
38
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
39
|
+
private _syncBindings: Array<{
|
|
40
|
+
property: string,
|
|
41
|
+
stateObj: State<any>,
|
|
42
|
+
toState?: Function,
|
|
43
|
+
toComponent?: Function
|
|
44
|
+
}> = [];
|
|
45
|
+
|
|
36
46
|
constructor(id: string, options: HeadingOptions = {}) {
|
|
37
47
|
this._id = id;
|
|
38
48
|
this.id = id;
|
|
@@ -69,46 +79,79 @@ export class Heading {
|
|
|
69
79
|
return this;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
bind(event: string, handler: Function): this {
|
|
83
|
+
this._bindings.push({ event, handler });
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
88
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
89
|
+
throw new Error(`Heading.sync: Expected a State object for property "${property}"`);
|
|
90
|
+
}
|
|
91
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
/* -------------------------
|
|
73
96
|
* Render
|
|
74
97
|
* ------------------------- */
|
|
75
98
|
|
|
76
|
-
render(targetId?: string
|
|
99
|
+
render(targetId?: string): this {
|
|
100
|
+
// === 1. SETUP: Get or create container ===
|
|
77
101
|
let container: HTMLElement;
|
|
78
|
-
|
|
79
102
|
if (targetId) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const target = document.querySelector(targetId);
|
|
84
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
85
|
-
throw new Error(`Heading: Target element "${targetId}" not found`);
|
|
86
|
-
}
|
|
87
|
-
container = target;
|
|
103
|
+
const target = document.querySelector(targetId);
|
|
104
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
105
|
+
throw new Error(`Heading: Target "${targetId}" not found`);
|
|
88
106
|
}
|
|
107
|
+
container = target;
|
|
89
108
|
} else {
|
|
90
109
|
container = getOrCreateContainer(this._id);
|
|
91
110
|
}
|
|
92
|
-
|
|
93
111
|
this.container = container;
|
|
94
|
-
const { level, text, class: className, style } = this.state;
|
|
95
112
|
|
|
113
|
+
// === 2. PREPARE: Destructure state ===
|
|
114
|
+
const { text, level, style, class: className } = this.state;
|
|
115
|
+
|
|
116
|
+
// === 3. BUILD: Create DOM elements ===
|
|
96
117
|
const heading = document.createElement(`h${level}`) as HTMLHeadingElement;
|
|
118
|
+
heading.className = `jux-heading jux-heading-${level}`;
|
|
97
119
|
heading.id = this._id;
|
|
98
120
|
heading.textContent = text;
|
|
121
|
+
if (className) heading.className += ` ${className}`;
|
|
122
|
+
if (style) heading.setAttribute('style', style);
|
|
123
|
+
|
|
124
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
125
|
+
|
|
126
|
+
// Wire custom bindings from .bind() calls
|
|
127
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
128
|
+
heading.addEventListener(event, handler as EventListener);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Wire sync bindings from .sync() calls
|
|
132
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
133
|
+
if (property === 'text') {
|
|
134
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
135
|
+
|
|
136
|
+
stateObj.subscribe((val: any) => {
|
|
137
|
+
const transformed = transformToComponent(val);
|
|
138
|
+
heading.textContent = transformed;
|
|
139
|
+
this.state.text = transformed;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
99
143
|
|
|
100
|
-
|
|
101
|
-
heading.className = className;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (style) {
|
|
105
|
-
heading.setAttribute('style', style);
|
|
106
|
-
}
|
|
107
|
-
|
|
144
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
108
145
|
container.appendChild(heading);
|
|
109
|
-
|
|
110
146
|
return this;
|
|
111
147
|
}
|
|
148
|
+
|
|
149
|
+
renderTo(juxComponent: any): this {
|
|
150
|
+
if (!juxComponent?._id) {
|
|
151
|
+
throw new Error('Heading.renderTo: Invalid component');
|
|
152
|
+
}
|
|
153
|
+
return this.render(`#${juxComponent._id}`);
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
package/lib/components/hero.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hero component options
|
|
@@ -26,6 +27,9 @@ type HeroState = {
|
|
|
26
27
|
variant: string;
|
|
27
28
|
style: string;
|
|
28
29
|
class: string;
|
|
30
|
+
content: string;
|
|
31
|
+
backgroundOverlay: boolean;
|
|
32
|
+
centered: boolean;
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -45,6 +49,15 @@ export class Hero {
|
|
|
45
49
|
_id: string;
|
|
46
50
|
id: string;
|
|
47
51
|
|
|
52
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
53
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
54
|
+
private _syncBindings: Array<{
|
|
55
|
+
property: string,
|
|
56
|
+
stateObj: State<any>,
|
|
57
|
+
toState?: Function,
|
|
58
|
+
toComponent?: Function
|
|
59
|
+
}> = [];
|
|
60
|
+
|
|
48
61
|
constructor(id: string, options: HeroOptions = {}) {
|
|
49
62
|
this._id = id;
|
|
50
63
|
this.id = id;
|
|
@@ -57,7 +70,10 @@ export class Hero {
|
|
|
57
70
|
backgroundImage: options.backgroundImage ?? '',
|
|
58
71
|
variant: options.variant ?? 'default',
|
|
59
72
|
style: options.style ?? '',
|
|
60
|
-
class: options.class ?? ''
|
|
73
|
+
class: options.class ?? '',
|
|
74
|
+
content: '',
|
|
75
|
+
backgroundOverlay: false,
|
|
76
|
+
centered: false
|
|
61
77
|
};
|
|
62
78
|
}
|
|
63
79
|
|
|
@@ -105,70 +121,134 @@ export class Hero {
|
|
|
105
121
|
return this;
|
|
106
122
|
}
|
|
107
123
|
|
|
124
|
+
bind(event: string, handler: Function): this {
|
|
125
|
+
this._bindings.push({ event, handler });
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
130
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
131
|
+
throw new Error(`Hero.sync: Expected a State object for property "${property}"`);
|
|
132
|
+
}
|
|
133
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
/* -------------------------
|
|
109
138
|
* Render
|
|
110
139
|
* ------------------------- */
|
|
111
140
|
|
|
112
141
|
render(targetId?: string): this {
|
|
142
|
+
// === 1. SETUP: Get or create container ===
|
|
113
143
|
let container: HTMLElement;
|
|
114
|
-
|
|
115
144
|
if (targetId) {
|
|
116
145
|
const target = document.querySelector(targetId);
|
|
117
146
|
if (!target || !(target instanceof HTMLElement)) {
|
|
118
|
-
throw new Error(`Hero: Target
|
|
147
|
+
throw new Error(`Hero: Target "${targetId}" not found`);
|
|
119
148
|
}
|
|
120
149
|
container = target;
|
|
121
150
|
} else {
|
|
122
151
|
container = getOrCreateContainer(this._id);
|
|
123
152
|
}
|
|
124
|
-
|
|
125
153
|
this.container = container;
|
|
126
|
-
const { title, subtitle, cta, ctaLink, backgroundImage, variant, style, class: className } = this.state;
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
hero.id = this._id;
|
|
155
|
+
// === 2. PREPARE: Destructure state ===
|
|
156
|
+
const { title, subtitle, content, backgroundImage, backgroundOverlay, centered, style, class: className } = this.state;
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
// === 3. BUILD: Create DOM elements ===
|
|
159
|
+
const hero = document.createElement('section');
|
|
160
|
+
hero.className = 'jux-hero';
|
|
161
|
+
hero.id = this._id;
|
|
162
|
+
if (centered) hero.classList.add('jux-hero-centered');
|
|
163
|
+
if (className) hero.className += ` ${className}`;
|
|
164
|
+
if (style) hero.setAttribute('style', style);
|
|
139
165
|
|
|
140
166
|
if (backgroundImage) {
|
|
141
167
|
hero.style.backgroundImage = `url(${backgroundImage})`;
|
|
168
|
+
if (backgroundOverlay) {
|
|
169
|
+
const overlay = document.createElement('div');
|
|
170
|
+
overlay.className = 'jux-hero-overlay';
|
|
171
|
+
hero.appendChild(overlay);
|
|
172
|
+
}
|
|
142
173
|
}
|
|
143
174
|
|
|
144
|
-
const
|
|
145
|
-
|
|
175
|
+
const contentContainer = document.createElement('div');
|
|
176
|
+
contentContainer.className = 'jux-hero-content';
|
|
146
177
|
|
|
147
178
|
if (title) {
|
|
148
179
|
const titleEl = document.createElement('h1');
|
|
149
180
|
titleEl.className = 'jux-hero-title';
|
|
181
|
+
titleEl.id = `${this._id}-title`;
|
|
150
182
|
titleEl.textContent = title;
|
|
151
|
-
|
|
183
|
+
contentContainer.appendChild(titleEl);
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
if (subtitle) {
|
|
155
187
|
const subtitleEl = document.createElement('p');
|
|
156
188
|
subtitleEl.className = 'jux-hero-subtitle';
|
|
189
|
+
subtitleEl.id = `${this._id}-subtitle`;
|
|
157
190
|
subtitleEl.textContent = subtitle;
|
|
158
|
-
|
|
191
|
+
contentContainer.appendChild(subtitleEl);
|
|
159
192
|
}
|
|
160
193
|
|
|
161
|
-
if (
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
content.appendChild(ctaEl);
|
|
194
|
+
if (content) {
|
|
195
|
+
const contentEl = document.createElement('div');
|
|
196
|
+
contentEl.className = 'jux-hero-body';
|
|
197
|
+
contentEl.innerHTML = content;
|
|
198
|
+
contentContainer.appendChild(contentEl);
|
|
167
199
|
}
|
|
168
200
|
|
|
169
|
-
hero.appendChild(
|
|
170
|
-
|
|
201
|
+
hero.appendChild(contentContainer);
|
|
202
|
+
|
|
203
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
204
|
+
|
|
205
|
+
// Wire custom bindings from .bind() calls
|
|
206
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
207
|
+
hero.addEventListener(event, handler as EventListener);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Wire sync bindings from .sync() calls
|
|
211
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
212
|
+
if (property === 'title') {
|
|
213
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
214
|
+
|
|
215
|
+
stateObj.subscribe((val: any) => {
|
|
216
|
+
const transformed = transformToComponent(val);
|
|
217
|
+
const titleEl = document.getElementById(`${this._id}-title`);
|
|
218
|
+
if (titleEl) {
|
|
219
|
+
titleEl.textContent = transformed;
|
|
220
|
+
}
|
|
221
|
+
this.state.title = transformed;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else if (property === 'subtitle') {
|
|
225
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
226
|
+
|
|
227
|
+
stateObj.subscribe((val: any) => {
|
|
228
|
+
const transformed = transformToComponent(val);
|
|
229
|
+
const subtitleEl = document.getElementById(`${this._id}-subtitle`);
|
|
230
|
+
if (subtitleEl) {
|
|
231
|
+
subtitleEl.textContent = transformed;
|
|
232
|
+
}
|
|
233
|
+
this.state.subtitle = transformed;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else if (property === 'content') {
|
|
237
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
238
|
+
|
|
239
|
+
stateObj.subscribe((val: any) => {
|
|
240
|
+
const transformed = transformToComponent(val);
|
|
241
|
+
const contentEl = hero.querySelector('.jux-hero-body');
|
|
242
|
+
if (contentEl) {
|
|
243
|
+
contentEl.innerHTML = transformed;
|
|
244
|
+
}
|
|
245
|
+
this.state.content = transformed;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
171
249
|
|
|
250
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
251
|
+
container.appendChild(hero);
|
|
172
252
|
return this;
|
|
173
253
|
}
|
|
174
254
|
|
|
@@ -176,14 +256,9 @@ export class Hero {
|
|
|
176
256
|
* Render to another Jux component's container
|
|
177
257
|
*/
|
|
178
258
|
renderTo(juxComponent: any): this {
|
|
179
|
-
if (!juxComponent
|
|
180
|
-
throw new Error('Hero.renderTo: Invalid component
|
|
259
|
+
if (!juxComponent?._id) {
|
|
260
|
+
throw new Error('Hero.renderTo: Invalid component');
|
|
181
261
|
}
|
|
182
|
-
|
|
183
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
184
|
-
throw new Error('Hero.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
262
|
return this.render(`#${juxComponent._id}`);
|
|
188
263
|
}
|
|
189
264
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon, renderEmoji } from './icons.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Icon component options
|
|
7
|
+
*/
|
|
8
|
+
export interface IconOptions {
|
|
9
|
+
value: string;
|
|
10
|
+
size?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
useEmoji?: boolean;
|
|
13
|
+
style?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Icon component state
|
|
19
|
+
*/
|
|
20
|
+
type IconState = {
|
|
21
|
+
value: string;
|
|
22
|
+
size: string;
|
|
23
|
+
color: string;
|
|
24
|
+
useEmoji: boolean;
|
|
25
|
+
style: string;
|
|
26
|
+
class: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Icon component - Render an icon from emoji, icon name, or image path
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* // Render Lucide icon from emoji
|
|
34
|
+
* jux.icon('myIcon', { value: '🚀' }).render('#container');
|
|
35
|
+
*
|
|
36
|
+
* // Render Lucide icon from name
|
|
37
|
+
* jux.icon('myIcon', { value: 'rocket', size: '32px', color: 'red' }).render('#container');
|
|
38
|
+
*
|
|
39
|
+
* // Render image icon
|
|
40
|
+
* jux.icon('myIcon', { value: '/path/icon.png' }).render('#container');
|
|
41
|
+
*
|
|
42
|
+
* // Force emoji rendering (skip conversion)
|
|
43
|
+
* jux.icon('myIcon', { value: '🚀', useEmoji: true }).render('#container');
|
|
44
|
+
*/
|
|
45
|
+
export class Icon {
|
|
46
|
+
state: IconState;
|
|
47
|
+
container: HTMLElement | null = null;
|
|
48
|
+
_id: string;
|
|
49
|
+
id: string;
|
|
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
|
+
|
|
60
|
+
constructor(id: string, options: IconOptions) {
|
|
61
|
+
this._id = id;
|
|
62
|
+
this.id = id;
|
|
63
|
+
|
|
64
|
+
this.state = {
|
|
65
|
+
value: options.value,
|
|
66
|
+
size: options.size ?? '24px',
|
|
67
|
+
color: options.color ?? '',
|
|
68
|
+
useEmoji: options.useEmoji ?? false,
|
|
69
|
+
style: options.style ?? '',
|
|
70
|
+
class: options.class ?? ''
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* -------------------------
|
|
75
|
+
* Fluent API
|
|
76
|
+
* ------------------------- */
|
|
77
|
+
|
|
78
|
+
value(value: string): this {
|
|
79
|
+
this.state.value = value;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
size(value: string): this {
|
|
84
|
+
this.state.size = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
color(value: string): this {
|
|
89
|
+
this.state.color = value;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
useEmoji(value: boolean): this {
|
|
94
|
+
this.state.useEmoji = value;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
style(value: string): this {
|
|
99
|
+
this.state.style = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class(value: string): this {
|
|
104
|
+
this.state.class = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bind(event: string, handler: Function): this {
|
|
109
|
+
this._bindings.push({ event, handler });
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
114
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
115
|
+
throw new Error(`Icon.sync: Expected a State object for property "${property}"`);
|
|
116
|
+
}
|
|
117
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* -------------------------
|
|
122
|
+
* Render (5-Step Pattern)
|
|
123
|
+
* ------------------------- */
|
|
124
|
+
|
|
125
|
+
render(targetId?: string): this {
|
|
126
|
+
// === 1. SETUP: Get or create container ===
|
|
127
|
+
let container: HTMLElement;
|
|
128
|
+
if (targetId) {
|
|
129
|
+
const target = document.querySelector(targetId);
|
|
130
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
131
|
+
throw new Error(`Icon: Target element "${targetId}" not found`);
|
|
132
|
+
}
|
|
133
|
+
container = target;
|
|
134
|
+
} else {
|
|
135
|
+
container = getOrCreateContainer(this._id);
|
|
136
|
+
}
|
|
137
|
+
this.container = container;
|
|
138
|
+
|
|
139
|
+
// === 2. PREPARE: Destructure state ===
|
|
140
|
+
const { value, size, color, useEmoji, style, class: className } = this.state;
|
|
141
|
+
|
|
142
|
+
// === 3. BUILD: Create DOM elements ===
|
|
143
|
+
const wrapper = document.createElement('span');
|
|
144
|
+
wrapper.className = 'jux-icon';
|
|
145
|
+
wrapper.id = this._id;
|
|
146
|
+
if (className) wrapper.className += ` ${className}`;
|
|
147
|
+
if (style) wrapper.setAttribute('style', style);
|
|
148
|
+
|
|
149
|
+
const iconElement = useEmoji ? renderEmoji(value) : renderIcon(value);
|
|
150
|
+
iconElement.style.width = size;
|
|
151
|
+
iconElement.style.height = size;
|
|
152
|
+
if (color) iconElement.style.color = color;
|
|
153
|
+
|
|
154
|
+
wrapper.appendChild(iconElement);
|
|
155
|
+
|
|
156
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
157
|
+
|
|
158
|
+
// Wire custom bindings from .bind() calls
|
|
159
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
160
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Wire sync bindings from .sync() calls
|
|
164
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
165
|
+
if (property === 'value') {
|
|
166
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
167
|
+
|
|
168
|
+
stateObj.subscribe((val: any) => {
|
|
169
|
+
const transformed = transformToComponent(val);
|
|
170
|
+
this.state.value = transformed;
|
|
171
|
+
|
|
172
|
+
// Re-render icon
|
|
173
|
+
wrapper.innerHTML = '';
|
|
174
|
+
const newIcon = this.state.useEmoji ? renderEmoji(transformed) : renderIcon(transformed);
|
|
175
|
+
newIcon.style.width = this.state.size;
|
|
176
|
+
newIcon.style.height = this.state.size;
|
|
177
|
+
if (this.state.color) newIcon.style.color = this.state.color;
|
|
178
|
+
wrapper.appendChild(newIcon);
|
|
179
|
+
|
|
180
|
+
requestAnimationFrame(() => {
|
|
181
|
+
if ((window as any).lucide) {
|
|
182
|
+
(window as any).lucide.createIcons();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else if (property === 'size') {
|
|
188
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
189
|
+
|
|
190
|
+
stateObj.subscribe((val: any) => {
|
|
191
|
+
const transformed = transformToComponent(val);
|
|
192
|
+
const icon = wrapper.querySelector('img, svg, span');
|
|
193
|
+
if (icon instanceof HTMLElement) {
|
|
194
|
+
icon.style.width = transformed;
|
|
195
|
+
icon.style.height = transformed;
|
|
196
|
+
}
|
|
197
|
+
this.state.size = transformed;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (property === 'color') {
|
|
201
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
202
|
+
|
|
203
|
+
stateObj.subscribe((val: any) => {
|
|
204
|
+
const transformed = transformToComponent(val);
|
|
205
|
+
const icon = wrapper.querySelector('img, svg, span');
|
|
206
|
+
if (icon instanceof HTMLElement) {
|
|
207
|
+
icon.style.color = transformed;
|
|
208
|
+
}
|
|
209
|
+
this.state.color = transformed;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
215
|
+
container.appendChild(wrapper);
|
|
216
|
+
|
|
217
|
+
requestAnimationFrame(() => {
|
|
218
|
+
if ((window as any).lucide) {
|
|
219
|
+
(window as any).lucide.createIcons();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render to another Jux component's container
|
|
228
|
+
*/
|
|
229
|
+
renderTo(juxComponent: any): this {
|
|
230
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
231
|
+
throw new Error('Icon.renderTo: Invalid component - not an object');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
235
|
+
throw new Error('Icon.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return this.render(`#${juxComponent._id}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Factory helper
|
|
244
|
+
*/
|
|
245
|
+
export function icon(id: string, options: IconOptions): Icon {
|
|
246
|
+
return new Icon(id, options);
|
|
247
|
+
}
|