juxscript 1.0.19 → 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 +92 -60
- 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 +697 -274
- 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 +105 -53
- 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 +54 -91
- 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/package.json +1 -2
package/lib/components/badge.ts
CHANGED
|
@@ -1,87 +1,64 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Badge component options
|
|
5
|
-
*/
|
|
6
4
|
export interface BadgeOptions {
|
|
7
5
|
text?: string;
|
|
8
|
-
variant?: '
|
|
9
|
-
size?: 'sm' | 'md' | 'lg';
|
|
10
|
-
pill?: boolean;
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
|
|
7
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
8
|
style?: string;
|
|
12
9
|
class?: string;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Badge component state
|
|
17
|
-
*/
|
|
18
12
|
type BadgeState = {
|
|
19
13
|
text: string;
|
|
20
14
|
variant: string;
|
|
21
15
|
size: string;
|
|
22
|
-
pill: boolean;
|
|
23
16
|
style: string;
|
|
24
17
|
class: string;
|
|
25
18
|
};
|
|
26
19
|
|
|
27
|
-
/**
|
|
28
|
-
* Badge component - Status indicators, counts, labels
|
|
29
|
-
*
|
|
30
|
-
* Usage:
|
|
31
|
-
* jux.badge('status', {
|
|
32
|
-
* text: 'Active',
|
|
33
|
-
* variant: 'success',
|
|
34
|
-
* pill: true
|
|
35
|
-
* }).render('#card');
|
|
36
|
-
*
|
|
37
|
-
* jux.badge('count', { text: '5' }).render('#notifications');
|
|
38
|
-
*/
|
|
39
20
|
export class Badge {
|
|
40
21
|
state: BadgeState;
|
|
41
22
|
container: HTMLElement | null = null;
|
|
42
23
|
_id: string;
|
|
43
24
|
id: string;
|
|
44
25
|
|
|
26
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
27
|
+
private _syncBindings: Array<{
|
|
28
|
+
property: string,
|
|
29
|
+
stateObj: State<any>,
|
|
30
|
+
toState?: Function,
|
|
31
|
+
toComponent?: Function
|
|
32
|
+
}> = [];
|
|
33
|
+
|
|
45
34
|
constructor(id: string, options: BadgeOptions = {}) {
|
|
46
35
|
this._id = id;
|
|
47
36
|
this.id = id;
|
|
48
37
|
|
|
49
38
|
this.state = {
|
|
50
39
|
text: options.text ?? '',
|
|
51
|
-
variant: options.variant ?? '
|
|
40
|
+
variant: options.variant ?? 'primary',
|
|
52
41
|
size: options.size ?? 'md',
|
|
53
|
-
pill: options.pill ?? false,
|
|
54
42
|
style: options.style ?? '',
|
|
55
43
|
class: options.class ?? ''
|
|
56
44
|
};
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
/* -------------------------
|
|
60
|
-
* Fluent API
|
|
61
|
-
* ------------------------- */
|
|
62
|
-
|
|
63
47
|
text(value: string): this {
|
|
64
48
|
this.state.text = value;
|
|
65
|
-
this._updateElement();
|
|
66
49
|
return this;
|
|
67
50
|
}
|
|
68
51
|
|
|
69
|
-
variant(value: '
|
|
52
|
+
variant(value: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'): this {
|
|
70
53
|
this.state.variant = value;
|
|
71
|
-
this._updateElement();
|
|
72
54
|
return this;
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
size(value: 'sm' | 'md' | 'lg'): this {
|
|
57
|
+
size(value: 'xs' | 'sm' | 'md' | 'lg' | 'xl'): this {
|
|
76
58
|
this.state.size = value;
|
|
77
59
|
return this;
|
|
78
60
|
}
|
|
79
61
|
|
|
80
|
-
pill(value: boolean): this {
|
|
81
|
-
this.state.pill = value;
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
62
|
style(value: string): this {
|
|
86
63
|
this.state.style = value;
|
|
87
64
|
return this;
|
|
@@ -92,61 +69,75 @@ export class Badge {
|
|
|
92
69
|
return this;
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
private _updateElement(): void {
|
|
100
|
-
const element = document.getElementById(this._id);
|
|
101
|
-
if (element) {
|
|
102
|
-
element.textContent = this.state.text;
|
|
103
|
-
element.className = `jux-badge jux-badge-${this.state.variant} jux-badge-${this.state.size}`;
|
|
104
|
-
if (this.state.pill) {
|
|
105
|
-
element.classList.add('jux-badge-pill');
|
|
106
|
-
}
|
|
107
|
-
if (this.state.class) {
|
|
108
|
-
element.className += ` ${this.state.class}`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
72
|
+
bind(event: string, handler: Function): this {
|
|
73
|
+
this._bindings.push({ event, handler });
|
|
74
|
+
return this;
|
|
111
75
|
}
|
|
112
76
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
77
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
78
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
79
|
+
throw new Error(`Badge.sync: Expected a State object for property "${property}"`);
|
|
80
|
+
}
|
|
81
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
116
84
|
|
|
117
85
|
render(targetId?: string): this {
|
|
86
|
+
// === 1. SETUP: Get or create container ===
|
|
118
87
|
let container: HTMLElement;
|
|
119
|
-
|
|
120
88
|
if (targetId) {
|
|
121
89
|
const target = document.querySelector(targetId);
|
|
122
90
|
if (!target || !(target instanceof HTMLElement)) {
|
|
123
|
-
throw new Error(`Badge: Target
|
|
91
|
+
throw new Error(`Badge: Target "${targetId}" not found`);
|
|
124
92
|
}
|
|
125
93
|
container = target;
|
|
126
94
|
} else {
|
|
127
95
|
container = getOrCreateContainer(this._id);
|
|
128
96
|
}
|
|
129
|
-
|
|
130
97
|
this.container = container;
|
|
131
|
-
const { text, variant, size, pill, style, class: className } = this.state;
|
|
132
98
|
|
|
99
|
+
// === 2. PREPARE: Destructure state ===
|
|
100
|
+
const { text, variant, size, style, class: className } = this.state;
|
|
101
|
+
|
|
102
|
+
// === 3. BUILD: Create DOM elements ===
|
|
133
103
|
const badge = document.createElement('span');
|
|
134
104
|
badge.className = `jux-badge jux-badge-${variant} jux-badge-${size}`;
|
|
135
105
|
badge.id = this._id;
|
|
136
106
|
badge.textContent = text;
|
|
107
|
+
if (className) badge.className += ` ${className}`;
|
|
108
|
+
if (style) badge.setAttribute('style', style);
|
|
109
|
+
|
|
110
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
111
|
+
|
|
112
|
+
// Wire custom bindings from .bind() calls
|
|
113
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
114
|
+
badge.addEventListener(event, handler as EventListener);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Wire sync bindings from .sync() calls
|
|
118
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
119
|
+
if (property === 'text') {
|
|
120
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
121
|
+
|
|
122
|
+
stateObj.subscribe((val: any) => {
|
|
123
|
+
const transformed = transformToComponent(val);
|
|
124
|
+
badge.textContent = transformed;
|
|
125
|
+
this.state.text = transformed;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (property === 'variant') {
|
|
129
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
130
|
+
|
|
131
|
+
stateObj.subscribe((val: any) => {
|
|
132
|
+
const transformed = transformToComponent(val);
|
|
133
|
+
badge.classList.remove(`jux-badge-${this.state.variant}`);
|
|
134
|
+
this.state.variant = transformed;
|
|
135
|
+
badge.classList.add(`jux-badge-${transformed}`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
badge.classList.add('jux-badge-pill');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (className) {
|
|
143
|
-
badge.className += ` ${className}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (style) {
|
|
147
|
-
badge.setAttribute('style', style);
|
|
148
|
-
}
|
|
149
|
-
|
|
140
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
150
141
|
container.appendChild(badge);
|
|
151
142
|
return this;
|
|
152
143
|
}
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
import { State } from '../reactivity/state.js';
|
|
3
|
+
import {
|
|
4
|
+
ChartDataPoint,
|
|
5
|
+
BarAreaChartOptions,
|
|
6
|
+
BarAreaChartState,
|
|
7
|
+
ChartTheme,
|
|
8
|
+
ChartStyleMode,
|
|
9
|
+
ChartOrientation,
|
|
10
|
+
ChartDirection,
|
|
11
|
+
LegendOrientation,
|
|
12
|
+
ChartPropertyMapping,
|
|
13
|
+
ChartStateObject
|
|
14
|
+
} from './chart-types.js';
|
|
15
|
+
import {
|
|
16
|
+
lightenColor,
|
|
17
|
+
getThemeConfig,
|
|
18
|
+
createLegend,
|
|
19
|
+
createDataTable,
|
|
20
|
+
applyThemeStyles
|
|
21
|
+
} from './chart-utils.js';
|
|
3
22
|
import {
|
|
4
23
|
googleTheme,
|
|
5
24
|
seriesaTheme,
|
|
@@ -384,6 +403,105 @@ export class BarChart {
|
|
|
384
403
|
return this;
|
|
385
404
|
}
|
|
386
405
|
|
|
406
|
+
/* -------------------------
|
|
407
|
+
* Reactivity Support
|
|
408
|
+
* ------------------------- */
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Sync a single property to a state object
|
|
412
|
+
*/
|
|
413
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
414
|
+
const transform = toComponent || ((v: any) => v);
|
|
415
|
+
|
|
416
|
+
stateObj.subscribe((val: any) => {
|
|
417
|
+
const transformed = transform(val);
|
|
418
|
+
|
|
419
|
+
// Map property to correct method
|
|
420
|
+
switch (property) {
|
|
421
|
+
case 'data': this.data(transformed); break;
|
|
422
|
+
case 'title': this.title(transformed); break;
|
|
423
|
+
case 'subtitle': this.subtitle(transformed); break;
|
|
424
|
+
case 'width': this.width(transformed); break;
|
|
425
|
+
case 'height': this.height(transformed); break;
|
|
426
|
+
case 'theme': this.theme(transformed); break;
|
|
427
|
+
case 'styleMode': this.styleMode(transformed); break;
|
|
428
|
+
case 'borderRadius': this.borderRadius(transformed); break;
|
|
429
|
+
case 'showTicksX': this.showTicksX(transformed); break;
|
|
430
|
+
case 'showTicksY': this.showTicksY(transformed); break;
|
|
431
|
+
case 'showLegend': this.showLegend(transformed); break;
|
|
432
|
+
case 'showDataLabels': this.showDataLabels(transformed); break;
|
|
433
|
+
case 'showDataTable': this.showDataTable(transformed); break;
|
|
434
|
+
case 'animate': this.animate(transformed); break;
|
|
435
|
+
case 'animationDuration': this.animationDuration(transformed); break;
|
|
436
|
+
case 'legendOrientation': this.legendOrientation(transformed); break;
|
|
437
|
+
case 'chartOrientation': this.chartOrientation(transformed); break;
|
|
438
|
+
case 'chartDirection': this.chartDirection(transformed); break;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Sync multiple properties from a state object
|
|
447
|
+
*/
|
|
448
|
+
syncState(stateObject: ChartStateObject, mapping?: ChartPropertyMapping): this {
|
|
449
|
+
// Default mapping: camelCase state names to method names
|
|
450
|
+
const defaultMapping: ChartPropertyMapping = {
|
|
451
|
+
chartType: 'type', // Not used in bar chart, ignored
|
|
452
|
+
chartTheme: 'theme',
|
|
453
|
+
chartStyleMode: 'styleMode',
|
|
454
|
+
borderRadius: 'borderRadius',
|
|
455
|
+
chartTitle: 'title',
|
|
456
|
+
chartWidth: 'width',
|
|
457
|
+
chartHeight: 'height',
|
|
458
|
+
showTicksX: 'showTicksX',
|
|
459
|
+
showTicksY: 'showTicksY',
|
|
460
|
+
showLegend: 'showLegend',
|
|
461
|
+
showDataTable: 'showDataTable',
|
|
462
|
+
showDataLabels: 'showDataLabels',
|
|
463
|
+
animate: 'animate',
|
|
464
|
+
animationDuration: 'animationDuration',
|
|
465
|
+
legendOrientation: 'legendOrientation',
|
|
466
|
+
chartOrientation: 'chartOrientation',
|
|
467
|
+
chartDirection: 'chartDirection'
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const finalMapping = { ...defaultMapping, ...mapping };
|
|
471
|
+
|
|
472
|
+
// Iterate through state object and bind each property
|
|
473
|
+
Object.entries(stateObject).forEach(([key, stateObj]) => {
|
|
474
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const methodOrFunction = finalMapping[key];
|
|
479
|
+
|
|
480
|
+
if (!methodOrFunction) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (typeof methodOrFunction === 'function') {
|
|
485
|
+
// Custom function mapping
|
|
486
|
+
stateObj.subscribe(methodOrFunction);
|
|
487
|
+
} else {
|
|
488
|
+
// String mapping to method name
|
|
489
|
+
const methodName = methodOrFunction as keyof this;
|
|
490
|
+
const method = this[methodName];
|
|
491
|
+
|
|
492
|
+
if (typeof method !== 'function') {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
stateObj.subscribe((val: any) => {
|
|
497
|
+
(method as Function).call(this, val);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
return this;
|
|
503
|
+
}
|
|
504
|
+
|
|
387
505
|
/* -------------------------
|
|
388
506
|
* Update chart
|
|
389
507
|
* ------------------------- */
|
|
@@ -1049,59 +1167,13 @@ export class BarChart {
|
|
|
1049
1167
|
}
|
|
1050
1168
|
|
|
1051
1169
|
private _applyTheme(themeName: string): void {
|
|
1052
|
-
const
|
|
1053
|
-
google: googleTheme,
|
|
1054
|
-
seriesa: seriesaTheme,
|
|
1055
|
-
hr: hrTheme,
|
|
1056
|
-
figma: figmaTheme,
|
|
1057
|
-
notion: notionTheme,
|
|
1058
|
-
chalk: chalkTheme,
|
|
1059
|
-
mint: mintTheme
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
const theme = themes[themeName];
|
|
1170
|
+
const theme = getThemeConfig(themeName as ChartTheme);
|
|
1063
1171
|
if (!theme) return;
|
|
1064
1172
|
|
|
1065
1173
|
// Apply colors
|
|
1066
1174
|
this.state.colors = theme.colors;
|
|
1067
1175
|
|
|
1068
|
-
|
|
1069
|
-
const baseStyleId = 'jux-barchart-base-styles';
|
|
1070
|
-
if (!document.getElementById(baseStyleId)) {
|
|
1071
|
-
const style = document.createElement('style');
|
|
1072
|
-
style.id = baseStyleId;
|
|
1073
|
-
style.textContent = this._getBaseStyles();
|
|
1074
|
-
document.head.appendChild(style);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// Inject font (once per theme)
|
|
1078
|
-
if (theme.font && !document.querySelector(`link[href="${theme.font}"]`)) {
|
|
1079
|
-
const link = document.createElement('link');
|
|
1080
|
-
link.rel = 'stylesheet';
|
|
1081
|
-
link.href = theme.font;
|
|
1082
|
-
document.head.appendChild(link);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Apply theme-specific styles
|
|
1086
|
-
const styleId = `jux-barchart-theme-${themeName}`;
|
|
1087
|
-
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
|
1088
|
-
|
|
1089
|
-
if (!styleElement) {
|
|
1090
|
-
styleElement = document.createElement('style');
|
|
1091
|
-
styleElement.id = styleId;
|
|
1092
|
-
document.head.appendChild(styleElement);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Generate CSS with theme variables
|
|
1096
|
-
const variablesCSS = Object.entries(theme.variables)
|
|
1097
|
-
.map(([key, value]) => ` ${key}: ${value};`)
|
|
1098
|
-
.join('\n');
|
|
1099
|
-
|
|
1100
|
-
styleElement.textContent = `
|
|
1101
|
-
.jux-barchart.theme-${themeName} {
|
|
1102
|
-
${variablesCSS}
|
|
1103
|
-
}
|
|
1104
|
-
`;
|
|
1176
|
+
applyThemeStyles(themeName as ChartTheme, 'jux-barchart', this._getBaseStyles());
|
|
1105
1177
|
}
|
|
1106
1178
|
|
|
1107
1179
|
private _applyThemeToWrapper(wrapper: HTMLElement): void {
|
package/lib/components/button.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon } from './icons.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Button component options
|
|
@@ -7,7 +8,6 @@ import { State } from '../reactivity/state.js';
|
|
|
7
8
|
export interface ButtonOptions {
|
|
8
9
|
label?: string;
|
|
9
10
|
icon?: string;
|
|
10
|
-
click?: (e: Event) => void;
|
|
11
11
|
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string;
|
|
12
12
|
size?: 'small' | 'medium' | 'large';
|
|
13
13
|
disabled?: boolean;
|
|
@@ -25,7 +25,6 @@ export interface ButtonOptions {
|
|
|
25
25
|
type ButtonState = {
|
|
26
26
|
label: string;
|
|
27
27
|
icon: string;
|
|
28
|
-
click: ((e: Event) => void) | null;
|
|
29
28
|
variant: string;
|
|
30
29
|
size: string;
|
|
31
30
|
disabled: boolean;
|
|
@@ -46,8 +45,14 @@ export class Button {
|
|
|
46
45
|
_id: string;
|
|
47
46
|
id: string;
|
|
48
47
|
|
|
49
|
-
// Store bind
|
|
48
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
50
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
|
+
}> = [];
|
|
51
56
|
|
|
52
57
|
constructor(id: string, options?: ButtonOptions) {
|
|
53
58
|
this._id = id;
|
|
@@ -58,7 +63,6 @@ export class Button {
|
|
|
58
63
|
this.state = {
|
|
59
64
|
label: opts.label ?? 'Button',
|
|
60
65
|
icon: opts.icon ?? '',
|
|
61
|
-
click: opts.click ?? null,
|
|
62
66
|
variant: opts.variant ?? 'primary',
|
|
63
67
|
size: opts.size ?? 'medium',
|
|
64
68
|
disabled: opts.disabled ?? false,
|
|
@@ -85,20 +89,7 @@ export class Button {
|
|
|
85
89
|
return this;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
this.state.click = callback;
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Bind event handler (stores for wiring in render)
|
|
95
|
-
*/
|
|
96
|
-
bind(event: string, handler: Function): this {
|
|
97
|
-
this._bindings.push({ event, handler });
|
|
98
|
-
return this;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
variant(value: string): this {
|
|
92
|
+
variant(value: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string): this {
|
|
102
93
|
this.state.variant = value;
|
|
103
94
|
return this;
|
|
104
95
|
}
|
|
@@ -143,91 +134,132 @@ export class Button {
|
|
|
143
134
|
return this;
|
|
144
135
|
}
|
|
145
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
|
+
|
|
146
158
|
/* -------------------------
|
|
147
159
|
* Render
|
|
148
160
|
* ------------------------- */
|
|
149
161
|
|
|
150
162
|
render(targetId?: string): this {
|
|
163
|
+
// === 1. SETUP: Get or create container ===
|
|
151
164
|
let container: HTMLElement;
|
|
152
|
-
|
|
153
165
|
if (targetId) {
|
|
154
166
|
const target = document.querySelector(targetId);
|
|
155
167
|
if (!target || !(target instanceof HTMLElement)) {
|
|
156
|
-
throw new Error(`Button: Target
|
|
168
|
+
throw new Error(`Button: Target "${targetId}" not found`);
|
|
157
169
|
}
|
|
158
170
|
container = target;
|
|
159
171
|
} else {
|
|
160
172
|
container = getOrCreateContainer(this._id);
|
|
161
173
|
}
|
|
162
|
-
|
|
163
174
|
this.container = container;
|
|
164
|
-
const { label, icon, click, variant, size, disabled, loading, iconPosition, fullWidth, type, style, class: className } = this.state;
|
|
165
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 ===
|
|
166
182
|
const button = document.createElement('button');
|
|
167
183
|
button.className = `jux-button jux-button-${variant} jux-button-${size}`;
|
|
168
184
|
button.id = this._id;
|
|
169
|
-
button.type = type;
|
|
170
185
|
button.disabled = disabled || loading;
|
|
171
186
|
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (loading) {
|
|
177
|
-
button.classList.add('jux-button-loading');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (className) {
|
|
181
|
-
button.className += ` ${className}`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (style) {
|
|
185
|
-
button.setAttribute('style', style);
|
|
186
|
-
}
|
|
187
|
+
if (className) button.className += ` ${className}`;
|
|
188
|
+
if (style) button.setAttribute('style', style);
|
|
187
189
|
|
|
188
|
-
// Build button content
|
|
189
190
|
if (icon && iconPosition === 'left') {
|
|
190
191
|
const iconEl = document.createElement('span');
|
|
191
|
-
iconEl.className = 'jux-button-icon
|
|
192
|
-
iconEl.
|
|
192
|
+
iconEl.className = 'jux-button-icon';
|
|
193
|
+
iconEl.appendChild(renderIcon(icon));
|
|
193
194
|
button.appendChild(iconEl);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
button.appendChild(labelEl);
|
|
197
|
+
const textEl = document.createElement('span');
|
|
198
|
+
textEl.textContent = loading ? 'Loading...' : text;
|
|
199
|
+
button.appendChild(textEl);
|
|
200
200
|
|
|
201
201
|
if (icon && iconPosition === 'right') {
|
|
202
202
|
const iconEl = document.createElement('span');
|
|
203
|
-
iconEl.className = 'jux-button-icon
|
|
204
|
-
iconEl.
|
|
203
|
+
iconEl.className = 'jux-button-icon';
|
|
204
|
+
iconEl.appendChild(renderIcon(icon));
|
|
205
205
|
button.appendChild(iconEl);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
//
|
|
209
|
-
if (click) {
|
|
210
|
-
button.addEventListener('click', click);
|
|
211
|
-
}
|
|
208
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
212
209
|
|
|
213
|
-
//
|
|
210
|
+
// Wire custom bindings from .bind() calls
|
|
214
211
|
this._bindings.forEach(({ event, handler }) => {
|
|
215
212
|
button.addEventListener(event, handler as EventListener);
|
|
216
213
|
});
|
|
217
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));
|
|
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 ===
|
|
218
248
|
container.appendChild(button);
|
|
249
|
+
|
|
250
|
+
requestAnimationFrame(() => {
|
|
251
|
+
if ((window as any).lucide) {
|
|
252
|
+
(window as any).lucide.createIcons();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
219
256
|
return this;
|
|
220
257
|
}
|
|
221
258
|
|
|
222
|
-
renderTo(juxComponent:
|
|
223
|
-
if (!juxComponent
|
|
224
|
-
throw new Error('Button.renderTo: Invalid component
|
|
259
|
+
renderTo(juxComponent: any): this {
|
|
260
|
+
if (!juxComponent?._id) {
|
|
261
|
+
throw new Error('Button.renderTo: Invalid component');
|
|
225
262
|
}
|
|
226
|
-
|
|
227
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
228
|
-
throw new Error('Button.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
263
|
return this.render(`#${juxComponent._id}`);
|
|
232
264
|
}
|
|
233
265
|
}
|