canvasframework 0.3.19 → 0.3.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.
@@ -0,0 +1,700 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Composant de graphiques avec support Line, Bar, Pie, Doughnut
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class Chart extends Component {
9
+ constructor(framework, options = {}) {
10
+ super(framework, options);
11
+
12
+ this.type = options.type || 'line'; // 'line', 'bar', 'pie', 'doughnut', 'area'
13
+ this.data = options.data || { labels: [], datasets: [] };
14
+ this.title = options.title || '';
15
+ this.showLegend = options.showLegend !== false;
16
+ this.showGrid = options.showGrid !== false;
17
+ this.showValues = options.showValues || false;
18
+ this.animated = options.animated !== false;
19
+ this.colors = options.colors || this.getDefaultColors();
20
+
21
+ // Marges et padding
22
+ this.padding = options.padding || 40;
23
+ this.legendHeight = this.showLegend ? 60 : 0;
24
+ this.titleHeight = this.title ? 40 : 0;
25
+
26
+ // Zone de dessin du graphique
27
+ this.chartArea = {
28
+ x: this.padding,
29
+ y: this.padding + this.titleHeight,
30
+ width: this.width - this.padding * 2,
31
+ height: this.height - this.padding * 2 - this.legendHeight - this.titleHeight
32
+ };
33
+
34
+ // Animation
35
+ this.animationProgress = this.animated ? 0 : 1;
36
+ this.animationDuration = 1000;
37
+ this.animationStartTime = null;
38
+
39
+ // Interaction
40
+ this.hoveredIndex = -1;
41
+ this.hoveredDatasetIndex = -1;
42
+
43
+ // Options spécifiques par type
44
+ this.lineOptions = {
45
+ tension: options.tension || 0.4, // Courbure des lignes
46
+ pointRadius: options.pointRadius || 4,
47
+ lineWidth: options.lineWidth || 2,
48
+ fill: options.fill || false
49
+ };
50
+
51
+ this.barOptions = {
52
+ barWidth: options.barWidth || null, // Auto si null
53
+ groupGap: options.groupGap || 10,
54
+ barGap: options.barGap || 4
55
+ };
56
+
57
+ this.pieOptions = {
58
+ innerRadius: options.innerRadius || 0, // >0 pour doughnut
59
+ startAngle: options.startAngle || -Math.PI / 2,
60
+ labelDistance: options.labelDistance || 1.3
61
+ };
62
+
63
+ // Calculer les échelles
64
+ this.calculateScales();
65
+
66
+ // Démarrer l'animation si activée
67
+ if (this.animated) {
68
+ this.startAnimation();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Couleurs par défaut
74
+ */
75
+ getDefaultColors() {
76
+ return [
77
+ '#6200EE', '#03DAC6', '#FF6F00', '#E91E63',
78
+ '#2196F3', '#4CAF50', '#FFC107', '#9C27B0',
79
+ '#00BCD4', '#FF5722', '#795548', '#607D8B'
80
+ ];
81
+ }
82
+
83
+ /**
84
+ * Calcule les échelles min/max pour les axes
85
+ */
86
+ calculateScales() {
87
+ if (this.type === 'pie' || this.type === 'doughnut') {
88
+ // Pas d'échelles pour les graphiques circulaires
89
+ this.totalValue = this.data.datasets[0]?.data.reduce((a, b) => a + b, 0) || 0;
90
+ return;
91
+ }
92
+
93
+ let allValues = [];
94
+ this.data.datasets.forEach(dataset => {
95
+ allValues = allValues.concat(dataset.data);
96
+ });
97
+
98
+ this.minValue = Math.min(0, ...allValues);
99
+ this.maxValue = Math.max(...allValues);
100
+
101
+ // Ajouter une marge de 10%
102
+ const range = this.maxValue - this.minValue;
103
+ this.minValue -= range * 0.1;
104
+ this.maxValue += range * 0.1;
105
+ }
106
+
107
+ /**
108
+ * Démarre l'animation
109
+ */
110
+ startAnimation() {
111
+ this.animationStartTime = Date.now();
112
+ this.animationProgress = 0;
113
+
114
+ const animate = () => {
115
+ const elapsed = Date.now() - this.animationStartTime;
116
+ this.animationProgress = Math.min(elapsed / this.animationDuration, 1);
117
+
118
+ // Easing
119
+ this.animationProgress = this.easeOutCubic(this.animationProgress);
120
+
121
+ this.requestRender();
122
+
123
+ if (this.animationProgress < 1) {
124
+ requestAnimationFrame(animate);
125
+ }
126
+ };
127
+
128
+ requestAnimationFrame(animate);
129
+ }
130
+
131
+ /**
132
+ * Fonction d'easing
133
+ */
134
+ easeOutCubic(t) {
135
+ return 1 - Math.pow(1 - t, 3);
136
+ }
137
+
138
+ /**
139
+ * Convertit une valeur en coordonnée Y
140
+ */
141
+ valueToY(value) {
142
+ const range = this.maxValue - this.minValue;
143
+ const ratio = (value - this.minValue) / range;
144
+ return this.chartArea.y + this.chartArea.height - (ratio * this.chartArea.height);
145
+ }
146
+
147
+ /**
148
+ * Convertit un index en coordonnée X
149
+ */
150
+ indexToX(index, total) {
151
+ const step = this.chartArea.width / (total - 1 || 1);
152
+ return this.chartArea.x + index * step;
153
+ }
154
+
155
+ /**
156
+ * Dessine le titre
157
+ */
158
+ drawTitle(ctx) {
159
+ if (!this.title) return;
160
+
161
+ ctx.save();
162
+ ctx.fillStyle = this.platform === 'material' ? '#000000' : '#000000';
163
+ ctx.font = 'bold 18px -apple-system, Roboto, sans-serif';
164
+ ctx.textAlign = 'center';
165
+ ctx.textBaseline = 'middle';
166
+ ctx.fillText(this.title, this.x + this.width / 2, this.y + this.titleHeight / 2);
167
+ ctx.restore();
168
+ }
169
+
170
+ /**
171
+ * Dessine la grille
172
+ */
173
+ drawGrid(ctx) {
174
+ if (!this.showGrid || this.type === 'pie' || this.type === 'doughnut') return;
175
+
176
+ ctx.save();
177
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
178
+ ctx.lineWidth = 1;
179
+
180
+ // Lignes horizontales
181
+ const steps = 5;
182
+ for (let i = 0; i <= steps; i++) {
183
+ const y = this.y + this.chartArea.y + (this.chartArea.height / steps) * i;
184
+
185
+ ctx.beginPath();
186
+ ctx.moveTo(this.x + this.chartArea.x, y);
187
+ ctx.lineTo(this.x + this.chartArea.x + this.chartArea.width, y);
188
+ ctx.stroke();
189
+
190
+ // Labels
191
+ const value = this.maxValue - (this.maxValue - this.minValue) * (i / steps);
192
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
193
+ ctx.font = '12px -apple-system, Roboto, sans-serif';
194
+ ctx.textAlign = 'right';
195
+ ctx.textBaseline = 'middle';
196
+ ctx.fillText(value.toFixed(1), this.x + this.chartArea.x - 5, y);
197
+ }
198
+
199
+ ctx.restore();
200
+ }
201
+
202
+ /**
203
+ * Dessine un graphique en ligne
204
+ */
205
+ drawLineChart(ctx) {
206
+ const numPoints = this.data.labels.length;
207
+
208
+ this.data.datasets.forEach((dataset, datasetIndex) => {
209
+ const color = dataset.color || this.colors[datasetIndex % this.colors.length];
210
+
211
+ ctx.save();
212
+
213
+ // Zone remplie (area chart)
214
+ if (this.lineOptions.fill || this.type === 'area') {
215
+ ctx.beginPath();
216
+ ctx.moveTo(
217
+ this.x + this.indexToX(0, numPoints),
218
+ this.y + this.valueToY(0)
219
+ );
220
+
221
+ dataset.data.forEach((value, index) => {
222
+ const x = this.x + this.indexToX(index, numPoints);
223
+ const y = this.y + this.valueToY(value * this.animationProgress);
224
+
225
+ if (index === 0) {
226
+ ctx.lineTo(x, y);
227
+ } else {
228
+ // Courbe de Bézier pour lisser
229
+ const prevX = this.x + this.indexToX(index - 1, numPoints);
230
+ const prevY = this.y + this.valueToY(dataset.data[index - 1] * this.animationProgress);
231
+ const cpX = prevX + (x - prevX) * this.lineOptions.tension;
232
+
233
+ ctx.bezierCurveTo(cpX, prevY, cpX, y, x, y);
234
+ }
235
+ });
236
+
237
+ ctx.lineTo(
238
+ this.x + this.indexToX(numPoints - 1, numPoints),
239
+ this.y + this.valueToY(0)
240
+ );
241
+ ctx.closePath();
242
+
243
+ ctx.fillStyle = color + '33'; // 20% opacité
244
+ ctx.fill();
245
+ }
246
+
247
+ // Ligne
248
+ ctx.beginPath();
249
+ dataset.data.forEach((value, index) => {
250
+ const x = this.x + this.indexToX(index, numPoints);
251
+ const y = this.y + this.valueToY(value * this.animationProgress);
252
+
253
+ if (index === 0) {
254
+ ctx.moveTo(x, y);
255
+ } else {
256
+ const prevX = this.x + this.indexToX(index - 1, numPoints);
257
+ const prevY = this.y + this.valueToY(dataset.data[index - 1] * this.animationProgress);
258
+ const cpX = prevX + (x - prevX) * this.lineOptions.tension;
259
+
260
+ ctx.bezierCurveTo(cpX, prevY, cpX, y, x, y);
261
+ }
262
+ });
263
+
264
+ ctx.strokeStyle = color;
265
+ ctx.lineWidth = this.lineOptions.lineWidth;
266
+ ctx.stroke();
267
+
268
+ // Points
269
+ dataset.data.forEach((value, index) => {
270
+ const x = this.x + this.indexToX(index, numPoints);
271
+ const y = this.y + this.valueToY(value * this.animationProgress);
272
+
273
+ const isHovered = this.hoveredIndex === index &&
274
+ this.hoveredDatasetIndex === datasetIndex;
275
+ const radius = isHovered ? this.lineOptions.pointRadius * 1.5 :
276
+ this.lineOptions.pointRadius;
277
+
278
+ ctx.beginPath();
279
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
280
+ ctx.fillStyle = '#FFFFFF';
281
+ ctx.fill();
282
+ ctx.strokeStyle = color;
283
+ ctx.lineWidth = 2;
284
+ ctx.stroke();
285
+
286
+ // Valeur au survol
287
+ if (isHovered && this.showValues) {
288
+ ctx.fillStyle = color;
289
+ ctx.font = 'bold 12px -apple-system, Roboto, sans-serif';
290
+ ctx.textAlign = 'center';
291
+ ctx.fillText(value.toFixed(1), x, y - 15);
292
+ }
293
+ });
294
+
295
+ ctx.restore();
296
+ });
297
+
298
+ // Labels X
299
+ this.drawXLabels(ctx);
300
+ }
301
+
302
+ /**
303
+ * Dessine un graphique en barres
304
+ */
305
+ drawBarChart(ctx) {
306
+ const numBars = this.data.labels.length;
307
+ const numDatasets = this.data.datasets.length;
308
+ const groupWidth = this.chartArea.width / numBars;
309
+ const barWidth = this.barOptions.barWidth ||
310
+ (groupWidth - this.barOptions.groupGap * 2) / numDatasets -
311
+ this.barOptions.barGap;
312
+
313
+ this.data.datasets.forEach((dataset, datasetIndex) => {
314
+ const color = dataset.color || this.colors[datasetIndex % this.colors.length];
315
+
316
+ dataset.data.forEach((value, index) => {
317
+ const groupX = this.x + this.chartArea.x + index * groupWidth;
318
+ const barX = groupX + this.barOptions.groupGap +
319
+ datasetIndex * (barWidth + this.barOptions.barGap);
320
+
321
+ const barHeight = Math.abs(this.valueToY(value) - this.valueToY(0)) *
322
+ this.animationProgress;
323
+ const barY = this.y + this.valueToY(value * this.animationProgress);
324
+
325
+ const isHovered = this.hoveredIndex === index &&
326
+ this.hoveredDatasetIndex === datasetIndex;
327
+
328
+ ctx.fillStyle = isHovered ? this.adjustColor(color, 20) : color;
329
+ ctx.fillRect(barX, barY, barWidth, barHeight);
330
+
331
+ // Bordure
332
+ if (this.platform === 'material') {
333
+ ctx.strokeStyle = this.adjustColor(color, -20);
334
+ ctx.lineWidth = 1;
335
+ ctx.strokeRect(barX, barY, barWidth, barHeight);
336
+ }
337
+
338
+ // Valeur
339
+ if (this.showValues || isHovered) {
340
+ ctx.fillStyle = '#000000';
341
+ ctx.font = '11px -apple-system, Roboto, sans-serif';
342
+ ctx.textAlign = 'center';
343
+ ctx.fillText(
344
+ value.toFixed(1),
345
+ barX + barWidth / 2,
346
+ barY - 5
347
+ );
348
+ }
349
+ });
350
+ });
351
+
352
+ // Labels X
353
+ this.drawXLabels(ctx);
354
+ }
355
+
356
+ /**
357
+ * Dessine un graphique circulaire (Pie/Doughnut)
358
+ */
359
+ drawPieChart(ctx) {
360
+ const centerX = this.x + this.width / 2;
361
+ const centerY = this.y + this.chartArea.y + this.chartArea.height / 2;
362
+ const radius = Math.min(this.chartArea.width, this.chartArea.height) / 2 - 20;
363
+ const innerRadius = this.type === 'doughnut' ?
364
+ (this.pieOptions.innerRadius || radius * 0.5) : 0;
365
+
366
+ let currentAngle = this.pieOptions.startAngle;
367
+ const dataset = this.data.datasets[0];
368
+
369
+ dataset.data.forEach((value, index) => {
370
+ const sliceAngle = (value / this.totalValue) * Math.PI * 2 * this.animationProgress;
371
+ const color = dataset.colors?.[index] || this.colors[index % this.colors.length];
372
+
373
+ const isHovered = this.hoveredIndex === index;
374
+ const displayRadius = isHovered ? radius + 10 : radius;
375
+
376
+ // Slice
377
+ ctx.save();
378
+ ctx.beginPath();
379
+ ctx.arc(centerX, centerY, displayRadius, currentAngle, currentAngle + sliceAngle);
380
+ ctx.arc(centerX, centerY, innerRadius, currentAngle + sliceAngle, currentAngle, true);
381
+ ctx.closePath();
382
+
383
+ ctx.fillStyle = color;
384
+ ctx.fill();
385
+
386
+ // Bordure
387
+ ctx.strokeStyle = '#FFFFFF';
388
+ ctx.lineWidth = 2;
389
+ ctx.stroke();
390
+ ctx.restore();
391
+
392
+ // Label
393
+ if (this.animationProgress > 0.8) {
394
+ const labelAngle = currentAngle + sliceAngle / 2;
395
+ const labelRadius = displayRadius * this.pieOptions.labelDistance;
396
+ const labelX = centerX + Math.cos(labelAngle) * labelRadius;
397
+ const labelY = centerY + Math.sin(labelAngle) * labelRadius;
398
+
399
+ ctx.fillStyle = '#000000';
400
+ ctx.font = '12px -apple-system, Roboto, sans-serif';
401
+ ctx.textAlign = 'center';
402
+ ctx.textBaseline = 'middle';
403
+
404
+ const percentage = ((value / this.totalValue) * 100).toFixed(1);
405
+ ctx.fillText(`${percentage}%`, labelX, labelY);
406
+
407
+ if (this.data.labels[index]) {
408
+ ctx.font = 'bold 11px -apple-system, Roboto, sans-serif';
409
+ ctx.fillText(this.data.labels[index], labelX, labelY + 15);
410
+ }
411
+ }
412
+
413
+ currentAngle += sliceAngle;
414
+ });
415
+
416
+ // Valeur centrale pour doughnut
417
+ if (this.type === 'doughnut' && this.animationProgress > 0.8) {
418
+ ctx.fillStyle = '#000000';
419
+ ctx.font = 'bold 24px -apple-system, Roboto, sans-serif';
420
+ ctx.textAlign = 'center';
421
+ ctx.textBaseline = 'middle';
422
+ ctx.fillText(this.totalValue.toFixed(0), centerX, centerY);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Dessine les labels de l'axe X
428
+ */
429
+ drawXLabels(ctx) {
430
+ ctx.save();
431
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
432
+ ctx.font = '12px -apple-system, Roboto, sans-serif';
433
+ ctx.textAlign = 'center';
434
+ ctx.textBaseline = 'top';
435
+
436
+ this.data.labels.forEach((label, index) => {
437
+ const x = this.x + this.indexToX(index, this.data.labels.length);
438
+ const y = this.y + this.chartArea.y + this.chartArea.height + 10;
439
+ ctx.fillText(label, x, y);
440
+ });
441
+
442
+ ctx.restore();
443
+ }
444
+
445
+ /**
446
+ * Dessine la légende
447
+ */
448
+ drawLegend(ctx) {
449
+ if (!this.showLegend) return;
450
+
451
+ const legendY = this.y + this.height - this.legendHeight + 10;
452
+ const itemWidth = 120;
453
+ const itemsPerRow = Math.floor(this.width / itemWidth);
454
+
455
+ ctx.save();
456
+
457
+ this.data.datasets.forEach((dataset, index) => {
458
+ const color = dataset.color || this.colors[index % this.colors.length];
459
+ const row = Math.floor(index / itemsPerRow);
460
+ const col = index % itemsPerRow;
461
+
462
+ const x = this.x + col * itemWidth + 20;
463
+ const y = legendY + row * 25;
464
+
465
+ // Carré de couleur
466
+ ctx.fillStyle = color;
467
+ ctx.fillRect(x, y, 12, 12);
468
+
469
+ // Label
470
+ ctx.fillStyle = '#000000';
471
+ ctx.font = '12px -apple-system, Roboto, sans-serif';
472
+ ctx.textAlign = 'left';
473
+ ctx.textBaseline = 'middle';
474
+ ctx.fillText(dataset.label || `Dataset ${index + 1}`, x + 18, y + 6);
475
+ });
476
+
477
+ ctx.restore();
478
+ }
479
+
480
+ /**
481
+ * Ajuste la luminosité d'une couleur
482
+ */
483
+ adjustColor(color, amount) {
484
+ const num = parseInt(color.replace('#', ''), 16);
485
+ const r = Math.min(255, Math.max(0, (num >> 16) + amount));
486
+ const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amount));
487
+ const b = Math.min(255, Math.max(0, (num & 0x0000FF) + amount));
488
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
489
+ }
490
+
491
+ /**
492
+ * Gère le survol
493
+ */
494
+ handleHover(x, y) {
495
+ const relativeX = x - this.x;
496
+ const relativeY = y - this.y;
497
+
498
+ if (this.type === 'pie' || this.type === 'doughnut') {
499
+ this.handlePieHover(relativeX, relativeY);
500
+ } else if (this.type === 'line' || this.type === 'area') {
501
+ this.handleLineHover(relativeX, relativeY);
502
+ } else if (this.type === 'bar') {
503
+ this.handleBarHover(relativeX, relativeY);
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Gère le survol pour les graphiques circulaires
509
+ */
510
+ handlePieHover(x, y) {
511
+ const centerX = this.width / 2;
512
+ const centerY = this.chartArea.y + this.chartArea.height / 2;
513
+
514
+ const dx = x - centerX;
515
+ const dy = y - centerY;
516
+ const distance = Math.sqrt(dx * dx + dy * dy);
517
+ const angle = Math.atan2(dy, dx);
518
+
519
+ const radius = Math.min(this.chartArea.width, this.chartArea.height) / 2 - 20;
520
+ const innerRadius = this.type === 'doughnut' ? (this.pieOptions.innerRadius || radius * 0.5) : 0;
521
+
522
+ if (distance >= innerRadius && distance <= radius) {
523
+ let currentAngle = this.pieOptions.startAngle;
524
+ const dataset = this.data.datasets[0];
525
+
526
+ for (let i = 0; i < dataset.data.length; i++) {
527
+ const sliceAngle = (dataset.data[i] / this.totalValue) * Math.PI * 2;
528
+
529
+ let normalizedAngle = angle - this.pieOptions.startAngle;
530
+ if (normalizedAngle < 0) normalizedAngle += Math.PI * 2;
531
+
532
+ if (normalizedAngle >= (currentAngle - this.pieOptions.startAngle) &&
533
+ normalizedAngle <= (currentAngle - this.pieOptions.startAngle + sliceAngle)) {
534
+ this.hoveredIndex = i;
535
+ this.requestRender();
536
+ return;
537
+ }
538
+
539
+ currentAngle += sliceAngle;
540
+ }
541
+ }
542
+
543
+ this.hoveredIndex = -1;
544
+ this.requestRender();
545
+ }
546
+
547
+ /**
548
+ * Gère le survol pour les graphiques en ligne
549
+ */
550
+ handleLineHover(x, y) {
551
+ let closestIndex = -1;
552
+ let closestDataset = -1;
553
+ let closestDistance = Infinity;
554
+
555
+ this.data.datasets.forEach((dataset, datasetIndex) => {
556
+ dataset.data.forEach((value, index) => {
557
+ const pointX = this.indexToX(index, this.data.labels.length);
558
+ const pointY = this.valueToY(value);
559
+
560
+ const dx = x - pointX;
561
+ const dy = y - pointY;
562
+ const distance = Math.sqrt(dx * dx + dy * dy);
563
+
564
+ if (distance < closestDistance && distance < 20) {
565
+ closestDistance = distance;
566
+ closestIndex = index;
567
+ closestDataset = datasetIndex;
568
+ }
569
+ });
570
+ });
571
+
572
+ if (this.hoveredIndex !== closestIndex || this.hoveredDatasetIndex !== closestDataset) {
573
+ this.hoveredIndex = closestIndex;
574
+ this.hoveredDatasetIndex = closestDataset;
575
+ this.requestRender();
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Gère le survol pour les graphiques en barres
581
+ */
582
+ handleBarHover(x, y) {
583
+ const numBars = this.data.labels.length;
584
+ const numDatasets = this.data.datasets.length;
585
+ const groupWidth = this.chartArea.width / numBars;
586
+ const barWidth = this.barOptions.barWidth ||
587
+ (groupWidth - this.barOptions.groupGap * 2) / numDatasets -
588
+ this.barOptions.barGap;
589
+
590
+ let found = false;
591
+
592
+ this.data.datasets.forEach((dataset, datasetIndex) => {
593
+ dataset.data.forEach((value, index) => {
594
+ const groupX = this.chartArea.x + index * groupWidth;
595
+ const barX = groupX + this.barOptions.groupGap +
596
+ datasetIndex * (barWidth + this.barOptions.barGap);
597
+ const barY = this.valueToY(value);
598
+ const barHeight = Math.abs(this.valueToY(value) - this.valueToY(0));
599
+
600
+ if (x >= barX && x <= barX + barWidth &&
601
+ y >= barY && y <= barY + barHeight) {
602
+ this.hoveredIndex = index;
603
+ this.hoveredDatasetIndex = datasetIndex;
604
+ found = true;
605
+ }
606
+ });
607
+ });
608
+
609
+ if (!found) {
610
+ this.hoveredIndex = -1;
611
+ this.hoveredDatasetIndex = -1;
612
+ }
613
+
614
+ this.requestRender();
615
+ }
616
+
617
+ /**
618
+ * Vérifie si un point est dans le composant
619
+ */
620
+ isPointInside(x, y) {
621
+ return x >= this.x && x <= this.x + this.width &&
622
+ y >= this.y && y <= this.y + this.height;
623
+ }
624
+
625
+ /**
626
+ * Gère le mouvement de la souris
627
+ */
628
+ onMove(x, y) {
629
+ this.handleHover(x, y);
630
+ }
631
+
632
+ /**
633
+ * Demande un nouveau rendu
634
+ */
635
+ requestRender() {
636
+ if (this.framework && this.framework.requestRender) {
637
+ this.framework.requestRender();
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Met à jour les données du graphique
643
+ */
644
+ updateData(newData) {
645
+ this.data = newData;
646
+ this.calculateScales();
647
+
648
+ if (this.animated) {
649
+ this.startAnimation();
650
+ } else {
651
+ this.requestRender();
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Dessine le composant
657
+ */
658
+ draw(ctx) {
659
+ ctx.save();
660
+
661
+ // Background
662
+ ctx.fillStyle = '#FFFFFF';
663
+ ctx.fillRect(this.x, this.y, this.width, this.height);
664
+
665
+ // Bordure
666
+ if (this.platform === 'material') {
667
+ ctx.strokeStyle = '#E0E0E0';
668
+ ctx.lineWidth = 1;
669
+ ctx.strokeRect(this.x, this.y, this.width, this.height);
670
+ }
671
+
672
+ // Titre
673
+ this.drawTitle(ctx);
674
+
675
+ // Grille
676
+ this.drawGrid(ctx);
677
+
678
+ // Graphique selon le type
679
+ switch (this.type) {
680
+ case 'line':
681
+ case 'area':
682
+ this.drawLineChart(ctx);
683
+ break;
684
+ case 'bar':
685
+ this.drawBarChart(ctx);
686
+ break;
687
+ case 'pie':
688
+ case 'doughnut':
689
+ this.drawPieChart(ctx);
690
+ break;
691
+ }
692
+
693
+ // Légende
694
+ this.drawLegend(ctx);
695
+
696
+ ctx.restore();
697
+ }
698
+ }
699
+
700
+ export default Chart;
@@ -8,248 +8,408 @@ import Component from '../core/Component.js';
8
8
  class Tabs extends Component {
9
9
  constructor(framework, options = {}) {
10
10
  super(framework, options);
11
+
11
12
  this.tabs = options.tabs || [];
12
13
  this.selectedIndex = options.selectedIndex || 0;
13
14
  this.onChange = options.onChange;
14
15
  this.platform = framework.platform;
15
- this.height = options.height || 48;
16
- this.indicatorColor = options.indicatorColor || (framework.platform === 'material' ? '#6200EE' : '#007AFF');
17
- this.textColor = options.textColor || '#000000';
16
+ this.height = options.height || 56;
17
+
18
+ this.indicatorColor = options.indicatorColor ||
19
+ (this.platform === 'material' ? '#6200EE' : '#007AFF');
20
+ this.textColor = options.textColor ||
21
+ (this.platform === 'material' ? '#000000' : '#8E8E93');
18
22
  this.selectedTextColor = options.selectedTextColor || this.indicatorColor;
19
23
 
20
- // 🔹 Système de ripples amélioré
24
+ // Ripple pour Material
21
25
  this.ripples = [];
22
26
  this.animationFrame = null;
23
27
  this.lastAnimationTime = 0;
24
28
 
25
- // 🔹 Pour détecter les doubles événements
26
- this.lastEventTime = 0;
27
- this.lastEventCoords = { x: -1, y: -1 };
29
+ // Animation pour Cupertino
30
+ this.pressedTabIndex = -1;
31
+ this.pressAnimation = 0;
32
+
33
+ // ✅ Structure: tableau de tableaux d'enfants
34
+ // tabChildren[0] = [enfants du tab 0]
35
+ // tabChildren[1] = [enfants du tab 1]
36
+ this.tabChildren = this.tabs.map(() => []);
28
37
 
38
+ // ✅ Configuration: nombre d'enfants par tab
39
+ // Si défini, distribue automatiquement les enfants
40
+ // Ex: childrenPerTab = [3, 2] => 3 enfants pour tab 0, 2 pour tab 1
41
+ this.childrenPerTab = options.childrenPerTab || null;
42
+ this.currentTabIndex = 0;
43
+ this.childAddCount = 0; // Compteur d'enfants ajoutés
44
+
45
+ // Gestionnaire de clic
29
46
  this.onPress = this.handlePress.bind(this);
47
+
48
+ // Position par défaut
49
+ this.position = options.position || (this.platform === 'cupertino' ? 'bottom' : 'top');
50
+
51
+ if (this.position === 'bottom' && !options.y) {
52
+ this.y = framework.height - this.height;
53
+ } else if (this.position === 'top' && !options.y) {
54
+ this.y = options.appbar || 0;
55
+ }
56
+
57
+ // Zone de contenu (sous les tabs)
58
+ this.contentY = this.y + this.height;
59
+ this.contentHeight = framework.height - this.height;
30
60
  }
31
61
 
32
62
  /**
33
- * Démarre l'animation des ripples
34
- * @private
63
+ * Définit le tab actuel pour l'ajout d'enfants (appelé par UIBuilder)
64
+ * @param {number} tabIndex - Index du tab
35
65
  */
36
- startRippleAnimation() {
37
- const animate = (timestamp) => {
38
- if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
39
- const deltaTime = timestamp - this.lastAnimationTime;
40
- this.lastAnimationTime = timestamp;
41
-
42
- let needsUpdate = false;
43
-
44
- // Mettre à jour chaque ripple
45
- for (let i = this.ripples.length - 1; i >= 0; i--) {
46
- const ripple = this.ripples[i];
47
-
48
- // Animer le rayon (expansion)
49
- if (ripple.radius < ripple.maxRadius) {
50
- ripple.radius += (ripple.maxRadius / 300) * deltaTime;
51
- needsUpdate = true;
52
- }
53
-
54
- // Animer l'opacité (fade out)
55
- if (ripple.radius >= ripple.maxRadius * 0.4) {
56
- ripple.opacity -= (0.003 * deltaTime);
57
- if (ripple.opacity < 0) ripple.opacity = 0;
58
- needsUpdate = true;
59
- }
60
-
61
- // Supprimer les ripples terminés
62
- if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
63
- this.ripples.splice(i, 1);
64
- needsUpdate = true;
65
- }
66
- }
66
+ setCurrentTab(tabIndex) {
67
+ if (tabIndex >= 0 && tabIndex < this.tabChildren.length) {
68
+ this.currentTabIndex = tabIndex;
69
+ }
70
+ }
67
71
 
68
- // Redessiner si nécessaire
69
- if (needsUpdate) {
70
- this.requestRender();
72
+ /**
73
+ * Ajoute un enfant au tab en cours
74
+ * Distribution automatique: divise les enfants équitablement entre les tabs
75
+ * @param {Component} child - Composant enfant
76
+ * @returns {Component} L'enfant ajouté
77
+ */
78
+ add(child) {
79
+ // Coordonnées relatives à la zone de contenu
80
+ child.x = child.x || 0;
81
+ child.y = child.y || 0;
82
+
83
+ // Dimensions par défaut
84
+ if (!child.width) child.width = this.framework.width;
85
+
86
+ // Marquer l'enfant comme appartenant à ce Tabs
87
+ child.parentTabs = this;
88
+
89
+ // ✅ Calculer quel tab doit recevoir cet enfant
90
+ // On distribue équitablement les enfants entre les tabs
91
+ const totalChildren = this.tabChildren.reduce((sum, arr) => sum + arr.length, 0);
92
+ const childrenPerTab = Math.ceil(totalChildren / this.tabs.length);
93
+
94
+ // Trouver le premier tab qui n'est pas encore plein
95
+ let targetTabIndex = 0;
96
+ for (let i = 0; i < this.tabChildren.length; i++) {
97
+ if (this.tabChildren[i].length < childrenPerTab) {
98
+ targetTabIndex = i;
99
+ break;
71
100
  }
72
-
73
- // Continuer l'animation
74
- if (this.ripples.length > 0) {
75
- this.animationFrame = requestAnimationFrame(animate);
76
- } else {
77
- this.animationFrame = null;
78
- this.lastAnimationTime = 0;
101
+ // Si tous les tabs ont childrenPerTab enfants, recommencer à 0
102
+ if (i === this.tabChildren.length - 1) {
103
+ targetTabIndex = totalChildren % this.tabs.length;
79
104
  }
80
- };
81
-
82
- if (this.ripples.length > 0 && !this.animationFrame) {
83
- this.animationFrame = requestAnimationFrame(animate);
84
105
  }
106
+
107
+ // Ajouter au tableau du tab calculé
108
+ this.tabChildren[targetTabIndex].push(child);
109
+
110
+ // Visibilité selon le tab sélectionné
111
+ child.visible = (targetTabIndex === this.selectedIndex);
112
+
113
+ return child;
85
114
  }
86
115
 
87
116
  /**
88
- * Demander un redessin
89
- * @private
117
+ * Met à jour la visibilité des enfants selon l'onglet sélectionné
90
118
  */
91
- requestRender() {
92
- if (this.framework && this.framework.requestRender) {
93
- this.framework.requestRender();
94
- }
119
+ updateChildrenVisibility() {
120
+ this.tabChildren.forEach((children, tabIdx) => {
121
+ const isVisible = (tabIdx === this.selectedIndex);
122
+ children.forEach(child => {
123
+ child.visible = isVisible;
124
+ });
125
+ });
95
126
  }
96
127
 
97
128
  /**
98
- * Nettoyer l'animation lors de la destruction
129
+ * Retourne tous les enfants du tab sélectionné
99
130
  */
100
- destroy() {
101
- if (this.animationFrame) {
102
- cancelAnimationFrame(this.animationFrame);
103
- this.animationFrame = null;
104
- }
105
- if (super.destroy) {
106
- super.destroy();
107
- }
131
+ getActiveChildren() {
132
+ return this.tabChildren[this.selectedIndex] || [];
108
133
  }
109
134
 
110
135
  handlePress(x, y) {
111
- const now = performance.now();
136
+ // D'abord vérifier les clics sur les enfants
137
+ if (y > this.y + this.height && this.checkChildClick(x, y)) {
138
+ return;
139
+ }
140
+
141
+ // Ensuite vérifier les clics sur la barre de tabs
142
+ if (!this.isPointInside(x, y)) return;
112
143
 
113
- // 🔹 Détecter si c'est le même événement physique (mousedown + click)
114
- const deltaTime = now - this.lastEventTime;
115
- const deltaX = Math.abs(x - this.lastEventCoords.x);
116
- const deltaY = Math.abs(y - this.lastEventCoords.y);
144
+ const tabWidth = this.width / this.tabs.length;
145
+ const index = Math.floor((x - this.x) / tabWidth);
117
146
 
118
- const isDoubleEvent = deltaX < 2 && deltaY < 2 && deltaTime < 50;
147
+ if (index < 0 || index >= this.tabs.length) return;
119
148
 
120
- if (isDoubleEvent) {
121
- return; // Ignorer le double événement
149
+ // Ripple pour Material
150
+ if (this.platform === 'material') {
151
+ const rippleCenterX = this.x + index * tabWidth + tabWidth / 2;
152
+ const maxRippleRadius = Math.min(tabWidth * 0.6, this.height * 0.8);
153
+
154
+ this.ripples.push({
155
+ x: rippleCenterX,
156
+ y: this.y + this.height / 2,
157
+ radius: 0,
158
+ maxRadius: maxRippleRadius,
159
+ opacity: 1
160
+ });
161
+
162
+ if (!this.animationFrame) this.startRippleAnimation();
163
+ }
164
+ // Animation Cupertino
165
+ else if (this.platform === 'cupertino') {
166
+ this.pressedTabIndex = index;
167
+ this.pressAnimation = 1;
168
+ this.requestRender();
169
+ setTimeout(() => this.animatePressRelease(), 100);
122
170
  }
123
171
 
124
- // 🔹 Enregistrer cet événement
125
- this.lastEventTime = now;
126
- this.lastEventCoords = { x, y };
172
+ // Changement d'onglet
173
+ if (index !== this.selectedIndex) {
174
+ this.selectedIndex = index;
175
+ this.updateChildrenVisibility();
176
+ if (this.onChange) this.onChange(index, this.tabs[index]);
177
+ }
127
178
 
128
- const tabWidth = this.width / this.tabs.length;
129
- const index = Math.floor((x - this.x) / tabWidth);
179
+ this.requestRender();
180
+ }
181
+
182
+ /**
183
+ * ✅ Vérifie les clics sur les enfants du tab actif
184
+ */
185
+ checkChildClick(x, y) {
186
+ const adjustedY = y - (this.framework.scrollOffset || 0);
187
+ const activeChildren = this.getActiveChildren();
130
188
 
131
- if (index >= 0 && index < this.tabs.length) {
132
- // Ripple pour Material
133
- if (this.platform === 'material') {
134
- const rippleCenterX = this.x + index * tabWidth + tabWidth / 2;
135
-
136
- // Calculer la taille maximale du ripple
137
- const maxRippleRadius = Math.min(tabWidth * 0.6, this.height * 0.8);
138
-
139
- this.ripples.push({
140
- x: rippleCenterX,
141
- y: this.y + this.height / 2,
142
- radius: 0,
143
- maxRadius: maxRippleRadius,
144
- opacity: 1,
145
- createdAt: now,
146
- tabIndex: index
147
- });
189
+ // Parcourir en ordre inverse (derniers ajoutés = au dessus)
190
+ for (let i = activeChildren.length - 1; i >= 0; i--) {
191
+ const child = activeChildren[i];
192
+
193
+ if (!child.visible) continue;
194
+
195
+ // Calculer les coordonnées absolues de l'enfant
196
+ const childX = this.x + child.x;
197
+ const childY = this.contentY + child.y;
198
+
199
+ // Vérifier si le clic est dans l'enfant
200
+ if (adjustedY >= childY &&
201
+ adjustedY <= childY + child.height &&
202
+ x >= childX &&
203
+ x <= childX + child.width) {
148
204
 
149
- // Démarrer l'animation si elle n'est pas en cours
150
- if (!this.animationFrame) {
151
- this.startRippleAnimation();
205
+ // Si l'enfant a un onClick ou onPress, le déclencher
206
+ if (child.onClick) {
207
+ child.onClick();
208
+ return true;
209
+ } else if (child.onPress) {
210
+ child.onPress(x, adjustedY);
211
+ return true;
152
212
  }
153
-
154
- // Forcer un redessin
155
- this.requestRender();
156
213
  }
214
+ }
215
+
216
+ return false;
217
+ }
157
218
 
158
- // Changement d'onglet
159
- if (index !== this.selectedIndex) {
160
- this.selectedIndex = index;
161
- if (this.onChange) this.onChange(index, this.tabs[index]);
162
- }
219
+ animatePressRelease() {
220
+ let startTime = null;
221
+ const duration = 150;
222
+
223
+ const animate = (timestamp) => {
224
+ if (!startTime) startTime = timestamp;
225
+ const progress = Math.min((timestamp - startTime) / duration, 1);
163
226
 
227
+ this.pressAnimation = 1 - progress;
164
228
  this.requestRender();
165
- }
229
+
230
+ if (progress < 1) requestAnimationFrame(animate);
231
+ else {
232
+ this.pressAnimation = 0;
233
+ this.pressedTabIndex = -1;
234
+ }
235
+ };
236
+
237
+ requestAnimationFrame(animate);
166
238
  }
167
239
 
168
240
  isPointInside(x, y) {
169
- return x >= this.x && x <= this.x + this.width &&
241
+ return x >= this.x && x <= this.x + this.width &&
170
242
  y >= this.y && y <= this.y + this.height;
171
243
  }
172
244
 
245
+ startRippleAnimation() {
246
+ const animate = (timestamp) => {
247
+ if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
248
+ const deltaTime = timestamp - this.lastAnimationTime;
249
+ this.lastAnimationTime = timestamp;
250
+
251
+ let needsUpdate = false;
252
+
253
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
254
+ const ripple = this.ripples[i];
255
+
256
+ if (ripple.radius < ripple.maxRadius)
257
+ ripple.radius += (ripple.maxRadius / 300) * deltaTime;
258
+
259
+ if (ripple.radius >= ripple.maxRadius * 0.4) {
260
+ ripple.opacity -= (0.003 * deltaTime);
261
+ if (ripple.opacity < 0) ripple.opacity = 0;
262
+ }
263
+
264
+ if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95)
265
+ this.ripples.splice(i, 1);
266
+
267
+ needsUpdate = true;
268
+ }
269
+
270
+ if (needsUpdate) this.requestRender();
271
+
272
+ if (this.ripples.length > 0)
273
+ this.animationFrame = requestAnimationFrame(animate);
274
+ else
275
+ this.animationFrame = null;
276
+ };
277
+
278
+ if (this.ripples.length && !this.animationFrame)
279
+ this.animationFrame = requestAnimationFrame(animate);
280
+ }
281
+
282
+ requestRender() {
283
+ if (this.framework && this.framework.requestRender)
284
+ this.framework.requestRender();
285
+ }
286
+
287
+ /**
288
+ * ✅ Dessine les tabs et les enfants du tab actif
289
+ */
173
290
  draw(ctx) {
174
291
  ctx.save();
175
292
 
293
+ // ===== DESSINER LA BARRE DE TABS =====
294
+
176
295
  // Background
177
- ctx.fillStyle = '#FFFFFF';
296
+ ctx.fillStyle = this.platform === 'material' ? '#FFF' : '#F2F2F7';
178
297
  ctx.fillRect(this.x, this.y, this.width, this.height);
179
298
 
180
- // Bordure inférieure
181
- ctx.strokeStyle = '#E0E0E0';
182
- ctx.lineWidth = 1;
183
- ctx.beginPath();
184
- ctx.moveTo(this.x, this.y + this.height);
185
- ctx.lineTo(this.x + this.width, this.y + this.height);
186
- ctx.stroke();
187
-
188
- const tabWidth = this.width / this.tabs.length;
189
-
190
- // 🔹 Dessiner les ripples Material EN PREMIER
191
299
  if (this.platform === 'material') {
192
- this.drawRipples(ctx, tabWidth);
300
+ ctx.strokeStyle = '#E0E0E0';
301
+ ctx.lineWidth = 1;
302
+ ctx.beginPath();
303
+ ctx.moveTo(this.x, this.y + this.height);
304
+ ctx.lineTo(this.x + this.width, this.y + this.height);
305
+ ctx.stroke();
193
306
  }
194
-
195
- // Dessiner les onglets
307
+
308
+ const tabWidth = this.width / this.tabs.length;
309
+
310
+ // Ripples
311
+ if (this.platform === 'material') this.drawRipples(ctx, tabWidth);
312
+
196
313
  for (let i = 0; i < this.tabs.length; i++) {
197
314
  const tab = this.tabs[i];
198
315
  const tabX = this.x + i * tabWidth;
199
316
  const isSelected = i === this.selectedIndex;
317
+
318
+ // Cupertino pressed effect
319
+ if (this.platform === 'cupertino' && i === this.pressedTabIndex) {
320
+ ctx.fillStyle = `rgba(0,122,255,${0.1 * this.pressAnimation})`;
321
+ ctx.fillRect(tabX, this.y, tabWidth, this.height);
322
+ }
323
+
324
+ // Indicators
325
+ if (this.platform === 'cupertino' && isSelected) {
326
+ ctx.fillStyle = '#007AFF';
327
+ ctx.fillRect(tabX + tabWidth/2 - 15, this.y + this.height - 2, 30, 2);
328
+ }
329
+
200
330
  const color = isSelected ? this.selectedTextColor : this.textColor;
201
-
202
- // Icône (facultative)
331
+
332
+ // Icon
203
333
  if (tab.icon) {
204
- ctx.font = '20px sans-serif';
334
+ ctx.font = '24px -apple-system, sans-serif';
205
335
  ctx.textAlign = 'center';
206
336
  ctx.textBaseline = 'middle';
207
337
  ctx.fillStyle = color;
208
- ctx.fillText(tab.icon, tabX + tabWidth / 2, this.y + 16);
338
+ const iconY = this.platform === 'material' ? this.y + 18 : this.y + 20;
339
+ ctx.fillText(tab.icon, tabX + tabWidth/2, iconY);
209
340
  }
210
-
341
+
211
342
  // Label
212
- ctx.font = `${isSelected ? 'bold ' : ''}14px -apple-system, Roboto, sans-serif`;
343
+ const fontSize = this.platform === 'material' ? 14 : 12;
344
+ const fontWeight = isSelected ? '600' : '400';
345
+ ctx.font = `${fontWeight} ${fontSize}px -apple-system, Roboto, sans-serif`;
213
346
  ctx.fillStyle = color;
214
347
  ctx.textAlign = 'center';
215
348
  ctx.textBaseline = 'middle';
216
- const labelY = tab.icon ? this.y + 36 : this.y + this.height / 2;
217
- ctx.fillText(tab.label, tabX + tabWidth / 2, labelY);
218
-
219
- // Indicateur (Material)
349
+
350
+ const labelY = this.platform === 'material'
351
+ ? (tab.icon ? this.y + 36 : this.y + this.height / 2)
352
+ : (tab.icon ? this.y + 42 : this.y + this.height / 2);
353
+
354
+ ctx.fillText(tab.label, tabX + tabWidth/2, labelY);
355
+
356
+ // Material indicator
220
357
  if (isSelected && this.platform === 'material') {
221
358
  ctx.fillStyle = this.indicatorColor;
222
- ctx.fillRect(tabX, this.y + this.height - 2, tabWidth, 2);
359
+ ctx.fillRect(tabX, this.y + this.height - 3, tabWidth, 3);
223
360
  }
224
361
  }
225
-
362
+
226
363
  ctx.restore();
364
+
365
+ // ===== DESSINER LES ENFANTS DU TAB ACTIF =====
366
+ const activeChildren = this.getActiveChildren();
367
+
368
+ for (let child of activeChildren) {
369
+ if (child.visible) {
370
+ ctx.save();
371
+
372
+ // Sauvegarder les coordonnées originales
373
+ const originalX = child.x;
374
+ const originalY = child.y;
375
+
376
+ // Ajuster les coordonnées pour être absolues
377
+ child.x = this.x + originalX;
378
+ child.y = this.contentY + originalY;
379
+
380
+ // Dessiner l'enfant
381
+ child.draw(ctx);
382
+
383
+ // Restaurer les coordonnées originales
384
+ child.x = originalX;
385
+ child.y = originalY;
386
+
387
+ ctx.restore();
388
+ }
389
+ }
227
390
  }
228
391
 
229
- /**
230
- * Dessine les ripples (Material)
231
- * @private
232
- */
233
392
  drawRipples(ctx, tabWidth) {
234
- // Sauvegarder le contexte
235
393
  ctx.save();
236
-
237
- // Créer un masque de clipping pour limiter les ripples aux onglets
238
394
  ctx.beginPath();
239
395
  ctx.rect(this.x, this.y, this.width, this.height);
240
396
  ctx.clip();
241
397
 
242
398
  for (let ripple of this.ripples) {
243
399
  ctx.globalAlpha = ripple.opacity;
244
- ctx.fillStyle = this.indicatorColor || '#6200EE';
400
+ ctx.fillStyle = this.indicatorColor;
245
401
  ctx.beginPath();
246
- ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
402
+ ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI*2);
247
403
  ctx.fill();
248
404
  }
249
405
 
250
- // Restaurer le contexte
251
406
  ctx.restore();
252
407
  }
408
+
409
+ destroy() {
410
+ if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
411
+ if (super.destroy) super.destroy();
412
+ }
253
413
  }
254
414
 
255
415
  export default Tabs;
@@ -51,6 +51,7 @@ import PasswordInput from '../components/PasswordInput.js';
51
51
  import InputTags from '../components/InputTags.js';
52
52
  import InputDatalist from '../components/InputDatalist.js';
53
53
  import Banner from '../components/Banner.js';
54
+ import Chart from '../components/Chart.js';
54
55
 
55
56
  // Utils
56
57
  import SafeArea from '../utils/SafeArea.js';
@@ -121,6 +122,7 @@ const FIXED_COMPONENT_TYPES = new Set([
121
122
  Drawer,
122
123
  Dialog,
123
124
  Modal,
125
+ Tabs,
124
126
  FAB,
125
127
  Toast,
126
128
  Banner,
package/core/UIBuilder.js CHANGED
@@ -52,6 +52,7 @@ import PasswordInput from '../components/PasswordInput.js';
52
52
  import InputTags from '../components/InputTags.js';
53
53
  import InputDatalist from '../components/InputDatalist.js';
54
54
  import Banner from '../components/Banner.js';
55
+ import Chart from '../components/Chart.js';
55
56
 
56
57
  // Features
57
58
  import PullToRefresh from '../features/PullToRefresh.js';
@@ -131,6 +132,8 @@ const Components = {
131
132
  Column,
132
133
  Positioned,
133
134
  Banner,
135
+ Chart,
136
+ QRCode,
134
137
  Stack
135
138
  };
136
139
 
package/index.js CHANGED
@@ -59,6 +59,7 @@ export { default as PasswordInput } from './components/PasswordInput.js';
59
59
  export { default as InputTags } from './components/InputTags.js';
60
60
  export { default as InputDatalist } from './components/InputDatalist.js';
61
61
  export { default as Banner } from './components/Banner.js';
62
+ export { default as Chart } from './components/Chart.js';
62
63
 
63
64
  // Utils
64
65
  export { default as SafeArea } from './utils/SafeArea.js';
@@ -100,7 +101,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
100
101
 
101
102
  // Version du framework
102
103
 
103
- export const VERSION = '0.3.19';
104
+ export const VERSION = '0.3.21';
104
105
 
105
106
 
106
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",