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
@@ -17,7 +17,6 @@ export interface RadioOptions {
17
17
  options?: RadioOption[];
18
18
  value?: string;
19
19
  name?: string;
20
- onChange?: (value: string) => void;
21
20
  orientation?: 'vertical' | 'horizontal';
22
21
  style?: string;
23
22
  class?: string;
@@ -45,26 +44,35 @@ type RadioState = {
45
44
  * { label: 'Medium', value: 'm' },
46
45
  * { label: 'Large', value: 'l' }
47
46
  * ],
48
- * value: 'm',
49
- * onChange: (val) => console.log(val)
50
- * }).render('#form');
47
+ * value: 'm'
48
+ * })
49
+ * .bind('change', (e) => console.log(e.target.value))
50
+ * .render('#form');
51
51
  *
52
- * // Two-way binding
52
+ * // Two-way binding with state
53
53
  * const sizeState = state('m');
54
- * jux.radio('size').bind(sizeState).render('#form');
54
+ * jux.radio('size')
55
+ * .sync('value', sizeState)
56
+ * .render('#form');
55
57
  */
56
58
  export class Radio {
57
59
  state: RadioState;
58
60
  container: HTMLElement | null = null;
59
61
  _id: string;
60
62
  id: string;
61
- private _onChange?: (value: string) => void;
62
- private _boundState?: State<string>;
63
+
64
+ // CRITICAL: Store bind/sync instructions for deferred wiring
65
+ private _bindings: Array<{ event: string, handler: Function }> = [];
66
+ private _syncBindings: Array<{
67
+ property: string,
68
+ stateObj: State<any>,
69
+ toState?: Function,
70
+ toComponent?: Function
71
+ }> = [];
63
72
 
64
73
  constructor(id: string, options: RadioOptions = {}) {
65
74
  this._id = id;
66
75
  this.id = id;
67
- this._onChange = options.onChange;
68
76
 
69
77
  this.state = {
70
78
  options: options.options ?? [],
@@ -116,26 +124,28 @@ export class Radio {
116
124
  return this;
117
125
  }
118
126
 
119
- onChange(handler: (value: string) => void): this {
120
- this._onChange = handler;
127
+ /**
128
+ * Bind event handler (stores for wiring in render)
129
+ * DOM events only: change, click, focus, blur, etc.
130
+ */
131
+ bind(event: string, handler: Function): this {
132
+ this._bindings.push({ event, handler });
121
133
  return this;
122
134
  }
123
135
 
124
136
  /**
125
- * Two-way binding to state
137
+ * Two-way sync with state (stores for wiring in render)
138
+ *
139
+ * @param property - Component property to sync ('value', 'options')
140
+ * @param stateObj - State object to sync with
141
+ * @param toState - Optional transform function when going from component to state
142
+ * @param toComponent - Optional transform function when going from state to component
126
143
  */
127
- bind(stateObj: State<string>): this {
128
- this._boundState = stateObj;
129
-
130
- // Update radio when state changes
131
- stateObj.subscribe((val) => {
132
- this.state.value = val;
133
- this._updateElement();
134
- });
135
-
136
- // Update state when radio changes
137
- this.onChange((value) => stateObj.set(value));
138
-
144
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
145
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
146
+ throw new Error(`Radio.sync: Expected a State object for property "${property}"`);
147
+ }
148
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
139
149
  return this;
140
150
  }
141
151
 
@@ -160,65 +170,154 @@ export class Radio {
160
170
  * ------------------------- */
161
171
 
162
172
  render(targetId?: string): this {
173
+ // === 1. SETUP: Get or create container ===
163
174
  let container: HTMLElement;
164
-
165
175
  if (targetId) {
166
176
  const target = document.querySelector(targetId);
167
177
  if (!target || !(target instanceof HTMLElement)) {
168
- throw new Error(`Radio: Target element "${targetId}" not found`);
178
+ throw new Error(`Radio: Target "${targetId}" not found`);
169
179
  }
170
180
  container = target;
171
181
  } else {
172
182
  container = getOrCreateContainer(this._id);
173
183
  }
174
-
175
184
  this.container = container;
185
+
186
+ // === 2. PREPARE: Destructure state and check sync flags ===
176
187
  const { options, value, name, orientation, style, class: className } = this.state;
188
+ const hasValueSync = this._syncBindings.some(b => b.property === 'value');
177
189
 
190
+ // === 3. BUILD: Create DOM elements ===
178
191
  const wrapper = document.createElement('div');
179
192
  wrapper.className = `jux-radio jux-radio-${orientation}`;
180
193
  wrapper.id = this._id;
181
- wrapper.setAttribute('role', 'radiogroup');
194
+ if (className) wrapper.className += ` ${className}`;
195
+ if (style) wrapper.setAttribute('style', style);
182
196
 
183
- if (className) {
184
- wrapper.className += ` ${className}`;
185
- }
197
+ const inputs: HTMLInputElement[] = [];
186
198
 
187
- if (style) {
188
- wrapper.setAttribute('style', style);
189
- }
199
+ options.forEach((option) => {
200
+ const radioItem = document.createElement('div');
201
+ radioItem.className = 'jux-radio-item';
190
202
 
191
- options.forEach((opt, index) => {
192
203
  const label = document.createElement('label');
193
204
  label.className = 'jux-radio-label';
194
205
 
195
206
  const input = document.createElement('input');
196
207
  input.type = 'radio';
197
208
  input.className = 'jux-radio-input';
198
- input.id = `${this._id}-${index}`;
199
209
  input.name = name;
200
- input.value = opt.value;
201
- input.checked = opt.value === value;
202
- input.disabled = opt.disabled ?? false;
203
-
204
- input.addEventListener('change', (e) => {
205
- const target = e.target as HTMLInputElement;
206
- this.state.value = target.value;
207
- if (this._onChange) {
208
- this._onChange(target.value);
209
- }
210
- });
210
+ input.value = option.value;
211
+ input.checked = option.value === value;
212
+ input.disabled = option.disabled || false;
213
+ inputs.push(input);
214
+
215
+ const checkmark = document.createElement('span');
216
+ checkmark.className = 'jux-radio-checkmark';
217
+
218
+ const text = document.createElement('span');
219
+ text.className = 'jux-radio-text';
220
+ text.textContent = option.label;
211
221
 
212
222
  label.appendChild(input);
223
+ label.appendChild(checkmark);
224
+ label.appendChild(text);
225
+ radioItem.appendChild(label);
226
+ wrapper.appendChild(radioItem);
227
+ });
228
+
229
+ // === 4. WIRE: Attach event listeners and sync bindings ===
213
230
 
214
- const span = document.createElement('span');
215
- span.className = 'jux-radio-text';
216
- span.textContent = opt.label;
217
- label.appendChild(span);
231
+ // Default behavior (only if NOT using sync)
232
+ if (!hasValueSync) {
233
+ inputs.forEach(input => {
234
+ input.addEventListener('change', () => {
235
+ if (input.checked) {
236
+ this.state.value = input.value;
237
+ }
238
+ });
239
+ });
240
+ }
241
+
242
+ // Wire custom bindings from .bind() calls
243
+ this._bindings.forEach(({ event, handler }) => {
244
+ wrapper.addEventListener(event, handler as EventListener);
245
+ });
218
246
 
219
- wrapper.appendChild(label);
247
+ // Wire sync bindings from .sync() calls
248
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
249
+ if (property === 'value') {
250
+ const transformToState = toState || ((v: any) => v);
251
+ const transformToComponent = toComponent || ((v: any) => String(v));
252
+
253
+ let isUpdating = false;
254
+
255
+ // State → Component
256
+ stateObj.subscribe((val: any) => {
257
+ if (isUpdating) return;
258
+ const transformed = transformToComponent(val);
259
+ inputs.forEach(input => {
260
+ input.checked = input.value === transformed;
261
+ });
262
+ this.state.value = transformed;
263
+ });
264
+
265
+ // Component → State
266
+ inputs.forEach(input => {
267
+ input.addEventListener('change', () => {
268
+ if (isUpdating || !input.checked) return;
269
+ isUpdating = true;
270
+
271
+ const transformed = transformToState(input.value);
272
+ this.state.value = input.value;
273
+ stateObj.set(transformed);
274
+
275
+ setTimeout(() => { isUpdating = false; }, 0);
276
+ });
277
+ });
278
+ }
279
+ else if (property === 'options') {
280
+ const transformToComponent = toComponent || ((v: any) => v);
281
+
282
+ stateObj.subscribe((val: any) => {
283
+ const transformed = transformToComponent(val);
284
+ this.state.options = transformed;
285
+
286
+ // Re-render options
287
+ wrapper.innerHTML = '';
288
+ transformed.forEach((option: any) => {
289
+ const radioItem = document.createElement('div');
290
+ radioItem.className = 'jux-radio-item';
291
+
292
+ const label = document.createElement('label');
293
+ label.className = 'jux-radio-label';
294
+
295
+ const input = document.createElement('input');
296
+ input.type = 'radio';
297
+ input.className = 'jux-radio-input';
298
+ input.name = this.state.name;
299
+ input.value = option.value;
300
+ input.checked = option.value === this.state.value;
301
+ input.disabled = option.disabled || false;
302
+
303
+ const checkmark = document.createElement('span');
304
+ checkmark.className = 'jux-radio-checkmark';
305
+
306
+ const text = document.createElement('span');
307
+ text.className = 'jux-radio-text';
308
+ text.textContent = option.label;
309
+
310
+ label.appendChild(input);
311
+ label.appendChild(checkmark);
312
+ label.appendChild(text);
313
+ radioItem.appendChild(label);
314
+ wrapper.appendChild(radioItem);
315
+ });
316
+ });
317
+ }
220
318
  });
221
319
 
320
+ // === 5. RENDER: Append to DOM and finalize ===
222
321
  container.appendChild(wrapper);
223
322
  return this;
224
323
  }
@@ -14,7 +14,6 @@ export interface SelectOptions {
14
14
  label?: string;
15
15
  disabled?: boolean;
16
16
  name?: string;
17
- onChange?: (value: string) => void;
18
17
  style?: string;
19
18
  class?: string;
20
19
  }
@@ -35,13 +34,19 @@ export class Select {
35
34
  container: HTMLElement | null = null;
36
35
  _id: string;
37
36
  id: string;
38
- private _onChange?: (value: string) => void;
39
- private _boundState?: State<string>;
37
+
38
+ // CRITICAL: Store bind/sync instructions for deferred wiring
39
+ private _bindings: Array<{ event: string, handler: Function }> = [];
40
+ private _syncBindings: Array<{
41
+ property: string,
42
+ stateObj: State<any>,
43
+ toState?: Function,
44
+ toComponent?: Function
45
+ }> = [];
40
46
 
41
47
  constructor(id: string, options: SelectOptions = {}) {
42
48
  this._id = id;
43
49
  this.id = id;
44
- this._onChange = options.onChange;
45
50
 
46
51
  this.state = {
47
52
  options: options.options ?? [],
@@ -55,6 +60,10 @@ export class Select {
55
60
  };
56
61
  }
57
62
 
63
+ /* -------------------------
64
+ * Fluent API
65
+ * ------------------------- */
66
+
58
67
  options(value: SelectOption[]): this {
59
68
  this.state.options = value;
60
69
  return this;
@@ -102,35 +111,35 @@ export class Select {
102
111
  return this;
103
112
  }
104
113
 
105
- onChange(handler: (value: string) => void): this {
106
- this._onChange = handler;
114
+ /**
115
+ * Bind event handler (stores for wiring in render)
116
+ * DOM events only: change, focus, blur, etc.
117
+ */
118
+ bind(event: string, handler: Function): this {
119
+ this._bindings.push({ event, handler });
107
120
  return this;
108
121
  }
109
122
 
110
123
  /**
111
- * Two-way binding to state
124
+ * Two-way sync with state (stores for wiring in render)
125
+ *
126
+ * @param property - Component property to sync ('value', 'label', 'disabled', 'options')
127
+ * @param stateObj - State object to sync with
128
+ * @param toState - Optional transform function when going from component to state
129
+ * @param toComponent - Optional transform function when going from state to component
112
130
  */
113
- bind(stateObj: State<string>): this {
114
- this._boundState = stateObj;
115
-
116
- // Update select when state changes
117
- stateObj.subscribe((val) => {
118
- this.state.value = val;
119
- this._updateElement();
120
- });
121
-
122
- // Update state when select changes
123
- const originalOnChange = this._onChange;
124
- this._onChange = (value) => {
125
- stateObj.set(value);
126
- if (originalOnChange) {
127
- originalOnChange(value);
128
- }
129
- };
130
-
131
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
132
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
133
+ throw new Error(`Select.sync: Expected a State object for property "${property}"`);
134
+ }
135
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
131
136
  return this;
132
137
  }
133
138
 
139
+ /* -------------------------
140
+ * Helpers
141
+ * ------------------------- */
142
+
134
143
  private _updateElement(): void {
135
144
  const select = document.getElementById(`${this._id}-select`) as HTMLSelectElement;
136
145
  if (select) {
@@ -143,35 +152,35 @@ export class Select {
143
152
  return this.state.value;
144
153
  }
145
154
 
155
+ /* -------------------------
156
+ * Render
157
+ * ------------------------- */
158
+
146
159
  render(targetId?: string): this {
160
+ // === 1. SETUP: Get or create container ===
147
161
  let container: HTMLElement;
148
-
149
162
  if (targetId) {
150
163
  const target = document.querySelector(targetId);
151
164
  if (!target || !(target instanceof HTMLElement)) {
152
- throw new Error(`Select: Target element "${targetId}" not found`);
165
+ throw new Error(`Select: Target "${targetId}" not found`);
153
166
  }
154
167
  container = target;
155
168
  } else {
156
169
  container = getOrCreateContainer(this._id);
157
170
  }
158
-
159
171
  this.container = container;
160
- const { options, value, placeholder, label, disabled, name, style, class: className } = this.state;
161
172
 
173
+ // === 2. PREPARE: Destructure state and check sync flags ===
174
+ const { value, options, label, disabled, name, style, class: className } = this.state;
175
+ const hasValueSync = this._syncBindings.some(b => b.property === 'value');
176
+
177
+ // === 3. BUILD: Create DOM elements ===
162
178
  const wrapper = document.createElement('div');
163
179
  wrapper.className = 'jux-select';
164
180
  wrapper.id = this._id;
181
+ if (className) wrapper.className += ` ${className}`;
182
+ if (style) wrapper.setAttribute('style', style);
165
183
 
166
- if (className) {
167
- wrapper.className += ` ${className}`;
168
- }
169
-
170
- if (style) {
171
- wrapper.setAttribute('style', style);
172
- }
173
-
174
- // Label
175
184
  if (label) {
176
185
  const labelEl = document.createElement('label');
177
186
  labelEl.className = 'jux-select-label';
@@ -184,87 +193,86 @@ export class Select {
184
193
  select.className = 'jux-select-element';
185
194
  select.id = `${this._id}-select`;
186
195
  select.name = name;
196
+ select.value = value;
187
197
  select.disabled = disabled;
188
198
 
189
- // Placeholder option
190
- if (placeholder) {
191
- const placeholderOpt = document.createElement('option');
192
- placeholderOpt.value = '';
193
- placeholderOpt.textContent = placeholder;
194
- placeholderOpt.disabled = true;
195
- placeholderOpt.selected = value === '';
196
- select.appendChild(placeholderOpt);
197
- }
198
-
199
- // Options
200
199
  options.forEach(opt => {
201
200
  const option = document.createElement('option');
202
201
  option.value = opt.value;
203
202
  option.textContent = opt.label;
204
- option.disabled = opt.disabled ?? false;
205
- option.selected = opt.value === value;
203
+ if (opt.value === value) option.selected = true;
206
204
  select.appendChild(option);
207
205
  });
208
206
 
209
- select.addEventListener('change', (e) => {
210
- const target = e.target as HTMLSelectElement;
211
- this.state.value = target.value;
212
- if (this._onChange) {
213
- this._onChange(target.value);
214
- }
215
- });
216
-
217
207
  wrapper.appendChild(select);
218
- container.appendChild(wrapper);
219
-
220
- // Add default styles if not already present
221
- this._injectDefaultStyles();
222
-
223
- return this;
224
- }
225
208
 
226
- private _injectDefaultStyles(): void {
227
- const styleId = 'jux-select-styles';
228
- if (document.getElementById(styleId)) return;
209
+ // === 4. WIRE: Attach event listeners and sync bindings ===
229
210
 
230
- const style = document.createElement('style');
231
- style.id = styleId;
232
- style.textContent = `
233
- .jux-select {
234
- margin-bottom: 16px;
235
- }
211
+ // Default behavior (only if NOT using sync)
212
+ if (!hasValueSync) {
213
+ select.addEventListener('change', () => {
214
+ this.state.value = select.value;
215
+ });
216
+ }
236
217
 
237
- .jux-select-label {
238
- display: block;
239
- margin-bottom: 6px;
240
- font-weight: 500;
241
- color: #374151;
242
- }
218
+ // Wire custom bindings from .bind() calls
219
+ this._bindings.forEach(({ event, handler }) => {
220
+ wrapper.addEventListener(event, handler as EventListener);
221
+ });
243
222
 
244
- .jux-select-element {
245
- width: 100%;
246
- padding: 8px 12px;
247
- border: 1px solid #d1d5db;
248
- border-radius: 6px;
249
- font-size: 14px;
250
- background: white;
251
- cursor: pointer;
252
- transition: border-color 0.2s;
253
- box-sizing: border-box;
223
+ // Wire sync bindings from .sync() calls
224
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
225
+ if (property === 'value') {
226
+ const transformToState = toState || ((v: any) => v);
227
+ const transformToComponent = toComponent || ((v: any) => String(v));
228
+
229
+ let isUpdating = false;
230
+
231
+ // State → Component
232
+ stateObj.subscribe((val: any) => {
233
+ if (isUpdating) return;
234
+ const transformed = transformToComponent(val);
235
+ if (select.value !== transformed) {
236
+ select.value = transformed;
237
+ this.state.value = transformed;
238
+ }
239
+ });
240
+
241
+ // Component → State
242
+ select.addEventListener('change', () => {
243
+ if (isUpdating) return;
244
+ isUpdating = true;
245
+
246
+ const transformed = transformToState(select.value);
247
+ this.state.value = select.value;
248
+ stateObj.set(transformed);
249
+
250
+ setTimeout(() => { isUpdating = false; }, 0);
251
+ });
254
252
  }
255
-
256
- .jux-select-element:focus {
257
- outline: none;
258
- border-color: #3b82f6;
259
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
253
+ else if (property === 'options') {
254
+ const transformToComponent = toComponent || ((v: any) => v);
255
+
256
+ stateObj.subscribe((val: any) => {
257
+ const transformed = transformToComponent(val);
258
+ this.state.options = transformed;
259
+
260
+ // Re-render options
261
+ select.innerHTML = '';
262
+ transformed.forEach((opt: any) => {
263
+ const option = document.createElement('option');
264
+ option.value = opt.value;
265
+ option.textContent = opt.label;
266
+ if (opt.value === this.state.value) option.selected = true;
267
+ select.appendChild(option);
268
+ });
269
+ });
260
270
  }
271
+ });
261
272
 
262
- .jux-select-element:disabled {
263
- background-color: #f3f4f6;
264
- cursor: not-allowed;
265
- }
266
- `;
267
- document.head.appendChild(style);
273
+ // === 5. RENDER: Append to DOM and finalize ===
274
+ container.appendChild(wrapper);
275
+ return this;
268
276
  }
269
277
 
270
278
  renderTo(juxComponent: any): this {