juxscript 1.0.19 → 1.0.21

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 (77) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +212 -165
  3. package/lib/components/badge.ts +93 -103
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +63 -122
  7. package/lib/components/card.ts +109 -155
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/charts/lib/chart-types.ts +159 -0
  13. package/lib/components/charts/lib/chart-utils.ts +160 -0
  14. package/lib/components/charts/lib/chart.ts +707 -0
  15. package/lib/components/checkbox.ts +264 -127
  16. package/lib/components/code.ts +75 -108
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +195 -147
  20. package/lib/components/dialog.ts +187 -157
  21. package/lib/components/divider.ts +85 -191
  22. package/lib/components/docs-data.json +544 -2027
  23. package/lib/components/dropdown.ts +178 -136
  24. package/lib/components/element.ts +227 -171
  25. package/lib/components/fileupload.ts +285 -228
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +46 -69
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +107 -95
  30. package/lib/components/icon.ts +160 -0
  31. package/lib/components/icons.ts +175 -0
  32. package/lib/components/include.ts +153 -5
  33. package/lib/components/input.ts +174 -374
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +378 -240
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +103 -97
  38. package/lib/components/modal.ts +138 -144
  39. package/lib/components/nav.ts +169 -90
  40. package/lib/components/paragraph.ts +49 -150
  41. package/lib/components/progress.ts +118 -200
  42. package/lib/components/radio.ts +297 -149
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +184 -186
  45. package/lib/components/sidebar.ts +152 -140
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +258 -188
  48. package/lib/components/table.ts +1117 -170
  49. package/lib/components/tabs.ts +162 -145
  50. package/lib/components/theme-toggle.ts +108 -169
  51. package/lib/components/tooltip.ts +86 -157
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +86 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -2
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1246
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1250
  69. package/lib/components/chart.ts +0 -127
  70. package/lib/components/doughnutchart.ts +0 -1191
  71. package/lib/components/footer.ts +0 -165
  72. package/lib/components/header.ts +0 -187
  73. package/lib/components/layout.ts +0 -239
  74. package/lib/components/main.ts +0 -137
  75. package/lib/layouts/default.jux +0 -8
  76. package/lib/layouts/figma.jux +0 -0
  77. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -0,0 +1,421 @@
