juxscript 1.0.3 → 1.0.5
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/README.md +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/barchart.ts +1248 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +199 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1969 -423
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +508 -77
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +280 -0
- package/lib/components/sidebar.ts +87 -37
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +178 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/lib/themes/charts.js +126 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
package/lib/components/modal.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Modal component options
|
|
@@ -9,6 +9,8 @@ export interface ModalOptions {
|
|
|
9
9
|
showCloseButton?: boolean;
|
|
10
10
|
closeOnBackdropClick?: boolean;
|
|
11
11
|
size?: 'small' | 'medium' | 'large';
|
|
12
|
+
style?: string;
|
|
13
|
+
class?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -21,6 +23,8 @@ type ModalState = {
|
|
|
21
23
|
closeOnBackdropClick: boolean;
|
|
22
24
|
size: string;
|
|
23
25
|
isOpen: boolean;
|
|
26
|
+
style: string;
|
|
27
|
+
class: string;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -35,22 +39,26 @@ type ModalState = {
|
|
|
35
39
|
* modal.render();
|
|
36
40
|
* modal.open();
|
|
37
41
|
*/
|
|
38
|
-
export class Modal
|
|
39
|
-
state
|
|
42
|
+
export class Modal {
|
|
43
|
+
state: ModalState;
|
|
40
44
|
container: HTMLElement | null = null;
|
|
45
|
+
_id: string;
|
|
46
|
+
id: string;
|
|
41
47
|
|
|
42
|
-
constructor(
|
|
43
|
-
|
|
44
|
-
this.
|
|
48
|
+
constructor(id: string, options: ModalOptions = {}) {
|
|
49
|
+
this._id = id;
|
|
50
|
+
this.id = id;
|
|
45
51
|
|
|
46
|
-
this.state =
|
|
52
|
+
this.state = {
|
|
47
53
|
title: options.title ?? '',
|
|
48
54
|
content: options.content ?? '',
|
|
49
55
|
showCloseButton: options.showCloseButton ?? true,
|
|
50
56
|
closeOnBackdropClick: options.closeOnBackdropClick ?? true,
|
|
51
57
|
size: options.size ?? 'medium',
|
|
52
|
-
isOpen: false
|
|
53
|
-
|
|
58
|
+
isOpen: false,
|
|
59
|
+
style: options.style ?? '',
|
|
60
|
+
class: options.class ?? ''
|
|
61
|
+
};
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/* -------------------------
|
|
@@ -82,6 +90,20 @@ export class Modal extends Reactive {
|
|
|
82
90
|
return this;
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
style(value: string): this {
|
|
94
|
+
this.state.style = value;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class(value: string): this {
|
|
99
|
+
this.state.class = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* -------------------------
|
|
104
|
+
* Modal controls
|
|
105
|
+
* ------------------------- */
|
|
106
|
+
|
|
85
107
|
open(): this {
|
|
86
108
|
this.state.isOpen = true;
|
|
87
109
|
if (this.container) {
|
|
@@ -90,7 +112,6 @@ export class Modal extends Reactive {
|
|
|
90
112
|
modalEl.classList.add('jux-modal-open');
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
|
-
this.emit('open');
|
|
94
115
|
return this;
|
|
95
116
|
}
|
|
96
117
|
|
|
@@ -102,7 +123,6 @@ export class Modal extends Reactive {
|
|
|
102
123
|
modalEl.classList.remove('jux-modal-open');
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
|
-
this.emit('close');
|
|
106
126
|
return this;
|
|
107
127
|
}
|
|
108
128
|
|
|
@@ -120,16 +140,24 @@ export class Modal extends Reactive {
|
|
|
120
140
|
}
|
|
121
141
|
container = target;
|
|
122
142
|
} else {
|
|
123
|
-
container = getOrCreateContainer(this.
|
|
143
|
+
container = getOrCreateContainer(this._id);
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
this.container = container;
|
|
127
|
-
const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen } = this.state;
|
|
147
|
+
const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen, style, class: className } = this.state;
|
|
128
148
|
|
|
129
149
|
// Modal backdrop
|
|
130
150
|
const modal = document.createElement('div');
|
|
131
151
|
modal.className = `jux-modal jux-modal-${size}`;
|
|
132
|
-
modal.id = this.
|
|
152
|
+
modal.id = this._id;
|
|
153
|
+
|
|
154
|
+
if (className) {
|
|
155
|
+
modal.className += ` ${className}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (style) {
|
|
159
|
+
modal.setAttribute('style', style);
|
|
160
|
+
}
|
|
133
161
|
|
|
134
162
|
if (isOpen) {
|
|
135
163
|
modal.classList.add('jux-modal-open');
|
|
@@ -183,11 +211,12 @@ export class Modal extends Reactive {
|
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
// Event binding - escape key
|
|
186
|
-
|
|
214
|
+
const escapeHandler = (e: KeyboardEvent) => {
|
|
187
215
|
if (e.key === 'Escape' && this.state.isOpen) {
|
|
188
216
|
this.close();
|
|
189
217
|
}
|
|
190
|
-
}
|
|
218
|
+
};
|
|
219
|
+
document.addEventListener('keydown', escapeHandler);
|
|
191
220
|
|
|
192
221
|
return this;
|
|
193
222
|
}
|
|
@@ -200,17 +229,17 @@ export class Modal extends Reactive {
|
|
|
200
229
|
throw new Error('Modal.renderTo: Invalid component - not an object');
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
if (!juxComponent.
|
|
204
|
-
throw new Error('Modal.renderTo: Invalid component - missing
|
|
232
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
233
|
+
throw new Error('Modal.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
205
234
|
}
|
|
206
235
|
|
|
207
|
-
return this.render(`#${juxComponent.
|
|
236
|
+
return this.render(`#${juxComponent._id}`);
|
|
208
237
|
}
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
/**
|
|
212
241
|
* Factory helper
|
|
213
242
|
*/
|
|
214
|
-
export function modal(
|
|
215
|
-
return new Modal(
|
|
243
|
+
export function modal(id: string, options: ModalOptions = {}): Modal {
|
|
244
|
+
return new Modal(id, options);
|
|
216
245
|
}
|
package/lib/components/nav.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { req } from './req.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Nav item configuration
|
|
@@ -15,6 +16,8 @@ export interface NavItem {
|
|
|
15
16
|
export interface NavOptions {
|
|
16
17
|
items?: NavItem[];
|
|
17
18
|
variant?: 'default' | 'pills' | 'tabs';
|
|
19
|
+
style?: string;
|
|
20
|
+
class?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -23,6 +26,8 @@ export interface NavOptions {
|
|
|
23
26
|
type NavState = {
|
|
24
27
|
items: NavItem[];
|
|
25
28
|
variant: string;
|
|
29
|
+
style: string;
|
|
30
|
+
class: string;
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -38,18 +43,35 @@ type NavState = {
|
|
|
38
43
|
* });
|
|
39
44
|
* nav.render();
|
|
40
45
|
*/
|
|
41
|
-
export class Nav
|
|
42
|
-
state
|
|
46
|
+
export class Nav {
|
|
47
|
+
state: NavState;
|
|
43
48
|
container: HTMLElement | null = null;
|
|
49
|
+
_id: string;
|
|
50
|
+
id: string;
|
|
44
51
|
|
|
45
|
-
constructor(
|
|
46
|
-
|
|
47
|
-
this.
|
|
52
|
+
constructor(id: string, options: NavOptions = {}) {
|
|
53
|
+
this._id = id;
|
|
54
|
+
this.id = id;
|
|
48
55
|
|
|
49
|
-
this.state =
|
|
56
|
+
this.state = {
|
|
50
57
|
items: options.items ?? [],
|
|
51
|
-
variant: options.variant ?? 'default'
|
|
52
|
-
|
|
58
|
+
variant: options.variant ?? 'default',
|
|
59
|
+
style: options.style ?? '',
|
|
60
|
+
class: options.class ?? ''
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Auto-set active state based on current path
|
|
64
|
+
this._setActiveStates();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set active state on items based on current request path
|
|
69
|
+
*/
|
|
70
|
+
private _setActiveStates(): void {
|
|
71
|
+
this.state.items = this.state.items.map(item => ({
|
|
72
|
+
...item,
|
|
73
|
+
active: req.isActiveNavItem(item.href)
|
|
74
|
+
}));
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
/* -------------------------
|
|
@@ -58,11 +80,13 @@ export class Nav extends Reactive {
|
|
|
58
80
|
|
|
59
81
|
items(value: NavItem[]): this {
|
|
60
82
|
this.state.items = value;
|
|
83
|
+
this._setActiveStates();
|
|
61
84
|
return this;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
addItem(item: NavItem): this {
|
|
65
|
-
this.state.items.
|
|
88
|
+
this.state.items = [...this.state.items, item];
|
|
89
|
+
this._setActiveStates();
|
|
66
90
|
return this;
|
|
67
91
|
}
|
|
68
92
|
|
|
@@ -71,6 +95,16 @@ export class Nav extends Reactive {
|
|
|
71
95
|
return this;
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
style(value: string): this {
|
|
99
|
+
this.state.style = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class(value: string): this {
|
|
104
|
+
this.state.class = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/* -------------------------
|
|
75
109
|
* Render
|
|
76
110
|
* ------------------------- */
|
|
@@ -85,15 +119,23 @@ export class Nav extends Reactive {
|
|
|
85
119
|
}
|
|
86
120
|
container = target;
|
|
87
121
|
} else {
|
|
88
|
-
container = getOrCreateContainer(this.
|
|
122
|
+
container = getOrCreateContainer(this._id);
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
this.container = container;
|
|
92
|
-
const { items, variant } = this.state;
|
|
126
|
+
const { items, variant, style, class: className } = this.state;
|
|
93
127
|
|
|
94
128
|
const nav = document.createElement('nav');
|
|
95
129
|
nav.className = `jux-nav jux-nav-${variant}`;
|
|
96
|
-
nav.id = this.
|
|
130
|
+
nav.id = this._id;
|
|
131
|
+
|
|
132
|
+
if (className) {
|
|
133
|
+
nav.className += ` ${className}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (style) {
|
|
137
|
+
nav.setAttribute('style', style);
|
|
138
|
+
}
|
|
97
139
|
|
|
98
140
|
items.forEach(item => {
|
|
99
141
|
const link = document.createElement('a');
|
|
@@ -120,17 +162,17 @@ export class Nav extends Reactive {
|
|
|
120
162
|
throw new Error('Nav.renderTo: Invalid component - not an object');
|
|
121
163
|
}
|
|
122
164
|
|
|
123
|
-
if (!juxComponent.
|
|
124
|
-
throw new Error('Nav.renderTo: Invalid component - missing
|
|
165
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
166
|
+
throw new Error('Nav.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
125
167
|
}
|
|
126
168
|
|
|
127
|
-
return this.render(`#${juxComponent.
|
|
169
|
+
return this.render(`#${juxComponent._id}`);
|
|
128
170
|
}
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
/**
|
|
132
174
|
* Factory helper
|
|
133
175
|
*/
|
|
134
|
-
export function nav(
|
|
135
|
-
return new Nav(
|
|
176
|
+
export function nav(id: string, options: NavOptions = {}): Nav {
|
|
177
|
+
return new Nav(id, options);
|
|
136
178
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Paragraph options
|
|
5
|
+
*/
|
|
6
|
+
export interface ParagraphOptions {
|
|
7
|
+
text?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
style?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Paragraph state
|
|
14
|
+
*/
|
|
15
|
+
type ParagraphState = {
|
|
16
|
+
text: string;
|
|
17
|
+
class: string;
|
|
18
|
+
style: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Paragraph component - semantic paragraph element
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* jux.paragraph('intro', { text: 'Welcome to JUX' }).render('#app');
|
|
26
|
+
* jux.paragraph('description').text('A simple framework').render('#app');
|
|
27
|
+
*/
|
|
28
|
+
export class Paragraph {
|
|
29
|
+
state: ParagraphState;
|
|
30
|
+
container: HTMLElement | null = null;
|
|
31
|
+
_id: string;
|
|
32
|
+
id: string;
|
|
33
|
+
|
|
34
|
+
constructor(id: string, options: ParagraphOptions = {}) {
|
|
35
|
+
this._id = id;
|
|
36
|
+
this.id = id;
|
|
37
|
+
|
|
38
|
+
this.state = {
|
|
39
|
+
text: options.text ?? '',
|
|
40
|
+
class: options.class ?? '',
|
|
41
|
+
style: options.style ?? ''
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* -------------------------
|
|
46
|
+
* Fluent API
|
|
47
|
+
* ------------------------- */
|
|
48
|
+
|
|
49
|
+
text(value: string): this {
|
|
50
|
+
this.state.text = value;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class(value: string): this {
|
|
55
|
+
this.state.class = value;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
style(value: string): this {
|
|
60
|
+
this.state.style = value;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* -------------------------
|
|
65
|
+
* Render
|
|
66
|
+
* ------------------------- */
|
|
67
|
+
|
|
68
|
+
render(targetId?: string | HTMLElement): this {
|
|
69
|
+
let container: HTMLElement;
|
|
70
|
+
|
|
71
|
+
if (targetId) {
|
|
72
|
+
if (targetId instanceof HTMLElement) {
|
|
73
|
+
container = targetId;
|
|
74
|
+
} else {
|
|
75
|
+
const target = document.querySelector(targetId);
|
|
76
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
77
|
+
throw new Error(`Paragraph: Target element "${targetId}" not found`);
|
|
78
|
+
}
|
|
79
|
+
container = target;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
container = getOrCreateContainer(this._id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.container = container;
|
|
86
|
+
const { text, class: className, style } = this.state;
|
|
87
|
+
|
|
88
|
+
const p = document.createElement('p');
|
|
89
|
+
p.id = this._id;
|
|
90
|
+
p.textContent = text;
|
|
91
|
+
|
|
92
|
+
if (className) {
|
|
93
|
+
p.className = className;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (style) {
|
|
97
|
+
p.setAttribute('style', style);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
container.appendChild(p);
|
|
101
|
+
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Factory helper
|
|
108
|
+
*/
|
|
109
|
+
export function paragraph(id: string, options: ParagraphOptions = {}): Paragraph {
|
|
110
|
+
return new Paragraph(id, options);
|
|
111
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Progress component options
|
|
6
|
+
*/
|
|
7
|
+
export interface ProgressOptions {
|
|
8
|
+
value?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
showPercentage?: boolean;
|
|
12
|
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
striped?: boolean;
|
|
15
|
+
animated?: boolean;
|
|
16
|
+
style?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Progress component state
|
|
22
|
+
*/
|
|
23
|
+
type ProgressState = {
|
|
24
|
+
value: number;
|
|
25
|
+
max: number;
|
|
26
|
+
label: string;
|
|
27
|
+
showPercentage: boolean;
|
|
28
|
+
variant: string;
|
|
29
|
+
size: string;
|
|
30
|
+
striped: boolean;
|
|
31
|
+
animated: boolean;
|
|
32
|
+
style: string;
|
|
33
|
+
class: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Progress component - Progress bar for loading/completion
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* jux.progress('upload', {
|
|
41
|
+
* value: 45,
|
|
42
|
+
* max: 100,
|
|
43
|
+
* label: 'Uploading...',
|
|
44
|
+
* showPercentage: true,
|
|
45
|
+
* animated: true
|
|
46
|
+
* }).render('#app');
|
|
47
|
+
*
|
|
48
|
+
* // Update progress
|
|
49
|
+
* const prog = jux.progress('upload').render('#app');
|
|
50
|
+
* prog.value(75);
|
|
51
|
+
*/
|
|
52
|
+
export class Progress {
|
|
53
|
+
state: ProgressState;
|
|
54
|
+
container: HTMLElement | null = null;
|
|
55
|
+
_id: string;
|
|
56
|
+
id: string;
|
|
57
|
+
private _boundState?: State<number>;
|
|
58
|
+
|
|
59
|
+
constructor(id: string, options: ProgressOptions = {}) {
|
|
60
|
+
this._id = id;
|
|
61
|
+
this.id = id;
|
|
62
|
+
|
|
63
|
+
this.state = {
|
|
64
|
+
value: options.value ?? 0,
|
|
65
|
+
max: options.max ?? 100,
|
|
66
|
+
label: options.label ?? '',
|
|
67
|
+
showPercentage: options.showPercentage ?? false,
|
|
68
|
+
variant: options.variant ?? 'default',
|
|
69
|
+
size: options.size ?? 'md',
|
|
70
|
+
striped: options.striped ?? false,
|
|
71
|
+
animated: options.animated ?? false,
|
|
72
|
+
style: options.style ?? '',
|
|
73
|
+
class: options.class ?? ''
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* -------------------------
|
|
78
|
+
* Fluent API
|
|
79
|
+
* ------------------------- */
|
|
80
|
+
|
|
81
|
+
value(value: number): this {
|
|
82
|
+
this.state.value = Math.max(0, Math.min(value, this.state.max));
|
|
83
|
+
this._updateElement();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
max(value: number): this {
|
|
88
|
+
this.state.max = value;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
label(value: string): this {
|
|
93
|
+
this.state.label = value;
|
|
94
|
+
this._updateElement();
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
showPercentage(value: boolean): this {
|
|
99
|
+
this.state.showPercentage = value;
|
|
100
|
+
this._updateElement();
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
variant(value: 'default' | 'success' | 'warning' | 'error' | 'info'): this {
|
|
105
|
+
this.state.variant = value;
|
|
106
|
+
this._updateElement();
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
size(value: 'sm' | 'md' | 'lg'): this {
|
|
111
|
+
this.state.size = value;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
striped(value: boolean): this {
|
|
116
|
+
this.state.striped = value;
|
|
117
|
+
this._updateElement();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
animated(value: boolean): this {
|
|
122
|
+
this.state.animated = value;
|
|
123
|
+
this._updateElement();
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
style(value: string): this {
|
|
128
|
+
this.state.style = value;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class(value: string): this {
|
|
133
|
+
this.state.class = value;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Two-way binding to state
|
|
139
|
+
*/
|
|
140
|
+
bind(stateObj: State<number>): this {
|
|
141
|
+
this._boundState = stateObj;
|
|
142
|
+
|
|
143
|
+
stateObj.subscribe((val) => {
|
|
144
|
+
this.value(val);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* -------------------------
|
|
151
|
+
* Helpers
|
|
152
|
+
* ------------------------- */
|
|
153
|
+
|
|
154
|
+
private _updateElement(): void {
|
|
155
|
+
const wrapper = document.getElementById(this._id);
|
|
156
|
+
const bar = document.getElementById(`${this._id}-bar`);
|
|
157
|
+
const labelEl = document.getElementById(`${this._id}-label`);
|
|
158
|
+
|
|
159
|
+
// If element has a value attribute set externally (e.g., by bindValue), sync state
|
|
160
|
+
if (wrapper && wrapper.hasAttribute('data-value')) {
|
|
161
|
+
const externalValue = parseFloat(wrapper.getAttribute('data-value') || '0');
|
|
162
|
+
this.state.value = externalValue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (bar) {
|
|
166
|
+
const percentage = (this.state.value / this.state.max) * 100;
|
|
167
|
+
bar.style.width = `${percentage}%`;
|
|
168
|
+
bar.setAttribute('aria-valuenow', this.state.value.toString());
|
|
169
|
+
bar.className = `jux-progress-bar jux-progress-bar-${this.state.variant}`;
|
|
170
|
+
|
|
171
|
+
if (this.state.striped) {
|
|
172
|
+
bar.classList.add('jux-progress-bar-striped');
|
|
173
|
+
}
|
|
174
|
+
if (this.state.animated) {
|
|
175
|
+
bar.classList.add('jux-progress-bar-animated');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (labelEl) {
|
|
180
|
+
const percentage = Math.round((this.state.value / this.state.max) * 100);
|
|
181
|
+
const text = this.state.showPercentage
|
|
182
|
+
? `${this.state.label} ${percentage}%`.trim()
|
|
183
|
+
: this.state.label;
|
|
184
|
+
labelEl.textContent = text;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getPercentage(): number {
|
|
189
|
+
return Math.round((this.state.value / this.state.max) * 100);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* -------------------------
|
|
193
|
+
* Render
|
|
194
|
+
* ------------------------- */
|
|
195
|
+
|
|
196
|
+
render(targetId?: string): this {
|
|
197
|
+
let container: HTMLElement;
|
|
198
|
+
|
|
199
|
+
if (targetId) {
|
|
200
|
+
const target = document.querySelector(targetId);
|
|
201
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
202
|
+
throw new Error(`Progress: Target element "${targetId}" not found`);
|
|
203
|
+
}
|
|
204
|
+
container = target;
|
|
205
|
+
} else {
|
|
206
|
+
container = getOrCreateContainer(this._id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.container = container;
|
|
210
|
+
const { value, max, label, showPercentage, variant, size, striped, animated, style, class: className } = this.state;
|
|
211
|
+
|
|
212
|
+
const wrapper = document.createElement('div');
|
|
213
|
+
wrapper.className = `jux-progress jux-progress-${size}`;
|
|
214
|
+
wrapper.id = this._id;
|
|
215
|
+
|
|
216
|
+
if (className) {
|
|
217
|
+
wrapper.className += ` ${className}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (style) {
|
|
221
|
+
wrapper.setAttribute('style', style);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Label
|
|
225
|
+
if (label || showPercentage) {
|
|
226
|
+
const labelEl = document.createElement('div');
|
|
227
|
+
labelEl.className = 'jux-progress-label';
|
|
228
|
+
labelEl.id = `${this._id}-label`;
|
|
229
|
+
const percentage = Math.round((value / max) * 100);
|
|
230
|
+
const text = showPercentage
|
|
231
|
+
? `${label} ${percentage}%`.trim()
|
|
232
|
+
: label;
|
|
233
|
+
labelEl.textContent = text;
|
|
234
|
+
wrapper.appendChild(labelEl);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Progress track
|
|
238
|
+
const track = document.createElement('div');
|
|
239
|
+
track.className = 'jux-progress-track';
|
|
240
|
+
|
|
241
|
+
// Progress bar
|
|
242
|
+
const bar = document.createElement('div');
|
|
243
|
+
bar.className = `jux-progress-bar jux-progress-bar-${variant}`;
|
|
244
|
+
bar.id = `${this._id}-bar`;
|
|
245
|
+
bar.setAttribute('role', 'progressbar');
|
|
246
|
+
bar.setAttribute('aria-valuenow', value.toString());
|
|
247
|
+
bar.setAttribute('aria-valuemin', '0');
|
|
248
|
+
bar.setAttribute('aria-valuemax', max.toString());
|
|
249
|
+
|
|
250
|
+
const percentage = (value / max) * 100;
|
|
251
|
+
bar.style.width = `${percentage}%`;
|
|
252
|
+
|
|
253
|
+
if (striped) {
|
|
254
|
+
bar.classList.add('jux-progress-bar-striped');
|
|
255
|
+
}
|
|
256
|
+
if (animated) {
|
|
257
|
+
bar.classList.add('jux-progress-bar-animated');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
track.appendChild(bar);
|
|
261
|
+
wrapper.appendChild(track);
|
|
262
|
+
container.appendChild(wrapper);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderTo(juxComponent: any): this {
|
|
267
|
+
if (!juxComponent?._id) {
|
|
268
|
+
throw new Error('Progress.renderTo: Invalid component');
|
|
269
|
+
}
|
|
270
|
+
return this.render(`#${juxComponent._id}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function progress(id: string, options: ProgressOptions = {}): Progress {
|
|
275
|
+
return new Progress(id, options);
|
|
276
|
+
}
|