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
@@ -1,62 +1,52 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
- /**
4
- * Tab configuration
5
- */
6
4
  export interface Tab {
7
5
  id: string;
8
6
  label: string;
9
- content: string | (() => string);
7
+ content: string | HTMLElement;
8
+ icon?: string;
10
9
  }
11
10
 
12
- /**
13
- * Tabs component options
14
- */
15
11
  export interface TabsOptions {
16
12
  tabs?: Tab[];
17
13
  activeTab?: string;
14
+ variant?: 'default' | 'pills' | 'underline';
18
15
  style?: string;
19
16
  class?: string;
20
17
  }
21
18
 
22
- /**
23
- * Tabs component state
24
- */
25
19
  type TabsState = {
26
20
  tabs: Tab[];
27
21
  activeTab: string;
22
+ variant: string;
28
23
  style: string;
29
24
  class: string;
30
25
  };
31
26
 
32
- /**
33
- * Tabs component
34
- *
35
- * Usage:
36
- * const tabs = jux.tabs('myTabs', {
37
- * tabs: [
38
- * { id: 'tab1', label: 'Tab 1', content: 'Content 1' },
39
- * { id: 'tab2', label: 'Tab 2', content: 'Content 2' }
40
- * ]
41
- * });
42
- * tabs.render();
43
- */
44
27
  export class Tabs {
45
28
  state: TabsState;
46
29
  container: HTMLElement | null = null;
47
30
  _id: string;
48
31
  id: string;
49
32
 
33
+ // CRITICAL: Store bind/sync instructions for deferred wiring
34
+ private _bindings: Array<{ event: string, handler: Function }> = [];
35
+ private _syncBindings: Array<{
36
+ property: string,
37
+ stateObj: State<any>,
38
+ toState?: Function,
39
+ toComponent?: Function
40
+ }> = [];
41
+
50
42
  constructor(id: string, options: TabsOptions = {}) {
51
43
  this._id = id;
52
44
  this.id = id;
53
45
 
54
- const tabs = options.tabs ?? [];
55
- const activeTab = options.activeTab ?? (tabs.length > 0 ? tabs[0].id : '');
56
-
57
46
  this.state = {
58
- tabs,
59
- activeTab,
47
+ tabs: options.tabs ?? [],
48
+ activeTab: options.activeTab ?? (options.tabs?.[0]?.id ?? ''),
49
+ variant: options.variant ?? 'default',
60
50
  style: options.style ?? '',
61
51
  class: options.class ?? ''
62
52
  };
@@ -78,6 +68,12 @@ export class Tabs {
78
68
 
79
69
  activeTab(value: string): this {
80
70
  this.state.activeTab = value;
71
+ this._updateActiveTab();
72
+ return this;
73
+ }
74
+
75
+ variant(value: 'default' | 'pills' | 'underline'): this {
76
+ this.state.variant = value;
81
77
  return this;
82
78
  }
83
79
 
@@ -91,143 +87,228 @@ export class Tabs {
91
87
  return this;
92
88
  }
93
89
 
94
- /* -------------------------
95
- * Helpers
96
- * ------------------------- */
90
+ bind(event: string, handler: Function): this {
91
+ this._bindings.push({ event, handler });
92
+ return this;
93
+ }
97
94
 
98
- private _updateActiveTab(tabId: string): void {
99
- if (!this.container) return;
95
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
96
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
97
+ throw new Error(`Tabs.sync: Expected a State object for property "${property}"`);
98
+ }
99
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
100
+ return this;
101
+ }
100
102
 
103
+ private _updateActiveTab(): void {
104
+ if (!this.container) return;
101
105
  const wrapper = this.container.querySelector(`#${this._id}`);
102
106
  if (!wrapper) return;
103
107
 
104
- this.state.activeTab = tabId;
105
-
106
108
  // Update tab buttons
107
- const tabButtons = wrapper.querySelectorAll('.jux-tab-button');
108
- tabButtons.forEach((btn, index) => {
109
- if (this.state.tabs[index]?.id === tabId) {
110
- btn.classList.add('jux-tab-button-active');
111
- } else {
112
- btn.classList.remove('jux-tab-button-active');
113
- }
109
+ wrapper.querySelectorAll('.jux-tab-button').forEach(btn => {
110
+ btn.classList.toggle('active', btn.getAttribute('data-tab-id') === this.state.activeTab);
114
111
  });
115
112
 
116
113
  // Update tab panels
117
- const tabPanels = wrapper.querySelectorAll('.jux-tab-panel');
118
- tabPanels.forEach(panel => {
119
- const panelId = panel.getAttribute('data-tab-id');
120
- if (panelId === tabId) {
121
- panel.classList.add('jux-tab-panel-active');
122
- } else {
123
- panel.classList.remove('jux-tab-panel-active');
124
- }
114
+ wrapper.querySelectorAll('.jux-tab-panel').forEach(panel => {
115
+ panel.classList.toggle('active', panel.getAttribute('data-tab-id') === this.state.activeTab);
125
116
  });
126
117
  }
127
118
 
128
- /* -------------------------
129
- * Render
130
- * ------------------------- */
131
-
132
119
  render(targetId?: string): this {
120
+ // === 1. SETUP: Get or create container ===
133
121
  let container: HTMLElement;
134
-
135
122
  if (targetId) {
136
123
  const target = document.querySelector(targetId);
137
124
  if (!target || !(target instanceof HTMLElement)) {
138
- throw new Error(`Tabs: Target element "${targetId}" not found`);
125
+ throw new Error(`Tabs: Target "${targetId}" not found`);
139
126
  }
140
127
  container = target;
141
128
  } else {
142
129
  container = getOrCreateContainer(this._id);
143
130
  }
144
-
145
131
  this.container = container;
146
- const { tabs, activeTab, style, class: className } = this.state;
147
132
 
133
+ // === 2. PREPARE: Destructure state and check sync flags ===
134
+ const { tabs, activeTab, variant, style, class: className } = this.state;
135
+ const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
136
+
137
+ // === 3. BUILD: Create DOM elements ===
148
138
  const wrapper = document.createElement('div');
149
- wrapper.className = 'jux-tabs';
139
+ wrapper.className = `jux-tabs jux-tabs-${variant}`;
150
140
  wrapper.id = this._id;
141
+ if (className) wrapper.className += ` ${className}`;
142
+ if (style) wrapper.setAttribute('style', style);
151
143
 
152
- if (className) {
153
- wrapper.className += ` ${className}`;
154
- }
144
+ const tabList = document.createElement('div');
145
+ tabList.className = 'jux-tabs-list';
155
146
 
156
- if (style) {
157
- wrapper.setAttribute('style', style);
158
- }
159
-
160
- // Tab headers
161
- const tabHeaders = document.createElement('div');
162
- tabHeaders.className = 'jux-tabs-header';
163
-
164
- tabs.forEach(tab => {
165
- const tabBtn = document.createElement('button');
166
- tabBtn.className = 'jux-tab-button';
167
- tabBtn.textContent = tab.label;
147
+ const tabPanels = document.createElement('div');
148
+ tabPanels.className = 'jux-tabs-panels';
168
149
 
150
+ tabs.forEach((tab, index) => {
151
+ // Tab button
152
+ const tabButton = document.createElement('button');
153
+ tabButton.className = 'jux-tabs-button';
154
+ tabButton.setAttribute('data-tab', tab.id);
155
+ if (tab.id === activeTab) tabButton.classList.add('jux-tabs-button-active');
156
+ tabButton.textContent = tab.label;
157
+ tabList.appendChild(tabButton);
158
+
159
+ // Tab panel
160
+ const tabPanel = document.createElement('div');
161
+ tabPanel.className = 'jux-tabs-panel';
162
+ tabPanel.setAttribute('data-tab', tab.id);
169
163
  if (tab.id === activeTab) {
170
- tabBtn.classList.add('jux-tab-button-active');
164
+ tabPanel.classList.add('jux-tabs-panel-active');
165
+ } else {
166
+ tabPanel.style.display = 'none';
167
+ }
168
+ if (typeof tab.content === 'string') {
169
+ tabPanel.innerHTML = tab.content;
170
+ } else {
171
+ tabPanel.appendChild(tab.content);
171
172
  }
173
+ tabPanels.appendChild(tabPanel);
174
+ });
175
+
176
+ wrapper.appendChild(tabList);
177
+ wrapper.appendChild(tabPanels);
172
178
 
173
- tabHeaders.appendChild(tabBtn);
179
+ // === 4. WIRE: Attach event listeners and sync bindings ===
174
180
 
175
- // Event binding - tab click
176
- tabBtn.addEventListener('click', () => {
177
- this._updateActiveTab(tab.id);
181
+ // Default tab switching behavior (only if NOT using sync)
182
+ if (!hasActiveTabSync) {
183
+ tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
184
+ button.addEventListener('click', () => {
185
+ const tabId = button.getAttribute('data-tab')!;
186
+ this.state.activeTab = tabId;
187
+ this._switchTab(tabId, wrapper);
188
+ });
178
189
  });
179
- });
190
+ }
180
191
 
181
- wrapper.appendChild(tabHeaders);
192
+ // Wire custom bindings from .bind() calls
193
+ this._bindings.forEach(({ event, handler }) => {
194
+ wrapper.addEventListener(event, handler as EventListener);
195
+ });
182
196
 
183
- // Tab panels
184
- const tabPanels = document.createElement('div');
185
- tabPanels.className = 'jux-tabs-panels';
197
+ // Wire sync bindings from .sync() calls
198
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
199
+ if (property === 'activeTab') {
200
+ const transformToState = toState || ((v: any) => String(v));
201
+ const transformToComponent = toComponent || ((v: any) => String(v));
202
+
203
+ let isUpdating = false;
204
+
205
+ // State → Component
206
+ stateObj.subscribe((val: any) => {
207
+ if (isUpdating) return;
208
+ const transformed = transformToComponent(val);
209
+ this.state.activeTab = transformed;
210
+ this._switchTab(transformed, wrapper);
211
+ });
212
+
213
+ // Component → State (tab clicks)
214
+ tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
215
+ button.addEventListener('click', () => {
216
+ if (isUpdating) return;
217
+ isUpdating = true;
218
+
219
+ const tabId = button.getAttribute('data-tab')!;
220
+ this.state.activeTab = tabId;
221
+ this._switchTab(tabId, wrapper);
222
+
223
+ const transformed = transformToState(tabId);
224
+ stateObj.set(transformed);
225
+
226
+ setTimeout(() => { isUpdating = false; }, 0);
227
+ });
228
+ });
229
+ }
230
+ else if (property === 'tabs') {
231
+ const transformToComponent = toComponent || ((v: any) => v);
232
+
233
+ stateObj.subscribe((val: any) => {
234
+ const transformed = transformToComponent(val);
235
+ this.state.tabs = transformed;
236
+
237
+ // Re-render tabs
238
+ tabList.innerHTML = '';
239
+ tabPanels.innerHTML = '';
240
+
241
+ transformed.forEach((tab: any) => {
242
+ const tabButton = document.createElement('button');
243
+ tabButton.className = 'jux-tabs-button';
244
+ tabButton.setAttribute('data-tab', tab.id);
245
+ if (tab.id === this.state.activeTab) tabButton.classList.add('jux-tabs-button-active');
246
+ tabButton.textContent = tab.label;
247
+ tabList.appendChild(tabButton);
248
+
249
+ const tabPanel = document.createElement('div');
250
+ tabPanel.className = 'jux-tabs-panel';
251
+ tabPanel.setAttribute('data-tab', tab.id);
252
+ if (tab.id === this.state.activeTab) {
253
+ tabPanel.classList.add('jux-tabs-panel-active');
254
+ } else {
255
+ tabPanel.style.display = 'none';
256
+ }
257
+ if (typeof tab.content === 'string') {
258
+ tabPanel.innerHTML = tab.content;
259
+ } else {
260
+ tabPanel.appendChild(tab.content);
261
+ }
262
+ tabPanels.appendChild(tabPanel);
263
+ });
264
+
265
+ // Re-wire click handlers
266
+ tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
267
+ button.addEventListener('click', () => {
268
+ const tabId = button.getAttribute('data-tab')!;
269
+ this.state.activeTab = tabId;
270
+ this._switchTab(tabId, wrapper);
271
+ });
272
+ });
273
+ });
274
+ }
275
+ });
186
276
 
187
- tabs.forEach(tab => {
188
- const panel = document.createElement('div');
189
- panel.className = 'jux-tab-panel';
190
- panel.setAttribute('data-tab-id', tab.id);
277
+ // === 5. RENDER: Append to DOM and finalize ===
278
+ container.appendChild(wrapper);
279
+ return this;
280
+ }
191
281
 
192
- if (tab.id === activeTab) {
193
- panel.classList.add('jux-tab-panel-active');
282
+ private _switchTab(tabId: string, wrapper: HTMLElement): void {
283
+ // Update buttons
284
+ wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
285
+ if (btn.getAttribute('data-tab') === tabId) {
286
+ btn.classList.add('jux-tabs-button-active');
287
+ } else {
288
+ btn.classList.remove('jux-tabs-button-active');
194
289
  }
290
+ });
195
291
 
196
- // Render content
197
- if (typeof tab.content === 'function') {
198
- panel.innerHTML = tab.content();
292
+ // Update panels
293
+ wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
294
+ if (panel.getAttribute('data-tab') === tabId) {
295
+ panel.classList.add('jux-tabs-panel-active');
296
+ (panel as HTMLElement).style.display = 'block';
199
297
  } else {
200
- panel.innerHTML = tab.content;
298
+ panel.classList.remove('jux-tabs-panel-active');
299
+ (panel as HTMLElement).style.display = 'none';
201
300
  }
202
-
203
- tabPanels.appendChild(panel);
204
301
  });
205
-
206
- wrapper.appendChild(tabPanels);
207
- container.appendChild(wrapper);
208
-
209
- return this;
210
302
  }