1
+ import { BaseChart, BaseChartState, ChartDataPoint } from './lib/BaseChart.js';
2
+
3
+ export interface BarChartOptions {
4
+ data?: ChartDataPoint[];
5
+ title?: string;
6
+ subtitle?: string;
7
+ xAxisLabel?: string;
8
+ yAxisLabel?: string;
9
+ showTicksX?: boolean;
10
+ showTicksY?: boolean;
11
+ showScaleX?: boolean;
12
+ showScaleY?: boolean;
13
+ scaleXUnit?: string;
14
+ scaleYUnit?: string;
15
+ showLegend?: boolean;
16
+ legendOrientation?: 'horizontal' | 'vertical';
17
+ showDataTable?: boolean;
18
+ showDataLabels?: boolean;
19
+ animate?: boolean;
20
+ animationDuration?: number;
21
+ chartOrientation?: 'vertical' | 'horizontal';
22
+ chartDirection?: 'normal' | 'reverse';
23
+ width?: number;
24
+ height?: number;
25
+ colors?: string[];
26
+ class?: string;
27
+ style?: string;
28
+ theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
29
+ styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
30
+ borderRadius?: number;
31
+ }
32
+
33
+ interface BarChartState extends BaseChartState {
34
+ data: ChartDataPoint[];
35
+ chartOrientation: 'vertical' | 'horizontal';
36
+ chartDirection: 'normal' | 'reverse';
37
+ }
38
+
39
+ export class BarChart extends BaseChart<BarChartState> {
40
+ constructor(id: string, options: BarChartOptions = {}) {
41
+ const defaultColors = [
42
+ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
43
+ '#ec4899', '#06b6d4', '#f97316', '#84cc16', '#6366f1'
44
+ ];
45
+
46
+ super(id, {
47
+ data: options.data ?? [],
48
+ title: options.title ?? '',
49
+ subtitle: options.subtitle ?? '',
50
+ xAxisLabel: options.xAxisLabel ?? '',
51
+ yAxisLabel: options.yAxisLabel ?? '',
52
+ showTicksX: options.showTicksX ?? true,
53
+ showTicksY: options.showTicksY ?? true,
54
+ showScaleX: options.showScaleX ?? true,
55
+ showScaleY: options.showScaleY ?? true,
56
+ scaleXUnit: options.scaleXUnit ?? '',
57
+ scaleYUnit: options.scaleYUnit ?? '',
58
+ showLegend: options.showLegend ?? false,
59
+ legendOrientation: options.legendOrientation ?? 'horizontal',
60
+ showDataTable: options.showDataTable ?? false,
61
+ showDataLabels: options.showDataLabels ?? true,
62
+ animate: options.animate ?? true,
63
+ animationDuration: options.animationDuration ?? 800,
64
+ chartOrientation: options.chartOrientation ?? 'vertical',
65
+ chartDirection: options.chartDirection ?? 'normal',
66
+ width: options.width ?? 600,
67
+ height: options.height ?? 400,
68
+ colors: options.colors ?? defaultColors,
69
+ class: options.class ?? '',
70
+ style: options.style ?? '',
71
+ theme: options.theme,
72
+ styleMode: options.styleMode ?? 'default',
73
+ borderRadius: options.borderRadius ?? 4
74
+ });
75
+ }
76
+
77
+ /* ═════════════════════════════════════════════════════════════════
78
+ * CHART-SPECIFIC FLUENT API
79
+ * ═════════════════════════════════════════════════════════════════ */
80
+
81
+ chartOrientation(value: 'vertical' | 'horizontal'): this {
82
+ this.state.chartOrientation = value;
83
+ if (this.container) this._updateChart();
84
+ return this;
85
+ }
86
+
87
+ chartDirection(value: 'normal' | 'reverse'): this {
88
+ this.state.chartDirection = value;
89
+ if (this.container) this._updateChart();
90
+ return this;
91
+ }
92
+
93
+ /* ═════════════════════════════════════════════════════════════════
94
+ * ABSTRACT METHOD IMPLEMENTATIONS
95
+ * ═════════════════════════════════════════════════════════════════ */
96
+
97
+ protected _getChartClassName(): string {
98
+ return 'jux-barchart';
99
+ }
100
+
101
+ protected _createSVG(): SVGSVGElement {
102
+ const { data, width, height, animate } = this.state;
103
+
104
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
105
+ svg.setAttribute('width', width.toString());
106
+ svg.setAttribute('height', height.toString());
107
+ svg.setAttribute('class', 'jux-barchart-svg');
108
+
109
+ if (!data.length) return svg;
110
+
111
+ if (animate) this._addAnimationStyles(svg);
112
+
113
+ const padding = { top: 40, right: 40, bottom: 60, left: 60 };
114
+ const chartWidth = width - padding.left - padding.right;
115
+ const chartHeight = height - padding.top - padding.bottom;
116
+ const maxValue = Math.max(...data.map(d => d.value));
117
+
118
+ if (this.state.chartOrientation === 'vertical') {
119
+ this._renderVerticalBars(svg, padding, chartWidth, chartHeight, maxValue);
120
+ } else {
121
+ this._renderHorizontalBars(svg, padding, chartWidth, chartHeight, maxValue);
122
+ }
123
+
124
+ return svg;
125
+ }
126
+
127
+ protected _getBaseStyles(): string {
128
+ return `
129
+ .jux-barchart {
130
+ font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
131
+ display: inline-block;
132
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
133
+ border-radius: 12px;
134
+ background: white;
135
+ padding: 24px;
136
+ }
137
+ .jux-barchart-title {
138
+ margin: 0 0 0.5rem 0;
139
+ font-size: 1.25rem;
140
+ font-weight: 600;
141
+ }
142
+ .jux-barchart-subtitle {
143
+ margin: 0 0 1rem 0;
144
+ font-size: 0.875rem;
145
+ color: #6b7280;
146
+ }
147
+ .jux-barchart-legend {
148
+ display: flex;
149
+ flex-wrap: wrap;
150
+ gap: 1rem;
151
+ margin-top: 1rem;
152
+ justify-content: center;
153
+ }
154
+ .jux-barchart-legend-item {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 0.5rem;
158
+ }
159
+ .jux-barchart-legend-swatch {
160
+ width: 12px;
161
+ height: 12px;
162
+ border-radius: 2px;
163
+ }
164
+ .jux-barchart-legend-label {
165
+ font-size: 0.875rem;
166
+ color: #374151;
167
+ }
168
+ .jux-barchart-table {
169
+ width: 100%;
170
+ margin-top: 1rem;
171
+ border-collapse: collapse;
172
+ font-size: 0.875rem;
173
+ }
174
+ .jux-barchart-table thead th {
175
+ text-align: center;
176
+ padding: 0.5rem;
177
+ border-bottom: 2px solid #e5e7eb;
178
+ font-weight: 600;
179
+ }
180
+ .jux-barchart-table tbody td {
181
+ padding: 0.5rem;
182
+ border-bottom: 1px solid #f3f4f6;
183
+ text-align: center;
184
+ }
185
+ .jux-barchart-svg {
186
+ font-family: inherit;
187
+ }
188
+ `;
189
+ }
190
+
191
+ /* ═════════════════════════════════════════════════════════════════
192
+ * PRIVATE RENDERING METHODS
193
+ * ═════════════════════════════════════════════════════════════════ */
194
+
195
+ private _addAnimationStyles(svg: SVGSVGElement): void {
196
+ const { animationDuration, chartOrientation, chartDirection } = this.state;
197
+ const animationId = `bar-grow-${this._id}`;
198
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
199
+
200
+ let transformOrigin = chartOrientation === 'horizontal'
201
+ ? (chartDirection === 'normal' ? 'left' : 'right')
202
+ : (chartDirection === 'reverse' ? 'top' : 'bottom');
203
+
204
+ let scaleAxis = chartOrientation === 'horizontal' ? 'scaleX' : 'scaleY';
205
+
206
+ style.textContent = `
207
+ @keyframes ${animationId} {
208
+ from { transform: ${scaleAxis}(0); opacity: 0; }
209
+ to { transform: ${scaleAxis}(1); opacity: 1; }
210
+ }
211
+ .jux-bar-animated {
212
+ transform-origin: ${transformOrigin};
213
+ animation: ${animationId} ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
214
+ }
215
+ .jux-label-animated {
216
+ opacity: 0;
217
+ animation: fadeIn ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
218
+ }
219
+ @keyframes fadeIn {
220
+ from { opacity: 0; }
221
+ to { opacity: 1; }
222
+ }
223
+ `;
224
+ svg.appendChild(style);
225
+ }
226
+
227
+ private _renderVerticalBars(svg: SVGSVGElement, padding: any, chartWidth: number, chartHeight: number, maxValue: number): void {
228
+ const { data, width, height, colors, xAxisLabel, yAxisLabel, showTicksX, showTicksY, showScaleX, showScaleY, scaleYUnit, chartDirection } = this.state;
229
+ const isReverse = chartDirection === 'reverse';
230
+ const yScale = chartHeight / maxValue;
231
+ const barWidth = chartWidth / data.length;
232
+ const barGap = barWidth * 0.2;
233
+ const actualBarWidth = barWidth - barGap;
234
+
235
+ // Y-axis
236
+ if (showScaleY) {
237
+ this._renderAxis(svg, padding.left, padding.top, padding.left, height - padding.bottom);
238
+ if (yAxisLabel && showTicksY) {
239
+ this._renderAxisLabel(svg, yAxisLabel, 20, padding.top + chartHeight / 2, -90);
240
+ }
241
+ if (showTicksY) {
242
+ this._renderYTicks(svg, maxValue, yScale, padding, width, height, scaleYUnit, isReverse, chartHeight);
243
+ }
244
+ }
245
+
246
+ // X-axis
247
+ if (showScaleX) {
248
+ const axisY = isReverse ? padding.top : height - padding.bottom;
249
+ this._renderAxis(svg, padding.left, axisY, width - padding.right, axisY);
250
+ if (xAxisLabel && showTicksX) {
251
+ this._renderAxisLabel(svg, xAxisLabel, padding.left + chartWidth / 2, height - 15, 0);
252
+ }
253
+ }
254
+
255
+ // Bars
256
+ data.forEach((point, index) => {
257
+ const x = padding.left + (index * barWidth) + (barGap / 2);
258
+ const barHeight = point.value * yScale;
259
+ const y = isReverse ? padding.top : height - padding.bottom - barHeight;
260
+ const color = point.color || colors[index % colors.length];
261
+
262
+ this._renderBar(svg, x, y, actualBarWidth, barHeight, color, index, point, false);
263
+
264
+ // Category label
265
+ if (showTicksX && showScaleX) {
266
+ this._renderCategoryLabel(svg, point.label, x + actualBarWidth / 2, height - padding.bottom + 20, index);
267
+ }
268
+ });
269
+ }
270
+
271
+ private _renderHorizontalBars(svg: SVGSVGElement, padding: any, chartWidth: number, chartHeight: number, maxValue: number): void {
272
+ const { data, width, height, colors, showScaleY, showTicksY, chartDirection } = this.state;
273
+ const isReverse = chartDirection === 'reverse';
274
+ const xScale = chartWidth / maxValue;
275
+ const barHeight = chartHeight / data.length;
276
+ const barGap = barHeight * 0.2;
277
+ const actualBarHeight = barHeight - barGap;
278
+
279
+ // Bars
280
+ data.forEach((point, index) => {
281
+ const y = padding.top + (index * barHeight) + (barGap / 2);
282
+ const barWidth = point.value * xScale;
283
+ const x = isReverse ? width - padding.right - barWidth : padding.left;
284
+ const color = point.color || colors[index % colors.length];
285
+
286
+ this._renderBar(svg, x, y, barWidth, actualBarHeight, color, index, point, true);
287
+
288
+ // Category label
289
+ if (showTicksY && showScaleY) {
290
+ this._renderCategoryLabel(svg, point.label, padding.left - 10, y + actualBarHeight / 2 + 4, index);
291
+ }
292
+ });
293
+ }
294
+
295
+ private _renderAxis(svg: SVGSVGElement, x1: number, y1: number, x2: number, y2: number): void {
296
+ const axis = document.createElementNS('http://www.w3.org/2000/svg', 'line');
297
+ axis.setAttribute('x1', x1.toString());
298
+ axis.setAttribute('y1', y1.toString());
299
+ axis.setAttribute('x2', x2.toString());
300
+ axis.setAttribute('y2', y2.toString());
301
+ axis.setAttribute('stroke', '#9ca3af');
302
+ axis.setAttribute('stroke-width', '2');
303
+ svg.appendChild(axis);
304
+ }
305
+
306
+ private _renderAxisLabel(svg: SVGSVGElement, text: string, x: number, y: number, rotate: number): void {
307
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
308
+ label.setAttribute('x', x.toString());
309
+ label.setAttribute('y', y.toString());
310
+ label.setAttribute('text-anchor', 'middle');
311
+ if (rotate !== 0) label.setAttribute('transform', `rotate(${rotate}, ${x}, ${y})`);
312
+ label.setAttribute('fill', '#6b7280');
313
+ label.setAttribute('font-size', '12');
314
+ label.setAttribute('font-weight', '500');
315
+ label.textContent = text;
316
+ svg.appendChild(label);
317
+ }
318
+
319
+ private _renderYTicks(svg: SVGSVGElement, maxValue: number, yScale: number, padding: any, width: number, height: number, unit: string, isReverse: boolean, chartHeight: number): void {
320
+ const numTicks = 5;
321
+ for (let i = 0; i <= numTicks; i++) {
322
+ const value = (maxValue / numTicks) * i;
323
+ const y = isReverse
324
+ ? padding.top + (value * yScale)
325
+ : height - padding.bottom - (value * yScale);
326
+
327
+ // Grid line
328
+ const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
329
+ gridLine.setAttribute('x1', padding.left.toString());
330
+ gridLine.setAttribute('y1', y.toString());
331
+ gridLine.setAttribute('x2', (width - padding.right).toString());
332
+ gridLine.setAttribute('y2', y.toString());
333
+ gridLine.setAttribute('stroke', '#e5e7eb');
334
+ gridLine.setAttribute('stroke-width', '1');
335
+ gridLine.setAttribute('stroke-dasharray', '4,4');
336
+ svg.appendChild(gridLine);
337
+
338
+ // Tick label
339
+ const tickLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
340
+ tickLabel.setAttribute('x', (padding.left - 10).toString());
341
+ tickLabel.setAttribute('y', (y + 4).toString());
342
+ tickLabel.setAttribute('text-anchor', 'end');
343
+ tickLabel.setAttribute('fill', '#6b7280');
344
+ tickLabel.setAttribute('font-size', '11');
345
+ tickLabel.textContent = Math.round(value).toString() + (unit || '');
346
+ svg.appendChild(tickLabel);
347
+ }
348
+ }
349
+
350
+ private _renderCategoryLabel(svg: SVGSVGElement, text: string, x: number, y: number, index: number): void {
351
+ const { animate } = this.state;
352
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
353
+ label.setAttribute('x', x.toString());
354
+ label.setAttribute('y', y.toString());
355
+ label.setAttribute('text-anchor', 'middle');
356
+ label.setAttribute('fill', '#6b7280');
357
+ label.setAttribute('font-size', '11');
358
+ label.textContent = text;
359
+
360
+ if (animate) {
361
+ label.classList.add('jux-label-animated');
362
+ label.style.animationDelay = `${index * 100 + 200}ms`;
363
+ }
364
+
365
+ svg.appendChild(label);
366
+ }
367
+
368
+ private _renderBar(svg: SVGSVGElement, x: number, y: number, width: number, height: number, color: string, index: number, point: ChartDataPoint, isHorizontal: boolean = false): void {
369
+ const { borderRadius, showDataLabels, animate, animationDuration } = this.state;
370
+
371
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
372
+ rect.setAttribute('x', x.toString());
373
+ rect.setAttribute('y', y.toString());
374
+ rect.setAttribute('width', width.toString());
375
+ rect.setAttribute('height', height.toString());
376
+ rect.setAttribute('rx', borderRadius.toString());
377
+ rect.setAttribute('ry', borderRadius.toString());
378
+ rect.setAttribute('fill', color);
379
+
380
+ if (animate) {
381
+ rect.classList.add('jux-bar-animated');
382
+ rect.style.animationDelay = `${index * 100}ms`;
383
+ }
384
+
385
+ svg.appendChild(rect);
386
+
387
+ // Value labels
388
+ if (showDataLabels) {
389
+ const valueLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
390
+
391
+ if (isHorizontal) {
392
+ valueLabel.setAttribute('x', (x + width / 2).toString());
393
+ valueLabel.setAttribute('y', (y + height / 2 + 4).toString());
394
+ valueLabel.setAttribute('fill', '#ffffff');
395
+ } else {
396
+ valueLabel.setAttribute('x', (x + width / 2).toString());
397
+ valueLabel.setAttribute('y', (y - 5).toString());
398
+ valueLabel.setAttribute('fill', '#374151');
399
+ }
400
+
401
+ valueLabel.setAttribute('text-anchor', 'middle');
402
+ valueLabel.setAttribute('font-weight', '600');
403
+ valueLabel.setAttribute('font-size', '11');
404
+ valueLabel.textContent = point.value.toString();
405
+
406
+ if (animate) {
407
+ valueLabel.classList.add('jux-label-animated');
408
+ valueLabel.style.animationDelay = `${index * 100 + animationDuration - 200}ms`;
409
+ }
410
+
411
+ svg.appendChild(valueLabel);
412
+ }
413
+ }
414
+ }
415
+
416
+ export function barchart(id: string, options: BarChartOptions = {}): BarChart {
417
+ return new BarChart(id, options);
418
+ }
419
+
420
+ // Re-export the data point type for convenience
421
+ export type BarChartDataPoint = ChartDataPoint;
@@ -0,0 +1,263 @@
1
+ import { BaseChart, BaseChartState, ChartDataPoint } from './lib/BaseChart.js';
2
+
3
+ export interface DoughnutChartOptions {
4
+ data?: ChartDataPoint[];
5
+ title?: string;
6
+ subtitle?: string;
7
+ showLegend?: boolean;
8
+ legendOrientation?: 'horizontal' | 'vertical';
9
+ showDataTable?: boolean;
10
+ showDataLabels?: boolean;
11
+ animate?: boolean;
12
+ animationDuration?: number;
13
+ width?: number;
14
+ height?: number;
15
+ colors?: string[];
16
+ class?: string;
17
+ style?: string;
18
+ theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
19
+ styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
20
+ borderRadius?: number;
21
+ }
22
+
23
+ interface DoughnutChartState extends BaseChartState {
24
+ data: ChartDataPoint[];
25
+ }
26
+
27
+ export class DoughnutChart extends BaseChart<DoughnutChartState> {
28
+ constructor(id: string, options: DoughnutChartOptions = {}) {
29
+ const defaultColors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];
30
+
31
+ super(id, {
32
+ data: options.data ?? [],
33
+ title: options.title ?? '',
34
+ subtitle: options.subtitle ?? '',
35
+ xAxisLabel: '',
36
+ yAxisLabel: '',
37
+ showTicksX: false,
38
+ showTicksY: false,
39
+ showScaleX: false,
40
+ showScaleY: false,
41
+ scaleXUnit: '',
42
+ scaleYUnit: '',
43
+ showLegend: options.showLegend ?? false,
44
+ legendOrientation: options.legendOrientation ?? 'horizontal',
45
+ showDataTable: options.showDataTable ?? false,
46
+ showDataLabels: options.showDataLabels ?? true,
47
+ animate: options.animate ?? true,
48
+ animationDuration: options.animationDuration ?? 800,
49
+ width: options.width ?? 600,
50
+ height: options.height ?? 400,
51
+ colors: options.colors ?? defaultColors,
52
+ class: options.class ?? '',
53
+ style: options.style ?? '',
54
+ theme: options.theme,
55
+ styleMode: options.styleMode ?? 'default',
56
+ borderRadius: options.borderRadius ?? 4
57
+ });
58
+ }
59
+
60
+ protected _getChartClassName(): string {
61
+ return 'jux-doughnutchart';
62
+ }
63
+
64
+ protected _createSVG(): SVGSVGElement {
65
+ const { data, width, height, colors, showDataLabels, animate } = this.state;
66
+
67
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
68
+ svg.setAttribute('width', width.toString());
69
+ svg.setAttribute('height', height.toString());
70
+ svg.setAttribute('class', 'jux-doughnutchart-svg');
71
+
72
+ if (!data.length) return svg;
73
+
74
+ if (animate) this._addAnimationStyles(svg);
75
+
76
+ const centerX = width / 2;
77
+ const centerY = height / 2;
78
+ const margin = showDataLabels ? 100 : 40;
79
+ const radius = Math.min(width, height) / 2 - margin;
80
+ const innerRadius = radius * 0.6;
81
+ const total = data.reduce((sum, point) => sum + point.value, 0);
82
+
83
+ this._renderDoughnutSlices(svg, centerX, centerY, radius, innerRadius, total);
84
+
85
+ return svg;
86
+ }
87
+
88
+ protected _getBaseStyles(): string {
89
+ return `
90
+ .jux-doughnutchart {
91
+ font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
92
+ display: inline-block;
93
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
94
+ border-radius: 12px;
95
+ background: white;
96
+ padding: 24px;
97
+ }
98
+ .jux-doughnutchart-title {
99
+ margin: 0 0 0.5rem 0;
100
+ font-size: 1.25rem;
101
+ font-weight: 600;
102
+ }
103
+ .jux-doughnutchart-subtitle {
104
+ margin: 0 0 1rem 0;
105
+ font-size: 0.875rem;
106
+ color: #6b7280;
107
+ }
108
+ .jux-doughnutchart-legend {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ gap: 1rem;
112
+ margin-top: 1rem;
113
+ justify-content: center;
114
+ }
115
+ .jux-doughnutchart-legend-item {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 0.5rem;
119
+ }
120
+ .jux-doughnutchart-legend-swatch {
121
+ width: 12px;
122
+ height: 12px;
123
+ border-radius: 2px;
124
+ }
125
+ .jux-doughnutchart-legend-label {
126
+ font-size: 0.875rem;
127
+ color: #374151;
128
+ }
129
+ .jux-doughnutchart-table {
130
+ width: 100%;
131
+ margin-top: 1.5rem;
132
+ border-collapse: collapse;
133
+ font-size: 0.875rem;
134
+ border: 1px solid #e5e7eb;
135
+ border-radius: 8px;
136
+ overflow: hidden;
137
+ }
138
+ .jux-doughnutchart-table thead {
139
+ background: #f9fafb;
140
+ }
141
+ .jux-doughnutchart-table thead th {
142
+ text-align: center;
143
+ padding: 12px 16px;
144
+ border-bottom: 2px solid #e5e7eb;
145
+ font-weight: 600;
146
+ }
147
+ .jux-doughnutchart-table tbody td {
148
+ padding: 12px 16px;
149
+ border-bottom: 1px solid #f3f4f6;
150
+ text-align: center;
151
+ }
152
+ .jux-doughnutchart-svg {
153
+ font-family: inherit;
154
+ }
155
+ `;
156
+ }
157
+
158
+ private _addAnimationStyles(svg: SVGSVGElement): void {
159
+ const { animationDuration } = this.state;
160
+ const animationId = `slice-scale-${this._id}`;
161
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
162
+
163
+ style.textContent = `
164
+ @keyframes ${animationId} {
165
+ from { transform: scale(0); opacity: 0; }
166
+ to { transform: scale(1); opacity: 1; }
167
+ }
168
+ .jux-slice-animated {
169
+ transform-origin: ${this.state.width / 2}px ${this.state.height / 2}px;
170
+ animation: ${animationId} ${animationDuration}ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
171
+ opacity: 0;
172
+ }
173
+ `;
174
+ svg.appendChild(style);
175
+ }
176
+
177
+ private _renderDoughnutSlices(svg: SVGSVGElement, centerX: number, centerY: number, radius: number, innerRadius: number, total: number): void {
178
+ const { data, colors, animate, showDataLabels } = this.state;
179
+ let currentAngle = -90;
180
+
181
+ data.forEach((point, index) => {
182
+ const color = point.color || colors[index % colors.length];
183
+ const sliceAngle = (point.value / total) * 360;
184
+ const path = this._createDoughnutSlice(centerX, centerY, radius, innerRadius, currentAngle, currentAngle + sliceAngle);
185
+
186
+ const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
187
+ pathEl.setAttribute('d', path);
188
+ pathEl.setAttribute('fill', color);
189
+ pathEl.setAttribute('stroke', 'white');
190
+ pathEl.setAttribute('stroke-width', '2');
191
+
192
+ if (animate) {
193
+ pathEl.classList.add('jux-slice-animated');
194
+ pathEl.style.animationDelay = `${index * 150}ms`;
195
+ }
196
+
197
+ svg.appendChild(pathEl);
198
+
199
+ // ✅ Add data labels if enabled
200
+ if (showDataLabels) {
201
+ const midAngle = currentAngle + sliceAngle / 2;
202
+ const labelRadius = (radius + innerRadius) / 2;
203
+ const labelX = centerX + labelRadius * Math.cos((midAngle * Math.PI) / 180);
204
+ const labelY = centerY + labelRadius * Math.sin((midAngle * Math.PI) / 180);
205
+
206
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
207
+ label.setAttribute('x', labelX.toString());
208
+ label.setAttribute('y', labelY.toString());
209
+ label.setAttribute('text-anchor', 'middle');
210
+ label.setAttribute('dominant-baseline', 'middle');
211
+ label.setAttribute('fill', '#ffffff');
212
+ label.setAttribute('font-weight', '600');
213
+ label.setAttribute('font-size', '14');
214
+ label.textContent = point.value.toString();
215
+
216
+ if (animate) {
217
+ label.style.opacity = '0';
218
+ label.style.animation = `fadeIn 400ms ease-out ${index * 150 + 400}ms forwards`;
219
+ }
220
+
221
+ svg.appendChild(label);
222
+ }
223
+
224
+ currentAngle += sliceAngle;
225
+ });
226
+
227
+ // Center circle
228
+ const centerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
229
+ centerCircle.setAttribute('cx', centerX.toString());
230
+ centerCircle.setAttribute('cy', centerY.toString());
231
+ centerCircle.setAttribute('r', innerRadius.toString());
232
+ centerCircle.setAttribute('fill', 'white');
233
+ svg.appendChild(centerCircle);
234
+ }
235
+
236
+ private _createDoughnutSlice(centerX: number, centerY: number, outerRadius: number, innerRadius: number, startAngle: number, endAngle: number): string {
237
+ const startRad = (startAngle * Math.PI) / 180;
238
+ const endRad = (endAngle * Math.PI) / 180;
239
+
240
+ const x1 = centerX + outerRadius * Math.cos(startRad);
241
+ const y1 = centerY + outerRadius * Math.sin(startRad);
242
+ const x2 = centerX + outerRadius * Math.cos(endRad);
243
+ const y2 = centerY + outerRadius * Math.sin(endRad);
244
+ const x3 = centerX + innerRadius * Math.cos(endRad);
245
+ const y3 = centerY + innerRadius * Math.sin(endRad);
246
+ const x4 = centerX + innerRadius * Math.cos(startRad);
247
+ const y4 = centerY + innerRadius * Math.sin(startRad);
248
+
249
+ const largeArcFlag = endAngle - startAngle > 180 ? 1 : 0;
250
+
251
+ return [
252
+ `M ${x1} ${y1}`,
253
+ `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
254
+ `L ${x3} ${y3}`,
255
+ `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x4} ${y4}`,
256
+ 'Z'
257
+ ].join(' ');
258
+ }
259
+ }
260
+
261
+ export function doughnutchart(id: string, options: DoughnutChartOptions = {}): DoughnutChart {
262
+ return new DoughnutChart(id, options);
263
+ }