juxscript 1.1.404 → 1.1.408

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 (154) hide show
  1. package/dist/components/button.d.ts +1 -0
  2. package/dist/components/button.d.ts.map +1 -1
  3. package/dist/components/button.js +37 -0
  4. package/dist/components/button.js.map +1 -1
  5. package/dist/components/c.d.ts +53 -0
  6. package/dist/components/c.d.ts.map +1 -0
  7. package/dist/components/c.js +127 -0
  8. package/dist/components/c.js.map +1 -0
  9. package/dist/components/charts/barChart.d.ts +119 -0
  10. package/dist/components/charts/barChart.d.ts.map +1 -0
  11. package/dist/components/charts/barChart.js +644 -0
  12. package/dist/components/charts/barChart.js.map +1 -0
  13. package/dist/components/charts/lineChart.d.ts +104 -0
  14. package/dist/components/charts/lineChart.d.ts.map +1 -0
  15. package/dist/components/charts/lineChart.js +466 -0
  16. package/dist/components/charts/lineChart.js.map +1 -0
  17. package/dist/components/charts/pieChart.d.ts +93 -0
  18. package/dist/components/charts/pieChart.d.ts.map +1 -0
  19. package/dist/components/charts/pieChart.js +397 -0
  20. package/dist/components/charts/pieChart.js.map +1 -0
  21. package/dist/components/checkbox.d.ts +2 -0
  22. package/dist/components/checkbox.d.ts.map +1 -1
  23. package/dist/components/checkbox.js +47 -0
  24. package/dist/components/checkbox.js.map +1 -1
  25. package/dist/components/flex.d.ts +91 -0
  26. package/dist/components/flex.d.ts.map +1 -0
  27. package/dist/components/flex.js +166 -0
  28. package/dist/components/flex.js.map +1 -0
  29. package/dist/components/g.d.ts +21 -0
  30. package/dist/components/g.d.ts.map +1 -0
  31. package/dist/components/g.js +52 -0
  32. package/dist/components/g.js.map +1 -0
  33. package/dist/components/input.d.ts +2 -0
  34. package/dist/components/input.d.ts.map +1 -1
  35. package/dist/components/input.js +21 -2
  36. package/dist/components/input.js.map +1 -1
  37. package/dist/components/jtable.d.ts +47 -0
  38. package/dist/components/jtable.d.ts.map +1 -0
  39. package/dist/components/jtable.js +307 -0
  40. package/dist/components/jtable.js.map +1 -0
  41. package/dist/components/link.d.ts +1 -0
  42. package/dist/components/link.d.ts.map +1 -1
  43. package/dist/components/link.js +17 -0
  44. package/dist/components/link.js.map +1 -1
  45. package/dist/components/list.d.ts +1 -0
  46. package/dist/components/list.d.ts.map +1 -1
  47. package/dist/components/list.js +18 -0
  48. package/dist/components/list.js.map +1 -1
  49. package/dist/components/menu.d.ts +108 -0
  50. package/dist/components/menu.d.ts.map +1 -0
  51. package/dist/components/menu.js +665 -0
  52. package/dist/components/menu.js.map +1 -0
  53. package/dist/components/nav.d.ts +1 -0
  54. package/dist/components/nav.d.ts.map +1 -1
  55. package/dist/components/nav.js +19 -0
  56. package/dist/components/nav.js.map +1 -1
  57. package/dist/components/radio.d.ts +1 -0
  58. package/dist/components/radio.d.ts.map +1 -1
  59. package/dist/components/radio.js +23 -0
  60. package/dist/components/radio.js.map +1 -1
  61. package/dist/components/routes.d.ts +17 -0
  62. package/dist/components/routes.d.ts.map +1 -1
  63. package/dist/components/routes.js +86 -0
  64. package/dist/components/routes.js.map +1 -1
  65. package/dist/components/select.d.ts +1 -0
  66. package/dist/components/select.d.ts.map +1 -1
  67. package/dist/components/select.js +17 -0
  68. package/dist/components/select.js.map +1 -1
  69. package/dist/components/table.d.ts +1 -0
  70. package/dist/components/table.d.ts.map +1 -1
  71. package/dist/components/table.js +20 -0
  72. package/dist/components/table.js.map +1 -1
  73. package/dist/components/tabs.d.ts +17 -1
  74. package/dist/components/tabs.d.ts.map +1 -1
  75. package/dist/components/tabs.js +50 -8
  76. package/dist/components/tabs.js.map +1 -1
  77. package/dist/components/tag.d.ts +1 -0
  78. package/dist/components/tag.d.ts.map +1 -1
  79. package/dist/components/tag.js +16 -0
  80. package/dist/components/tag.js.map +1 -1
  81. package/dist/components/widgets/calendar.d.ts +74 -0
  82. package/dist/components/widgets/calendar.d.ts.map +1 -0
  83. package/dist/components/widgets/calendar.js +308 -0
  84. package/dist/components/widgets/calendar.js.map +1 -0
  85. package/dist/components/widgets/canvas-ai.d.ts +12 -0
  86. package/dist/components/widgets/canvas-ai.d.ts.map +1 -0
  87. package/dist/components/widgets/canvas-ai.js +97 -0
  88. package/dist/components/widgets/canvas-ai.js.map +1 -0
  89. package/dist/components/widgets/canvas-compile.d.ts +36 -0
  90. package/dist/components/widgets/canvas-compile.d.ts.map +1 -0
  91. package/dist/components/widgets/canvas-compile.js +379 -0
  92. package/dist/components/widgets/canvas-compile.js.map +1 -0
  93. package/dist/components/widgets/canvas-persist.d.ts +11 -0
  94. package/dist/components/widgets/canvas-persist.d.ts.map +1 -0
  95. package/dist/components/widgets/canvas-persist.js +60 -0
  96. package/dist/components/widgets/canvas-persist.js.map +1 -0
  97. package/dist/components/widgets/canvas-registry.d.ts +42 -0
  98. package/dist/components/widgets/canvas-registry.d.ts.map +1 -0
  99. package/dist/components/widgets/canvas-registry.js +338 -0
  100. package/dist/components/widgets/canvas-registry.js.map +1 -0
  101. package/dist/components/widgets/canvas-styles.d.ts +2 -0
  102. package/dist/components/widgets/canvas-styles.d.ts.map +1 -0
  103. package/dist/components/widgets/canvas-styles.js +215 -0
  104. package/dist/components/widgets/canvas-styles.js.map +1 -0
  105. package/dist/components/widgets/canvas.d.ts +125 -0
  106. package/dist/components/widgets/canvas.d.ts.map +1 -0
  107. package/dist/components/widgets/canvas.js +1359 -0
  108. package/dist/components/widgets/canvas.js.map +1 -0
  109. package/dist/components/widgets/sidebar.d.ts +100 -0
  110. package/dist/components/widgets/sidebar.d.ts.map +1 -0
  111. package/dist/components/widgets/sidebar.js +434 -0
  112. package/dist/components/widgets/sidebar.js.map +1 -0
  113. package/dist/components/widgets/stepper.d.ts +87 -0
  114. package/dist/components/widgets/stepper.d.ts.map +1 -0
  115. package/dist/components/widgets/stepper.js +388 -0
  116. package/dist/components/widgets/stepper.js.map +1 -0
  117. package/dist/generated/jux-registry.d.ts +24 -0
  118. package/dist/generated/jux-registry.d.ts.map +1 -0
  119. package/dist/generated/jux-registry.js +90 -0
  120. package/dist/generated/jux-registry.js.map +1 -0
  121. package/dist/index.d.ts +39 -23
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +38 -24
  124. package/dist/index.js.map +1 -1
  125. package/dist/state/pageState.d.ts +6 -0
  126. package/dist/state/pageState.d.ts.map +1 -1
  127. package/dist/state/pageState.js +24 -16
  128. package/dist/state/pageState.js.map +1 -1
  129. package/dist/styles/layout-regions-observer.d.ts +7 -0
  130. package/dist/styles/layout-regions-observer.d.ts.map +1 -0
  131. package/dist/styles/layout-regions-observer.js +52 -0
  132. package/dist/styles/layout-regions-observer.js.map +1 -0
  133. package/dist/utils/colors.d.ts +0 -3
  134. package/dist/utils/colors.d.ts.map +1 -1
  135. package/dist/utils/colors.js +20 -6
  136. package/dist/utils/colors.js.map +1 -1
  137. package/dist/utils/resolveContent.d.ts +11 -0
  138. package/dist/utils/resolveContent.d.ts.map +1 -0
  139. package/dist/utils/resolveContent.js +37 -0
  140. package/dist/utils/resolveContent.js.map +1 -0
  141. package/dist/utils/theme.d.ts +58 -0
  142. package/dist/utils/theme.d.ts.map +1 -0
  143. package/dist/utils/theme.js +172 -0
  144. package/dist/utils/theme.js.map +1 -0
  145. package/dist/widgets/canvas.d.ts +3 -69
  146. package/dist/widgets/canvas.d.ts.map +1 -1
  147. package/dist/widgets/canvas.js +3 -791
  148. package/dist/widgets/canvas.js.map +1 -1
  149. package/juxconfig.example.js +19 -0
  150. package/machinery/compiler4.js +103 -9
  151. package/machinery/errors-client.js +171 -67
  152. package/machinery/jux-errors.js +218 -0
  153. package/machinery/serve.js +67 -0
  154. package/package.json +1 -1
