juxscript 1.0.20 → 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 (76) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +143 -92
  3. package/lib/components/badge.ts +93 -94
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +40 -131
  7. package/lib/components/card.ts +57 -79
  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/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
  13. package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
  14. package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
  15. package/lib/components/checkbox.ts +255 -204
  16. package/lib/components/code.ts +31 -78
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +180 -147
  20. package/lib/components/dialog.ts +218 -221
  21. package/lib/components/divider.ts +63 -87
  22. package/lib/components/docs-data.json +498 -2404
  23. package/lib/components/dropdown.ts +191 -236
  24. package/lib/components/element.ts +196 -145
  25. package/lib/components/fileupload.ts +253 -167
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +31 -97
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +51 -114
  30. package/lib/components/icon.ts +33 -120
  31. package/lib/components/icons.ts +2 -1
  32. package/lib/components/include.ts +76 -3
  33. package/lib/components/input.ts +155 -407
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +358 -261
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +63 -152
  38. package/lib/components/modal.ts +42 -129
  39. package/lib/components/nav.ts +79 -101
  40. package/lib/components/paragraph.ts +38 -102
  41. package/lib/components/progress.ts +108 -166
  42. package/lib/components/radio.ts +283 -234
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +189 -199
  45. package/lib/components/sidebar.ts +110 -141
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +254 -183
  48. package/lib/components/table.ts +1078 -208
  49. package/lib/components/tabs.ts +42 -106
  50. package/lib/components/theme-toggle.ts +73 -165
  51. package/lib/components/tooltip.ts +85 -316
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +67 -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 -1
  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 -1128
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1322
  69. package/lib/components/doughnutchart.ts +0 -1259
  70. package/lib/components/footer.ts +0 -165
  71. package/lib/components/header.ts +0 -187
  72. package/lib/components/layout.ts +0 -239
  73. package/lib/components/main.ts +0 -137
  74. package/lib/layouts/default.jux +0 -8
  75. package/lib/layouts/figma.jux +0 -0
  76. /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
+ }