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,22 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
2
  import { State } from '../reactivity/state.js';
3
+ import {
4
+ ChartDataPoint,
5
+ DoughnutChartOptions as DoughnutChartOptionsBase,
6
+ DoughnutChartState as DoughnutChartStateBase,
7
+ ChartTheme,
8
+ ChartStyleMode,
9
+ LegendOrientation,
10
+ ChartPropertyMapping,
11
+ ChartStateObject
12
+ } from './chart-types.js';
13
+ import {
14
+ lightenColor,
15
+ getThemeConfig,
16
+ createLegend,
17
+ createDataTable,
18
+ applyThemeStyles
19
+ } from './chart-utils.js';
3
20
  import {
4
21
  googleTheme,
5
22
  seriesaTheme,
@@ -658,7 +675,7 @@ export class DoughnutChart {
658
675
  path.setAttribute('stroke-dasharray', '8,4');
659
676
  } else if (styleMode === 'glow') {
660
677
  path.setAttribute('fill', color);
661
-
678
+
662
679
  const filterId = `glow-slice-${this._id}-${index}`;
663
680
  const defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
664
681
  if (!svg.querySelector('defs')) {
@@ -701,24 +718,24 @@ export class DoughnutChart {
701
718
  if (showDataLabels) {
702
719
  const middleAngle = startAngle + (sliceAngle / 2);
703
720
  const middleRad = (middleAngle * Math.PI) / 180;
704
-
721
+
705
722
  // Line start (from outer edge)
706
723
  const lineStartRadius = radius + 5;
707
724
  const lineStartX = centerX + lineStartRadius * Math.cos(middleRad);
708
725
  const lineStartY = centerY + lineStartRadius * Math.sin(middleRad);
709
-
726
+
710
727
  // Bubble position (along the line)
711
728
  const bubbleRadius = radius + 45;
712
729
  const bubbleX = centerX + bubbleRadius * Math.cos(middleRad);
713
730
  const bubbleY = centerY + bubbleRadius * Math.sin(middleRad);
714
-
731
+
715
732
  // Label position (beyond the bubble)
716
733
  const labelDistance = radius + 80;
717
734
  const externalLabelX = centerX + labelDistance * Math.cos(middleRad);
718
735
  const externalLabelY = centerY + labelDistance * Math.sin(middleRad);
719
736
 
720
737
  const lineGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
721
-
738
+
722
739
  if (animate) {
723
740
  lineGroup.classList.add('jux-label-animated');
724
741
  lineGroup.style.animationDelay = `${index * 150 + animationDuration + 100}ms`;
@@ -800,7 +817,7 @@ export class DoughnutChart {
800
817
  const externalLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
801
818
  externalLabel.setAttribute('x', externalLabelX.toString());
802
819
  externalLabel.setAttribute('y', externalLabelY.toString());
803
-
820
+
804
821
  // Anchor based on which side of the circle
805
822
  const anchor = middleAngle > -90 && middleAngle < 90 ? 'start' : 'end';
806
823
  externalLabel.setAttribute('text-anchor', anchor);
@@ -818,12 +835,12 @@ export class DoughnutChart {
818
835
  // Add hover effect
819
836
  path.style.cursor = 'pointer';
820
837
  path.style.transition = 'transform 0.2s, opacity 0.2s';
821
-
838
+
822
839
  path.addEventListener('mouseenter', () => {
823
840
  path.style.transform = 'scale(1.05)';
824
841
  path.style.opacity = '0.9';
825
842
  });
826
-
843
+
827
844
  path.addEventListener('mouseleave', () => {
828
845
  path.style.transform = 'scale(1)';
829
846
  path.style.opacity = '1';
@@ -843,6 +860,103 @@ export class DoughnutChart {
843
860
  // Not used for doughnut chart
844
861
  }
845
862
 
863
+ /* -------------------------
864
+ * Reactivity Support
865
+ * ------------------------- */
866
+
867
+ /**
868
+ * Sync a single property to a state object
869
+ */
870
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
871
+ const transform = toComponent || ((v: any) => v);
872
+
873
+ stateObj.subscribe((val: any) => {
874
+ const transformed = transform(val);
875
+
876
+ // Map property to correct method
877
+ switch (property) {
878
+ case 'data': this.data(transformed); break;
879
+ case 'title': this.title(transformed); break;
880
+ case 'subtitle': this.subtitle(transformed); break;
881
+ case 'width': this.width(transformed); break;
882
+ case 'height': this.height(transformed); break;
883
+ case 'theme': this.theme(transformed); break;
884
+ case 'styleMode': this.styleMode(transformed); break;
885
+ case 'borderRadius': this.borderRadius(transformed); break;
886
+ case 'showLegend': this.showLegend(transformed); break;
887
+ case 'showDataLabels': this.showDataLabels(transformed); break;
888
+ case 'showDataTable': this.showDataTable(transformed); break;
889
+ case 'animate': this.animate(transformed); break;
890
+ case 'animationDuration': this.animationDuration(transformed); break;
891
+ case 'legendOrientation': this.legendOrientation(transformed); break;
892
+ }
893
+ });
894
+
895
+ return this;
896
+ }
897
+
898
+ /**
899
+ * Sync multiple properties from a state object
900
+ */
901
+ syncState(stateObject: ChartStateObject, mapping?: ChartPropertyMapping): this {
902
+ // Default mapping: camelCase state names to method names
903
+ const defaultMapping: ChartPropertyMapping = {
904
+ chartType: 'type', // Not used in doughnut chart, ignored
905
+ chartTheme: 'theme',
906
+ chartStyleMode: 'styleMode',
907
+ borderRadius: 'borderRadius',
908
+ chartTitle: 'title',
909
+ chartWidth: 'width',
910
+ chartHeight: 'height',
911
+ showLegend: 'showLegend',
912
+ showDataTable: 'showDataTable',
913
+ showDataLabels: 'showDataLabels',
914
+ animate: 'animate',
915
+ animationDuration: 'animationDuration',
916
+ legendOrientation: 'legendOrientation',
917
+ // Note: Doughnut charts don't use these, but we handle them gracefully
918
+ showTicksX: null,
919
+ showTicksY: null,
920
+ chartOrientation: null,
921
+ chartDirection: null
922
+ };
923
+
924
+ const finalMapping = { ...defaultMapping, ...mapping };
925
+
926
+ // Iterate through state object and bind each property
927
+ Object.entries(stateObject).forEach(([key, stateObj]) => {
928
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
929
+ return;
930
+ }
931
+
932
+ const methodOrFunction = finalMapping[key];
933
+
934
+ // Skip null mappings (properties not applicable to this chart type)
935
+ if (methodOrFunction === null || !methodOrFunction) {
936
+ return;
937
+ }
938
+
939
+ if (typeof methodOrFunction === 'function') {
940
+ // Custom function mapping
941
+ stateObj.subscribe(methodOrFunction);
942
+ } else {
943
+ // String mapping to method name
944
+ const methodName = methodOrFunction as keyof this;
945
+ const method = this[methodName];
946
+
947
+ if (typeof method !== 'function') {
948
+ return;
949
+ }
950
+
951
+ stateObj.subscribe((val: any) => {
952
+ (method as Function).call(this, val);
953
+ });
954
+ }
955
+ });
956
+
957
+ return this;
958
+ }
959
+
846
960
  /* -------------------------
847
961
  * Legend and Data Table
848
962
  * ------------------------- */
@@ -968,59 +1082,13 @@ export class DoughnutChart {
968
1082
  }
969
1083
 
970
1084
  private _applyTheme(themeName: string): void {
971
- const themes: Record<string, any> = {
972
- google: googleTheme,
973
- seriesa: seriesaTheme,
974
- hr: hrTheme,
975
- figma: figmaTheme,
976
- notion: notionTheme,
977
- chalk: chalkTheme,
978
- mint: mintTheme
979
- };
980
-
981
- const theme = themes[themeName];
1085
+ const theme = getThemeConfig(themeName as ChartTheme);
982
1086
  if (!theme) return;
983
1087
 
984
1088
  // Apply colors
985
1089
  this.state.colors = theme.colors;
986
1090
 
987
- // Inject base styles (once)
988
- const baseStyleId = 'jux-doughnutchart-base-styles';
989
- if (!document.getElementById(baseStyleId)) {
990
- const style = document.createElement('style');
991
- style.id = baseStyleId;
992
- style.textContent = this._getBaseStyles();
993
- document.head.appendChild(style);
994
- }
995
-
996
- // Inject font (once per theme)
997
- if (theme.font && !document.querySelector(`link[href="${theme.font}"]`)) {
998
- const link = document.createElement('link');
999
- link.rel = 'stylesheet';
1000
- link.href = theme.font;
1001
- document.head.appendChild(link);
1002
- }
1003
-
1004
- // Apply theme-specific styles
1005
- const styleId = `jux-doughnutchart-theme-${themeName}`;
1006
- let styleElement = document.getElementById(styleId) as HTMLStyleElement;
1007
-
1008
- if (!styleElement) {
1009
- styleElement = document.createElement('style');
1010
- styleElement.id = styleId;
1011
- document.head.appendChild(styleElement);
1012
- }
1013
-
1014
- // Generate CSS with theme variables
1015
- const variablesCSS = Object.entries(theme.variables)
1016
- .map(([key, value]) => ` ${key}: ${value};`)
1017
- .join('\n');
1018
-
1019
- styleElement.textContent = `
1020
- .jux-doughnutchart.theme-${themeName} {
1021
- ${variablesCSS}
1022
- }
1023
- `;
1091
+ applyThemeStyles(themeName as ChartTheme, 'jux-doughnutchart', this._getBaseStyles());
1024
1092
  }
1025
1093
 
1026
1094
  private _applyThemeToWrapper(wrapper: HTMLElement): void {
@@ -1131,7 +1199,7 @@ ${variablesCSS}
1131
1199
  .jux-doughnutchart-svg {
1132
1200
  font-family: inherit;
1133
1201
  }
1134
- `;
1202
+ `;
1135
1203
  }
1136
1204
 
1137
1205
  render(targetId?: string | HTMLElement): this {
@@ -1,77 +1,75 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
3
+ import { renderIcon } from './icons.js';
2
4
 
3
- /**
4
- * Dropdown menu item
5
- */
6
- export interface DropdownItem {
5
+ export interface DropdownOption {
7
6
  label: string;
8
- value?: string;
9
- icon?: string;
10
- click?: () => void;
7
+ value: string;
11
8
  disabled?: boolean;
12
- divider?: boolean;
13
9
  }
14
10
 
15
- /**
16
- * Dropdown component options
17
- */
18
11
  export interface DropdownOptions {
19
- items?: DropdownItem[];
20
- trigger?: string;
21
- position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
12
+ options?: DropdownOption[];
13
+ value?: string;
14
+ placeholder?: string;
15
+ label?: string;
16
+ disabled?: boolean;
22
17
  style?: string;
23
18
  class?: string;
24
19
  }
25
20
 
26
- /**
27
- * Dropdown component state
28
- */
21
+ export interface DropdownItem {
22
+ label: string;
23
+ value?: string;
24
+ disabled?: boolean;
25
+ onClick?: () => void;
26
+ }
27
+
29
28
  type DropdownState = {
30
- items: DropdownItem[];
31
- trigger: string;
32
- position: string;
29
+ options: DropdownOption[];
30
+ value: string;
31
+ placeholder: string;
32
+ label: string;
33
+ disabled: boolean;
33
34
  style: string;
34
35
  class: string;
35
36
  isOpen: boolean;
37
+ items: any[];
38
+ trigger: string;
39
+ position: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
36
40
  };
37
41
 
38
- /**
39
- * Dropdown Menu component - Context menus, action menus
40
- *
41
- * Usage:
42
- * jux.dropdown('actions', {
43
- * trigger: 'Actions ▾',
44
- * position: 'bottom-right',
45
- * items: [
46
- * { label: 'Edit', icon: '✏️', click: () => console.log('Edit') },
47
- * { label: 'Delete', icon: '🗑️', click: () => console.log('Delete') },
48
- * { divider: true },
49
- * { label: 'Archive', click: () => console.log('Archive') }
50
- * ]
51
- * }).render('#toolbar');
52
- *
53
- * // Control programmatically
54
- * const dd = jux.dropdown('my-dropdown').render();
55
- * dd.open();
56
- * dd.close();
57
- */
58
42
  export class Dropdown {
59
43
  state: DropdownState;
60
44
  container: HTMLElement | null = null;
61
45
  _id: string;
62
46
  id: string;
63
47
 
48
+ // CRITICAL: Store bind/sync instructions for deferred wiring
49
+ private _bindings: Array<{ event: string, handler: Function }> = [];
50
+ private _syncBindings: Array<{
51
+ property: string,
52
+ stateObj: State<any>,
53
+ toState?: Function,
54
+ toComponent?: Function
55
+ }> = [];
56
+
64
57
  constructor(id: string, options: DropdownOptions = {}) {
65
58
  this._id = id;
66
59
  this.id = id;
67
60
 
68
61
  this.state = {
69
- items: options.items ?? [],
70
- trigger: options.trigger ?? 'Menu',
71
- position: options.position ?? 'bottom-left',
62
+ options: options.options ?? [],
63
+ value: options.value ?? '',
64
+ placeholder: options.placeholder ?? 'Select...',
65
+ label: options.label ?? '',
66
+ disabled: options.disabled ?? false,
72
67
  style: options.style ?? '',
73
68
  class: options.class ?? '',
74
- isOpen: false
69
+ isOpen: false,
70
+ items: [],
71
+ trigger: '',
72
+ position: 'bottom-left'
75
73
  };
76
74
  }
77
75
 
@@ -109,6 +107,19 @@ export class Dropdown {
109
107
  return this;
110
108
  }
111
109
 
110
+ bind(event: string, handler: Function): this {
111
+ this._bindings.push({ event, handler });
112
+ return this;
113
+ }
114
+
115
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
116
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
117
+ throw new Error(`Dropdown.sync: Expected a State object for property "${property}"`);
118
+ }
119
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
120
+ return this;
121
+ }
122
+
112
123
  /* -------------------------
113
124
  * Methods
114
125
  * ------------------------- */
@@ -142,47 +153,37 @@ export class Dropdown {
142
153
  * ------------------------- */
143
154
 
144
155
  render(targetId?: string): this {
156
+ // === 1. SETUP: Get or create container ===
145
157
  let container: HTMLElement;
146
-
147
158
  if (targetId) {
148
159
  const target = document.querySelector(targetId);
149
160
  if (!target || !(target instanceof HTMLElement)) {
150
- throw new Error(`Dropdown: Target element "${targetId}" not found`);
161
+ throw new Error(`Dropdown: Target "${targetId}" not found`);
151
162
  }
152
163
  container = target;
153
164
  } else {
154
165
  container = getOrCreateContainer(this._id);
155
166
  }
156
-
157
167
  this.container = container;
158
- const { items, trigger, position, style, class: className } = this.state;
159
168
 
160
- // Wrapper
169
+ // === 2. PREPARE: Destructure state and check sync flags ===
170
+ const { label, items, style, class: className } = this.state;
171
+ const hasItemsSync = this._syncBindings.some(b => b.property === 'items');
172
+
173
+ // === 3. BUILD: Create DOM elements ===
161
174
  const wrapper = document.createElement('div');
162
175
  wrapper.className = 'jux-dropdown';
163
176
  wrapper.id = this._id;
177
+ if (className) wrapper.className += ` ${className}`;
178
+ if (style) wrapper.setAttribute('style', style);
164
179
 
165
- if (className) {
166
- wrapper.className += ` ${className}`;
167
- }
180
+ const button = document.createElement('button');
181
+ button.className = 'jux-dropdown-button';
182
+ button.textContent = label;
168
183
 
169
- if (style) {
170
- wrapper.setAttribute('style', style);
171
- }
172
-
173
- // Trigger button
174
- const triggerBtn = document.createElement('button');
175
- triggerBtn.className = 'jux-dropdown-trigger';
176
- triggerBtn.textContent = trigger;
177
- triggerBtn.addEventListener('click', (e) => {
178
- e.stopPropagation();
179
- this.toggle();
180
- });
181
-
182
- // Menu
183
184
  const menu = document.createElement('div');
184
- menu.className = `jux-dropdown-menu jux-dropdown-menu-${position}`;
185
- menu.id = `${this._id}-menu`;
185
+ menu.className = 'jux-dropdown-menu';
186
+ menu.style.display = 'none';
186
187
 
187
188
  items.forEach(item => {
188
189
  if (item.divider) {
@@ -190,41 +191,127 @@ export class Dropdown {
190
191
  divider.className = 'jux-dropdown-divider';
191
192
  menu.appendChild(divider);
192
193
  } else {
193
- const menuItem = document.createElement('button');
194
+ const menuItem = document.createElement('a');
194
195
  menuItem.className = 'jux-dropdown-item';
195
- menuItem.disabled = item.disabled ?? false;
196
+
197
+ if (item.href) {
198
+ menuItem.href = item.href;
199
+ }
196
200
 
197
201
  if (item.icon) {
198
202
  const icon = document.createElement('span');
199
- icon.className = 'jux-dropdown-item-icon';
200
- icon.textContent = item.icon;
203
+ icon.className = 'jux-dropdown-icon';
204
+ icon.appendChild(renderIcon(item.icon));
201
205
  menuItem.appendChild(icon);
202
206
  }
203
207
 
204
- const label = document.createElement('span');
205
- label.className = 'jux-dropdown-item-label';
206
- label.textContent = item.label;
207
- menuItem.appendChild(label);
208
+ const text = document.createElement('span');
209
+ text.textContent = item.label;
210
+ menuItem.appendChild(text);
208
211
 
209
- menuItem.addEventListener('click', () => {
210
- if (item.click) {
211
- item.click();
212
- }
213
- this.close();
214
- });
212
+ if (item.click) {
213
+ menuItem.addEventListener('click', (e) => {
214
+ e.preventDefault();
215
+ item.click!();
216
+ menu.style.display = 'none';
217
+ });
218
+ }
215
219
 
216
220
  menu.appendChild(menuItem);
217
221
  }
218
222
  });
219
223
 
220
- wrapper.appendChild(triggerBtn);
224
+ wrapper.appendChild(button);
221
225
  wrapper.appendChild(menu);
222
- container.appendChild(wrapper);
226
+
227
+ // Toggle functionality
228
+ button.addEventListener('click', () => {
229
+ menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
230
+ });
223
231
 
224
232
  // Close on outside click
225
233
  document.addEventListener('click', (e) => {
226
234
  if (!wrapper.contains(e.target as Node)) {
227
- this.close();
235
+ menu.style.display = 'none';
236
+ }
237
+ });
238
+
239
+ // === 4. WIRE: Attach event listeners and sync bindings ===
240
+
241
+ // Wire custom bindings from .bind() calls
242
+ this._bindings.forEach(({ event, handler }) => {
243
+ wrapper.addEventListener(event, handler as EventListener);
244
+ });
245
+
246
+ // Wire sync bindings from .sync() calls
247
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
248
+ if (property === 'items') {
249
+ const transformToComponent = toComponent || ((v: any) => v);
250
+
251
+ stateObj.subscribe((val: any) => {
252
+ const transformed = transformToComponent(val);
253
+ this.state.items = transformed;
254
+
255
+ // Re-render menu items
256
+ menu.innerHTML = '';
257
+ transformed.forEach((item: any) => {
258
+ if (item.divider) {
259
+ const divider = document.createElement('div');
260
+ divider.className = 'jux-dropdown-divider';
261
+ menu.appendChild(divider);
262
+ } else {
263
+ const menuItem = document.createElement('a');
264
+ menuItem.className = 'jux-dropdown-item';
265
+
266
+ if (item.href) menuItem.href = item.href;
267
+
268
+ if (item.icon) {
269
+ const icon = document.createElement('span');
270
+ icon.className = 'jux-dropdown-icon';
271
+ icon.appendChild(renderIcon(item.icon));
272
+ menuItem.appendChild(icon);
273
+ }
274
+
275
+ const text = document.createElement('span');
276
+ text.textContent = item.label;
277
+ menuItem.appendChild(text);
278
+
279
+ if (item.click) {
280
+ menuItem.addEventListener('click', (e) => {
281
+ e.preventDefault();
282
+ item.click!();
283
+ menu.style.display = 'none';
284
+ });
285
+ }
286
+
287
+ menu.appendChild(menuItem);
288
+ }
289
+ });
290
+
291
+ requestAnimationFrame(() => {
292
+ if ((window as any).lucide) {
293
+ (window as any).lucide.createIcons();
294
+ }
295
+ });
296
+ });
297
+ }
298
+ else if (property === 'label') {
299
+ const transformToComponent = toComponent || ((v: any) => String(v));
300
+
301
+ stateObj.subscribe((val: any) => {
302
+ const transformed = transformToComponent(val);
303
+ button.textContent = transformed;
304
+ this.state.label = transformed;
305
+ });
306
+ }
307
+ });
308
+
309
+ // === 5. RENDER: Append to DOM and finalize ===
310
+ container.appendChild(wrapper);
311
+
312
+ requestAnimationFrame(() => {
313
+ if ((window as any).lucide) {
314
+ (window as any).lucide.createIcons();
228
315
  }
229
316
  });
230
317