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,707 @@
1
+ import { getOrCreateContainer } from '../../helpers.js';
2
+ import { State } from '../../../reactivity/state.js';
3
+
4
+ /**
5
+ * Chart component using Chart.js library
6
+ *
7
+ * DEPENDENCY: Requires Chart.js to be loaded
8
+ * Add to your HTML: <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ * Or install: npm install chart.js
10
+ */
11
+
12
+ export interface ChartOptions {
13
+ type?: string;
14
+ data?: any;
15
+ options?: any;
16
+ width?: number;
17
+ height?: number;
18
+ title?: string;
19
+ subtitle?: string;
20
+ xAxisLabel?: string;
21
+ yAxisLabel?: string;
22
+ style?: string;
23
+ class?: string;
24
+ }
25
+
26
+ type ChartState = {
27
+ type: string;
28
+ data: any;
29
+ options: any;
30
+ width: number;
31
+ height: number;
32
+ title: string;
33
+ subtitle: string;
34
+ xAxisLabel: string;
35
+ yAxisLabel: string;
36
+ style: string;
37
+ class: string;
38
+ };
39
+
40
+ export class Chart {
41
+ state: ChartState;
42
+ container: HTMLElement | null = null;
43
+ _id: string;
44
+ id: string;
45
+ chartInstance: any = null;
46
+
47
+ // CRITICAL: Store bind/sync instructions for deferred wiring
48
+ private _bindings: Array<{ event: string, handler: Function }> = [];
49
+ private _syncBindings: Array<{
50
+ property: string,
51
+ stateObj: State<any>,
52
+ toState?: Function,
53
+ toComponent?: Function
54
+ }> = [];
55
+
56
+ private _chartJsCheckAttempts = 0;
57
+ private _maxCheckAttempts = 50; // 5 seconds with 100ms intervals
58
+ private static _dataLabelsRegistered = false; // Track if plugin is registered
59
+
60
+ constructor(id: string, options: ChartOptions = {}) {
61
+ this._id = id;
62
+ this.id = id;
63
+
64
+ this.state = {
65
+ type: options.type ?? 'line',
66
+ data: options.data ?? { labels: [], datasets: [] },
67
+ options: options.options ?? {},
68
+ width: options.width ?? 400,
69
+ height: options.height ?? 300,
70
+ title: options.title ?? '',
71
+ subtitle: options.subtitle ?? '',
72
+ xAxisLabel: options.xAxisLabel ?? '',
73
+ yAxisLabel: options.yAxisLabel ?? '',
74
+ style: options.style ?? '',
75
+ class: options.class ?? ''
76
+ };
77
+ }
78
+
79
+ /* -------------------------
80
+ * Fluent API
81
+ * ------------------------- */
82
+
83
+ type(value: string): this {
84
+ this.state.type = value;
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Change chart type dynamically (recreates chart)
90
+ */
91
+ changeType(value: string): this {
92
+ this.state.type = value;
93
+
94
+ if (this.chartInstance) {
95
+ // Properly destroy existing chart and clear canvas
96
+ this.chartInstance.destroy();
97
+ this.chartInstance = null;
98
+
99
+ // Clear canvas completely
100
+ const canvas = document.getElementById(`${this._id}-canvas`) as HTMLCanvasElement;
101
+ if (canvas) {
102
+ const ctx = canvas.getContext('2d');
103
+ if (ctx) {
104
+ // Clear the entire canvas
105
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
106
+
107
+ // Reset canvas dimensions to force complete redraw
108
+ const { width, height } = this.state;
109
+ canvas.width = width;
110
+ canvas.height = height;
111
+
112
+ try {
113
+ // Apply pie/doughnut specific colors if needed
114
+ const chartData = this._preparePieChartColors(value, this.state.data);
115
+
116
+ this.chartInstance = new (window as any).Chart(ctx, {
117
+ type: value,
118
+ data: chartData,
119
+ options: this.state.options
120
+ });
121
+ console.log(`✓ Chart "${this._id}" type changed to "${value}"`);
122
+ } catch (error) {
123
+ console.error(`Failed to change chart type to "${value}":`, error);
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Apply unique colors to each slice for pie/doughnut/polar charts
134
+ */
135
+ private _preparePieChartColors(type: string, data: any): any {
136
+ const isPieType = ['pie', 'doughnut', 'polarArea', 'radar'].includes(type);
137
+
138
+ if (!isPieType || !data || !data.datasets) {
139
+ return data;
140
+ }
141
+
142
+ // Clone data to avoid mutating original
143
+ const clonedData = JSON.parse(JSON.stringify(data));
144
+
145
+ // Get theme colors
146
+ const themeColors = this._getThemeColors();
147
+
148
+ clonedData.datasets.forEach((dataset: any) => {
149
+ if (!dataset.backgroundColor || typeof dataset.backgroundColor === 'string') {
150
+ // Generate colors for each data point
151
+ const colors = dataset.data.map((_: any, index: number) => {
152
+ return themeColors[index % themeColors.length];
153
+ });
154
+
155
+ dataset.backgroundColor = colors;
156
+ dataset.borderColor = colors.map((color: string) => color);
157
+ dataset.borderWidth = 2;
158
+ }
159
+ });
160
+
161
+ return clonedData;
162
+ }
163
+
164
+ /**
165
+ * Get current theme colors
166
+ */
167
+ private _getThemeColors(): string[] {
168
+ const themeValue = this.state.options.theme || 'google';
169
+
170
+ // Default color sets by theme
171
+ const themeColorSets: { [key: string]: string[] } = {
172
+ google: ['#4285F4', '#EA4335', '#FBBC04', '#34A853', '#FF6D01', '#46BDC6'],
173
+ seriesa: ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe', '#43e97b'],
174
+ hr: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'],
175
+ figma: ['#0ACF83', '#FF7262', '#1ABCFE', '#A259FF', '#F24E1E', '#FFC700'],
176
+ notion: ['#2EAADC', '#9B59B6', '#E67E22', '#E74C3C', '#F39C12', '#16A085'],
177
+ chalk: ['#96CEB4', '#FFEAA7', '#DFE6E9', '#74B9FF', '#FD79A8', '#A29BFE'],
178
+ mint: ['#26de81', '#20bf6b', '#0fb9b1', '#2bcbba', '#45aaf2', '#4b7bec']
179
+ };
180
+
181
+ return themeColorSets[themeValue] || themeColorSets.google;
182
+ }
183
+
184
+ data(value: any): this {
185
+ this.state.data = value;
186
+ this._updateChart();
187
+ return this;
188
+ }
189
+
190
+ options(value: any): this {
191
+ this.state.options = value;
192
+ this._updateChart();
193
+ return this;
194
+ }
195
+
196
+ width(value: number): this {
197
+ this.state.width = value;
198
+ return this;
199
+ }
200
+
201
+ height(value: number): this {
202
+ this.state.height = value;
203
+ return this;
204
+ }
205
+
206
+ style(value: string): this {
207
+ this.state.style = value;
208
+ return this;
209
+ }
210
+
211
+ class(value: string): this {
212
+ this.state.class = value;
213
+ return this;
214
+ }
215
+
216
+ title(value: string): this {
217
+ this.state.title = value;
218
+ return this;
219
+ }
220
+
221
+ subtitle(value: string): this {
222
+ this.state.subtitle = value;
223
+ return this;
224
+ }
225
+
226
+ xAxisLabel(value: string): this {
227
+ this.state.xAxisLabel = value;
228
+ return this;
229
+ }
230
+
231
+ yAxisLabel(value: string): this {
232
+ this.state.yAxisLabel = value;
233
+ return this;
234
+ }
235
+
236
+ /* -------------------------
237
+ * Fluent API - Chart.js Configuration
238
+ * ------------------------- */
239
+
240
+ // Chart.js options configuration methods
241
+ theme(value: string): this {
242
+ // Import theme colors and apply to chart
243
+ import('./charts.js').then(themes => {
244
+ const themeConfig = themes.chartThemes[value as keyof typeof themes.chartThemes];
245
+
246
+ if (!themeConfig) return;
247
+
248
+ // Apply theme colors to datasets
249
+ if (this.chartInstance && this.chartInstance.data.datasets) {
250
+ this.chartInstance.data.datasets.forEach((dataset: any, index: number) => {
251
+ const colorIndex = index % themeConfig.colors.length;
252
+ dataset.borderColor = themeConfig.colors[colorIndex];
253
+ dataset.backgroundColor = themeConfig.colors[colorIndex] + '33'; // 20% opacity
254
+ });
255
+ this.chartInstance.update();
256
+ }
257
+
258
+ // Store theme in state
259
+ this.state.options.theme = value;
260
+ });
261
+
262
+ return this;
263
+ }
264
+
265
+ styleMode(value: string): this {
266
+ // Style mode affects line tension and stepped property
267
+ if (!this.state.options.elements) this.state.options.elements = {};
268
+ if (!this.state.options.elements.line) this.state.options.elements.line = {};
269
+
270
+ // Reset both properties first
271
+ delete this.state.options.elements.line.stepped;
272
+ delete this.state.options.elements.line.tension;
273
+
274
+ switch (value) {
275
+ case 'smooth':
276
+ this.state.options.elements.line.tension = 0.4;
277
+ break;
278
+ case 'stepped':
279
+ this.state.options.elements.line.stepped = true;
280
+ this.state.options.elements.line.tension = 0;
281
+ break;
282
+ case 'linear': // Changed from 'straight' to avoid reserved word issues
283
+ this.state.options.elements.line.tension = 0;
284
+ break;
285
+ }
286
+
287
+ // Apply to dataset if chart is already rendered
288
+ if (this.chartInstance && this.chartInstance.data.datasets) {
289
+ this.chartInstance.data.datasets.forEach((dataset: any) => {
290
+ if (value === 'smooth') {
291
+ dataset.tension = 0.4;
292
+ delete dataset.stepped;
293
+ } else if (value === 'stepped') {
294
+ dataset.stepped = true;
295
+ dataset.tension = 0;
296
+ } else if (value === 'linear') {
297
+ dataset.tension = 0;
298
+ delete dataset.stepped;
299
+ }
300
+ });
301
+ }
302
+
303
+ this._updateChart();
304
+ return this;
305
+ }
306
+
307
+ borderRadius(value: number): this {
308
+ if (!this.state.options.elements) this.state.options.elements = {};
309
+ if (!this.state.options.elements.bar) this.state.options.elements.bar = {};
310
+ this.state.options.elements.bar.borderRadius = value;
311
+ this._updateChart();
312
+ return this;
313
+ }
314
+
315
+ showTicksX(value: boolean): this {
316
+ if (!this.state.options.scales) this.state.options.scales = {};
317
+ if (!this.state.options.scales.x) this.state.options.scales.x = {};
318
+ if (!this.state.options.scales.x.ticks) this.state.options.scales.x.ticks = {};
319
+ this.state.options.scales.x.ticks.display = value;
320
+ this._updateChart();
321
+ return this;
322
+ }
323
+
324
+ showTicksY(value: boolean): this {
325
+ if (!this.state.options.scales) this.state.options.scales = {};
326
+ if (!this.state.options.scales.y) this.state.options.scales.y = {};
327
+ if (!this.state.options.scales.y.ticks) this.state.options.scales.y.ticks = {};
328
+ this.state.options.scales.y.ticks.display = value;
329
+ this._updateChart();
330
+ return this;
331
+ }
332
+
333
+ showDataLabels(value: boolean): this {
334
+ if (!this.state.options.plugins) this.state.options.plugins = {};
335
+
336
+ if (value) {
337
+ // Enable datalabels plugin with proper configuration
338
+ this.state.options.plugins.datalabels = {
339
+ display: true,
340
+ color: '#fff',
341
+ backgroundColor: function (context: any) {
342
+ // Use dataset color or default
343
+ return context.dataset.backgroundColor || 'rgba(0, 0, 0, 0.7)';
344
+ },
345
+ borderRadius: 4,
346
+ padding: 6,
347
+ font: {
348
+ weight: 'bold',
349
+ size: 11
350
+ },
351
+ formatter: (value: any, context: any) => {
352
+ // Format numbers with commas
353
+ if (typeof value === 'number') {
354
+ return value.toLocaleString();
355
+ }
356
+ return value;
357
+ },
358
+ anchor: 'end',
359
+ align: 'end',
360
+ offset: 4
361
+ };
362
+ } else {
363
+ // Disable datalabels
364
+ this.state.options.plugins.datalabels = {
365
+ display: false
366
+ };
367
+ }
368
+
369
+ this._updateChart();
370
+ return this;
371
+ }
372
+
373
+ showLegend(value: boolean): this {
374
+ if (!this.state.options.plugins) this.state.options.plugins = {};
375
+ if (!this.state.options.plugins.legend) this.state.options.plugins.legend = {};
376
+ this.state.options.plugins.legend.display = value;
377
+ this._updateChart();
378
+ return this;
379
+ }
380
+
381
+ showDataTable(value: boolean): this {
382
+ // Store flag for custom rendering (not a Chart.js feature)
383
+ if (!this.state.options.custom) this.state.options.custom = {};
384
+ this.state.options.custom.showDataTable = value;
385
+ return this;
386
+ }
387
+
388
+ legendOrientation(value: string): this {
389
+ if (!this.state.options.plugins) this.state.options.plugins = {};
390
+ if (!this.state.options.plugins.legend) this.state.options.plugins.legend = {};
391
+ this.state.options.plugins.legend.position = value === 'horizontal' ? 'top' : 'right';
392
+ this._updateChart();
393
+ return this;
394
+ }
395
+
396
+ animate(value: boolean): this {
397
+ if (!this.state.options.animation) this.state.options.animation = {};
398
+ this.state.options.animation.duration = value ? 800 : 0;
399
+ this._updateChart();
400
+ return this;
401
+ }
402
+
403
+ animationDuration(value: number): this {
404
+ if (!this.state.options.animation) this.state.options.animation = {};
405
+ this.state.options.animation.duration = value;
406
+ this._updateChart();
407
+ return this;
408
+ }
409
+
410
+ chartOrientation(value: string): this {
411
+ if (!this.state.options.indexAxis) {
412
+ this.state.options.indexAxis = value === 'horizontal' ? 'y' : 'x';
413
+ this._updateChart();
414
+ }
415
+ return this;
416
+ }
417
+
418
+ chartDirection(value: string): this {
419
+ if (!this.state.options.scales) this.state.options.scales = {};
420
+ if (value === 'reverse') {
421
+ if (!this.state.options.scales.x) this.state.options.scales.x = {};
422
+ if (!this.state.options.scales.y) this.state.options.scales.y = {};
423
+ this.state.options.scales.x.reverse = true;
424
+ this.state.options.scales.y.reverse = true;
425
+ } else {
426
+ if (this.state.options.scales.x) this.state.options.scales.x.reverse = false;
427
+ if (this.state.options.scales.y) this.state.options.scales.y.reverse = false;
428
+ }
429
+ this._updateChart();
430
+ return this;
431
+ }
432
+
433
+ bind(event: string, handler: Function): this {
434
+ this._bindings.push({ event, handler });
435
+ return this;
436
+ }
437
+
438
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
439
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
440
+ throw new Error(`Chart.sync: Expected a State object for property "${property}"`);
441
+ }
442
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
443
+ return this;
444
+ }
445
+
446
+ /* -------------------------
447
+ * Helpers
448
+ * ------------------------- */
449
+
450
+ private _waitForChartJs(callback: () => void): void {
451
+ if ((window as any).Chart) {
452
+ // Register ChartDataLabels plugin if available and not already registered
453
+ if (!Chart._dataLabelsRegistered && (window as any).ChartDataLabels) {
454
+ try {
455
+ (window as any).Chart.register((window as any).ChartDataLabels);
456
+ Chart._dataLabelsRegistered = true;
457
+ console.log('✓ ChartDataLabels plugin registered');
458
+ } catch (error) {
459
+ console.warn('Failed to register ChartDataLabels plugin:', error);
460
+ }
461
+ }
462
+
463
+ callback();
464
+ return;
465
+ }
466
+
467
+ if (this._chartJsCheckAttempts >= this._maxCheckAttempts) {
468
+ console.error(
469
+ `Chart.js failed to load after ${this._maxCheckAttempts * 100}ms. ` +
470
+ 'Please ensure Chart.js is properly loaded in your page.'
471
+ );
472
+ this._renderFallbackUI();
473
+ return;
474
+ }
475
+
476
+ this._chartJsCheckAttempts++;
477
+ setTimeout(() => this._waitForChartJs(callback), 100);
478
+ }
479
+
480
+ private _renderFallbackUI(): void {
481
+ const wrapper = document.getElementById(this._id);
482
+ if (!wrapper) return;
483
+
484
+ const { data, type, width, height } = this.state;
485
+ const hasData = data?.labels?.length > 0 || data?.datasets?.length > 0;
486
+
487
+ wrapper.innerHTML = `
488
+ <div style="
489
+ display: flex;
490
+ flex-direction: column;
491
+ align-items: center;
492
+ justify-content: center;
493
+ height: 100%;
494
+ min-height: ${Math.max(height, 200)}px;
495
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
496
+ border-radius: 12px;
497
+ padding: 40px 20px;
498
+ text-align: center;
499
+ color: white;
500
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
501
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
502
+ ">
503
+ <div style="
504
+ background: rgba(255, 255, 255, 0.2);
505
+ backdrop-filter: blur(10px);
506
+ border-radius: 16px;
507
+ padding: 32px;
508
+ max-width: 500px;
509
+ ">
510
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 20px;">
511
+ <path d="M3 3v18h18"></path>
512
+ <path d="M18 17V9"></path>
513
+ <path d="M13 17V5"></path>
514
+ <path d="M8 17v-3"></path>
515
+ </svg>
516
+
517
+ <h3 style="
518
+ margin: 0 0 12px 0;
519
+ font-size: 24px;
520
+ font-weight: 700;
521
+ ">Chart.js Required</h3>
522
+
523
+ <p style="
524
+ margin: 0 0 24px 0;
525
+ font-size: 14px;
526
+ opacity: 0.9;
527
+ line-height: 1.6;
528
+ ">
529
+ This ${type} chart needs Chart.js library to render.<br>
530
+ ${hasData ? `Ready to display ${data.labels?.length || 0} data points.` : 'Waiting for data...'}
531
+ </p>
532
+
533
+ <div style="
534
+ background: rgba(0, 0, 0, 0.2);
535
+ border-radius: 8px;
536
+ padding: 16px;
537
+ text-align: left;
538
+ font-family: 'Monaco', 'Courier New', monospace;
539
+ font-size: 13px;
540
+ margin-bottom: 16px;
541
+ ">
542
+ <div style="color: #ffd700; margin-bottom: 8px;">📦 Add to your HTML:</div>
543
+ <code style="
544
+ display: block;
545
+ color: #fff;
546
+ line-height: 1.5;
547
+ word-break: break-all;
548
+ ">&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"&gt;&lt;/script&gt;</code>
549
+ </div>
550
+
551
+ <div style="
552
+ background: rgba(0, 0, 0, 0.2);
553
+ border-radius: 8px;
554
+ padding: 16px;
555
+ text-align: left;
556
+ font-family: 'Monaco', 'Courier New', monospace;
557
+ font-size: 13px;
558
+ ">
559
+ <div style="color: #ffd700; margin-bottom: 8px;">📦 Or install via npm:</div>
560
+ <code style="
561
+ display: block;
562
+ color: #fff;
563
+ line-height: 1.5;
564
+ ">npm install chart.js</code>
565
+ </div>
566
+
567
+ <button onclick="window.location.reload()" style="
568
+ margin-top: 24px;
569
+ padding: 12px 24px;
570
+ background: rgba(255, 255, 255, 0.9);
571
+ color: #667eea;
572
+ border: none;
573
+ border-radius: 8px;
574
+ font-size: 14px;
575
+ font-weight: 600;
576
+ cursor: pointer;
577
+ transition: all 0.2s;
578
+ " onmouseover="this.style.background='#fff'" onmouseout="this.style.background='rgba(255, 255, 255, 0.9)'">
579
+ 🔄 Reload Page
580
+ </button>
581
+ </div>
582
+ </div>
583
+ `;
584
+ }
585
+
586
+ private _updateChart(): void {
587
+ if (this.chartInstance) {
588
+ this.chartInstance.data = this.state.data;
589
+ this.chartInstance.options = this.state.options;
590
+ this.chartInstance.update();
591
+ }
592
+ }
593
+
594
+ destroy(): void {
595
+ if (this.chartInstance) {
596
+ this.chartInstance.destroy();
597
+ this.chartInstance = null;
598
+ }
599
+ }
600
+
601
+ /* -------------------------
602
+ * Render (5-Step Pattern)
603
+ * ------------------------- */
604
+
605
+ render(targetId?: string): this {
606
+ // === 1. SETUP: Get or create container ===
607
+ let container: HTMLElement;
608
+ if (targetId) {
609
+ const target = document.querySelector(targetId);
610
+ if (!target || !(target instanceof HTMLElement)) {
611
+ throw new Error(`Chart: Target "${targetId}" not found`);
612
+ }
613
+ container = target;
614
+ } else {
615
+ container = getOrCreateContainer(this._id);
616
+ }
617
+ this.container = container;
618
+
619
+ // === 2. PREPARE: Destructure state ===
620
+ const { type, data, options, width, height, style, class: className } = this.state;
621
+
622
+ // === 3. BUILD: Create DOM elements ===
623
+ const wrapper = document.createElement('div');
624
+ wrapper.className = 'jux-chart';
625
+ wrapper.id = this._id;
626
+ wrapper.style.cssText = `width: ${width}px; height: ${height}px; position: relative;`;
627
+ if (className) wrapper.className += ` ${className}`;
628
+ if (style) wrapper.setAttribute('style', wrapper.style.cssText + style);
629
+
630
+ const canvas = document.createElement('canvas');
631
+ canvas.id = `${this._id}-canvas`;
632
+ wrapper.appendChild(canvas);
633
+
634
+ // === 4. WIRE: Attach event listeners and sync bindings ===
635
+
636
+ // Wire custom bindings from .bind() calls
637
+ this._bindings.forEach(({ event, handler }) => {
638
+ wrapper.addEventListener(event, handler as EventListener);
639
+ });
640
+
641
+ // Initialize chart with retry logic
642
+ this._waitForChartJs(() => {
643
+ const ctx = canvas.getContext('2d');
644
+ if (ctx) {
645
+ try {
646
+ // Apply pie chart colors if needed
647
+ const chartData = this._preparePieChartColors(type, data);
648
+
649
+ this.chartInstance = new (window as any).Chart(ctx, {
650
+ type,
651
+ data: chartData,
652
+ options
653
+ });
654
+ console.log(`✓ Chart "${this._id}" rendered successfully`);
655
+ } catch (error) {
656
+ console.error(`Failed to create chart "${this._id}":`, error);
657
+ this._renderFallbackUI();
658
+ }
659
+ }
660
+ });
661
+
662
+ // Wire sync bindings from .sync() calls
663
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
664
+ if (property === 'data') {
665
+ const transformToComponent = toComponent || ((v: any) => v);
666
+
667
+ stateObj.subscribe((val: any) => {
668
+ const transformed = transformToComponent(val);
669
+ this.state.data = transformed;
670
+
671
+ if (this.chartInstance) {
672
+ this.chartInstance.data = transformed;
673
+ this.chartInstance.update();
674
+ }
675
+ });
676
+ }
677
+ else if (property === 'options') {
678
+ const transformToComponent = toComponent || ((v: any) => v);
679
+
680
+ stateObj.subscribe((val: any) => {
681
+ const transformed = transformToComponent(val);
682
+ this.state.options = transformed;
683
+
684
+ if (this.chartInstance) {
685
+ this.chartInstance.options = transformed;
686
+ this.chartInstance.update();
687
+ }
688
+ });
689
+ }
690
+ });
691
+
692
+ // === 5. RENDER: Append to DOM and finalize ===
693
+ container.appendChild(wrapper);
694
+ return this;
695
+ }
696
+
697
+ renderTo(juxComponent: any): this {
698
+ if (!juxComponent?._id) {
699
+ throw new Error('Chart.renderTo: Invalid component');
700
+ }
701
+ return this.render(`#${juxComponent._id}`);
702
+ }
703
+ }
704
+
705
+ export function chart(id: string, options: ChartOptions = {}): Chart {
706
+ return new Chart(id, options);
707
+ }