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
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Icon utilities for components
3
+ * Handles emoji-to-Lucide mapping, direct icon names, and image paths
4
+ */
5
+
6
+ const EMOJI_TO_LUCIDE: Record<string, string> = {
7
+ "✔️": "check-circle",
8
+ "✓": "check",
9
+ "❌": "x-circle",
10
+ "✗": "x",
11
+ "🔥": "flame",
12
+ "🚀": "rocket",
13
+ "⚙️": "settings",
14
+ "🏠": "home",
15
+ "👤": "user",
16
+ "💡": "lightbulb",
17
+ "🌈": "rainbow",
18
+ "🧪": "flask-conical",
19
+ "✉️": "mail",
20
+ "📞": "phone",
21
+ "🔍": "search",
22
+ "❤️": "heart",
23
+ "⭐": "star",
24
+ "⚠️": "alert-triangle",
25
+ "ℹ️": "info",
26
+ "❓": "help-circle",
27
+ "👁️": "eye",
28
+ "👁️‍🗨️": "eye-off",
29
+ "☰": "menu",
30
+ "🕐": "clock",
31
+ "📅": "calendar",
32
+ "⬇️": "chevron-down",
33
+ "⬆️": "chevron-up",
34
+ "⬅️": "chevron-left",
35
+ "➡️": "chevron-right",
36
+ "📈": "arrow-up",
37
+ "📉": "arrow-down",
38
+ "⬇": "download",
39
+ "📤": "upload",
40
+ "📄": "file-text",
41
+ "🗑️": "trash-2",
42
+ "✏️": "edit",
43
+ "📋": "clipboard",
44
+ "🔗": "link",
45
+ "↗️": "external-link",
46
+ "☀️": "sun",
47
+ "🌙": "moon",
48
+ "📊": "bar-chart-3",
49
+ "📁": "folder",
50
+ "💰": "coins",
51
+ "📧": "mail",
52
+ "✅": "square-check",
53
+ "🗓️": "calendar-days",
54
+ "💬": "message-circle",
55
+ "🌐": "globe",
56
+ "🔬": "microscope",
57
+ "💊": "pill",
58
+ "🔒": "lock",
59
+ "⚖️": "scale",
60
+ "🔌": "plug",
61
+ "🔐": "lock-keyhole",
62
+ "🏥": "cross",
63
+ "👥": "users",
64
+ "💚": "heart",
65
+ "💸": "banknote",
66
+ "🧾": "receipt",
67
+ "➕": "plus",
68
+ "➖": "minus",
69
+ "💾": "save"
70
+ };
71
+
72
+ const LUCIDE_CDN_URL = "https://unpkg.com/lucide@latest";
73
+
74
+ /**
75
+ * Render an icon from emoji, icon name, or image path
76
+ * @param value - Emoji (🚀), icon name (rocket), or image path (/icon.png)
77
+ * @returns HTMLElement containing the icon
78
+ *
79
+ * Usage:
80
+ * const icon = renderIcon('🚀'); // Lucide rocket icon
81
+ * const icon = renderIcon('rocket'); // Lucide rocket icon
82
+ * const icon = renderIcon('/icon.png'); // Image element
83
+ */
84
+ export function renderIcon(value: string): HTMLElement {
85
+ // Check if it's an image path (contains / or . or starts with http)
86
+ if (value.includes('/') || value.includes('.') || value.startsWith('http')) {
87
+ return createImageIcon(value);
88
+ }
89
+
90
+ // Check if it's an emoji that maps to Lucide
91
+ const lucideName = EMOJI_TO_LUCIDE[value];
92
+ if (lucideName) {
93
+ const element = createVectorIcon(lucideName);
94
+ ensureLucideLoaded();
95
+ return element;
96
+ }
97
+
98
+ // Check if it's a direct Lucide icon name (lowercase with hyphens)
99
+ if (/^[a-z][a-z0-9-]*$/.test(value)) {
100
+ const element = createVectorIcon(value);
101
+ ensureLucideLoaded();
102
+ return element;
103
+ }
104
+
105
+ // Fallback: render as emoji
106
+ return createEmojiFallback(value);
107
+ }
108
+
109
+ /**
110
+ * Render raw emoji without conversion
111
+ * @param emoji - The emoji character
112
+ * @returns HTMLElement containing just the emoji
113
+ */
114
+ export function renderEmoji(emoji: string): HTMLElement {
115
+ return createEmojiFallback(emoji);
116
+ }
117
+
118
+ /**
119
+ * Ensures Lucide is loaded and icons are rendered
120
+ */
121
+ function ensureLucideLoaded(): void {
122
+ if ((window as any).lucide) {
123
+ // Already loaded, render immediately
124
+ (window as any).lucide.createIcons();
125
+ return;
126
+ }
127
+
128
+ // Not loaded yet, inject script
129
+ if (!document.querySelector(`script[src="${LUCIDE_CDN_URL}"]`)) {
130
+ const script = document.createElement('script');
131
+ script.src = LUCIDE_CDN_URL;
132
+ script.onload = () => {
133
+ if ((window as any).lucide) {
134
+ (window as any).lucide.createIcons();
135
+ }
136
+ };
137
+ document.head.appendChild(script);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Create Lucide icon element
143
+ */
144
+ function createVectorIcon(name: string): HTMLElement {
145
+ const iconEl = document.createElement('i');
146
+ iconEl.setAttribute('data-lucide', name);
147
+ iconEl.style.width = '24px';
148
+ iconEl.style.height = '24px';
149
+ iconEl.style.display = 'inline-block';
150
+ return iconEl;
151
+ }
152
+
153
+ /**
154
+ * Create image icon element
155
+ */
156
+ function createImageIcon(src: string): HTMLImageElement {
157
+ const img = document.createElement('img');
158
+ img.src = src;
159
+ img.style.width = '24px';
160
+ img.style.height = '24px';
161
+ img.style.display = 'inline-block';
162
+ img.style.objectFit = 'contain';
163
+ return img;
164
+ }
165
+
166
+ /**
167
+ * Render native emoji or text
168
+ */
169
+ function createEmojiFallback(emoji: string): HTMLSpanElement {
170
+ const span = document.createElement('span');
171
+ span.textContent = emoji;
172
+ span.style.display = 'inline-block';
173
+ return span;
174
+ }
@@ -5,7 +5,7 @@ import { ErrorHandler } from './error-handler.js';
5
5
  * Auto-detects resource type from URL and provides simple, fluent API
6
6
  */
7
7
 
8
- type IncludeType = 'stylesheet' | 'script' | 'image' | 'font' | 'preload' | 'prefetch' | 'module';
8
+ type IncludeType = 'stylesheet' | 'script' | 'image' | 'font' | 'preload' | 'prefetch' | 'module' | 'json';
9
9
  type IncludeLocation = 'head' | 'body-start' | 'body-end';
10
10
 
11
11
  interface IncludeOptions {
@@ -35,6 +35,7 @@ export class Include {
35
35
  private detectType(url: string): IncludeType {
36
36
  if (url.endsWith('.css')) return 'stylesheet';
37
37
  if (url.endsWith('.js') || url.endsWith('.mjs')) return 'script';
38
+ if (url.endsWith('.json')) return 'json';
38
39
  if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) return 'image';
39
40
  if (url.match(/\.(woff|woff2|ttf|otf|eot)$/i)) return 'font';
40
41
  return 'preload';
@@ -82,6 +83,65 @@ export class Include {
82
83
  return this;
83
84
  }
84
85
 
86
+ withJson(): this {
87
+ this.type = 'json';
88
+ return this;
89
+ }
90
+
91
+ /* -------------------------
92
+ * JSON Fetching
93
+ * ------------------------- */
94
+
95
+ /**
96
+ * Fetch and parse JSON file
97
+ * Returns a Promise that resolves to the parsed JSON data
98
+ *
99
+ * Usage:
100
+ * const config = await jux.include('config.json').asJson();
101
+ * const data = await jux.include('/api/data').asJson();
102
+ */
103
+ async asJson<T = any>(): Promise<T> {
104
+ try {
105
+ const response = await fetch(this.url);
106
+
107
+ if (!response.ok) {
108
+ throw new Error(`HTTP error! status: ${response.status}`);
109
+ }
110
+
111
+ const data = await response.json();
112
+ console.log(`✓ JSON loaded: ${this.url}`);
113
+ return data;
114
+ } catch (error: any) {
115
+ ErrorHandler.captureError({
116
+ component: 'Include',
117
+ method: 'asJson',
118
+ message: error.message,
119
+ stack: error.stack,
120
+ timestamp: new Date(),
121
+ context: {
122
+ url: this.url,
123
+ error: 'json_fetch_failed'
124
+ }
125
+ });
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Fetch JSON and execute callback with data
132
+ *
133
+ * Usage:
134
+ * jux.include('config.json').onJson(data => {
135
+ * console.log('Config:', data);
136
+ * });
137
+ */
138
+ onJson<T = any>(callback: (data: T) => void): this {
139
+ this.asJson<T>().then(callback).catch(error => {
140
+ console.error('Failed to load JSON:', error);
141
+ });
142
+ return this;
143
+ }
144
+
85
145
  /* -------------------------
86
146
  * Options
87
147
  * ------------------------- */
@@ -128,6 +188,12 @@ export class Include {
128
188
  render(): this {
129
189
  if (typeof document === 'undefined') return this;
130
190
 
191
+ // Don't render JSON type (it's fetched via asJson() instead)
192
+ if (this.type === 'json') {
193
+ console.warn('Include: JSON files should be loaded with .asJson() instead of .render()');
194
+ return this;
195
+ }
196
+
131
197
  try {
132
198
  this.remove();
133
199
 
@@ -284,9 +350,18 @@ export class Include {
284
350
  * jux.include('app.mjs').withModule();
285
351
  * jux.include('custom.js').withJs({ async: true, defer: true });
286
352
  * jux.include('https://cdn.com/lib.js').inHead().defer();
353
+ *
354
+ * // For JSON:
355
+ * const data = await jux.include('config.json').asJson();
356
+ * jux.include('data.json').onJson(data => console.log(data));
287
357
  */
288
358
  export function include(urlOrFile: string): Include {
289
359
  const imp = new Include(urlOrFile);
290
- imp.render();
360
+
361
+ // Don't auto-render JSON files
362
+ if (imp['type'] !== 'json') {
363
+ imp.render();
364
+ }
365
+
291
366
  return imp;
292
367
  }
@@ -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,27 +50,22 @@ type InputState = {
48
50
  errorMessage?: string;
49
51
  };
50
52
 
51
- /**
52
- * Input component
53
- */
54
53
  export class Input {
55
54
  state: InputState;
56
55
  container: HTMLElement | null = null;
57
56
  _id: string;
58
57
  id: string;
59
- private _onChange?: (value: string) => void;
60
58
  private _onValidate?: (value: string) => boolean | string;
61
59
 
62
- // Store bind() instructions
60
+ // Store bind() instructions (DOM events only)
63
61
  private _bindings: Array<{ event: string, handler: Function }> = [];
64
62
 
65
- // Store sync() instructions
63
+ // Store sync() instructions (state synchronization)
66
64
  private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
67
65
 
68
66
  constructor(id: string, options: InputOptions = {}) {
69
67
  this._id = id;
70
68
  this.id = id;
71
- this._onChange = options.onChange;
72
69
  this._onValidate = options.onValidate;
73
70
 
74
71
  this.state = {
@@ -76,6 +73,7 @@ export class Input {
76
73
  value: options.value ?? '',
77
74
  placeholder: options.placeholder ?? '',
78
75
  label: options.label ?? '',
76
+ icon: options.icon ?? '',
79
77
  required: options.required ?? false,
80
78
  disabled: options.disabled ?? false,
81
79
  name: options.name ?? id,
@@ -116,6 +114,11 @@ export class Input {
116
114
  return this;
117
115
  }
118
116
 
117
+ icon(value: string): this {
118
+ this.state.icon = value;
119
+ return this;
120
+ }
121
+
119
122
  required(value: boolean): this {
120
123
  this.state.required = value;
121
124
  return this;
@@ -183,11 +186,6 @@ export class Input {
183
186
  return this;
184
187
  }
185
188
 
186
- onChange(handler: (value: string) => void): this {
187
- this._onChange = handler;
188
- return this;
189
- }
190
-
191
189
  onValidate(handler: (value: string) => boolean | string): this {
192
190
  this._onValidate = handler;
193
191
  return this;
@@ -195,6 +193,7 @@ export class Input {
195
193
 
196
194
  /**
197
195
  * Bind event handler (stores for wiring in render)
196
+ * DOM events only: input, change, blur, focus, etc.
198
197
  */
199
198
  bind(event: string, handler: Function): this {
200
199
  this._bindings.push({ event, handler });
@@ -210,6 +209,9 @@ export class Input {
210
209
  * @param toComponent - Optional transform function when going from state to component
211
210
  */
212
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
+ }
213
215
  this._syncBindings.push({ property, stateObj, toState, toComponent });
214
216
  return this;
215
217
  }
@@ -356,8 +358,8 @@ export class Input {
356
358
  * ------------------------- */
357
359
 
358
360
  render(targetId?: string): this {
361
+ // === 1. SETUP: Get container ===
359
362
  let container: HTMLElement;
360
-
361
363
  if (targetId) {
362
364
  const target = document.querySelector(targetId);
363
365
  if (!target || !(target instanceof HTMLElement)) {
@@ -367,21 +369,22 @@ export class Input {
367
369
  } else {
368
370
  container = getOrCreateContainer(this._id);
369
371
  }
370
-
371
372
  this.container = container;
372
- const { type, value, placeholder, label, required, disabled, name, rows, min, max, step, minLength, maxLength, pattern, style, class: className } = this.state;
373
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 ===
374
383
  const wrapper = document.createElement('div');
375
384
  wrapper.className = 'jux-input';
376
385
  wrapper.id = this._id;
377
-
378
- if (className) {
379
- wrapper.className += ` ${className}`;
380
- }
381
-
382
- if (style) {
383
- wrapper.setAttribute('style', style);
384
- }
386
+ if (className) wrapper.className += ` ${className}`;
387
+ if (style) wrapper.setAttribute('style', style);
385
388
 
386
389
  // Label
387
390
  const labelEl = document.createElement('label');
@@ -394,13 +397,24 @@ export class Input {
394
397
  requiredSpan.textContent = ' *';
395
398
  labelEl.appendChild(requiredSpan);
396
399
  }
397
- if (label) {
398
- wrapper.appendChild(labelEl);
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);
399
414
  }
400
415
 
401
- // Input/Textarea
416
+ // Input/Textarea element
402
417
  let inputEl: HTMLInputElement | HTMLTextAreaElement;
403
-
404
418
  if (type === 'textarea') {
405
419
  inputEl = document.createElement('textarea');
406
420
  inputEl.rows = rows;
@@ -431,54 +445,51 @@ export class Input {
431
445
  inputEl.required = required;
432
446
  inputEl.disabled = disabled;
433
447
 
434
- // Input event handler - from onChange option
435
- inputEl.addEventListener('input', (e) => {
436
- const target = e.target as HTMLInputElement | HTMLTextAreaElement;
437
- this.state.value = target.value;
438
-
439
- this._clearError();
440
-
441
- if (this._onChange) {
442
- this._onChange(target.value);
443
- }
444
- });
445
-
446
- inputEl.addEventListener('blur', () => {
447
- this.validate();
448
- });
449
-
450
- wrapper.appendChild(inputEl);
448
+ inputContainer.appendChild(inputEl);
449
+ wrapper.appendChild(inputContainer);
451
450
 
452
- // Error message element
451
+ // Error element
453
452
  const errorEl = document.createElement('div');
454
453
  errorEl.className = 'jux-input-error';
455
454
  errorEl.id = `${this._id}-error`;
456
455
  errorEl.style.display = 'none';
457
456
  wrapper.appendChild(errorEl);
458
457
 
459
- // Character counter for maxLength
458
+ // Character counter
460
459
  if (maxLength && (type === 'text' || type === 'textarea')) {
461
460
  const counterEl = document.createElement('div');
462
461
  counterEl.className = 'jux-input-counter';
463
462
  counterEl.id = `${this._id}-counter`;
464
463
  counterEl.textContent = `${value.length}/${maxLength}`;
464
+ wrapper.appendChild(counterEl);
465
465
 
466
+ // Wire counter immediately
466
467
  inputEl.addEventListener('input', () => {
467
468
  counterEl.textContent = `${inputEl.value.length}/${maxLength}`;
468
469
  });
470
+ }
469
471
 
470
- 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
+ });
471
480
  }
472
481
 
473
- container.appendChild(wrapper);
474
- this._injectDefaultStyles();
482
+ // Always add blur validation
483
+ inputEl.addEventListener('blur', () => {
484
+ this.validate();
485
+ });
475
486
 
476
- // === Wire up event bindings ===
487
+ // Wire up custom event bindings (from .bind() calls)
477
488
  this._bindings.forEach(({ event, handler }) => {
478
489
  wrapper.addEventListener(event, handler as EventListener);
479
490
  });
480
491
 
481
- // === Wire up sync bindings (TWO-WAY) ===
492
+ // Wire up sync bindings (from .sync() calls)
482
493
  this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
483
494
  if (property === 'value') {
484
495
  // Default transforms
@@ -503,13 +514,18 @@ export class Input {
503
514
  inputEl.addEventListener('input', () => {
504
515
  if (isUpdating) return;
505
516
  isUpdating = true;
517
+
506
518
  const transformed = transformToState(inputEl.value);
519
+ this.state.value = inputEl.value;
520
+ this._clearError();
521
+
507
522
  stateObj.set(transformed);
523
+
508
524
  setTimeout(() => { isUpdating = false; }, 0);
509
525
  });
510
526
  }
511
527
  else if (property === 'label') {
512
- // Sync label
528
+ // Sync label (one-way: state → component)
513
529
  const transformToComponent = toComponent || ((v: any) => String(v));
514
530
 
515
531
  stateObj.subscribe((val: any) => {
@@ -520,6 +536,17 @@ export class Input {
520
536
  }
521
537
  });
522
538
 
539
+ // === 5. RENDER: Append to DOM and finalize ===
540
+ container.appendChild(wrapper);
541
+ this._injectDefaultStyles();
542
+
543
+ // Trigger Lucide icon rendering
544
+ requestAnimationFrame(() => {
545
+ if ((window as any).lucide) {
546
+ (window as any).lucide.createIcons();
547
+ }
548
+ });
549
+
523
550
  return this;
524
551
  }
525
552
 
@@ -534,6 +561,31 @@ export class Input {
534
561
  margin-bottom: 16px;
535
562
  }
536
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
+
537
589
  .jux-input-label {
538
590
  display: block;
539
591
  margin-bottom: 6px;