juxscript 1.0.19 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) 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 +92 -60
  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 +697 -274
  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 +105 -53
  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 +54 -91
  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/package.json +1 -2
@@ -21,21 +21,6 @@ type ParagraphState = {
21
21
 
22
22
  /**
23
23
  * Paragraph component - semantic paragraph element
24
- *
25
- * Usage:
26
- * jux.paragraph('intro', { text: 'Welcome to JUX' }).render('#app');
27
- * jux.paragraph('description').text('A simple framework').render('#app');
28
- *
29
- * // With state binding
30
- * jux.paragraph('counter')
31
- * .text('Count: 0')
32
- * .bind('text', count, (val) => `Count: ${val}`)
33
- * .render('#app');
34
- *
35
- * // With sync (one-way for paragraph)
36
- * jux.paragraph('display')
37
- * .sync('text', count, (val) => `Count: ${val}`)
38
- * .render('#app');
39
24
  */
40
25
  export class Paragraph {
41
26
  state: ParagraphState;
@@ -43,11 +28,14 @@ export class Paragraph {
43
28
  _id: string;
44
29
  id: string;
45
30
 
46
- // Store bind() instructions
47
- private _bindings: Array<{ event: string, handler: Function, stateObj?: State<any>, transform?: Function }> = [];
48
-
49
- // Store sync() instructions
50
- private _syncBindings: Array<{ property: string, stateObj: State<any>, transform?: Function }> = [];
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
+ }> = [];
51
39
 
52
40
  constructor(id: string, options: ParagraphOptions = {}) {
53
41
  this._id = id;
@@ -80,36 +68,26 @@ export class Paragraph {
80
68
  }
81
69
 
82
70
  /**
83
- * Bind event or state (stores for wiring in render)
71
+ * Bind event handler (stores for wiring in render)
72
+ * DOM events only: click, mouseenter, etc.
84
73
  */
85
- bind(property: string, source: State<any> | Function, transform?: Function): this {
86
- if (typeof source === 'function') {
87
- // Event binding
88
- this._bindings.push({ event: property, handler: source });
89
- } else {
90
- // Validate it's a State object
91
- if (!source || typeof source.subscribe !== 'function') {
92
- throw new Error(`Paragraph.bind: Expected a State object, got ${typeof source}. Did you pass 'state.value' instead of 'state'?`);
93
- }
94
- // State binding
95
- this._bindings.push({ event: property, handler: () => { }, stateObj: source, transform });
96
- }
74
+ bind(event: string, handler: Function): this {
75
+ this._bindings.push({ event, handler });
97
76
  return this;
98
77
  }
99
78
 
100
79
  /**
101
- * Sync with state (one-way for paragraph: State → Component)
80
+ * Sync with state (one-way: State → Component)
102
81
  *
103
82
  * @param property - Component property to sync ('text', 'class', 'style')
104
83
  * @param stateObj - State object to sync with
105
- * @param transform - Optional transform function when going from state to component
84
+ * @param transform - Optional transform function from state to component
106
85
  */
107
- sync(property: string, stateObj: State<any>, transform?: Function): this {
108
- // Validate it's a State object
86
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
109
87
  if (!stateObj || typeof stateObj.subscribe !== 'function') {
110
- throw new Error(`Paragraph.sync: Expected a State object, got ${typeof stateObj}. Did you pass 'state.value' instead of 'state'?`);
88
+ throw new Error(`Paragraph.sync: Expected a State object for property "${property}"`);
111
89
  }
112
- this._syncBindings.push({ property, stateObj, transform });
90
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
113
91
  return this;
114
92
  }
115
93
 
@@ -117,77 +95,62 @@ export class Paragraph {
117
95
  * Render
118
96
  * ------------------------- */
119
97
 
120
- render(targetId?: string | HTMLElement): this {
98
+ render(targetId?: string): this {
99
+ // === 1. SETUP: Get or create container ===
121
100
  let container: HTMLElement;
122
-
123
101
  if (targetId) {
124
- if (targetId instanceof HTMLElement) {
125
- container = targetId;
126
- } else {
127
- const target = document.querySelector(targetId);
128
- if (!target || !(target instanceof HTMLElement)) {
129
- throw new Error(`Paragraph: Target element "${targetId}" not found`);
130
- }
131
- container = target;
102
+ const target = document.querySelector(targetId);
103
+ if (!target || !(target instanceof HTMLElement)) {
104
+ throw new Error(`Paragraph: Target "${targetId}" not found`);
132
105
  }
106
+ container = target;
133
107
  } else {
134
108
  container = getOrCreateContainer(this._id);
135
109
  }
136
-
137
110
  this.container = container;
138
- const { text, class: className, style } = this.state;
139
-
140
- const p = document.createElement('p');
141
- p.id = this._id;
142
- p.textContent = text;
143
111
 
144
- if (className) {
145
- p.className = className;
146
- }
112
+ // === 2. PREPARE: Destructure state ===
113
+ const { text, style, class: className } = this.state;
147
114
 
148
- if (style) {
149
- p.setAttribute('style', style);
150
- }
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);
151
122
 
152
- container.appendChild(p);
123
+ // === 4. WIRE: Attach event listeners and sync bindings ===
153
124
 
154
- // Wire up bind() bindings after DOM element is created
155
- this._bindings.forEach(({ event, handler, stateObj, transform }) => {
156
- if (stateObj) {
157
- // State binding - subscribe to state changes
158
- stateObj.subscribe((val: any) => {
159
- const transformed = transform ? transform(val) : val;
160
- if (event === 'text') {
161
- p.textContent = transformed;
162
- this.state.text = transformed;
163
- }
164
- });
165
- } else {
166
- // Event binding
167
- p.addEventListener(event, handler as EventListener);
168
- }
125
+ // Wire custom bindings from .bind() calls
126
+ this._bindings.forEach(({ event, handler }) => {
127
+ paragraph.addEventListener(event, handler as EventListener);
169
128
  });
170
129
 
171
- // Wire up sync() bindings (State → Component)
172
- this._syncBindings.forEach(({ property, stateObj, transform }) => {
173
- stateObj.subscribe((val: any) => {
174
- const transformed = transform ? transform(val) : val;
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));
175
134
 
176
- if (property === 'text') {
177
- p.textContent = transformed;
135
+ stateObj.subscribe((val: any) => {
136
+ const transformed = transformToComponent(val);
137
+ paragraph.textContent = transformed;
178
138
  this.state.text = transformed;
179
- } else if (property === 'class') {
180
- p.className = transformed;
181
- this.state.class = transformed;
182
- } else if (property === 'style') {
183
- p.setAttribute('style', transformed);
184
- this.state.style = transformed;
185
- }
186
- });
139
+ });
140
+ }
187
141
  });
188
142
 
143
+ // === 5. RENDER: Append to DOM and finalize ===
144
+ container.appendChild(paragraph);
189
145
  return this;
190
146
  }
147
+
148
+ renderTo(juxComponent: any): this {
149
+ if (!juxComponent?._id) {
150
+ throw new Error('Paragraph.renderTo: Invalid component');
151
+ }
152
+ return this.render(`#${juxComponent._id}`);
153
+ }
191
154
  }
192
155
 
193
156
  /**
@@ -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
  }