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/button.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
|
* Button component options
|
|
@@ -6,7 +8,6 @@ import { getOrCreateContainer } from './helpers.js';
|
|
|
6
8
|
export interface ButtonOptions {
|
|
7
9
|
label?: string;
|
|
8
10
|
icon?: string;
|
|
9
|
-
click?: (e: Event) => void;
|
|
10
11
|
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string;
|
|
11
12
|
size?: 'small' | 'medium' | 'large';
|
|
12
13
|
disabled?: boolean;
|
|
@@ -24,7 +25,6 @@ export interface ButtonOptions {
|
|
|
24
25
|
type ButtonState = {
|
|
25
26
|
label: string;
|
|
26
27
|
icon: string;
|
|
27
|
-
click: ((e: Event) => void) | null;
|
|
28
28
|
variant: string;
|
|
29
29
|
size: string;
|
|
30
30
|
disabled: boolean;
|
|
@@ -38,12 +38,6 @@ type ButtonState = {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Button component
|
|
41
|
-
*
|
|
42
|
-
* Usage:
|
|
43
|
-
* jux.button('myButton').label('Click Me').click(() => console.log('hi')).render();
|
|
44
|
-
*
|
|
45
|
-
* // Or with options
|
|
46
|
-
* jux.button('myButton', { label: 'Click Me', click: () => console.log('hi') }).render();
|
|
47
41
|
*/
|
|
48
42
|
export class Button {
|
|
49
43
|
state: ButtonState;
|
|
@@ -51,6 +45,15 @@ export class Button {
|
|
|
51
45
|
_id: string;
|
|
52
46
|
id: string;
|
|
53
47
|
|
|
48
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
49
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
50
|
+
private _syncBindings: Array<{
|
|
51
|
+
property: string,
|
|
52
|
+
stateObj: State<any>,
|
|
53
|
+
toState?: Function,
|
|
54
|
+
toComponent?: Function
|
|
55
|
+
}> = [];
|
|
56
|
+
|
|
54
57
|
constructor(id: string, options?: ButtonOptions) {
|
|
55
58
|
this._id = id;
|
|
56
59
|
this.id = id;
|
|
@@ -60,7 +63,6 @@ export class Button {
|
|
|
60
63
|
this.state = {
|
|
61
64
|
label: opts.label ?? 'Button',
|
|
62
65
|
icon: opts.icon ?? '',
|
|
63
|
-
click: opts.click ?? null,
|
|
64
66
|
variant: opts.variant ?? 'primary',
|
|
65
67
|
size: opts.size ?? 'medium',
|
|
66
68
|
disabled: opts.disabled ?? false,
|
|
@@ -77,195 +79,191 @@ export class Button {
|
|
|
77
79
|
* Fluent API
|
|
78
80
|
* ------------------------- */
|
|
79
81
|
|
|
80
|
-
/**
|
|
81
|
-
* Set button label text
|
|
82
|
-
*/
|
|
83
82
|
label(value: string): this {
|
|
84
83
|
this.state.label = value;
|
|
85
84
|
return this;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
/**
|
|
89
|
-
* Set button icon (emoji or text)
|
|
90
|
-
*/
|
|
91
87
|
icon(value: string): this {
|
|
92
88
|
this.state.icon = value;
|
|
93
89
|
return this;
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
* Set click handler
|
|
98
|
-
*/
|
|
99
|
-
click(callback: (e: Event) => void): this {
|
|
100
|
-
this.state.click = callback;
|
|
101
|
-
return this;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Set button variant/style
|
|
106
|
-
*/
|
|
107
|
-
variant(value: string): this {
|
|
92
|
+
variant(value: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string): this {
|
|
108
93
|
this.state.variant = value;
|
|
109
94
|
return this;
|
|
110
95
|
}
|
|
111
96
|
|
|
112
|
-
/**
|
|
113
|
-
* Set button size
|
|
114
|
-
*/
|
|
115
97
|
size(value: 'small' | 'medium' | 'large'): this {
|
|
116
98
|
this.state.size = value;
|
|
117
99
|
return this;
|
|
118
100
|
}
|
|
119
101
|
|
|
120
|
-
/**
|
|
121
|
-
* Set disabled state
|
|
122
|
-
*/
|
|
123
102
|
disabled(value: boolean): this {
|
|
124
103
|
this.state.disabled = value;
|
|
125
104
|
return this;
|
|
126
105
|
}
|
|
127
106
|
|
|
128
|
-
/**
|
|
129
|
-
* Set loading state
|
|
130
|
-
*/
|
|
131
107
|
loading(value: boolean): this {
|
|
132
108
|
this.state.loading = value;
|
|
133
109
|
return this;
|
|
134
110
|
}
|
|
135
111
|
|
|
136
|
-
/**
|
|
137
|
-
* Set icon position (left or right)
|
|
138
|
-
*/
|
|
139
112
|
iconPosition(value: 'left' | 'right'): this {
|
|
140
113
|
this.state.iconPosition = value;
|
|
141
114
|
return this;
|
|
142
115
|
}
|
|
143
116
|
|
|
144
|
-
/**
|
|
145
|
-
* Set full width
|
|
146
|
-
*/
|
|
147
117
|
fullWidth(value: boolean): this {
|
|
148
118
|
this.state.fullWidth = value;
|
|
149
119
|
return this;
|
|
150
120
|
}
|
|
151
121
|
|
|
152
|
-
/**
|
|
153
|
-
* Set button type attribute
|
|
154
|
-
*/
|
|
155
122
|
type(value: 'button' | 'submit' | 'reset'): this {
|
|
156
123
|
this.state.type = value;
|
|
157
124
|
return this;
|
|
158
125
|
}
|
|
159
126
|
|
|
160
|
-
/**
|
|
161
|
-
* Set inline style
|
|
162
|
-
*/
|
|
163
127
|
style(value: string): this {
|
|
164
128
|
this.state.style = value;
|
|
165
129
|
return this;
|
|
166
130
|
}
|
|
167
131
|
|
|
168
|
-
/**
|
|
169
|
-
* Set additional CSS classes
|
|
170
|
-
*/
|
|
171
132
|
class(value: string): this {
|
|
172
133
|
this.state.class = value;
|
|
173
134
|
return this;
|
|
174
135
|
}
|
|
175
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Bind DOM events (click, hover, etc.)
|
|
139
|
+
* Stores handlers for wiring in render()
|
|
140
|
+
*/
|
|
141
|
+
bind(event: string, handler: Function): this {
|
|
142
|
+
this._bindings.push({ event, handler });
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Two-way state synchronization
|
|
148
|
+
* Stores sync instructions for wiring in render()
|
|
149
|
+
*/
|
|
150
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
151
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
152
|
+
throw new Error(`Button.sync: Expected a State object for property "${property}"`);
|
|
153
|
+
}
|
|
154
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
176
158
|
/* -------------------------
|
|
177
159
|
* Render
|
|
178
160
|
* ------------------------- */
|
|
179
161
|
|
|
180
|
-
/**
|
|
181
|
-
* Render button to target element
|
|
182
|
-
*/
|
|
183
162
|
render(targetId?: string): this {
|
|
163
|
+
// === 1. SETUP: Get or create container ===
|
|
184
164
|
let container: HTMLElement;
|
|
185
|
-
|
|
186
165
|
if (targetId) {
|
|
187
166
|
const target = document.querySelector(targetId);
|
|
188
167
|
if (!target || !(target instanceof HTMLElement)) {
|
|
189
|
-
throw new Error(`Button: Target
|
|
168
|
+
throw new Error(`Button: Target "${targetId}" not found`);
|
|
190
169
|
}
|
|
191
170
|
container = target;
|
|
192
171
|
} else {
|
|
193
172
|
container = getOrCreateContainer(this._id);
|
|
194
173
|
}
|
|
195
|
-
|
|
196
174
|
this.container = container;
|
|
197
|
-
const { label, icon, click, variant, size, disabled, loading, iconPosition, fullWidth, type, style, class: className } = this.state;
|
|
198
175
|
|
|
176
|
+
// === 2. PREPARE: Destructure state ===
|
|
177
|
+
const { label: text, variant, size, disabled, icon, iconPosition, loading, style, class: className } = this.state;
|
|
178
|
+
const hasTextSync = this._syncBindings.some(b => b.property === 'text');
|
|
179
|
+
const hasDisabledSync = this._syncBindings.some(b => b.property === 'disabled');
|
|
180
|
+
|
|
181
|
+
// === 3. BUILD: Create DOM elements ===
|
|
199
182
|
const button = document.createElement('button');
|
|
200
183
|
button.className = `jux-button jux-button-${variant} jux-button-${size}`;
|
|
201
184
|
button.id = this._id;
|
|
202
|
-
button.type = type;
|
|
203
185
|
button.disabled = disabled || loading;
|
|
204
186
|
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (loading) {
|
|
210
|
-
button.classList.add('jux-button-loading');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (className) {
|
|
214
|
-
button.className += ` ${className}`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (style) {
|
|
218
|
-
button.setAttribute('style', style);
|
|
219
|
-
}
|
|
187
|
+
if (className) button.className += ` ${className}`;
|
|
188
|
+
if (style) button.setAttribute('style', style);
|
|
220
189
|
|
|
221
|
-
// Build button content
|
|
222
190
|
if (icon && iconPosition === 'left') {
|
|
223
191
|
const iconEl = document.createElement('span');
|
|
224
|
-
iconEl.className = 'jux-button-icon
|
|
225
|
-
iconEl.
|
|
192
|
+
iconEl.className = 'jux-button-icon';
|
|
193
|
+
iconEl.appendChild(renderIcon(icon));
|
|
226
194
|
button.appendChild(iconEl);
|
|
227
195
|
}
|
|
228
196
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
button.appendChild(labelEl);
|
|
197
|
+
const textEl = document.createElement('span');
|
|
198
|
+
textEl.textContent = loading ? 'Loading...' : text;
|
|
199
|
+
button.appendChild(textEl);
|
|
233
200
|
|
|
234
201
|
if (icon && iconPosition === 'right') {
|
|
235
202
|
const iconEl = document.createElement('span');
|
|
236
|
-
iconEl.className = 'jux-button-icon
|
|
237
|
-
iconEl.
|
|
203
|
+
iconEl.className = 'jux-button-icon';
|
|
204
|
+
iconEl.appendChild(renderIcon(icon));
|
|
238
205
|
button.appendChild(iconEl);
|
|
239
206
|
}
|
|
240
207
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
208
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
209
|
+
|
|
210
|
+
// Wire custom bindings from .bind() calls
|
|
211
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
212
|
+
button.addEventListener(event, handler as EventListener);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Wire sync bindings from .sync() calls
|
|
216
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
217
|
+
if (property === 'text') {
|
|
218
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
245
219
|
|
|
220
|
+
stateObj.subscribe((val: any) => {
|
|
221
|
+
const transformed = transformToComponent(val);
|
|
222
|
+
textEl.textContent = this.state.loading ? 'Loading...' : transformed;
|
|
223
|
+
this.state.label = transformed;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (property === 'disabled') {
|
|
227
|
+
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
228
|
+
|
|
229
|
+
stateObj.subscribe((val: any) => {
|
|
230
|
+
const transformed = transformToComponent(val);
|
|
231
|
+
button.disabled = transformed || this.state.loading;
|
|
232
|
+
this.state.disabled = transformed;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (property === 'loading') {
|
|
236
|
+
const transformToComponent = toComponent || ((v: any) => Boolean(v));
|
|
237
|
+
|
|
238
|
+
stateObj.subscribe((val: any) => {
|
|
239
|
+
const transformed = transformToComponent(val);
|
|
240
|
+
button.disabled = this.state.disabled || transformed;
|
|
241
|
+
textEl.textContent = transformed ? 'Loading...' : this.state.label;
|
|
242
|
+
this.state.loading = transformed;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
246
248
|
container.appendChild(button);
|
|
249
|
+
|
|
250
|
+
requestAnimationFrame(() => {
|
|
251
|
+
if ((window as any).lucide) {
|
|
252
|
+
(window as any).lucide.createIcons();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
247
256
|
return this;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
renderTo(juxComponent: this): this {
|
|
254
|
-
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
255
|
-
throw new Error('Button.renderTo: Invalid component - not an object');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
259
|
-
throw new Error('Button.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
259
|
+
renderTo(juxComponent: any): this {
|
|
260
|
+
if (!juxComponent?._id) {
|
|
261
|
+
throw new Error('Button.renderTo: Invalid component');
|
|
260
262
|
}
|
|
261
|
-
|
|
262
263
|
return this.render(`#${juxComponent._id}`);
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
/**
|
|
267
|
-
* Factory helper
|
|
268
|
-
*/
|
|
269
267
|
export function button(id: string, options?: ButtonOptions): Button {
|
|
270
268
|
return new Button(id, options);
|
|
271
269
|
}
|
package/lib/components/card.ts
CHANGED
|
@@ -1,94 +1,68 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Card component options
|
|
5
|
-
*/
|
|
6
4
|
export interface CardOptions {
|
|
7
5
|
title?: string;
|
|
8
|
-
subtitle?: string;
|
|
9
6
|
content?: string;
|
|
10
|
-
|
|
11
|
-
variant?:
|
|
7
|
+
footer?: string;
|
|
8
|
+
variant?: string;
|
|
9
|
+
hoverable?: boolean;
|
|
12
10
|
style?: string;
|
|
13
11
|
class?: string;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
/**
|
|
17
|
-
* Card component state
|
|
18
|
-
*/
|
|
19
14
|
type CardState = {
|
|
20
15
|
title: string;
|
|
21
|
-
subtitle: string;
|
|
22
16
|
content: string;
|
|
23
|
-
|
|
17
|
+
footer: string;
|
|
24
18
|
variant: string;
|
|
19
|
+
hoverable: boolean;
|
|
25
20
|
style: string;
|
|
26
21
|
class: string;
|
|
27
|
-
hasActions: boolean;
|
|
28
22
|
};
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* Card component
|
|
32
|
-
*
|
|
33
|
-
* Usage:
|
|
34
|
-
* const card = jux.card('myCard', {
|
|
35
|
-
* title: 'Card Title',
|
|
36
|
-
* content: 'Card content here'
|
|
37
|
-
* });
|
|
38
|
-
* card.render();
|
|
39
|
-
*
|
|
40
|
-
* // Add actions (any components)
|
|
41
|
-
* jux.button('view-btn').label('View').render('#myCard-actions');
|
|
42
|
-
* jux.button('delete-btn').label('Delete').render('#myCard-actions');
|
|
43
|
-
*/
|
|
44
24
|
export class Card {
|
|
45
25
|
state: CardState;
|
|
46
26
|
container: HTMLElement | null = null;
|
|
47
27
|
_id: string;
|
|
48
28
|
id: string;
|
|
49
29
|
|
|
30
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
31
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
32
|
+
private _syncBindings: Array<{
|
|
33
|
+
property: string,
|
|
34
|
+
stateObj: State<any>,
|
|
35
|
+
toState?: Function,
|
|
36
|
+
toComponent?: Function
|
|
37
|
+
}> = [];
|
|
38
|
+
|
|
50
39
|
constructor(id: string, options: CardOptions = {}) {
|
|
51
40
|
this._id = id;
|
|
52
41
|
this.id = id;
|
|
53
42
|
|
|
54
43
|
this.state = {
|
|
55
44
|
title: options.title ?? '',
|
|
56
|
-
subtitle: options.subtitle ?? '',
|
|
57
45
|
content: options.content ?? '',
|
|
58
|
-
|
|
46
|
+
footer: options.footer ?? '',
|
|
59
47
|
variant: options.variant ?? 'default',
|
|
48
|
+
hoverable: options.hoverable ?? false,
|
|
60
49
|
style: options.style ?? '',
|
|
61
|
-
class: options.class ?? ''
|
|
62
|
-
hasActions: false
|
|
50
|
+
class: options.class ?? ''
|
|
63
51
|
};
|
|
64
52
|
}
|
|
65
53
|
|
|
66
|
-
/* -------------------------
|
|
67
|
-
* Fluent API
|
|
68
|
-
* ------------------------- */
|
|
69
|
-
|
|
70
54
|
title(value: string): this {
|
|
71
55
|
this.state.title = value;
|
|
72
56
|
return this;
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
subtitle(value: string): this {
|
|
76
|
-
this.state.subtitle = value;
|
|
77
|
-
return this;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
59
|
content(value: string): this {
|
|
81
60
|
this.state.content = value;
|
|
82
61
|
return this;
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
|
|
86
|
-
this.state.
|
|
87
|
-
return this;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
variant(value: string): this {
|
|
91
|
-
this.state.variant = value;
|
|
64
|
+
footer(value: string): this {
|
|
65
|
+
this.state.footer = value;
|
|
92
66
|
return this;
|
|
93
67
|
}
|
|
94
68
|
|
|
@@ -102,118 +76,120 @@ export class Card {
|
|
|
102
76
|
return this;
|
|
103
77
|
}
|
|
104
78
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* Creates an empty actions container that components can render into
|
|
108
|
-
*/
|
|
109
|
-
withActions(): this {
|
|
110
|
-
this.state.hasActions = true;
|
|
79
|
+
bind(event: string, handler: Function): this {
|
|
80
|
+
this._bindings.push({ event, handler });
|
|
111
81
|
return this;
|
|
112
82
|
}
|
|
113
83
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
84
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
85
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
86
|
+
throw new Error(`Card.sync: Expected a State object for property "${property}"`);
|
|
87
|
+
}
|
|
88
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
117
91
|
|
|
118
92
|
render(targetId?: string): this {
|
|
93
|
+
// === 1. SETUP: Get or create container ===
|
|
119
94
|
let container: HTMLElement;
|
|
120
|
-
|
|
121
95
|
if (targetId) {
|
|
122
96
|
const target = document.querySelector(targetId);
|
|
123
97
|
if (!target || !(target instanceof HTMLElement)) {
|
|
124
|
-
throw new Error(`Card: Target
|
|
98
|
+
throw new Error(`Card: Target "${targetId}" not found`);
|
|
125
99
|
}
|
|
126
100
|
container = target;
|
|
127
101
|
} else {
|
|
128
102
|
container = getOrCreateContainer(this._id);
|
|
129
103
|
}
|
|
130
|
-
|
|
131
104
|
this.container = container;
|
|
132
|
-
const { title, subtitle, content, image, variant, style, class: className, hasActions } = this.state;
|
|
133
105
|
|
|
134
|
-
//
|
|
106
|
+
// === 2. PREPARE: Destructure state ===
|
|
107
|
+
const { title, content, footer, variant, hoverable, style, class: className } = this.state;
|
|
108
|
+
|
|
109
|
+
// === 3. BUILD: Create DOM elements ===
|
|
135
110
|
const card = document.createElement('div');
|
|
136
111
|
card.className = `jux-card jux-card-${variant}`;
|
|
137
112
|
card.id = this._id;
|
|
113
|
+
if (hoverable) card.classList.add('jux-card-hoverable');
|
|
114
|
+
if (className) card.className += ` ${className}`;
|
|
115
|
+
if (style) card.setAttribute('style', style);
|
|
138
116
|
|
|
139
|
-
if (className) {
|
|
140
|
-
card.className += ` ${className}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (style) {
|
|
144
|
-
card.setAttribute('style', style);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Image
|
|
148
|
-
if (image) {
|
|
149
|
-
const img = document.createElement('img');
|
|
150
|
-
img.className = 'jux-card-image';
|
|
151
|
-
img.src = image;
|
|
152
|
-
img.alt = title || 'Card image';
|
|
153
|
-
card.appendChild(img);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Content wrapper
|
|
157
|
-
const cardBody = document.createElement('div');
|
|
158
|
-
cardBody.className = 'jux-card-body';
|
|
159
|
-
|
|
160
|
-
// Title
|
|
161
117
|
if (title) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
118
|
+
const header = document.createElement('div');
|
|
119
|
+
header.className = 'jux-card-header';
|
|
120
|
+
header.textContent = title;
|
|
121
|
+
card.appendChild(header);
|
|
166
122
|
}
|
|
167
123
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
subtitleEl.textContent = subtitle;
|
|
173
|
-
cardBody.appendChild(subtitleEl);
|
|
174
|
-
}
|
|
124
|
+
const body = document.createElement('div');
|
|
125
|
+
body.className = 'jux-card-body';
|
|
126
|
+
body.innerHTML = content;
|
|
127
|
+
card.appendChild(body);
|
|
175
128
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
cardBody.appendChild(contentEl);
|
|
129
|
+
if (footer) {
|
|
130
|
+
const footerEl = document.createElement('div');
|
|
131
|
+
footerEl.className = 'jux-card-footer';
|
|
132
|
+
footerEl.innerHTML = footer;
|
|
133
|
+
card.appendChild(footerEl);
|
|
182
134
|
}
|
|
183
135
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
136
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
137
|
+
|
|
138
|
+
// Wire custom bindings from .bind() calls
|
|
139
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
140
|
+
card.addEventListener(event, handler as EventListener);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Wire sync bindings from .sync() calls
|
|
144
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
145
|
+
if (property === 'title') {
|
|
146
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
147
|
+
|
|
148
|
+
stateObj.subscribe((val: any) => {
|
|
149
|
+
const transformed = transformToComponent(val);
|
|
150
|
+
const header = card.querySelector('.jux-card-header');
|
|
151
|
+
if (header) {
|
|
152
|
+
header.textContent = transformed;
|
|
153
|
+
}
|
|
154
|
+
this.state.title = transformed;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (property === 'content') {
|
|
158
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
159
|
+
|
|
160
|
+
stateObj.subscribe((val: any) => {
|
|
161
|
+
const transformed = transformToComponent(val);
|
|
162
|
+
body.innerHTML = transformed;
|
|
163
|
+
this.state.content = transformed;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (property === 'footer') {
|
|
167
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
168
|
+
|
|
169
|
+
stateObj.subscribe((val: any) => {
|
|
170
|
+
const transformed = transformToComponent(val);
|
|
171
|
+
const footerEl = card.querySelector('.jux-card-footer');
|
|
172
|
+
if (footerEl) {
|
|
173
|
+
footerEl.innerHTML = transformed;
|
|
174
|
+
}
|
|
175
|
+
this.state.footer = transformed;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
193
179
|
|
|
180
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
194
181
|
container.appendChild(card);
|
|
195
182
|
return this;
|
|
196
183
|
}
|
|
197
184
|
|
|
198
|
-
/**
|
|
199
|
-
* Render to another Jux component's container
|
|
200
|
-
*/
|
|
201
185
|
renderTo(juxComponent: any): this {
|
|
202
|
-
if (!juxComponent
|
|
203
|
-
throw new Error('Card.renderTo: Invalid component
|
|
186
|
+
if (!juxComponent?._id) {
|
|
187
|
+
throw new Error('Card.renderTo: Invalid component');
|
|
204
188
|
}
|
|
205
|
-
|
|
206
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
207
|
-
throw new Error('Card.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
189
|
return this.render(`#${juxComponent._id}`);
|
|
211
190
|
}
|
|
212
191
|
}
|
|
213
192
|
|
|
214
|
-
/**
|
|
215
|
-
* Factory helper
|
|
216
|
-
*/
|
|
217
193
|
export function card(id: string, options: CardOptions = {}): Card {
|
|
218
194
|
return new Card(id, options);
|
|
219
195
|
}
|