juxscript 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/lib/components/alert.ts +124 -128
  2. package/lib/components/areachart.ts +169 -287
  3. package/lib/components/areachartsmooth.ts +2 -2
  4. package/lib/components/badge.ts +63 -72
  5. package/lib/components/barchart.ts +120 -48
  6. package/lib/components/button.ts +99 -101
  7. package/lib/components/card.ts +97 -121
  8. package/lib/components/chart-types.ts +159 -0
  9. package/lib/components/chart-utils.ts +160 -0
  10. package/lib/components/chart.ts +628 -48
  11. package/lib/components/checkbox.ts +137 -51
  12. package/lib/components/code.ts +89 -75
  13. package/lib/components/container.ts +1 -1
  14. package/lib/components/datepicker.ts +93 -78
  15. package/lib/components/dialog.ts +163 -130
  16. package/lib/components/divider.ts +111 -193
  17. package/lib/components/docs-data.json +711 -264
  18. package/lib/components/doughnutchart.ts +125 -57
  19. package/lib/components/dropdown.ts +172 -85
  20. package/lib/components/element.ts +66 -61
  21. package/lib/components/fileupload.ts +142 -171
  22. package/lib/components/heading.ts +64 -21
  23. package/lib/components/hero.ts +109 -34
  24. package/lib/components/icon.ts +247 -0
  25. package/lib/components/icons.ts +174 -0
  26. package/lib/components/include.ts +77 -2
  27. package/lib/components/input.ts +174 -125
  28. package/lib/components/list.ts +120 -79
  29. package/lib/components/menu.ts +97 -2
  30. package/lib/components/modal.ts +144 -63
  31. package/lib/components/nav.ts +153 -52
  32. package/lib/components/paragraph.ts +78 -28
  33. package/lib/components/progress.ts +83 -107
  34. package/lib/components/radio.ts +151 -52
  35. package/lib/components/select.ts +110 -102
  36. package/lib/components/sidebar.ts +148 -105
  37. package/lib/components/switch.ts +124 -125
  38. package/lib/components/table.ts +214 -137
  39. package/lib/components/tabs.ts +194 -113
  40. package/lib/components/theme-toggle.ts +38 -7
  41. package/lib/components/tooltip.ts +207 -47
  42. package/lib/jux.ts +24 -5
  43. package/lib/reactivity/state.ts +13 -299
  44. package/package.json +1 -2
@@ -1,4 +1,5 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
5
  * Paragraph options
