juxscript 1.0.19 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/lib/components/badge.ts
CHANGED
|
@@ -1,79 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
|
|
3
|
+
// Event definitions
|
|
4
|
+
const TRIGGER_EVENTS = [] as const;
|
|
5
|
+
const CALLBACK_EVENTS = [] as const;
|
|
2
6
|
|
|
3
|
-
/**
|
|
4
|
-
* Badge component options
|
|
5
|
-
*/
|
|
6
7
|
export interface BadgeOptions {
|
|
7
8
|
text?: string;
|
|
8
9
|
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
9
|
-
size?: 'sm' | 'md' | 'lg';
|
|
10
10
|
pill?: boolean;
|
|
11
11
|
style?: string;
|
|
12
12
|
class?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Badge component state
|
|
17
|
-
*/
|
|
18
15
|
type BadgeState = {
|
|
19
16
|
text: string;
|
|
20
17
|
variant: string;
|
|
21
|
-
size: string;
|
|
22
18
|
pill: boolean;
|
|
23
19
|
style: string;
|
|
24
20
|
class: string;
|
|
25
21
|
};
|
|
26
22
|
|
|
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
|
-
export class Badge {
|
|
40
|
-
state: BadgeState;
|
|
41
|
-
container: HTMLElement | null = null;
|
|
42
|
-
_id: string;
|
|
43
|
-
id: string;
|
|
44
|
-
|
|
23
|
+
export class Badge extends BaseComponent<BadgeState> {
|
|
45
24
|
constructor(id: string, options: BadgeOptions = {}) {
|
|
46
|
-
|
|
47
|
-
this.id = id;
|
|
48
|
-
|
|
49
|
-
this.state = {
|
|
25
|
+
super(id, {
|
|
50
26
|
text: options.text ?? '',
|
|
51
27
|
variant: options.variant ?? 'default',
|
|
52
|
-
size: options.size ?? 'md',
|
|
53
28
|
pill: options.pill ?? false,
|
|
54
29
|
style: options.style ?? '',
|
|
55
30
|
class: options.class ?? ''
|
|
56
|
-
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected getTriggerEvents(): readonly string[] {
|
|
35
|
+
return TRIGGER_EVENTS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected getCallbackEvents(): readonly string[] {
|
|
39
|
+
return CALLBACK_EVENTS;
|
|
57
40
|
}
|
|
58
41
|
|
|
59
|
-
/*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
42
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
43
|
+
* FLUENT API
|
|
44
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
45
|
+
|
|
46
|
+
// ✅ Inherited from BaseComponent
|
|
62
47
|
|
|
63
48
|
text(value: string): this {
|
|
64
49
|
this.state.text = value;
|
|
65
|
-
this._updateElement();
|
|
66
50
|
return this;
|
|
67
51
|
}
|
|
68
52
|
|
|
69
53
|
variant(value: 'default' | 'success' | 'warning' | 'error' | 'info'): this {
|
|
70
54
|
this.state.variant = value;
|
|
71
|
-
this._updateElement();
|
|
72
|
-
return this;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
size(value: 'sm' | 'md' | 'lg'): this {
|
|
76
|
-
this.state.size = value;
|
|
77
55
|
return this;
|
|
78
56
|
}
|
|
79
57
|
|
|
@@ -82,80 +60,92 @@ export class Badge {
|
|
|
82
60
|
return this;
|
|
83
61
|
}
|
|
84
62
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
class(value: string): this {
|
|
91
|
-
this.state.class = value;
|
|
92
|
-
return this;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* -------------------------
|
|
96
|
-
* Helpers
|
|
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
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/* -------------------------
|
|
114
|
-
* Render
|
|
115
|
-
* ------------------------- */
|
|
63
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
64
|
+
* RENDER
|
|
65
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
116
66
|
|
|
117
67
|
render(targetId?: string): this {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (targetId) {
|
|
121
|
-
const target = document.querySelector(targetId);
|
|
122
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
123
|
-
throw new Error(`Badge: Target element "${targetId}" not found`);
|
|
124
|
-
}
|
|
125
|
-
container = target;
|
|
126
|
-
} else {
|
|
127
|
-
container = getOrCreateContainer(this._id);
|
|
128
|
-
}
|
|
68
|
+
const container = this._setupContainer(targetId);
|
|
129
69
|
|
|
130
|
-
|
|
131
|
-
const { text, variant, size, pill, style, class: className } = this.state;
|
|
70
|
+
const { text, variant, pill, style, class: className } = this.state;
|
|
132
71
|
|
|
133
72
|
const badge = document.createElement('span');
|
|
134
|
-
badge.className = `jux-badge jux-badge-${variant}
|
|
73
|
+
badge.className = `jux-badge jux-badge-${variant}`;
|
|
74
|
+
if (pill) badge.classList.add('jux-badge-pill');
|
|
135
75
|
badge.id = this._id;
|
|
76
|
+
if (className) badge.className += ` ${className}`;
|
|
77
|
+
if (style) badge.setAttribute('style', style);
|
|
136
78
|
badge.textContent = text;
|
|
137
79
|
|
|
138
|
-
|
|
139
|
-
badge.classList.add('jux-badge-pill');
|
|
140
|
-
}
|
|
80
|
+
this._wireStandardEvents(badge);
|
|
141
81
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
82
|
+
// Wire sync for text
|
|
83
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
84
|
+
if (property === 'text') {
|
|
85
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
86
|
+
stateObj.subscribe((val: any) => {
|
|
87
|
+
badge.textContent = transform(val);
|
|
88
|
+
this.state.text = transform(val);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
149
92
|
|
|
150
93
|
container.appendChild(badge);
|
|
94
|
+
this._injectBadgeStyles();
|
|
95
|
+
|
|
151
96
|
return this;
|
|
152
97
|
}
|
|
153
98
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
99
|
+
private _injectBadgeStyles(): void {
|
|
100
|
+
const styleId = 'jux-badge-styles';
|
|
101
|
+
if (document.getElementById(styleId)) return;
|
|
102
|
+
|
|
103
|
+
const style = document.createElement('style');
|
|
104
|
+
style.id = styleId;
|
|
105
|
+
style.textContent = `
|
|
106
|
+
.jux-badge {
|
|
107
|
+
display: inline-block;
|
|
108
|
+
padding: 0.25rem 0.75rem;
|
|
109
|
+
font-size: 0.75rem;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
line-height: 1;
|
|
112
|
+
text-align: center;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
vertical-align: baseline;
|
|
115
|
+
border-radius: 0.25rem;
|
|
116
|
+
transition: all 0.2s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.jux-badge-pill {
|
|
120
|
+
border-radius: 10rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.jux-badge-default {
|
|
124
|
+
color: #374151;
|
|
125
|
+
background-color: #e5e7eb;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.jux-badge-success {
|
|
129
|
+
color: #065f46;
|
|
130
|
+
background-color: #d1fae5;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.jux-badge-warning {
|
|
134
|
+
color: #92400e;
|
|
135
|
+
background-color: #fef3c7;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.jux-badge-error {
|
|
139
|
+
color: #991b1b;
|
|
140
|
+
background-color: #fee2e2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.jux-badge-info {
|
|
144
|
+
color: #1e40af;
|
|
145
|
+
background-color: #dbeafe;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
document.head.appendChild(style);
|
|
159
149
|
}
|
|
160
150
|
}
|
|
161
151
|
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { State } from '../../reactivity/state.js';
|
|
2
|
+
import { getOrCreateContainer } from '../helpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base class for all JUX components
|
|
6
|
+
* Provides common storage, event routing, and lifecycle methods
|
|
7
|
+
*
|
|
8
|
+
* Children must provide:
|
|
9
|
+
* - TRIGGER_EVENTS constant (readonly string[])
|
|
10
|
+
* - CALLBACK_EVENTS constant (readonly string[])
|
|
11
|
+
* - render() implementation
|
|
12
|
+
*/
|
|
13
|
+
export abstract class BaseComponent<TState extends Record<string, any>> {
|
|
14
|
+
// Common properties (all components have these)
|
|
15
|
+
state: TState;
|
|
16
|
+
container: HTMLElement | null = null;
|
|
17
|
+
_id: string;
|
|
18
|
+
id: string;
|
|
19
|
+
|
|
20
|
+
// Event & sync storage (populated by bind() and sync())
|
|
21
|
+
protected _bindings: Array<{ event: string, handler: Function }> = [];
|
|
22
|
+
protected _syncBindings: Array<{
|
|
23
|
+
property: string,
|
|
24
|
+
stateObj: State<any>,
|
|
25
|
+
toState?: Function,
|
|
26
|
+
toComponent?: Function
|
|
27
|
+
}> = [];
|
|
28
|
+
protected _triggerHandlers: Map<string, Function> = new Map();
|
|
29
|
+
protected _callbackHandlers: Map<string, Function> = new Map();
|
|
30
|
+
|
|
31
|
+
constructor(id: string, initialState: TState) {
|
|
32
|
+
this._id = id;
|
|
33
|
+
this.id = id;
|
|
34
|
+
this.state = initialState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
38
|
+
* ABSTRACT METHODS (Child must implement)
|
|
39
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
40
|
+
|
|
41
|
+
protected abstract getTriggerEvents(): readonly string[];
|
|
42
|
+
protected abstract getCallbackEvents(): readonly string[];
|
|
43
|
+
abstract render(targetId?: string): this;
|
|
44
|
+
|
|
45
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
46
|
+
* COMMON FLUENT API (Inherited by all components)
|
|
47
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set component style
|
|
51
|
+
*/
|
|
52
|
+
style(value: string): this {
|
|
53
|
+
(this.state as any).style = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set component class
|
|
59
|
+
*/
|
|
60
|
+
class(value: string): this {
|
|
61
|
+
(this.state as any).class = value;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
66
|
+
* CSS CLASS MANAGEMENT
|
|
67
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add a CSS class to the component
|
|
71
|
+
*/
|
|
72
|
+
addClass(value: string): this {
|
|
73
|
+
const current = (this.state as any).class || '';
|
|
74
|
+
const classes = current.split(' ').filter((c: string) => c);
|
|
75
|
+
if (!classes.includes(value)) {
|
|
76
|
+
classes.push(value);
|
|
77
|
+
(this.state as any).class = classes.join(' ');
|
|
78
|
+
if (this.container) this.container.classList.add(value);
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove a CSS class from the component
|
|
85
|
+
*/
|
|
86
|
+
removeClass(value: string): this {
|
|
87
|
+
const current = (this.state as any).class || '';
|
|
88
|
+
const classes = current.split(' ').filter((c: string) => c && c !== value);
|
|
89
|
+
(this.state as any).class = classes.join(' ');
|
|
90
|
+
if (this.container) this.container.classList.remove(value);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Toggle a CSS class on the component
|
|
96
|
+
*/
|
|
97
|
+
toggleClass(value: string): this {
|
|
98
|
+
const current = (this.state as any).class || '';
|
|
99
|
+
const hasClass = current.split(' ').includes(value);
|
|
100
|
+
return hasClass ? this.removeClass(value) : this.addClass(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
104
|
+
* VISIBILITY CONTROL
|
|
105
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set component visibility
|
|
109
|
+
*/
|
|
110
|
+
visible(value: boolean): this {
|
|
111
|
+
(this.state as any).visible = value;
|
|
112
|
+
if (this.container) {
|
|
113
|
+
this.container.style.display = value ? '' : 'none';
|
|
114
|
+
}
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Show the component
|
|
120
|
+
*/
|
|
121
|
+
show(): this {
|
|
122
|
+
return this.visible(true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Hide the component
|
|
127
|
+
*/
|
|
128
|
+
hide(): this {
|
|
129
|
+
return this.visible(false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Toggle component visibility
|
|
134
|
+
*/
|
|
135
|
+
toggleVisibility(): this {
|
|
136
|
+
const isVisible = (this.state as any).visible ?? true;
|
|
137
|
+
return this.visible(!isVisible);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
141
|
+
* ATTRIBUTE MANAGEMENT
|
|
142
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Set a single HTML attribute
|
|
146
|
+
*/
|
|
147
|
+
attr(name: string, value: string): this {
|
|
148
|
+
const attrs = (this.state as any).attributes || {};
|
|
149
|
+
(this.state as any).attributes = { ...attrs, [name]: value };
|
|
150
|
+
if (this.container) this.container.setAttribute(name, value);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set multiple HTML attributes
|
|
156
|
+
*/
|
|
157
|
+
attrs(attributes: Record<string, string>): this {
|
|
158
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
159
|
+
this.attr(name, value);
|
|
160
|
+
});
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Remove an HTML attribute
|
|
166
|
+
*/
|
|
167
|
+
removeAttr(name: string): this {
|
|
168
|
+
const attrs = (this.state as any).attributes || {};
|
|
169
|
+
delete attrs[name];
|
|
170
|
+
if (this.container) this.container.removeAttribute(name);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
175
|
+
* DISABLED STATE
|
|
176
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Set disabled state for interactive elements
|
|
180
|
+
*/
|
|
181
|
+
disabled(value: boolean): this {
|
|
182
|
+
(this.state as any).disabled = value;
|
|
183
|
+
if (this.container) {
|
|
184
|
+
const inputs = this.container.querySelectorAll('input, button, select, textarea');
|
|
185
|
+
inputs.forEach(el => {
|
|
186
|
+
(el as HTMLInputElement).disabled = value;
|
|
187
|
+
});
|
|
188
|
+
this.container.setAttribute('aria-disabled', String(value));
|
|
189
|
+
}
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Enable the component
|
|
195
|
+
*/
|
|
196
|
+
enable(): this {
|
|
197
|
+
return this.disabled(false);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Disable the component
|
|
202
|
+
*/
|
|
203
|
+
disable(): this {
|
|
204
|
+
return this.disabled(true);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
208
|
+
* LOADING STATE
|
|
209
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Set loading state
|
|
213
|
+
*/
|
|
214
|
+
loading(value: boolean): this {
|
|
215
|
+
(this.state as any).loading = value;
|
|
216
|
+
if (this.container) {
|
|
217
|
+
if (value) {
|
|
218
|
+
this.container.classList.add('jux-loading');
|
|
219
|
+
this.container.setAttribute('aria-busy', 'true');
|
|
220
|
+
} else {
|
|
221
|
+
this.container.classList.remove('jux-loading');
|
|
222
|
+
this.container.removeAttribute('aria-busy');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
229
|
+
* FOCUS MANAGEMENT
|
|
230
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Focus the first focusable element in the component
|
|
234
|
+
*/
|
|
235
|
+
focus(): this {
|
|
236
|
+
if (this.container) {
|
|
237
|
+
const focusable = this.container.querySelector('input, button, select, textarea, [tabindex]');
|
|
238
|
+
if (focusable) (focusable as HTMLElement).focus();
|
|
239
|
+
}
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Blur the currently focused element in the component
|
|
245
|
+
*/
|
|
246
|
+
blur(): this {
|
|
247
|
+
if (this.container) {
|
|
248
|
+
const focused = this.container.querySelector(':focus');
|
|
249
|
+
if (focused) (focused as HTMLElement).blur();
|
|
250
|
+
}
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
255
|
+
* DOM MANIPULATION
|
|
256
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Remove the component from the DOM
|
|
260
|
+
*/
|
|
261
|
+
remove(): this {
|
|
262
|
+
if (this.container) {
|
|
263
|
+
this.container.remove();
|
|
264
|
+
this.container = null;
|
|
265
|
+
}
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
270
|
+
* EVENT BINDING (Shared logic)
|
|
271
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
272
|
+
|
|
273
|
+
bind(event: string, handler: Function): this {
|
|
274
|
+
if (this._isTriggerEvent(event)) {
|
|
275
|
+
this._triggerHandlers.set(event, handler);
|
|
276
|
+
} else if (this._isCallbackEvent(event)) {
|
|
277
|
+
this._callbackHandlers.set(event, handler);
|
|
278
|
+
} else {
|
|
279
|
+
this._bindings.push({ event, handler });
|
|
280
|
+
}
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Sync a component property with a State object
|
|
286
|
+
* @param property - The property to sync
|
|
287
|
+
* @param stateObj - The State object to sync with
|
|
288
|
+
* @param toStateOrTransform - Either toState function OR a simple transform function
|
|
289
|
+
* @param toComponent - Optional toComponent function (if toState was provided)
|
|
290
|
+
*/
|
|
291
|
+
sync(property: string, stateObj: State<any>, toStateOrTransform?: Function, toComponent?: Function): this {
|
|
292
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
293
|
+
throw new Error(`${this.constructor.name}.sync: Expected a State object for property "${property}"`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// If only 3 args provided, treat the function as toComponent (the common case)
|
|
297
|
+
const actualToState = (toComponent !== undefined) ? toStateOrTransform : undefined;
|
|
298
|
+
const actualToComponent = (toComponent !== undefined) ? toComponent : toStateOrTransform;
|
|
299
|
+
|
|
300
|
+
this._syncBindings.push({
|
|
301
|
+
property,
|
|
302
|
+
stateObj,
|
|
303
|
+
toState: actualToState,
|
|
304
|
+
toComponent: actualToComponent
|
|
305
|
+
});
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
protected _isTriggerEvent(event: string): boolean {
|
|
310
|
+
return this.getTriggerEvents().includes(event);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected _isCallbackEvent(event: string): boolean {
|
|
314
|
+
return this.getCallbackEvents().includes(event);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected _triggerCallback(eventName: string, ...args: any[]): void {
|
|
318
|
+
|
|
319
|
+
if (this._callbackHandlers.has(eventName)) {
|
|
320
|
+
const handler = this._callbackHandlers.get(eventName)!;
|
|
321
|
+
handler(...args);
|
|
322
|
+
} else {
|
|
323
|
+
console.warn(`🔍 No handler found for "${eventName}"`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
328
|
+
* COMMON RENDER HELPERS
|
|
329
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
330
|
+
|
|
331
|
+
protected _setupContainer(targetId?: string): HTMLElement {
|
|
332
|
+
let container: HTMLElement;
|
|
333
|
+
if (targetId) {
|
|
334
|
+
// Strip leading # if present
|
|
335
|
+
const id = targetId.startsWith('#') ? targetId.slice(1) : targetId;
|
|
336
|
+
const target = document.getElementById(id);
|
|
337
|
+
if (target) {
|
|
338
|
+
container = target;
|
|
339
|
+
} else {
|
|
340
|
+
// Gracefully create the container instead of throwing
|
|
341
|
+
console.warn(`[Jux] Target "${targetId}" not found, creating it with graceful fallback`);
|
|
342
|
+
container = getOrCreateContainer(id);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
container = getOrCreateContainer(this._id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Add universal component class for DOM inspection
|
|
349
|
+
// container.classList.add('jux-component');
|
|
350
|
+
|
|
351
|
+
this.container = container;
|
|
352
|
+
return container;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
protected _wireStandardEvents(element: HTMLElement): void {
|
|
356
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
357
|
+
element.addEventListener(event, handler as EventListener);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Automatically wire ALL sync bindings by calling the corresponding method
|
|
363
|
+
* if it exists on the component
|
|
364
|
+
*/
|
|
365
|
+
protected _wireAllSyncs(): void {
|
|
366
|
+
this._syncBindings.forEach(({ property, stateObj, toComponent }) => {
|
|
367
|
+
const transform = toComponent || ((v: any) => v);
|
|
368
|
+
|
|
369
|
+
// Check if component has a method matching the property name
|
|
370
|
+
const method = (this as any)[property];
|
|
371
|
+
|
|
372
|
+
if (typeof method === 'function') {
|
|
373
|
+
// Set initial value
|
|
374
|
+
const initialValue = transform(stateObj.value);
|
|
375
|
+
method.call(this, initialValue);
|
|
376
|
+
|
|
377
|
+
// Subscribe to changes
|
|
378
|
+
stateObj.subscribe((val: any) => {
|
|
379
|
+
const transformed = transform(val);
|
|
380
|
+
method.call(this, transformed);
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
console.warn(
|
|
384
|
+
`[Jux] ${this.constructor.name}.sync('${property}'): ` +
|
|
385
|
+
`No method .${property}() found. Property will not be synced.`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
renderTo(juxComponent: any): this {
|
|
392
|
+
if (!juxComponent?._id) {
|
|
393
|
+
throw new Error(`${this.constructor.name}.renderTo: Invalid component`);
|
|
394
|
+
}
|
|
395
|
+
return this.render(`#${juxComponent._id}`);
|
|
396
|
+
}
|
|
397
|
+
}
|