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,5 +1,6 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
2
  import { State } from '../reactivity/state.js';
3
+ import { renderIcon } from './icons.js';
3
4
 
4
5
  /**
5
6
  * Input component options
@@ -9,6 +10,7 @@ export interface InputOptions {
9
10
  value?: string;
10
11
  placeholder?: string;
11
12
  label?: string;
13
+ icon?: string;
12
14
  required?: boolean;
13
15
  disabled?: boolean;
14
16
  name?: string;
@@ -19,7 +21,6 @@ export interface InputOptions {
19
21
  minLength?: number;
20
22
  maxLength?: number;
21
23
  pattern?: string;
22
- onChange?: (value: string) => void;
23
24
  onValidate?: (value: string) => boolean | string;
24
25
  style?: string;
25
26
  class?: string;
@@ -33,6 +34,7 @@ type InputState = {
33
34
  value: string;
34
35
  placeholder: string;
35
36
  label: string;
37
+ icon: string;
36
38
  required: boolean;
37
39
  disabled: boolean;
38
40
  name: string;
@@ -48,44 +50,22 @@ type InputState = {
48
50
  errorMessage?: string;
49
51
  };
50
52
 
51
- /**
52
- * Input component - text input and textarea with validation
53
- *
54
- * Usage:
55
- * jux.input('username')
56
- * .label('Username')
57
- * .placeholder('Enter username')
58
- * .required(true)
59
- * .minLength(3)
60
- * .maxLength(20)
61
- * .render('#form');
62
- *
63
- * jux.input('age')
64
- * .type('number')
65
- * .min(0)
66
- * .max(120)
67
- * .step(1)
68
- * .render('#form');
69
- *
70
- * jux.input('bio')
71
- * .type('textarea')
72
- * .rows(5)
73
- * .maxLength(500)
74
- * .render('#form');
75
- */
76
53
  export class Input {
77
54
  state: InputState;
78
55
  container: HTMLElement | null = null;
79
56
  _id: string;
80
57
  id: string;
81
- private _onChange?: (value: string) => void;
82
58
  private _onValidate?: (value: string) => boolean | string;
83
- private _boundState?: State<string>;
59
+
60
+ // Store bind() instructions (DOM events only)
61
+ private _bindings: Array<{ event: string, handler: Function }> = [];
62
+
63
+ // Store sync() instructions (state synchronization)
64
+ private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
84
65
 
85
66
  constructor(id: string, options: InputOptions = {}) {
86
67
  this._id = id;
87
68
  this.id = id;
88
- this._onChange = options.onChange;
89
69
  this._onValidate = options.onValidate;
90
70
 
91
71
  this.state = {
@@ -93,6 +73,7 @@ export class Input {
93
73
  value: options.value ?? '',
94
74
  placeholder: options.placeholder ?? '',
95
75
  label: options.label ?? '',
76
+ icon: options.icon ?? '',
96
77
  required: options.required ?? false,
97
78
  disabled: options.disabled ?? false,
98
79
  name: options.name ?? id,
@@ -133,6 +114,11 @@ export class Input {
133
114
  return this;
134
115
  }
135
116
 
117
+ icon(value: string): this {
118
+ this.state.icon = value;
119
+ return this;
120
+ }
121
+
136
122
  required(value: boolean): this {
137
123
  this.state.required = value;
138
124
  return this;
@@ -154,54 +140,36 @@ export class Input {
154
140
  return this;
155
141
  }
156
142
 
157
- /**
158
- * Minimum value for number inputs
159
- */
160
143
  min(value: number): this {
161
144
  this.state.min = value;
162
145
  this._updateElement();
163
146
  return this;
164
147
  }
165
148
 
166
- /**
167
- * Maximum value for number inputs
168
- */
169
149
  max(value: number): this {
170
150
  this.state.max = value;
171
151
  this._updateElement();
172
152
  return this;
173
153
  }
174
154
 
175
- /**
176
- * Step value for number inputs
177
- */
178
155
  step(value: number): this {
179
156
  this.state.step = value;
180
157
  this._updateElement();
181
158
  return this;
182
159
  }
183
160
 
184
- /**
185
- * Minimum length for text inputs
186
- */
187
161
  minLength(value: number): this {
188
162
  this.state.minLength = value;
189
163
  this._updateElement();
190
164
  return this;
191
165
  }
192
166
 
193
- /**
194
- * Maximum length for text inputs
195
- */
196
167
  maxLength(value: number): this {
197
168
  this.state.maxLength = value;
198
169
  this._updateElement();
199
170
  return this;
200
171
  }
201
172
 
202
- /**
203
- * Pattern validation for text inputs (regex)
204
- */
205
173
  pattern(value: string): this {
206
174
  this.state.pattern = value;
207
175
  this._updateElement();
@@ -218,35 +186,33 @@ export class Input {
218
186
  return this;
219
187
  }
220
188
 
221
- onChange(handler: (value: string) => void): this {
222
- this._onChange = handler;
189
+ onValidate(handler: (value: string) => boolean | string): this {
190
+ this._onValidate = handler;
223
191
  return this;
224
192
  }
225
193
 
226
194
  /**
227
- * Custom validation function
228
- * Should return true if valid, or an error message string if invalid
195
+ * Bind event handler (stores for wiring in render)
196
+ * DOM events only: input, change, blur, focus, etc.
229
197
  */
230
- onValidate(handler: (value: string) => boolean | string): this {
231
- this._onValidate = handler;
198
+ bind(event: string, handler: Function): this {
199
+ this._bindings.push({ event, handler });
232
200
  return this;
233
201
  }
234
202
 
235
203
  /**
236
- * Two-way binding to state
204
+ * Two-way sync with state (stores for wiring in render)
205
+ *
206
+ * @param property - Component property to sync ('value', 'label', etc)
207
+ * @param stateObj - State object to sync with
208
+ * @param toState - Optional transform function when going from component to state
209
+ * @param toComponent - Optional transform function when going from state to component
237
210
  */
238
- bind(stateObj: State<string>): this {
239
- this._boundState = stateObj;
240
-
241
- // Subscribe to state changes
242
- stateObj.subscribe((val) => {
243
- this.state.value = val;
244
- this._updateElement();
245
- });
246
-
247
- // Update state on input change
248
- this.onChange((value) => stateObj.set(value));
249
-
211
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
212
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
213
+ throw new Error(`Input.sync: Expected a State object for property "${property}"`);
214
+ }
215
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
250
216
  return this;
251
217
  }
252
218
 
@@ -257,12 +223,10 @@ export class Input {
257
223
  private _validate(value: string): boolean | string {
258
224
  const { required, type, min, max, minLength, maxLength, pattern } = this.state;
259
225
 
260
- // Required check
261
226
  if (required && !value.trim()) {
262
227
  return 'This field is required';
263
228
  }
264
229
 
265
- // Number validation
266
230
  if (type === 'number' && value) {
267
231
  const numValue = Number(value);
268
232
 
@@ -279,7 +243,6 @@ export class Input {
279
243
  }
280
244
  }
281
245
 
282
- // Text length validation
283
246
  if ((type === 'text' || type === 'textarea') && value) {
284
247
  if (minLength !== undefined && value.length < minLength) {
285
248
  return `Must be at least ${minLength} characters`;
@@ -290,7 +253,6 @@ export class Input {
290
253
  }
291
254
  }
292
255
 
293
- // Pattern validation
294
256
  if (pattern && value) {
295
257
  const regex = new RegExp(pattern);
296
258
  if (!regex.test(value)) {
@@ -298,7 +260,6 @@ export class Input {
298
260
  }
299
261
  }
300
262
 
301
- // Custom validation
302
263
  if (this._onValidate) {
303
264
  const result = this._onValidate(value);
304
265
  if (result !== true) {
@@ -341,9 +302,6 @@ export class Input {
341
302
  this.state.errorMessage = undefined;
342
303
  }
343
304
 
344
- /**
345
- * Manually validate the current value
346
- */
347
305
  validate(): boolean {
348
306
  const result = this._validate(this.state.value);
349
307
 
@@ -400,8 +358,8 @@ export class Input {
400
358
  * ------------------------- */
401
359
 
402
360
  render(targetId?: string): this {
361
+ // === 1. SETUP: Get container ===
403
362
  let container: HTMLElement;
404
-
405
363
  if (targetId) {
406
364
  const target = document.querySelector(targetId);
407
365
  if (!target || !(target instanceof HTMLElement)) {
@@ -411,40 +369,52 @@ export class Input {
411
369
  } else {
412
370
  container = getOrCreateContainer(this._id);
413
371
  }
414
-
415
372
  this.container = container;
416
- const { type, value, placeholder, label, required, disabled, name, rows, min, max, step, minLength, maxLength, pattern, style, class: className } = this.state;
417
373
 
374
+ // === 2. PREPARE: Destructure state and check bindings ===
375
+ const {
376
+ type, value, placeholder, label, icon, required, disabled, name, rows,
377
+ min, max, step, minLength, maxLength, pattern, style, class: className
378
+ } = this.state;
379
+
380
+ const hasValueSync = this._syncBindings.some(binding => binding.property === 'value');
381
+
382
+ // === 3. BUILD: Create all DOM elements ===
418
383
  const wrapper = document.createElement('div');
419
384
  wrapper.className = 'jux-input';
420
385
  wrapper.id = this._id;
421
-
422
- if (className) {
423
- wrapper.className += ` ${className}`;
424
- }
425
-
426
- if (style) {
427
- wrapper.setAttribute('style', style);
428
- }
386
+ if (className) wrapper.className += ` ${className}`;
387
+ if (style) wrapper.setAttribute('style', style);
429
388
 
430
389
  // Label
431
- if (label) {
432
- const labelEl = document.createElement('label');
433
- labelEl.className = 'jux-input-label';
434
- labelEl.htmlFor = `${this._id}-input`;
435
- labelEl.textContent = label;
436
- if (required) {
437
- const requiredSpan = document.createElement('span');
438
- requiredSpan.className = 'jux-input-required';
439
- requiredSpan.textContent = ' *';
440
- labelEl.appendChild(requiredSpan);
441
- }
442
- wrapper.appendChild(labelEl);
390
+ const labelEl = document.createElement('label');
391
+ labelEl.className = 'jux-input-label';
392
+ labelEl.htmlFor = `${this._id}-input`;
393
+ labelEl.textContent = label;
394
+ if (required) {
395
+ const requiredSpan = document.createElement('span');
396
+ requiredSpan.className = 'jux-input-required';
397
+ requiredSpan.textContent = ' *';
398
+ labelEl.appendChild(requiredSpan);
399
+ }
400
+ if (label) wrapper.appendChild(labelEl);
401
+
402
+ // Input container
403
+ const inputContainer = document.createElement('div');
404
+ inputContainer.className = 'jux-input-container';
405
+ if (icon) inputContainer.classList.add('jux-input-with-icon');
406
+
407
+ // Icon
408
+ if (icon) {
409
+ const iconEl = document.createElement('span');
410
+ iconEl.className = 'jux-input-icon';
411
+ const iconElement = renderIcon(icon);
412
+ iconEl.appendChild(iconElement);
413
+ inputContainer.appendChild(iconEl);
443
414
  }
444
415
 
445
- // Input/Textarea
416
+ // Input/Textarea element
446
417
  let inputEl: HTMLInputElement | HTMLTextAreaElement;
447
-
448
418
  if (type === 'textarea') {
449
419
  inputEl = document.createElement('textarea');
450
420
  inputEl.rows = rows;
@@ -454,14 +424,12 @@ export class Input {
454
424
  inputEl = document.createElement('input');
455
425
  inputEl.type = type;
456
426
 
457
- // Number-specific attributes
458
427
  if (type === 'number') {
459
428
  if (min !== undefined) inputEl.min = String(min);
460
429
  if (max !== undefined) inputEl.max = String(max);
461
430
  if (step !== undefined) inputEl.step = String(step);
462
431
  }
463
432
 
464
- // Text-specific attributes
465
433
  if (type === 'text' || type === 'email' || type === 'tel' || type === 'url') {
466
434
  if (minLength !== undefined) inputEl.minLength = minLength;
467
435
  if (maxLength !== undefined) inputEl.maxLength = maxLength;
@@ -477,52 +445,108 @@ export class Input {
477
445
  inputEl.required = required;
478
446
  inputEl.disabled = disabled;
479
447
 
480
- // Input event handler
481
- inputEl.addEventListener('input', (e) => {
482
- const target = e.target as HTMLInputElement | HTMLTextAreaElement;
483
- this.state.value = target.value;
484
-
485
- // Clear error on input
486
- this._clearError();
487
-
488
- if (this._onChange) {
489
- this._onChange(target.value);
490
- }
491
- });
492
-
493
- // Blur event for validation
494
- inputEl.addEventListener('blur', () => {
495
- this.validate();
496
- });
497
-
498
- wrapper.appendChild(inputEl);
448
+ inputContainer.appendChild(inputEl);
449
+ wrapper.appendChild(inputContainer);
499
450
 
500
- // Error message element
451
+ // Error element
501
452
  const errorEl = document.createElement('div');
502
453
  errorEl.className = 'jux-input-error';
503
454
  errorEl.id = `${this._id}-error`;
504
455
  errorEl.style.display = 'none';
505
456
  wrapper.appendChild(errorEl);
506
457
 
507
- // Character counter for maxLength
458
+ // Character counter
508
459
  if (maxLength && (type === 'text' || type === 'textarea')) {
509
460
  const counterEl = document.createElement('div');
510
461
  counterEl.className = 'jux-input-counter';
511
462
  counterEl.id = `${this._id}-counter`;
512
463
  counterEl.textContent = `${value.length}/${maxLength}`;
464
+ wrapper.appendChild(counterEl);
513
465
 
466
+ // Wire counter immediately
514
467
  inputEl.addEventListener('input', () => {
515
468
  counterEl.textContent = `${inputEl.value.length}/${maxLength}`;
516
469
  });
470
+ }
517
471
 
518
- wrapper.appendChild(counterEl);
472
+ // === 4. WIRE: Add event listeners ===
473
+
474
+ // Default input handler (only if NOT using sync)
475
+ if (!hasValueSync) {
476
+ inputEl.addEventListener('input', () => {
477
+ this.state.value = inputEl.value;
478
+ this._clearError();
479
+ });
519
480
  }
520
481
 
521
- container.appendChild(wrapper);
482
+ // Always add blur validation
483
+ inputEl.addEventListener('blur', () => {
484
+ this.validate();
485
+ });
486
+
487
+ // Wire up custom event bindings (from .bind() calls)
488
+ this._bindings.forEach(({ event, handler }) => {
489
+ wrapper.addEventListener(event, handler as EventListener);
490
+ });
491
+
492
+ // Wire up sync bindings (from .sync() calls)
493
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
494
+ if (property === 'value') {
495
+ // Default transforms
496
+ const transformToState = toState || ((v: string) => {
497
+ return type === 'number' ? (parseInt(v) || 0) : v;
498
+ });
499
+ const transformToComponent = toComponent || ((v: any) => String(v));
500
+
501
+ let isUpdating = false;
502
+
503
+ // State → Input (when state changes, update input)
504
+ stateObj.subscribe((val: any) => {
505
+ if (isUpdating) return;
506
+ const transformed = transformToComponent(val);
507
+ if (inputEl.value !== transformed) {
508
+ inputEl.value = transformed;
509
+ this.state.value = transformed;
510
+ }
511
+ });
512
+
513
+ // Input → State (when input changes, update state)
514
+ inputEl.addEventListener('input', () => {
515
+ if (isUpdating) return;
516
+ isUpdating = true;
517
+
518
+ const transformed = transformToState(inputEl.value);
519
+ this.state.value = inputEl.value;
520
+ this._clearError();
521
+
522
+ stateObj.set(transformed);
523
+
524
+ setTimeout(() => { isUpdating = false; }, 0);
525
+ });
526
+ }
527
+ else if (property === 'label') {
528
+ // Sync label (one-way: state → component)
529
+ const transformToComponent = toComponent || ((v: any) => String(v));
530
+
531
+ stateObj.subscribe((val: any) => {
532
+ const transformed = transformToComponent(val);
533
+ labelEl.textContent = transformed;
534
+ this.state.label = transformed;
535
+ });
536
+ }
537
+ });
522
538
 
523
- // Add default styles if not already present
539
+ // === 5. RENDER: Append to DOM and finalize ===
540
+ container.appendChild(wrapper);
524
541
  this._injectDefaultStyles();
525
542
 
543
+ // Trigger Lucide icon rendering
544
+ requestAnimationFrame(() => {
545
+ if ((window as any).lucide) {
546
+ (window as any).lucide.createIcons();
547
+ }
548
+ });
549
+
526
550
  return this;
527
551
  }
528
552
 
@@ -537,6 +561,31 @@ export class Input {
537
561
  margin-bottom: 16px;
538
562
  }
539
563
 
564
+ .jux-input-container {
565
+ position: relative;
566
+ }
567
+
568
+ .jux-input-with-icon .jux-input-element {
569
+ padding-left: 40px;
570
+ }
571
+
572
+ .jux-input-icon {
573
+ position: absolute;
574
+ left: 12px;
575
+ top: 50%;
576
+ transform: translateY(-50%);
577
+ display: flex;
578
+ align-items: center;
579
+ justify-content: center;
580
+ color: #6b7280;
581
+ pointer-events: none;
582
+ }
583
+
584
+ .jux-input-icon svg {
585
+ width: 18px;
586
+ height: 18px;
587
+ }
588
+
540
589
  .jux-input-label {
541
590
  display: block;
542
591
  margin-bottom: 6px;