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,8 +1,7 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
3
+ import { renderIcon } from './icons.js';
2
4
 
3
- /**
4
- * List item interface
5
- */
6
5
  export interface ListItem {
7
6
  icon?: string;
8
7
  title?: string;
@@ -11,9 +10,6 @@ export interface ListItem {
11
10
  metadata?: string;
12
11
  }
13
12
 
14
- /**
15
- * List component options
16
- */
17
13
  export interface ListOptions {
18
14
  items?: ListItem[];
19
15
  header?: string;
@@ -27,9 +23,6 @@ export interface ListOptions {
27
23
  class?: string;
28
24
  }
29
25
 
30
- /**
31
- * List component state
32
- */
33
26
  type ListState = {
34
27
  items: ListItem[];
35
28
  header: string;
@@ -37,42 +30,28 @@ type ListState = {
37
30
  direction: string;
38
31
  selectable: boolean;
39
32
  selectedIndex: number | null;
33
+ ordered?: boolean;
40
34
  style: string;
41
35
  class: string;
42
36
  };
43
37
 
44
- /**
45
- * List component - renders a list of items with optional header
46
- *
47
- * Usage:
48
- * const myList = jux.list('myList', {
49
- * header: '✓ Accomplishments',
50
- * items: [
51
- * { icon: '✓', title: 'Task 1', body: 'Description', type: 'success' },
52
- * { icon: '⚠️', title: 'Task 2', body: 'Description', type: 'warning' }
53
- * ],
54
- * gap: '0.75rem',
55
- * selectable: true,
56
- * onItemClick: (item, index) => console.log('Clicked:', item, index)
57
- * });
58
- * myList.render();
59
- *
60
- * // Add item
61
- * myList.add({ icon: '🎉', title: 'New Task', body: 'Done!', type: 'success' });
62
- *
63
- * // Remove item by index
64
- * myList.remove(1);
65
- *
66
- * // Move item from index 0 to index 2
67
- * myList.move(0, 2);
68
- */
69
38
  export class List {
70
39
  state: ListState;
71
40
  container: HTMLElement | null = null;
72
41
  _id: string;
73
42
  id: string;
74
- private _onItemClick: ((item: ListItem, index: number, e: Event) => void) | null;
75
- private _onItemDoubleClick: ((item: ListItem, index: number, e: Event) => void) | null;
43
+
44
+ // CRITICAL: Store bind/sync instructions for deferred wiring
45
+ private _bindings: Array<{ event: string, handler: Function }> = [];
46
+ private _syncBindings: Array<{
47
+ property: string,
48
+ stateObj: State<any>,
49
+ toState?: Function,
50
+ toComponent?: Function
51
+ }> = [];
52
+
53
+ private _onItemClick: ((item: ListItem, index: number, e: Event) => void) | null = null;
54
+ private _onItemDoubleClick: ((item: ListItem, index: number, e: Event) => void) | null = null;
76
55
 
77
56
  constructor(id: string, options: ListOptions = {}) {
78
57
  this._id = id;
@@ -94,47 +73,50 @@ export class List {
94
73
  }
95
74
 
96
75
  /* -------------------------
97
- * Fluent API
98
- * ------------------------- */
76
+ * Fluent API
77
+ * ------------------------- */
99
78
 
100
79
  items(value: ListItem[]): this {
101
80
  this.state.items = value;
102
81
  return this;
103
82
  }
104
83
 
105
- header(value: string): this {
106
- this.state.header = value;
84
+ addItem(value: ListItem): this {
85
+ this.state.items = [...this.state.items, value];
107
86
  return this;
108
87
  }
109
88
 
110
- gap(value: string): this {
111
- this.state.gap = value;
89
+ ordered(value: boolean): this {
90
+ this.state.ordered = value;
112
91
  return this;
113
92
  }
114
93
 
115
- direction(value: 'vertical' | 'horizontal'): this {
116
- this.state.direction = value;
94
+ style(value: string): this {
95
+ this.state.style = value;
117
96
  return this;
118
97
  }
119
98
 
120
- selectable(value: boolean): this {
121
- this.state.selectable = value;
99
+ class(value: string): this {
100
+ this.state.class = value;
122
101
  return this;
123
102
  }
124
103
 
125
- style(value: string): this {
126
- this.state.style = value;
104
+ bind(event: string, handler: Function): this {
105
+ this._bindings.push({ event, handler });
127
106
  return this;
128
107
  }
129
108
 
130
- class(value: string): this {
131
- this.state.class = value;
109
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
110
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
111
+ throw new Error(`List.sync: Expected a State object for property "${property}"`);
112
+ }
113
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
132
114
  return this;
133
115
  }
134
116
 
135
117
  /* -------------------------
136
- * List operations
137
- * ------------------------- */
118
+ * List operations
119
+ * ------------------------- */
138
120
 
139
121
  add(item: ListItem, index?: number): this {
140
122
  const items = [...this.state.items];
@@ -242,18 +224,21 @@ export class List {
242
224
  }
243
225
 
244
226
  /* -------------------------
245
- * Helpers
246
- * ------------------------- */
227
+ * Helpers
228
+ * ------------------------- */
247
229
 
248
230
  private _updateDOM(): void {
249
231
  if (!this.container) return;
250
232
 
251
- // Clear and re-render
252
- this.container.innerHTML = '';
253
- this._renderContent();
233
+ const existingWrapper = this.container.querySelector(`#${this._id}`);
234
+ if (existingWrapper) {
235
+ existingWrapper.remove();
236
+ }
237
+
238
+ this._buildAndAppendList();
254
239
  }
255
240
 
256
- private _renderContent(): void {
241
+ private _buildAndAppendList(): void {
257
242
  if (!this.container) return;
258
243
 
259
244
  const { items, header, gap, direction, selectable, selectedIndex, style, class: className } = this.state;
@@ -296,7 +281,11 @@ export class List {
296
281
  if (item.icon) {
297
282
  const iconEl = document.createElement('span');
298
283
  iconEl.className = 'jux-list-item-icon';
299
- iconEl.textContent = item.icon;
284
+ iconEl.style.display = 'flex';
285
+ iconEl.style.alignItems = 'center';
286
+ iconEl.style.justifyContent = 'center';
287
+ const iconElement = renderIcon(item.icon);
288
+ iconEl.appendChild(iconElement);
300
289
  itemEl.appendChild(iconEl);
301
290
  }
302
291
 
@@ -353,49 +342,101 @@ export class List {
353
342
  }
354
343
 
355
344
  /* -------------------------
356
- * Render
357
- * ------------------------- */
345
+ * Render
346
+ * ------------------------- */
358
347
 
359
348
  render(targetId?: string): this {
349
+ // === 1. SETUP: Get or create container ===
360
350
  let container: HTMLElement;
361
-
362
351
  if (targetId) {
363
352
  const target = document.querySelector(targetId);
364
353
  if (!target || !(target instanceof HTMLElement)) {
365
- throw new Error(`List: Target element "${targetId}" not found`);
354
+ throw new Error(`List: Target "${targetId}" not found`);
366
355
  }
367
356
  container = target;
368
357
  } else {
369
358
  container = getOrCreateContainer(this._id);
370
359
  }
371
-
372
360
  this.container = container;
373
- this.container.innerHTML = '';
374
361
 
375
- this._renderContent();
362
+ // === 2. PREPARE: Destructure state ===
363
+ const { items, ordered, style, class: className } = this.state;
364
+
365
+ // === 3. BUILD: Create DOM elements ===
366
+ const list = document.createElement(ordered ? 'ol' : 'ul') as HTMLOListElement | HTMLUListElement;
367
+ list.className = `jux-list ${ordered ? 'jux-list-ordered' : 'jux-list-unordered'}`;
368
+ list.id = this._id;
369
+ if (className) list.className += ` ${className}`;
370
+ if (style) list.setAttribute('style', style);
371
+
372
+ items.forEach(item => {
373
+ const li = document.createElement('li');
374
+ li.className = 'jux-list-item';
375
+
376
+ // Handle ListItem object
377
+ if (item.title) {
378
+ li.textContent = item.title;
379
+ }
380
+ if (item.body) {
381
+ const bodyEl = document.createElement('div');
382
+ bodyEl.textContent = item.body;
383
+ li.appendChild(bodyEl);
384
+ }
385
+
386
+ list.appendChild(li);
387
+ });
388
+
389
+ // === 4. WIRE: Attach event listeners and sync bindings ===
390
+
391
+ // Wire custom bindings from .bind() calls
392
+ this._bindings.forEach(({ event, handler }) => {
393
+ list.addEventListener(event, handler as EventListener);
394
+ });
376
395
 
396
+ // Wire sync bindings from .sync() calls
397
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
398
+ if (property === 'items') {
399
+ const transformToComponent = toComponent || ((v: any) => v);
400
+
401
+ stateObj.subscribe((val: any) => {
402
+ const transformed = transformToComponent(val);
403
+ this.state.items = transformed;
404
+
405
+ // Re-render items
406
+ list.innerHTML = '';
407
+ transformed.forEach((item: ListItem) => {
408
+ const li = document.createElement('li');
409
+ li.className = 'jux-list-item';
410
+
411
+ // Handle ListItem object
412
+ if (item.title) {
413
+ li.textContent = item.title;
414
+ }
415
+ if (item.body) {
416
+ const bodyEl = document.createElement('div');
417
+ bodyEl.textContent = item.body;
418
+ li.appendChild(bodyEl);
419
+ }
420
+
421
+ list.appendChild(li);
422
+ });
423
+ });
424
+ }
425
+ });
426
+
427
+ // === 5. RENDER: Append to DOM and finalize ===
428
+ container.appendChild(list);
377
429
  return this;
378
430
  }
379
431
 
380
- /**
381
- * Render to another Jux component's container
382
- */
383
432
  renderTo(juxComponent: any): this {
384
- if (!juxComponent || typeof juxComponent !== 'object') {
385
- throw new Error('List.renderTo: Invalid component - not an object');
433
+ if (!juxComponent?._id) {
434
+ throw new Error('List.renderTo: Invalid component');
386
435
  }
387
-
388
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
389
- throw new Error('List.renderTo: Invalid component - missing _id (not a Jux component)');
390
- }
391
-
392
436
  return this.render(`#${juxComponent._id}`);
393
437
  }
394
438
  }
395
439
 
396
- /**
397
- * Factory helper
398
- */
399
440
  export function list(id: string, options: ListOptions = {}): List {
400
441
  return new List(id, options);
401
442
  }
@@ -1,5 +1,7 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { renderIcon } from './icons.js';
2
3
  import { req } from './req.js';
4
+ import { State } from '../reactivity/state.js';
3
5
 
4
6
  /**
5
7
  * Menu item configuration
@@ -54,6 +56,12 @@ export class Menu {
54
56
  _id: string;
55
57
  id: string;
56
58
 
59
+ // Store bind() instructions (DOM events only)
60
+ private _bindings: Array<{ event: string, handler: Function }> = [];
61
+
62
+ // Store sync() instructions (state synchronization)
63
+ private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
64
+
57
65
  constructor(id: string, options: MenuOptions = {}) {
58
66
  this._id = id;
59
67
  this.id = id;
@@ -84,6 +92,31 @@ export class Menu {
84
92
  }));
85
93
  }
86
94
 
95
+ /**
96
+ * Bind event handler (stores for wiring in render)
97
+ * DOM events only: click, mouseenter, mouseleave, etc.
98
+ */
99
+ bind(event: string, handler: Function): this {
100
+ this._bindings.push({ event, handler });
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Two-way sync with state (stores for wiring in render)
106
+ *
107
+ * @param property - Component property to sync ('items', 'orientation', etc)
108
+ * @param stateObj - State object to sync with
109
+ * @param toState - Optional transform function when going from component to state
110
+ * @param toComponent - Optional transform function when going from state to component
111
+ */
112
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
113
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
114
+ throw new Error(`Menu.sync: Expected a State object for property "${property}"`);
115
+ }
116
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
117
+ return this;
118
+ }
119
+
87
120
  /* -------------------------
88
121
  * Fluent API
89
122
  * ------------------------- */
@@ -141,7 +174,8 @@ export class Menu {
141
174
  if (item.icon) {
142
175
  const icon = document.createElement('span');
143
176
  icon.className = 'jux-menu-icon';
144
- icon.textContent = item.icon;
177
+ const iconElement = renderIcon(item.icon);
178
+ icon.appendChild(iconElement);
145
179
  link.appendChild(icon);
146
180
  }
147
181
 
@@ -158,7 +192,8 @@ export class Menu {
158
192
  if (item.icon) {
159
193
  const icon = document.createElement('span');
160
194
  icon.className = 'jux-menu-icon';
161
- icon.textContent = item.icon;
195
+ const iconElement = renderIcon(item.icon);
196
+ icon.appendChild(iconElement);
162
197
  button.appendChild(icon);
163
198
  }
164
199
 
@@ -195,6 +230,7 @@ export class Menu {
195
230
  * ------------------------- */
196
231
 
197
232
  render(targetId?: string): this {
233
+ // === 1. SETUP: Get container ===
198
234
  let container: HTMLElement;
199
235
 
200
236
  if (targetId) {
@@ -208,8 +244,11 @@ export class Menu {
208
244
  }
209
245
 
210
246
  this.container = container;
247
+
248
+ // === 2. PREPARE: Destructure state ===
211
249
  const { items, orientation, style, class: className } = this.state;
212
250
 
251
+ // === 3. BUILD: Create DOM elements ===
213
252
  const menu = document.createElement('nav');
214
253
  menu.className = `jux-menu jux-menu-${orientation}`;
215
254
  menu.id = this._id;
@@ -226,7 +265,63 @@ export class Menu {
226
265
  menu.appendChild(this._renderMenuItem(item));
227
266
  });
228
267
 
268
+ // === 4. WIRE: Add event listeners ===
269
+
270
+ // Wire up custom event bindings (from .bind() calls)
271
+ this._bindings.forEach(({ event, handler }) => {
272
+ menu.addEventListener(event, handler as EventListener);
273
+ });
274
+
275
+ // Wire up sync bindings (from .sync() calls)
276
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
277
+ if (property === 'items') {
278
+ // Sync items array
279
+ const transformToComponent = toComponent || ((v: any) => v);
280
+
281
+ stateObj.subscribe((val: any) => {
282
+ const transformed = transformToComponent(val);
283
+ this.state.items = transformed;
284
+ this._setActiveStates();
285
+
286
+ // Re-render menu items
287
+ const existingItems = menu.querySelectorAll('.jux-menu-item');
288
+ existingItems.forEach(item => item.remove());
289
+
290
+ this.state.items.forEach(item => {
291
+ menu.appendChild(this._renderMenuItem(item));
292
+ });
293
+
294
+ // Re-trigger icon rendering
295
+ requestAnimationFrame(() => {
296
+ if ((window as any).lucide) {
297
+ (window as any).lucide.createIcons();
298
+ }
299
+ });
300
+ });
301
+ }
302
+ else if (property === 'orientation') {
303
+ // Sync orientation
304
+ const transformToComponent = toComponent || ((v: any) => String(v));
305
+
306
+ stateObj.subscribe((val: any) => {
307
+ const transformed = transformToComponent(val);
308
+ menu.classList.remove(`jux-menu-${this.state.orientation}`);
309
+ this.state.orientation = transformed;
310
+ menu.classList.add(`jux-menu-${transformed}`);
311
+ });
312
+ }
313
+ });
314
+
315
+ // === 5. RENDER: Append to DOM and finalize ===
229
316
  container.appendChild(menu);
317
+
318
+ // Trigger Lucide icon rendering
319
+ requestAnimationFrame(() => {
320
+ if ((window as any).lucide) {
321
+ (window as any).lucide.createIcons();
322
+ }
323
+ });
324
+
230
325
  return this;
231
326
  }
232
327