@@ -20,10 +21,6 @@ type ParagraphState = {
20
21
 
21
22
  /**
22
23
  * 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
24
  */
28
25
  export class Paragraph {
29
26
  state: ParagraphState;
@@ -31,6 +28,15 @@ export class Paragraph {
31
28
  _id: string;
32
29
  id: string;
33
30
 
31
+ // CRITICAL: Store bind/sync instructions for deferred wiring
32
+ private _bindings: Array<{ event: string, handler: Function }> = [];
33
+ private _syncBindings: Array<{
34
+ property: string,
35
+ stateObj: State<any>,
36
+ toState?: Function,
37
+ toComponent?: Function
38
+ }> = [];
39
+
34
40
  constructor(id: string, options: ParagraphOptions = {}) {
35
41
  this._id = id;
36
42
  this.id = id;
@@ -61,45 +67,89 @@ export class Paragraph {
61
67
  return this;
62
68
  }
63
69
 
70
+ /**
71
+ * Bind event handler (stores for wiring in render)
72
+ * DOM events only: click, mouseenter, etc.
73
+ */
74
+ bind(event: string, handler: Function): this {
75
+ this._bindings.push({ event, handler });
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Sync with state (one-way: State → Component)
81
+ *
82
+ * @param property - Component property to sync ('text', 'class', 'style')
83
+ * @param stateObj - State object to sync with
84
+ * @param transform - Optional transform function from state to component
85
+ */
86
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
87
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
88
+ throw new Error(`Paragraph.sync: Expected a State object for property "${property}"`);
89
+ }
90
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
91
+ return this;
92
+ }
93
+
64
94
  /* -------------------------
65
95
  * Render
66
96
  * ------------------------- */
67
97
 
68
- render(targetId?: string | HTMLElement): this {
98
+ render(targetId?: string): this {
99
+ // === 1. SETUP: Get or create container ===
69
100
  let container: HTMLElement;
70
-
71
101
  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;
102
+ const target = document.querySelector(targetId);
103
+ if (!target || !(target instanceof HTMLElement)) {
104
+ throw new Error(`Paragraph: Target "${targetId}" not found`);
80
105
  }
106
+ container = target;
81
107
  } else {
82
108
  container = getOrCreateContainer(this._id);
83
109
  }
84
-
85
110
  this.container = container;
86
- const { text, class: className, style } = this.state;
87
111
 
88
- const p = document.createElement('p');
89
- p.id = this._id;
90
- p.textContent = text;
112
+ // === 2. PREPARE: Destructure state ===
113
+ const { text, style, class: className } = this.state;
114
+
115
+ // === 3. BUILD: Create DOM elements ===
116
+ const paragraph = document.createElement('p');
117
+ paragraph.className = 'jux-paragraph';
118
+ paragraph.id = this._id;
119
+ paragraph.textContent = text;
120
+ if (className) paragraph.className += ` ${className}`;
121
+ if (style) paragraph.setAttribute('style', style);
122
+
123
+ // === 4. WIRE: Attach event listeners and sync bindings ===
124
+
125
+ // Wire custom bindings from .bind() calls
126
+ this._bindings.forEach(({ event, handler }) => {
127
+ paragraph.addEventListener(event, handler as EventListener);
128
+ });
129
+
130
+ // Wire sync bindings from .sync() calls
131
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
132
+ if (property === 'text') {
133
+ const transformToComponent = toComponent || ((v: any) => String(v));
134
+
135
+ stateObj.subscribe((val: any) => {
136
+ const transformed = transformToComponent(val);
137
+ paragraph.textContent = transformed;
138
+ this.state.text = transformed;
139
+ });
140
+ }
141
+ });
91
142
 
92
- if (className) {
93
- p.className = className;
94
- }
143
+ // === 5. RENDER: Append to DOM and finalize ===
144
+ container.appendChild(paragraph);
145
+ return this;
146
+ }
95
147
 
96
- if (style) {
97
- p.setAttribute('style', style);
148
+ renderTo(juxComponent: any): this {
149
+ if (!juxComponent?._id) {
150
+ throw new Error('Paragraph.renderTo: Invalid component');
98
151
  }
99
-
100
- container.appendChild(p);
101
-
102
- return this;
152
+ return this.render(`#${juxComponent._id}`);
103
153
  }
104
154
  }
105
155
 
@@ -1,9 +1,6 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
2
  import { State } from '../reactivity/state.js';
3
3
 
4
- /**
5
- * Progress component options
6
- */
7
4
  export interface ProgressOptions {
8
5
  value?: number;
9
6
  max?: number;
@@ -17,9 +14,6 @@ export interface ProgressOptions {
17
14
  class?: string;
18
15
  }
19
16
 
20
- /**
21
- * Progress component state
22
- */
23
17
  type ProgressState = {
24
18
  value: number;
25
19
  max: number;
@@ -33,28 +27,20 @@ type ProgressState = {
33
27
  class: string;
34
28
  };
35
29
 
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
30
  export class Progress {
53
31
  state: ProgressState;
54
32
  container: HTMLElement | null = null;
55
33
  _id: string;
56
34
  id: string;
57
- private _boundState?: State<number>;
35
+
36
+ // CRITICAL: Store bind/sync instructions for deferred wiring
37
+ private _bindings: Array<{ event: string, handler: Function }> = [];
38
+ private _syncBindings: Array<{
39
+ property: string,
40
+ stateObj: State<any>,
41
+ toState?: Function,
42
+ toComponent?: Function
43
+ }> = [];
58
44
 
59
45
  constructor(id: string, options: ProgressOptions = {}) {
60
46
  this._id = id;
@@ -79,31 +65,29 @@ export class Progress {
79
65
  * ------------------------- */
80
66
 
81
67
  value(value: number): this {
82
- this.state.value = Math.max(0, Math.min(value, this.state.max));
83
- this._updateElement();
68
+ this.state.value = value;
69
+ this._updateProgress();
84
70
  return this;
85
71
  }
86
72
 
87
73
  max(value: number): this {
88
74
  this.state.max = value;
75
+ this._updateProgress();
89
76
  return this;
90
77
  }
91
78
 
92
79
  label(value: string): this {
93
80
  this.state.label = value;
94
- this._updateElement();
95
81
  return this;
96
82
  }
97
83
 
98
84
  showPercentage(value: boolean): this {
99
85
  this.state.showPercentage = value;
100
- this._updateElement();
101
86
  return this;
102
87
  }
103
88
 
104
89
  variant(value: 'default' | 'success' | 'warning' | 'error' | 'info'): this {
105
90
  this.state.variant = value;
106
- this._updateElement();
107
91
  return this;
108
92
  }
109
93
 
@@ -114,13 +98,11 @@ export class Progress {
114
98
 
115
99
  striped(value: boolean): this {
116
100
  this.state.striped = value;
117
- this._updateElement();
118
101
  return this;
119
102
  }
120
103
 
121
104
  animated(value: boolean): this {
122
105
  this.state.animated = value;
123
- this._updateElement();
124
106
  return this;
125
107
  }
126
108
 
@@ -134,131 +116,125 @@ export class Progress {
134
116
  return this;
135
117
  }
136
118
 
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
- });
119
+ bind(event: string, handler: Function): this {
120
+ this._bindings.push({ event, handler });
121
+ return this;
122
+ }
146
123
 
124
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
125
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
126
+ throw new Error(`Progress.sync: Expected a State object for property "${property}"`);
127
+ }
128
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
147
129
  return this;
148
130
  }
149
131
 
150
- /* -------------------------
151
- * Helpers
152
- * ------------------------- */
132
+ getPercentage(): number {
133
+ return Math.round((this.state.value / this.state.max) * 100);
134
+ }
153
135
 
154
- private _updateElement(): void {
155
- const wrapper = document.getElementById(this._id);
136
+ private _updateProgress(): void {
156
137
  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
- }
138
+ const percentageEl = document.getElementById(`${this._id}-percentage`);
164
139
 
165
140
  if (bar) {
166
- const percentage = (this.state.value / this.state.max) * 100;
141
+ const percentage = this.getPercentage();
167
142
  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
143
 
171
- if (this.state.striped) {
172
- bar.classList.add('jux-progress-bar-striped');
144
+ if (percentageEl) {
145
+ percentageEl.textContent = `${percentage}%`;
173
146
  }
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
147
  }
186
148
  }
187
149
 
188
- getPercentage(): number {
189
- return Math.round((this.state.value / this.state.max) * 100);
190
- }
191
-
192
150
  /* -------------------------
193
151
  * Render
194
152
  * ------------------------- */
195
153
 
196
154
  render(targetId?: string): this {
155
+ // === 1. SETUP: Get or create container ===
197
156
  let container: HTMLElement;
198
-
199
157
  if (targetId) {
200
158
  const target = document.querySelector(targetId);
201
159
  if (!target || !(target instanceof HTMLElement)) {
202
- throw new Error(`Progress: Target element "${targetId}" not found`);
160
+ throw new Error(`Progress: Target "${targetId}" not found`);
203
161
  }
204
162
  container = target;
205
163
  } else {
206
164
  container = getOrCreateContainer(this._id);
207
165
  }
208
-
209
166
  this.container = container;
210
- const { value, max, label, showPercentage, variant, size, striped, animated, style, class: className } = this.state;
211
167
 
168
+ // === 2. PREPARE: Destructure state ===
169
+ const { value, max, variant, showPercentage, label, style, class: className } = this.state;
170
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
171
+
172
+ // === 3. BUILD: Create DOM elements ===
212
173
  const wrapper = document.createElement('div');
213
- wrapper.className = `jux-progress jux-progress-${size}`;
174
+ wrapper.className = 'jux-progress';
214
175
  wrapper.id = this._id;
176
+ if (className) wrapper.className += ` ${className}`;
177
+ if (style) wrapper.setAttribute('style', style);
215
178
 
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) {
179
+ if (showPercentage || label) {
226
180
  const labelEl = document.createElement('div');
227
181
  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;
182
+ labelEl.textContent = label || `${Math.round(percentage)}%`;
234
183
  wrapper.appendChild(labelEl);
235
184
  }
236
185
 
237
- // Progress track
238
186
  const track = document.createElement('div');
239
187
  track.className = 'jux-progress-track';
240
188
 
241
- // Progress bar
242
189
  const bar = document.createElement('div');
243
190
  bar.className = `jux-progress-bar jux-progress-bar-${variant}`;
244
191
  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
192
  bar.style.width = `${percentage}%`;
252
193
 
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
194
  track.appendChild(bar);
261
195
  wrapper.appendChild(track);
196
+
197
+ // === 4. WIRE: Attach event listeners and sync bindings ===
198
+
199
+ // Wire custom bindings from .bind() calls
200
+ this._bindings.forEach(({ event, handler }) => {
201
+ wrapper.addEventListener(event, handler as EventListener);
202
+ });
203
+
204
+ // Wire sync bindings from .sync() calls
205
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
206
+ if (property === 'value') {
207
+ const transformToComponent = toComponent || ((v: any) => Number(v));
208
+
209
+ stateObj.subscribe((val: any) => {
210
+ const transformed = transformToComponent(val);
211
+ this.state.value = transformed;
212
+ const newPercentage = Math.min(100, Math.max(0, (transformed / this.state.max) * 100));
213
+ bar.style.width = `${newPercentage}%`;
214
+
215
+ if (showPercentage && !label) {
216
+ const labelEl = wrapper.querySelector('.jux-progress-label');
217
+ if (labelEl) {
218
+ labelEl.textContent = `${Math.round(newPercentage)}%`;
219
+ }
220
+ }
221
+ });
222
+ }
223
+ else if (property === 'label') {
224
+ const transformToComponent = toComponent || ((v: any) => String(v));
225
+
226
+ stateObj.subscribe((val: any) => {
227
+ const transformed = transformToComponent(val);
228
+ const labelEl = wrapper.querySelector('.jux-progress-label');
229
+ if (labelEl) {
230
+ labelEl.textContent = transformed;
231
+ }
232
+ this.state.label = transformed;
233
+ });
234
+ }
235
+ });
236
+
237
+ // === 5. RENDER: Append to DOM and finalize ===
262
238
  container.appendChild(wrapper);
263
239
  return this;
264
240
  }