211
303
 
212
- /**
213
- * Render to another Jux component's container
214
- */
215
304
  renderTo(juxComponent: any): this {
216
- if (!juxComponent || typeof juxComponent !== 'object') {
217
- throw new Error('Tabs.renderTo: Invalid component - not an object');
305
+ if (!juxComponent?._id) {
306
+ throw new Error('Tabs.renderTo: Invalid component');
218
307
  }
219
-
220
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
221
- throw new Error('Tabs.renderTo: Invalid component - missing _id (not a Jux component)');
222
- }
223
-
224
308
  return this.render(`#${juxComponent._id}`);
225
309
  }
226
310
  }
227
311
 
228
- /**
229
- * Factory helper
230
- */
231
312
  export function tabs(id: string, options: TabsOptions = {}): Tabs {
232
313
  return new Tabs(id, options);
233
314
  }
@@ -1,5 +1,6 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
2
  import { ErrorHandler } from './error-handler.js';
3
+ import { renderIcon } from './icons.js';
3
4
 
4
5
  /**
5
6
  * Theme configuration
@@ -237,10 +238,23 @@ export class ThemeToggle {
237
238
  // Update button variant
238
239
  if (this.state.variant === 'button' || this.state.variant === 'cycle') {
239
240
  const button = toggle.querySelector('button');
240
- if (button) {
241
- button.innerHTML = this.state.showLabel
242
- ? `${currentTheme.icon || ''} ${currentTheme.label}`.trim()
243
- : currentTheme.icon || currentTheme.label;
241
+ if (button && currentTheme.icon) {
242
+ button.innerHTML = '';
243
+ const iconElement = renderIcon(currentTheme.icon);
244
+ button.appendChild(iconElement);
245
+
246
+ if (this.state.showLabel) {
247
+ const labelSpan = document.createElement('span');
248
+ labelSpan.textContent = ` ${currentTheme.label}`;
249
+ button.appendChild(labelSpan);
250
+ }
251
+
252
+ // Trigger Lucide rendering
253
+ requestAnimationFrame(() => {
254
+ if ((window as any).lucide) {
255
+ (window as any).lucide.createIcons();
256
+ }
257
+ });
244
258
  }
245
259
  }
246
260
 
@@ -295,9 +309,18 @@ export class ThemeToggle {
295
309
  button.type = 'button';
296
310
  button.setAttribute('aria-label', 'Toggle theme');
297
311
 
298
- button.innerHTML = showLabel
299
- ? `${theme?.icon || ''} ${theme?.label || currentTheme}`.trim()
300
- : theme?.icon || theme?.label || currentTheme;
312
+ if (theme?.icon) {
313
+ const iconElement = renderIcon(theme.icon);
314
+ button.appendChild(iconElement);
315
+ }
316
+
317
+ if (showLabel) {
318
+ const labelSpan = document.createElement('span');
319
+ labelSpan.textContent = ` ${theme?.label || currentTheme}`;
320
+ button.appendChild(labelSpan);
321
+ } else if (!theme?.icon) {
322
+ button.textContent = theme?.label || currentTheme;
323
+ }
301
324
 
302
325
  button.addEventListener('click', () => {
303
326
  this.cycleTheme();
@@ -334,6 +357,14 @@ export class ThemeToggle {
334
357
  }
335
358
 
336
359
  container.appendChild(wrapper);
360
+
361
+ // Trigger Lucide icon rendering
362
+ requestAnimationFrame(() => {
363
+ if ((window as any).lucide) {
364
+ (window as any).lucide.createIcons();
365
+ }
366
+ });
367
+
337
368
  return this;
338
369
  }
339
370