@@ -0,0 +1,644 @@
1
+ import { pageState } from '../../state/pageState.js';
2
+ import generateId from '../../utils/idgen.js';
3
+ import { showTooltip, moveTooltip, hideTooltip, formatTooltipRow } from '../../utils/tooltip.js';
4
+ import { computeTrend } from '../../utils/trend.js';
5
+ // --- Design tokens ---
6
+ const FONT_FAMILY = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`;
7
+ const TOKENS = {
8
+ muted: 'hsl(215, 16%, 47%)',
9
+ border: 'hsl(220, 13%, 91%)',
10
+ grid: 'hsl(220, 13%, 93%)',
11
+ bg: '#ffffff',
12
+ title: 'hsl(222, 47%, 11%)',
13
+ hoverBg: 'rgba(0,0,0,0.04)',
14
+ activeBg: 'rgba(0,0,0,0.08)',
15
+ linkColor: 'hsl(217, 91%, 60%)',
16
+ };
17
+ const RATIOS = {
18
+ '16:9': 16 / 9,
19
+ '4:3': 4 / 3,
20
+ '3:2': 3 / 2,
21
+ '1:1': 1,
22
+ '2:1': 2,
23
+ '21:9': 21 / 9,
24
+ };
25
+ // --- Bar animation styles (injected once) ---
26
+ let animInjected = false;
27
+ function injectBarAnimations() {
28
+ if (animInjected)
29
+ return;
30
+ animInjected = true;
31
+ const el = document.createElement('style');
32
+ el.id = 'jux-bar-anims';
33
+ el.textContent = `
34
+ @keyframes jux-bar-grow-h {
35
+ from { transform: scaleX(0); opacity: 0; }
36
+ to { transform: scaleX(1); opacity: 0.85; }
37
+ }
38
+ @keyframes jux-bar-grow-v {
39
+ from { transform: scaleY(0); opacity: 0; }
40
+ to { transform: scaleY(1); opacity: 0.85; }
41
+ }
42
+ @keyframes jux-bar-fade-in {
43
+ from { opacity: 0; }
44
+ to { opacity: 1; }
45
+ }
46
+ .jux-bar-h {
47
+ transform-origin: left center;
48
+ animation: jux-bar-grow-h 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
49
+ }
50
+ .jux-bar-v {
51
+ transform-origin: center bottom;
52
+ animation: jux-bar-grow-v 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
53
+ }
54
+ .jux-bar-label {
55
+ animation: jux-bar-fade-in 0.3s ease-out both;
56
+ }
57
+ .jux-bar-clickable {
58
+ cursor: pointer;
59
+ transition: background-color 0.15s ease, transform 0.1s ease;
60
+ }
61
+ .jux-bar-clickable:hover {
62
+ background-color: rgba(0,0,0,0.04);
63
+ }
64
+ .jux-bar-clickable:active {
65
+ background-color: rgba(0,0,0,0.08);
66
+ transform: scale(0.98);
67
+ }
68
+ .jux-bar-selected {
69
+ outline: 2px solid hsl(217, 91%, 60%);
70
+ outline-offset: 2px;
71
+ }
72
+ `;
73
+ document.head.appendChild(el);
74
+ }
75
+ function resolvePalette(colors) {
76
+ if (Array.isArray(colors))
77
+ return colors;
78
+ // Fallback palettes
79
+ const palettes = {
80
+ green: ['hsl(142, 71%, 45%)', 'hsl(142, 71%, 35%)', 'hsl(142, 71%, 55%)', 'hsl(142, 71%, 25%)', 'hsl(142, 71%, 65%)', 'hsl(142, 71%, 40%)'],
81
+ blue: ['hsl(217, 91%, 60%)', 'hsl(217, 91%, 50%)', 'hsl(217, 91%, 70%)', 'hsl(217, 91%, 40%)', 'hsl(217, 91%, 75%)', 'hsl(217, 91%, 55%)'],
82
+ warm: ['hsl(25, 95%, 53%)', 'hsl(15, 95%, 53%)', 'hsl(35, 95%, 53%)', 'hsl(5, 95%, 53%)', 'hsl(45, 95%, 53%)', 'hsl(20, 95%, 53%)'],
83
+ cool: ['hsl(199, 89%, 48%)', 'hsl(210, 89%, 48%)', 'hsl(188, 89%, 48%)', 'hsl(220, 89%, 48%)', 'hsl(195, 89%, 48%)', 'hsl(205, 89%, 48%)'],
84
+ };
85
+ return palettes[colors] || palettes['green'];
86
+ }
87
+ function computeLayout(values, opts) {
88
+ const { orientation, width, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride } = opts;
89
+ const rows = values.length;
90
+ const maxVal = Math.max(...values.map(v => v.x));
91
+ const ratio = typeof aspectRatio === 'string' ? (RATIOS[aspectRatio] || 16 / 9) : (aspectRatio || 16 / 9);
92
+ const scale = width / 500;
93
+ const padding = Math.round(20 * scale);
94
+ const gap = Math.max(2, Math.round(6 * scale));
95
+ const barRadius = Math.max(3, Math.round(6 * scale));
96
+ const titleSize = Math.max(14, Math.round(18 * scale));
97
+ const subtitleSize = Math.max(11, Math.round(13 * scale));
98
+ const labelSize = Math.max(9, Math.round(12 * scale));
99
+ const tickSize = Math.max(8, Math.round(10 * scale));
100
+ let cardWidth, cardHeight, barThickness, labelWidth, maxBarLength, yAxisWidth;
101
+ const headerEstimate = padding + Math.round(titleSize * 1.4) + Math.round(subtitleSize * 1.4);
102
+ if (orientation === 'vertical') {
103
+ cardWidth = width;
104
+ barThickness = barThicknessOverride || Math.max(16, Math.round(28 * scale));
105
+ yAxisWidth = Math.round(36 * scale);
106
+ labelWidth = 0;
107
+ const labelRowHeight = Math.round(labelSize * 2) + Math.round(6 * scale);
108
+ const ratioHeight = Math.round(width / ratio);
109
+ maxBarLength = Math.max(ratioHeight - headerEstimate - labelRowHeight - padding * 2, 100);
110
+ cardHeight = headerEstimate + maxBarLength + labelRowHeight + padding * 2;
111
+ const barsNeeded = rows * (barThickness + gap);
112
+ if (barsNeeded + yAxisWidth + padding * 2 > cardWidth) {
113
+ barThickness = Math.max(8, Math.floor((cardWidth - yAxisWidth - padding * 2) / rows - gap));
114
+ }
115
+ }
116
+ else {
117
+ cardWidth = width;
118
+ barThickness = barThicknessOverride || Math.max(16, Math.round(28 * scale));
119
+ labelWidth = labelWidthOverride || Math.max(40, Math.round(70 * scale));
120
+ yAxisWidth = 0;
121
+ maxBarLength = cardWidth - labelWidth - padding * 3;
122
+ const rowHeight = barThickness + gap + gap + 1;
123
+ const tickRowHeight = Math.round(tickSize * 2);
124
+ const minContentHeight = headerEstimate + rows * rowHeight + tickRowHeight + padding * 2;
125
+ const ratioHeight = Math.round(width / ratio);
126
+ cardHeight = Math.max(ratioHeight, minContentHeight);
127
+ }
128
+ return { rows, maxVal, cardWidth, cardHeight, barThickness, labelWidth, maxBarLength, yAxisWidth, padding, gap, barRadius, titleSize, subtitleSize, labelSize, tickSize, scale };
129
+ }
130
+ function formatTick(val) {
131
+ if (val >= 1000)
132
+ return `${(val / 1000).toFixed(1)}k`;
133
+ return String(Math.round(val));
134
+ }
135
+ function mkDiv(id, css, className) {
136
+ const el = document.createElement('div');
137
+ el.id = id;
138
+ if (css)
139
+ el.setAttribute('style', css);
140
+ if (className)
141
+ el.className = className;
142
+ return el;
143
+ }
144
+ class BarChart {
145
+ constructor(id, values, options = {}) {
146
+ this._listeners = { click: [], select: [], drill: [] };
147
+ this._selectedIndex = -1;
148
+ this._lastClickDetail = null;
149
+ this._resizeObserver = null;
150
+ this._onChange = null;
151
+ this.id = id || generateId('bar');
152
+ this._values = values;
153
+ this._opts = {
154
+ orientation: options.orientation || 'horizontal',
155
+ colors: options.colors || 'green',
156
+ title: options.title || '',
157
+ subtitle: options.subtitle || '',
158
+ aspectRatio: options.aspectRatio || '16:9',
159
+ showValues: options.showValues ?? false,
160
+ showLegend: options.showLegend ?? undefined,
161
+ ticks: options.ticks ?? 4,
162
+ animate: options.animate ?? true,
163
+ stagger: options.stagger ?? 80,
164
+ responsive: options.responsive ?? true,
165
+ maintainAspectRatio: options.maintainAspectRatio ?? true,
166
+ selectable: options.selectable ?? false,
167
+ ...options,
168
+ };
169
+ this._seriesKeys = detectSeriesKeys(values, options.series);
170
+ if (this._opts.showLegend === undefined) {
171
+ this._opts.showLegend = this._seriesKeys.length > 1;
172
+ }
173
+ if (this._opts.animate)
174
+ injectBarAnimations();
175
+ this._palette = this._seriesKeys.length > 1
176
+ ? this._seriesKeys.map((s, i) => s.color || DEFAULT_SERIES_COLORS[i % DEFAULT_SERIES_COLORS.length])
177
+ : resolvePalette(this._opts.colors || 'green');
178
+ this._isInteractive = true; // always interactive — tooltips on all charts
179
+ this._wrapperEl = mkDiv(`${id}-wrapper`, `width: 100%; position: relative; box-sizing: border-box;`);
180
+ this._stateEl = document.createElement('button');
181
+ this._stateEl.id = id;
182
+ this._stateEl.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;';
183
+ this._wrapperEl.appendChild(this._stateEl);
184
+ }
185
+ // ═══════════════════════════════════════════════════════════
186
+ // PAGESTATE INTEGRATION
187
+ // ═══════════════════════════════════════════════════════════
188
+ getValue() { return this._lastClickDetail; }
189
+ setValue(val) { return this; }
190
+ getElement() { return this._stateEl; }
191
+ getSelectedIndex() { return this._selectedIndex; }
192
+ getOrientation() { return this._opts.orientation; }
193
+ getTitle() { return this._opts.title; }
194
+ getSubtitle() { return this._opts.subtitle; }
195
+ getSelectable() { return this._opts.selectable; }
196
+ setSelectedIndex(idx) { return this.select(idx); }
197
+ setTitle(val) { this._opts.title = val; return this; }
198
+ setSubtitle(val) { this._opts.subtitle = val; return this; }
199
+ setContent(val) { this._opts.title = val; return this; }
200
+ title(val) { this._opts.title = val; return this; }
201
+ subtitle(val) { this._opts.subtitle = val; return this; }
202
+ onChange(fn) {
203
+ this._onChange = fn;
204
+ return this;
205
+ }
206
+ get state() { return pageState[this.id]; }
207
+ get element() { return this._wrapperEl; }
208
+ // ═══════════════════════════════════════════════════════════
209
+ // CLICK HANDLING
210
+ // ═══════════════════════════════════════════════════════════
211
+ _makeClickEvent(i, seriesIdx = 0) {
212
+ const v = this._values[i];
213
+ const sk = this._seriesKeys[seriesIdx];
214
+ return {
215
+ chartId: this.id, index: i, seriesKey: sk.key,
216
+ label: v.label || `#${i + 1}`, value: v[sk.key] ?? v.x ?? 0,
217
+ drillKey: v.drill || null, href: v.href || null,
218
+ item: v, orientation: this._opts.orientation,
219
+ };
220
+ }
221
+ _handleBarClick(i, seriesIdx = 0) {
222
+ const detail = this._makeClickEvent(i, seriesIdx);
223
+ if (this._opts.selectable) {
224
+ const prev = this._selectedIndex;
225
+ this._selectedIndex = (this._selectedIndex === i) ? -1 : i;
226
+ this._updateSelection(prev, this._selectedIndex);
227
+ detail.selected = this._selectedIndex === i;
228
+ this._emit('select', detail);
229
+ }
230
+ this._lastClickDetail = detail;
231
+ if (this._opts.onClick)
232
+ this._opts.onClick(detail);
233
+ if (this._onChange)
234
+ this._onChange(detail);
235
+ this._emit('click', detail);
236
+ if (this._opts.stateKey) {
237
+ pageState[this.id][this._opts.stateKey] = detail;
238
+ this._emit('drill', detail);
239
+ }
240
+ this._stateEl.click();
241
+ if (detail.href) {
242
+ this._wrapperEl.dispatchEvent(new CustomEvent('jux:navigate', { bubbles: true, detail }));
243
+ }
244
+ this._wrapperEl.dispatchEvent(new CustomEvent('jux:bar-click', { bubbles: true, detail }));
245
+ }
246
+ _emit(type, detail) {
247
+ (this._listeners[type] || []).forEach(fn => fn(detail));
248
+ }
249
+ _updateSelection(prevIdx, newIdx) {
250
+ if (prevIdx >= 0) {
251
+ const prevEl = document.getElementById(`${this.id}-bar-${prevIdx}`);
252
+ if (prevEl)
253
+ prevEl.classList.remove('jux-bar-selected');
254
+ }
255
+ if (newIdx >= 0) {
256
+ const newEl = document.getElementById(`${this.id}-bar-${newIdx}`);
257
+ if (newEl)
258
+ newEl.classList.add('jux-bar-selected');
259
+ }
260
+ }
261
+ _wireInteractivity(containerEl, barEl, index, seriesIdx = 0) {
262
+ containerEl.classList.add('jux-bar-clickable');
263
+ containerEl.setAttribute('role', 'button');
264
+ containerEl.setAttribute('tabindex', '0');
265
+ const sk = this._seriesKeys[seriesIdx];
266
+ const v = this._values[index];
267
+ const label = v.label || `Item ${index + 1}`;
268
+ const val = v[sk.key] ?? v.x ?? 0;
269
+ containerEl.setAttribute('aria-label', `${label}: ${val}`);
270
+ containerEl.addEventListener('click', () => this._handleBarClick(index, seriesIdx));
271
+ containerEl.addEventListener('keydown', (e) => {
272
+ if (e.key === 'Enter' || e.key === ' ') {
273
+ e.preventDefault();
274
+ this._handleBarClick(index, seriesIdx);
275
+ }
276
+ });
277
+ const color = this._palette[seriesIdx % this._palette.length];
278
+ containerEl.addEventListener('mouseenter', (e) => {
279
+ barEl.style.opacity = '1';
280
+ let html = `<div style="font-weight:600;margin-bottom:2px;">${label}</div>`;
281
+ if (this._seriesKeys.length > 1) {
282
+ this._seriesKeys.forEach((s, si) => {
283
+ const c = this._palette[si % this._palette.length];
284
+ html += formatTooltipRow(s.label || s.key, v[s.key] ?? 0, c);
285
+ });
286
+ }
287
+ else {
288
+ html += formatTooltipRow('Value', val, color);
289
+ }
290
+ showTooltip(e, html);
291
+ });
292
+ containerEl.addEventListener('mousemove', (e) => moveTooltip(e));
293
+ containerEl.addEventListener('mouseleave', () => {
294
+ if (this._selectedIndex !== index)
295
+ barEl.style.opacity = '0.85';
296
+ hideTooltip();
297
+ });
298
+ }
299
+ _buildVertical(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys) {
300
+ const areaEl = mkDiv(`${id}-area`, `display: flex; flex-direction: row; align-items: flex-end; height: ${L.maxBarLength}px;`);
301
+ cardEl.appendChild(areaEl);
302
+ const yaxisEl = mkDiv(`${id}-yaxis`, `width: ${L.yAxisWidth}px; height: ${L.maxBarLength}px; position: relative; flex-shrink: 0;`);
303
+ areaEl.appendChild(yaxisEl);
304
+ for (let t = 0; t <= ticks; t++) {
305
+ const val = Math.round((L.maxVal / ticks) * t);
306
+ const bottom = Math.round((val / L.maxVal) * L.maxBarLength);
307
+ const tickEl = mkDiv(`${id}-ytick-${t}`, `position: absolute; bottom: ${bottom}px; right: 4px; font-size: ${L.tickSize}px; color: ${TOKENS.muted}; line-height: 1; transform: translateY(50%);`);
308
+ tickEl.textContent = formatTick(val);
309
+ yaxisEl.appendChild(tickEl);
310
+ }
311
+ const barsEl = mkDiv(`${id}-bars`, `display: flex; flex-direction: row; align-items: flex-end; gap: ${L.gap}px; flex: 1; height: ${L.maxBarLength}px; border-bottom: 1px solid ${TOKENS.grid}; border-left: 1px solid ${TOKENS.grid}; padding: 0 ${L.gap}px; position: relative;`);
312
+ areaEl.appendChild(barsEl);
313
+ for (let t = 1; t <= ticks; t++) {
314
+ const val = Math.round((L.maxVal / ticks) * t);
315
+ const bottom = Math.round((val / L.maxVal) * L.maxBarLength);
316
+ barsEl.appendChild(mkDiv(`${id}-hgrid-${t}`, `position: absolute; left: 0; right: 0; bottom: ${bottom}px; height: 1px; background: ${TOKENS.grid};`));
317
+ }
318
+ const subBarWidth = numSeries > 1 ? Math.max(6, Math.floor(L.barThickness / numSeries)) : L.barThickness;
319
+ values.forEach((v, i) => {
320
+ const colEl = mkDiv(`${id}-col-${i}`, `display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; min-width: 0; height: 100%; border-radius: 4px; padding: 2px;`);
321
+ barsEl.appendChild(colEl);
322
+ const groupEl = mkDiv(`${id}-grp-${i}`, `display:flex;flex-direction:row;align-items:flex-end;gap:1px;`);
323
+ colEl.appendChild(groupEl);
324
+ seriesKeys.forEach((sk, si) => {
325
+ const val = v[sk.key] ?? 0;
326
+ const color = palette[si % palette.length];
327
+ const barHeight = Math.max(2, Math.round((val / L.maxVal) * L.maxBarLength));
328
+ const delay = animate ? `animation-delay: ${(i * numSeries + si) * (stagger / numSeries)}ms;` : '';
329
+ const barClass = animate ? 'jux-bar-v' : '';
330
+ const barEl = mkDiv(`${id}-bar-${i}-${si}`, `width: ${subBarWidth}px; height: ${barHeight}px; background-color: ${color}; border-radius: ${L.barRadius}px ${L.barRadius}px 0 0; opacity: 0.85; transition: opacity 0.15s ease; ${delay}`, barClass);
331
+ groupEl.appendChild(barEl);
332
+ if (this._isInteractive)
333
+ this._wireInteractivity(barEl, barEl, i, si);
334
+ });
335
+ if (showValues && numSeries === 1) {
336
+ const valDelay = animate ? `animation-delay: ${i * stagger + 200}ms;` : '';
337
+ const valClass = animate ? 'jux-bar-label' : '';
338
+ const valEl = mkDiv(`${id}-val-${i}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted}; margin-bottom: 2px; text-align: center; ${valDelay}`, valClass);
339
+ valEl.textContent = String(v[seriesKeys[0].key] ?? 0);
340
+ colEl.insertBefore(valEl, groupEl);
341
+ }
342
+ });
343
+ const xlabelsEl = mkDiv(`${id}-xlabels`, `display: flex; flex-direction: row; gap: ${L.gap}px; padding-left: ${L.yAxisWidth}px; padding-top: ${Math.round(6 * L.scale)}px;`);
344
+ cardEl.appendChild(xlabelsEl);
345
+ values.forEach((v, i) => {
346
+ const label = v.label || `#${i + 1}`;
347
+ const lblDelay = animate ? `animation-delay: ${i * stagger}ms;` : '';
348
+ const lblClass = animate ? 'jux-bar-label' : '';
349
+ const lblEl = mkDiv(`${id}-lbl-${i}`, `flex: 1; text-align: center; font-size: ${L.tickSize}px; color: ${TOKENS.muted}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 2px; ${lblDelay}`, lblClass);
350
+ lblEl.textContent = label;
351
+ xlabelsEl.appendChild(lblEl);
352
+ });
353
+ }
354
+ _buildHorizontal(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys) {
355
+ const areaEl = mkDiv(`${id}-area`, `display: flex; flex-direction: column; gap: 0; width: 100%;`);
356
+ cardEl.appendChild(areaEl);
357
+ const subBarHeight = numSeries > 1 ? Math.max(6, Math.floor(L.barThickness / numSeries)) : L.barThickness;
358
+ const rowHeight = numSeries > 1 ? subBarHeight * numSeries + (numSeries - 1) : L.barThickness;
359
+ values.forEach((v, i) => {
360
+ const label = v.label || `#${i + 1}`;
361
+ const rowEl = mkDiv(`${id}-row-${i}`, `display: flex; flex-direction: row; align-items: center; min-height: ${rowHeight}px; margin-bottom: ${L.gap}px; border-bottom: 1px solid ${TOKENS.grid}; padding-bottom: ${L.gap}px; border-radius: 4px; padding: 2px;`);
362
+ areaEl.appendChild(rowEl);
363
+ const lblDelay = animate ? `animation-delay: ${i * stagger}ms;` : '';
364
+ const lblClass = animate ? 'jux-bar-label' : '';
365
+ const lblEl = mkDiv(`${id}-lbl-${i}`, `width: ${L.labelWidth}px; flex-shrink: 0; text-align: right; padding-right: ${Math.round(10 * L.scale)}px; font-size: ${L.labelSize}px; color: ${TOKENS.muted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-sizing: border-box; ${lblDelay}`, lblClass);
366
+ lblEl.textContent = label;
367
+ rowEl.appendChild(lblEl);
368
+ const barsCol = mkDiv(`${id}-bcol-${i}`, `display:flex;flex-direction:column;gap:1px;flex:1;`);
369
+ rowEl.appendChild(barsCol);
370
+ seriesKeys.forEach((sk, si) => {
371
+ const val = v[sk.key] ?? 0;
372
+ const barWidth = Math.max(2, Math.round((val / L.maxVal) * L.maxBarLength));
373
+ const color = palette[si % palette.length];
374
+ const delay = animate ? `animation-delay: ${(i * numSeries + si) * (stagger / numSeries)}ms;` : '';
375
+ const barClass = animate ? 'jux-bar-h' : '';
376
+ const barEl = mkDiv(`${id}-bar-${i}-${si}`, `width: ${barWidth}px; height: ${subBarHeight}px; background-color: ${color}; border-radius: ${L.barRadius}px; opacity: 0.85; transition: opacity 0.15s ease; ${delay}`, barClass);
377
+ barsCol.appendChild(barEl);
378
+ if (this._isInteractive)
379
+ this._wireInteractivity(barEl, barEl, i, si);
380
+ });
381
+ if (showValues && numSeries === 1) {
382
+ const valDelay = animate ? `animation-delay: ${i * stagger + 200}ms;` : '';
383
+ const valClass = animate ? 'jux-bar-label' : '';
384
+ const valEl = mkDiv(`${id}-val-${i}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted}; margin-left: ${Math.round(6 * L.scale)}px; ${valDelay}`, valClass);
385
+ valEl.textContent = String(v[seriesKeys[0].key] ?? 0);
386
+ rowEl.appendChild(valEl);
387
+ }
388
+ });
389
+ const xaxisEl = mkDiv(`${id}-xaxis`, `display: flex; flex-direction: row; justify-content: space-between; padding-left: ${L.labelWidth}px; padding-top: ${Math.round(4 * L.scale)}px;`);
390
+ cardEl.appendChild(xaxisEl);
391
+ for (let t = 0; t <= ticks; t++) {
392
+ const val = Math.round((L.maxVal / ticks) * t);
393
+ const tickEl = mkDiv(`${id}-xtick-${t}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted};`);
394
+ tickEl.textContent = formatTick(val);
395
+ xaxisEl.appendChild(tickEl);
396
+ }
397
+ }
398
+ // ═══════════════════════════════════════════════════════════
399
+ // BUILD CHART
400
+ // ═══════════════════════════════════════════════════════════
401
+ _hasTrend() {
402
+ const { trendTitle, trendSubtitle, autoTrend } = this._opts;
403
+ return !!(trendTitle || trendSubtitle || autoTrend);
404
+ }
405
+ _buildChart(resolvedWidth) {
406
+ this._wrapperEl.innerHTML = '';
407
+ this._wrapperEl.appendChild(this._stateEl);
408
+ const { orientation, aspectRatio, title, subtitle, showValues, showLegend, ticks, animate, stagger, maintainAspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride } = this._opts;
409
+ const values = this._values;
410
+ const palette = this._palette;
411
+ const seriesKeys = this._seriesKeys;
412
+ const numSeries = seriesKeys.length;
413
+ const id = this.id;
414
+ // Compute global max across all series
415
+ let globalMax = 0;
416
+ for (const sk of seriesKeys) {
417
+ for (const v of values) {
418
+ const val = v[sk.key] ?? 0;
419
+ if (val > globalMax)
420
+ globalMax = val;
421
+ }
422
+ }
423
+ if (globalMax === 0)
424
+ globalMax = 1;
425
+ const ratio = typeof aspectRatio === 'string' ? (RATIOS[aspectRatio] || 16 / 9) : (aspectRatio || 16 / 9);
426
+ const effectiveWidth = resolvedWidth;
427
+ const prelimL = computeLayout(values, { orientation, width: effectiveWidth, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride });
428
+ const ratioHeight = Math.round(effectiveWidth / ratio);
429
+ const scale = prelimL.scale;
430
+ const labelSize = Math.max(9, Math.round(11 * scale));
431
+ const legendH = showLegend ? Math.round(labelSize * 2) + prelimL.padding : 0;
432
+ const trendH = this._hasTrend() ? Math.round(56 * prelimL.scale) : 0;
433
+ let minContentHeight;
434
+ const headerEstimate = prelimL.padding + Math.round(prelimL.titleSize * 1.4) + Math.round(prelimL.subtitleSize * 1.4);
435
+ if (orientation === 'vertical') {
436
+ const labelRowHeight = Math.round(prelimL.labelSize * 2) + Math.round(6 * prelimL.scale);
437
+ const minBarArea = Math.max(100, values.length * 20);
438
+ minContentHeight = headerEstimate + minBarArea + labelRowHeight + prelimL.padding * 2 + legendH + trendH;
439
+ }
440
+ else {
441
+ const rowHeight = prelimL.barThickness + prelimL.gap + prelimL.gap + 1;
442
+ const tickRowHeight = Math.round(prelimL.tickSize * 2);
443
+ minContentHeight = headerEstimate + values.length * rowHeight + tickRowHeight + prelimL.padding * 2 + legendH + trendH;
444
+ }
445
+ const effectiveHeight = maintainAspectRatio
446
+ ? Math.max(ratioHeight + trendH, minContentHeight)
447
+ : Math.max(this._wrapperEl.clientHeight || ratioHeight + trendH, minContentHeight);
448
+ this._wrapperEl.style.height = `${effectiveHeight}px`;
449
+ const L = computeLayout(values, { orientation, width: effectiveWidth, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride });
450
+ L.cardWidth = effectiveWidth;
451
+ L.cardHeight = effectiveHeight;
452
+ L.maxVal = globalMax;
453
+ const finalHeaderEstimate = L.padding + Math.round(L.titleSize * 1.4) + Math.round(L.subtitleSize * 1.4);
454
+ if (orientation === 'vertical') {
455
+ const labelRowHeight = Math.round(L.labelSize * 2) + Math.round(6 * L.scale);
456
+ L.maxBarLength = Math.max(100, effectiveHeight - finalHeaderEstimate - labelRowHeight - L.padding * 2 - legendH - trendH);
457
+ }
458
+ else {
459
+ L.maxBarLength = effectiveWidth - L.labelWidth - L.padding * 3;
460
+ }
461
+ const cardEl = mkDiv(`${id}-card`, `background: ${TOKENS.bg}; border: 1px solid ${TOKENS.border}; border-radius: ${Math.round(12 * L.scale)}px; padding: ${L.padding}px; font-family: ${FONT_FAMILY}; box-shadow: 0 1px 3px rgba(0,0,0,0.04); width: 100%; height: 100%; box-sizing: border-box;`);
462
+ this._wrapperEl.appendChild(cardEl);
463
+ if (title) {
464
+ const titleEl = mkDiv(`${id}-title`, `font-size: ${L.titleSize}px; font-weight: 600; color: ${TOKENS.title}; letter-spacing: -0.025em; margin-bottom: 2px; line-height: 1.3;`);
465
+ titleEl.textContent = title;
466
+ cardEl.appendChild(titleEl);
467
+ }
468
+ if (subtitle) {
469
+ const subEl = mkDiv(`${id}-sub`, `font-size: ${L.subtitleSize}px; color: ${TOKENS.muted}; margin-bottom: ${L.padding}px; line-height: 1.3;`);
470
+ subEl.textContent = subtitle;
471
+ cardEl.appendChild(subEl);
472
+ }
473
+ if (orientation === 'vertical') {
474
+ this._buildVertical(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys);
475
+ }
476
+ else {
477
+ this._buildHorizontal(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys);
478
+ }
479
+ // Legend
480
+ if (showLegend && numSeries > 1) {
481
+ const legendDiv = document.createElement('div');
482
+ legendDiv.style.cssText = `display:flex;flex-wrap:wrap;gap:${Math.round(8 * L.scale)}px ${Math.round(16 * L.scale)}px;padding-top:${Math.round(8 * L.scale)}px;justify-content:center;`;
483
+ seriesKeys.forEach((sk, i) => {
484
+ const item = document.createElement('div');
485
+ item.style.cssText = `display:flex;align-items:center;gap:4px;font-size:${labelSize}px;color:${TOKENS.muted};`;
486
+ const dot = document.createElement('span');
487
+ dot.style.cssText = `width:${Math.round(10 * L.scale)}px;height:${Math.round(10 * L.scale)}px;border-radius:2px;background:${palette[i % palette.length]};flex-shrink:0;`;
488
+ item.appendChild(dot);
489
+ const lbl = document.createElement('span');
490
+ lbl.textContent = sk.label || sk.key;
491
+ item.appendChild(lbl);
492
+ legendDiv.appendChild(item);
493
+ });
494
+ cardEl.appendChild(legendDiv);
495
+ }
496
+ // Trend footer
497
+ this._renderTrend(cardEl, L.scale, L.padding);
498
+ }
499
+ _renderTrend(card, scale, padding) {
500
+ const { trendTitle, trendSubtitle, trendIcon, autoTrend } = this._opts;
501
+ let title = trendTitle;
502
+ let subtitle = trendSubtitle;
503
+ let icon = trendIcon;
504
+ if (autoTrend && !title) {
505
+ const sk = this._seriesKeys[0];
506
+ const vals = this._values.map(v => v[sk.key] ?? 0);
507
+ const labels = this._values.map(v => v.label || '');
508
+ const trend = computeTrend(vals, labels);
509
+ title = trend.title;
510
+ subtitle = subtitle || trend.subtitle;
511
+ icon = icon || trend.icon;
512
+ }
513
+ if (!title && !subtitle)
514
+ return;
515
+ const fontSize = Math.max(11, Math.round(13 * scale));
516
+ const subFontSize = Math.max(10, Math.round(11 * scale));
517
+ const footer = document.createElement('div');
518
+ footer.style.cssText = `padding-top:${Math.round(8 * scale)}px;border-top:1px solid hsl(220,13%,93%);margin-top:${Math.round(8 * scale)}px;`;
519
+ if (title) {
520
+ const titleEl = document.createElement('div');
521
+ titleEl.style.cssText = `font-size:${fontSize}px;font-weight:500;color:hsl(222,47%,11%);line-height:1.4;`;
522
+ titleEl.textContent = `${title}${icon ? ' ' + icon : ''}`;
523
+ footer.appendChild(titleEl);
524
+ }
525
+ if (subtitle) {
526
+ const subEl = document.createElement('div');
527
+ subEl.style.cssText = `font-size:${subFontSize}px;color:hsl(215,16%,47%);line-height:1.4;margin-top:1px;`;
528
+ subEl.textContent = subtitle;
529
+ footer.appendChild(subEl);
530
+ }
531
+ card.appendChild(footer);
532
+ }
533
+ // ═══════════════════════════════════════════════════════════
534
+ // PUBLIC API
535
+ // ═══════════════════════════════════════════════════════════
536
+ render(target) {
537
+ let parent = null;
538
+ if (target && typeof target === 'object' && 'element' in target) {
539
+ parent = target.element;
540
+ }
541
+ else if (target instanceof HTMLElement) {
542
+ parent = target;
543
+ }
544
+ else if (typeof target === 'string') {
545
+ parent = document.getElementById(target) || document.querySelector(target);
546
+ }
547
+ else {
548
+ parent = document.getElementById('app');
549
+ }
550
+ if (!parent)
551
+ return this;
552
+ parent.appendChild(this._wrapperEl);
553
+ const containerWidth = this._opts.responsive
554
+ ? (this._wrapperEl.clientWidth || parent.clientWidth || this._opts.width || 500)
555
+ : (this._opts.width || 500);
556
+ this._buildChart(containerWidth);
557
+ if (this._opts.responsive && typeof ResizeObserver !== 'undefined') {
558
+ let resizeTimer;
559
+ let skipCount = 2;
560
+ const ro = new ResizeObserver(() => {
561
+ if (skipCount > 0) {
562
+ skipCount--;
563
+ return;
564
+ }
565
+ clearTimeout(resizeTimer);
566
+ resizeTimer = setTimeout(() => {
567
+ const newWidth = this._wrapperEl.clientWidth;
568
+ if (newWidth > 0)
569
+ this._buildChart(newWidth);
570
+ }, 200);
571
+ });
572
+ ro.observe(this._wrapperEl);
573
+ this._resizeObserver = ro;
574
+ }
575
+ return this;
576
+ }
577
+ into(target) { return this.render(target); }
578
+ on(event, fn) {
579
+ if (!this._listeners[event])
580
+ this._listeners[event] = [];
581
+ this._listeners[event].push(fn);
582
+ return this;
583
+ }
584
+ off(event, fn) {
585
+ if (this._listeners[event])
586
+ this._listeners[event] = this._listeners[event].filter(f => f !== fn);
587
+ return this;
588
+ }
589
+ select(index) {
590
+ if (!this._opts.selectable)
591
+ return this;
592
+ const prev = this._selectedIndex;
593
+ this._selectedIndex = index;
594
+ this._updateSelection(prev, index);
595
+ return this;
596
+ }
597
+ clearSelection() {
598
+ const prev = this._selectedIndex;
599
+ this._selectedIndex = -1;
600
+ this._updateSelection(prev, -1);
601
+ return this;
602
+ }
603
+ destroy() {
604
+ if (this._resizeObserver)
605
+ this._resizeObserver.disconnect();
606
+ pageState.__unregister(this.id);
607
+ this._wrapperEl.remove();
608
+ }
609
+ }
610
+ // ═══════════════════════════════════════════════════════════
611
+ // FACTORY
612
+ // ═══════════════════════════════════════════════════════════
613
+ export function barChart(id, values, options = {}) {
614
+ const chart = new BarChart(id, values, options);
615
+ pageState.__register(chart);
616
+ return chart;
617
+ }
618
+ export { BarChart, RATIOS, TOKENS };
619
+ export default barChart;
620
+ function detectSeriesKeys(values, series) {
621
+ if (series && series.length > 0) {
622
+ return series;
623
+ }
624
+ if (values.length === 0) {
625
+ return [{ key: 'x', label: 'Value' }];
626
+ }
627
+ const firstItem = values[0];
628
+ const numericKeys = Object.keys(firstItem).filter(key => key !== 'label' && key !== 'drill' && key !== 'href' && typeof firstItem[key] === 'number');
629
+ if (numericKeys.length === 0) {
630
+ return [{ key: 'x', label: 'Value' }];
631
+ }
632
+ return numericKeys.map(key => ({
633
+ key,
634
+ label: key.charAt(0).toUpperCase() + key.slice(1),
635
+ }));
636
+ }
637
+ const DEFAULT_SERIES_COLORS = [
638
+ 'hsl(217, 91%, 60%)',
639
+ 'hsl(142, 71%, 45%)',
640
+ 'hsl(25, 95%, 53%)',
641
+ 'hsl(271, 81%, 56%)',
642
+ 'hsl(48, 96%, 53%)',
643
+ ];
644
+ //# sourceMappingURL=barChart.js.map