juxscript 1.0.20 → 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 +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- 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/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -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 -1
- 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 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- 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,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
|
|
3
|
+
// Event definitions
|
|
4
|
+
const TRIGGER_EVENTS = [] as const;
|
|
5
|
+
const CALLBACK_EVENTS = [] as const;
|
|
3
6
|
|
|
4
7
|
export interface BadgeOptions {
|
|
5
8
|
text?: string;
|
|
6
|
-
variant?: '
|
|
7
|
-
|
|
9
|
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
10
|
+
pill?: boolean;
|
|
8
11
|
style?: string;
|
|
9
12
|
class?: string;
|
|
10
13
|
}
|
|
@@ -12,141 +15,137 @@ export interface BadgeOptions {
|
|
|
12
15
|
type BadgeState = {
|
|
13
16
|
text: string;
|
|
14
17
|
variant: string;
|
|
15
|
-
|
|
18
|
+
pill: boolean;
|
|
16
19
|
style: string;
|
|
17
20
|
class: string;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
|
-
export class Badge {
|
|
21
|
-
state: BadgeState;
|
|
22
|
-
container: HTMLElement | null = null;
|
|
23
|
-
_id: string;
|
|
24
|
-
id: string;
|
|
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
|
-
|
|
23
|
+
export class Badge extends BaseComponent<BadgeState> {
|
|
34
24
|
constructor(id: string, options: BadgeOptions = {}) {
|
|
35
|
-
|
|
36
|
-
this.id = id;
|
|
37
|
-
|
|
38
|
-
this.state = {
|
|
25
|
+
super(id, {
|
|
39
26
|
text: options.text ?? '',
|
|
40
|
-
variant: options.variant ?? '
|
|
41
|
-
|
|
27
|
+
variant: options.variant ?? 'default',
|
|
28
|
+
pill: options.pill ?? false,
|
|
42
29
|
style: options.style ?? '',
|
|
43
30
|
class: options.class ?? ''
|
|
44
|
-
};
|
|
31
|
+
});
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return this;
|
|
34
|
+
protected getTriggerEvents(): readonly string[] {
|
|
35
|
+
return TRIGGER_EVENTS;
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return this;
|
|
38
|
+
protected getCallbackEvents(): readonly string[] {
|
|
39
|
+
return CALLBACK_EVENTS;
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
42
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
43
|
+
* FLUENT API
|
|
44
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
61
45
|
|
|
62
|
-
|
|
63
|
-
this.state.style = value;
|
|
64
|
-
return this;
|
|
65
|
-
}
|
|
46
|
+
// ✅ Inherited from BaseComponent
|
|
66
47
|
|
|
67
|
-
|
|
68
|
-
this.state.
|
|
48
|
+
text(value: string): this {
|
|
49
|
+
this.state.text = value;
|
|
69
50
|
return this;
|
|
70
51
|
}
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
this.
|
|
53
|
+
variant(value: 'default' | 'success' | 'warning' | 'error' | 'info'): this {
|
|
54
|
+
this.state.variant = value;
|
|
74
55
|
return this;
|
|
75
56
|
}
|
|
76
57
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`Badge.sync: Expected a State object for property "${property}"`);
|
|
80
|
-
}
|
|
81
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
58
|
+
pill(value: boolean): this {
|
|
59
|
+
this.state.pill = value;
|
|
82
60
|
return this;
|
|
83
61
|
}
|
|
84
62
|
|
|
63
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
64
|
+
* RENDER
|
|
65
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
66
|
+
|
|
85
67
|
render(targetId?: string): this {
|
|
86
|
-
|
|
87
|
-
let container: HTMLElement;
|
|
88
|
-
if (targetId) {
|
|
89
|
-
const target = document.querySelector(targetId);
|
|
90
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
91
|
-
throw new Error(`Badge: Target "${targetId}" not found`);
|
|
92
|
-
}
|
|
93
|
-
container = target;
|
|
94
|
-
} else {
|
|
95
|
-
container = getOrCreateContainer(this._id);
|
|
96
|
-
}
|
|
97
|
-
this.container = container;
|
|
68
|
+
const container = this._setupContainer(targetId);
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
const { text, variant, size, style, class: className } = this.state;
|
|
70
|
+
const { text, variant, pill, style, class: className } = this.state;
|
|
101
71
|
|
|
102
|
-
// === 3. BUILD: Create DOM elements ===
|
|
103
72
|
const badge = document.createElement('span');
|
|
104
|
-
badge.className = `jux-badge jux-badge-${variant}
|
|
73
|
+
badge.className = `jux-badge jux-badge-${variant}`;
|
|
74
|
+
if (pill) badge.classList.add('jux-badge-pill');
|
|
105
75
|
badge.id = this._id;
|
|
106
|
-
badge.textContent = text;
|
|
107
76
|
if (className) badge.className += ` ${className}`;
|
|
108
77
|
if (style) badge.setAttribute('style', style);
|
|
78
|
+
badge.textContent = text;
|
|
109
79
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Wire custom bindings from .bind() calls
|
|
113
|
-
this._bindings.forEach(({ event, handler }) => {
|
|
114
|
-
badge.addEventListener(event, handler as EventListener);
|
|
115
|
-
});
|
|
80
|
+
this._wireStandardEvents(badge);
|
|
116
81
|
|
|
117
|
-
// Wire sync
|
|
82
|
+
// Wire sync for text
|
|
118
83
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
119
84
|
if (property === 'text') {
|
|
120
|
-
const
|
|
121
|
-
|
|
85
|
+
const transform = toComponent || ((v: any) => String(v));
|
|
122
86
|
stateObj.subscribe((val: any) => {
|
|
123
|
-
|
|
124
|
-
|
|
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}`);
|
|
87
|
+
badge.textContent = transform(val);
|
|
88
|
+
this.state.text = transform(val);
|
|
136
89
|
});
|
|
137
90
|
}
|
|
138
91
|
});
|
|
139
92
|
|
|
140
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
141
93
|
container.appendChild(badge);
|
|
94
|
+
this._injectBadgeStyles();
|
|
95
|
+
|
|
142
96
|
return this;
|
|
143
97
|
}
|
|
144
98
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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);
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
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
|
+
}
|