canvasframework 0.5.58 → 0.5.59

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,840 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Conteneur avec pagination manuelle - l'utilisateur définit le contenu de chaque page
5
+ * @class
6
+ * @extends Component
7
+ *
8
+ * Exemple d'utilisation:
9
+ * const container = new PaginatedContainer(framework, { pageHeight: 400, width: 600 });
10
+ * container.addPage([child1, child2, child3]); // Page 1
11
+ * container.addPage([child4, child5]); // Page 2
12
+ * container.addPage([child6]); // Page 3
13
+ *
14
+ * Material: Boutons Material You avec numéros de page
15
+ * Cupertino: Boutons iOS style avec compteur
16
+ */
17
+ class PaginatedContainer extends Component {
18
+ /**
19
+ * Crée une instance de PaginatedContainer
20
+ * @param {CanvasFramework} framework - Framework parent
21
+ * @param {Object} [options={}] - Options de configuration
22
+ * @param {number} [options.pageHeight=400] - Hauteur d'une page
23
+ * @param {number} [options.padding=16] - Padding interne
24
+ * @param {number} [options.gap=12] - Espacement entre composants
25
+ * @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
26
+ * @param {number} [options.elevation=2] - Élévation (Material)
27
+ * @param {number} [options.borderRadius=8] - Rayon des coins
28
+ * @param {boolean} [options.showNavigation=true] - Afficher navigation
29
+ * @param {string} [options.navPosition='bottom'] - Position nav: 'top', 'bottom', 'both'
30
+ * @param {Function} [options.onPageChange] - Callback changement de page
31
+ */
32
+ constructor(framework, options = {}) {
33
+ super(framework, options);
34
+
35
+ this.platform = framework.platform;
36
+ this.pageHeight = options.pageHeight || 400;
37
+ this.padding = options.padding || 16;
38
+ this.gap = options.gap || 12;
39
+ this.bgColor = options.bgColor || '#FFFFFF';
40
+ this.elevation = options.elevation !== undefined ? options.elevation : 2;
41
+ this.borderRadius = options.borderRadius !== undefined ? options.borderRadius : 8;
42
+ this.showNavigation = options.showNavigation !== false;
43
+ this.navPosition = options.navPosition || 'bottom';
44
+ this.onPageChange = options.onPageChange || (() => {});
45
+
46
+ // Navigation
47
+ this.navHeight = this.platform === 'material' ? 56 : 50;
48
+ this.currentPage = 0;
49
+
50
+ // Pages - tableau de tableaux d'enfants
51
+ this.pages = [];
52
+
53
+ // Calcul hauteur totale
54
+ const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
55
+ const navBottomHeight = (this.showNavigation && this.navPosition !== 'top') ? this.navHeight : 0;
56
+ this.height = navTopHeight + this.pageHeight + navBottomHeight;
57
+
58
+ // Couleurs selon plateforme
59
+ if (this.platform === 'material') {
60
+ this.navColor = '#F5F5F5';
61
+ this.borderColor = '#E0E0E0';
62
+ this.activeColor = '#6200EE';
63
+ this.inactiveColor = '#757575';
64
+ this.shadowColor = 'rgba(0,0,0,0.15)';
65
+ this.buttonBgColor = '#FFFFFF';
66
+ this.buttonDisabledColor = '#F5F5F5';
67
+ } else {
68
+ this.navColor = '#F2F2F7';
69
+ this.borderColor = '#C6C6C8';
70
+ this.activeColor = '#007AFF';
71
+ this.inactiveColor = '#8E8E93';
72
+ this.shadowColor = 'rgba(0,0,0,0.1)';
73
+ this.buttonBgColor = '#FFFFFF';
74
+ this.buttonDisabledColor = '#E5E5EA';
75
+ }
76
+
77
+ // Élévation
78
+ this._applyElevationStyles();
79
+
80
+ // Navigation buttons
81
+ this.navButtons = [];
82
+ this.hoveredButton = null;
83
+ this.pressedButton = null;
84
+
85
+ // Pour l'effet ripple
86
+ this.rippleButton = null;
87
+ this.rippleTimer = null;
88
+ }
89
+
90
+ /**
91
+ * Applique les styles d'élévation
92
+ * @private
93
+ */
94
+ _applyElevationStyles() {
95
+ const elevationStyles = {
96
+ 0: { blur: 0, offsetY: 0, spread: 0, opacity: 0 },
97
+ 1: { blur: 2, offsetY: 1, spread: 0, opacity: 0.1 },
98
+ 2: { blur: 4, offsetY: 2, spread: 1, opacity: 0.15 },
99
+ 3: { blur: 8, offsetY: 4, spread: 2, opacity: 0.2 },
100
+ 4: { blur: 16, offsetY: 8, spread: 3, opacity: 0.25 },
101
+ 5: { blur: 24, offsetY: 12, spread: 4, opacity: 0.3 }
102
+ };
103
+
104
+ const style = elevationStyles[Math.min(this.elevation, 5)] || elevationStyles[0];
105
+
106
+ this.shadowBlur = style.blur;
107
+ this.shadowOffsetY = style.offsetY;
108
+ this.shadowSpread = style.spread;
109
+ this.shadowOpacity = style.opacity;
110
+
111
+ this._updateShadowColor();
112
+ }
113
+
114
+ /**
115
+ * Met à jour la couleur de l'ombre
116
+ * @private
117
+ */
118
+ _updateShadowColor() {
119
+ const rgbMatch = this.shadowColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
120
+ if (rgbMatch) {
121
+ const r = rgbMatch[1];
122
+ const g = rgbMatch[2];
123
+ const b = rgbMatch[3];
124
+ this._computedShadowColor = `rgba(${r}, ${g}, ${b}, ${this.shadowOpacity})`;
125
+ } else {
126
+ this._computedShadowColor = this.shadowColor;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Ajoute une nouvelle page avec ses enfants
132
+ * @param {Component[]} children - Tableau de composants pour cette page
133
+ * @returns {number} Index de la page ajoutée
134
+ */
135
+ addPage(children = []) {
136
+ const pageIndex = this.pages.length;
137
+ this.pages.push(children);
138
+ this.updateNavButtons();
139
+ this.updateChildrenPositions();
140
+ return pageIndex;
141
+ }
142
+
143
+ /**
144
+ * Définit le contenu d'une page spécifique
145
+ * @param {number} pageIndex - Index de la page
146
+ * @param {Component[]} children - Tableau de composants
147
+ */
148
+ setPage(pageIndex, children = []) {
149
+ if (pageIndex >= 0) {
150
+ // Étendre le tableau si nécessaire
151
+ while (this.pages.length <= pageIndex) {
152
+ this.pages.push([]);
153
+ }
154
+ this.pages[pageIndex] = children;
155
+ this.updateNavButtons();
156
+ if (pageIndex === this.currentPage) {
157
+ this.updateChildrenPositions();
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Récupère le contenu d'une page
164
+ * @param {number} pageIndex - Index de la page
165
+ * @returns {Component[]} Tableau des enfants de cette page
166
+ */
167
+ getPage(pageIndex) {
168
+ return this.pages[pageIndex] || [];
169
+ }
170
+
171
+ /**
172
+ * Retire une page
173
+ * @param {number} pageIndex - Index de la page à retirer
174
+ */
175
+ removePage(pageIndex) {
176
+ if (pageIndex >= 0 && pageIndex < this.pages.length) {
177
+ this.pages.splice(pageIndex, 1);
178
+
179
+ // Ajuster la page courante si nécessaire
180
+ if (this.currentPage >= this.pages.length && this.pages.length > 0) {
181
+ this.currentPage = this.pages.length - 1;
182
+ }
183
+
184
+ this.updateNavButtons();
185
+ this.updateChildrenPositions();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Nombre total de pages
191
+ * @returns {number}
192
+ */
193
+ get totalPages() {
194
+ return this.pages.length;
195
+ }
196
+
197
+ /**
198
+ * Met à jour les positions de tous les enfants de la page courante
199
+ * @private
200
+ */
201
+ updateChildrenPositions() {
202
+ if (this.currentPage >= this.pages.length) return;
203
+
204
+ const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
205
+ const pageY = this.y + navTopHeight;
206
+
207
+ const currentPageChildren = this.pages[this.currentPage];
208
+ let currentY = pageY + this.padding;
209
+
210
+ for (const child of currentPageChildren) {
211
+ // Positionner l'enfant
212
+ child.x = this.x + this.padding;
213
+ child.y = currentY;
214
+
215
+ // Ajuster la largeur si nécessaire (prend toute la largeur disponible)
216
+ if (!child.width || child.width === 0) {
217
+ child.width = this.width - (this.padding * 2);
218
+ }
219
+
220
+ // Incrémenter Y pour le prochain enfant
221
+ currentY += (child.height || 0) + this.gap;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Change de page
227
+ * @param {number} pageIndex - Index de la nouvelle page
228
+ */
229
+ goToPage(pageIndex) {
230
+ if (pageIndex >= 0 && pageIndex < this.totalPages && pageIndex !== this.currentPage) {
231
+ this.currentPage = pageIndex;
232
+ this.updateChildrenPositions();
233
+ this.updateNavButtons();
234
+ this.onPageChange(this.currentPage, this.totalPages);
235
+ if (this.framework && this.framework.redraw) {
236
+ this.framework.redraw();
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Page suivante
243
+ */
244
+ nextPage() {
245
+ if (this.currentPage < this.totalPages - 1) {
246
+ this.goToPage(this.currentPage + 1);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Page précédente
252
+ */
253
+ previousPage() {
254
+ if (this.currentPage > 0) {
255
+ this.goToPage(this.currentPage - 1);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Met à jour les boutons de navigation
261
+ * @private
262
+ */
263
+ updateNavButtons() {
264
+ this.navButtons = [];
265
+
266
+ if (!this.showNavigation || this.totalPages === 0) return;
267
+
268
+ const buttonSize = this.platform === 'material' ? 40 : 36;
269
+ const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
270
+
271
+ // Position des boutons
272
+ const leftX = this.x + 16;
273
+ const rightX = this.x + this.width - 16 - buttonSize;
274
+
275
+ // Navigation du bas (par défaut)
276
+ if (this.navPosition === 'bottom' || this.navPosition === 'both') {
277
+ const navBottomY = this.y + navTopHeight + this.pageHeight;
278
+ const buttonY = navBottomY + (this.navHeight - buttonSize) / 2;
279
+
280
+ this.navButtons.push({
281
+ x: leftX,
282
+ y: buttonY,
283
+ size: buttonSize,
284
+ action: 'prev',
285
+ disabled: this.currentPage === 0
286
+ });
287
+
288
+ this.navButtons.push({
289
+ x: rightX,
290
+ y: buttonY,
291
+ size: buttonSize,
292
+ action: 'next',
293
+ disabled: this.currentPage === this.totalPages - 1
294
+ });
295
+ }
296
+
297
+ // Navigation du haut
298
+ if (this.navPosition === 'top' || this.navPosition === 'both') {
299
+ const navTopY = this.y;
300
+ const buttonY = navTopY + (this.navHeight - buttonSize) / 2;
301
+
302
+ this.navButtons.push({
303
+ x: leftX,
304
+ y: buttonY,
305
+ size: buttonSize,
306
+ action: 'prev',
307
+ disabled: this.currentPage === 0
308
+ });
309
+
310
+ this.navButtons.push({
311
+ x: rightX,
312
+ y: buttonY,
313
+ size: buttonSize,
314
+ action: 'next',
315
+ disabled: this.currentPage === this.totalPages - 1
316
+ });
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Dessine l'ombre
322
+ * @private
323
+ */
324
+ drawShadow(ctx) {
325
+ if (this.elevation === 0) return;
326
+
327
+ ctx.save();
328
+
329
+ ctx.shadowColor = this._computedShadowColor;
330
+ ctx.shadowBlur = this.shadowBlur;
331
+ ctx.shadowOffsetX = 0;
332
+ ctx.shadowOffsetY = this.shadowOffsetY;
333
+
334
+ ctx.fillStyle = this.bgColor;
335
+
336
+ if (this.borderRadius > 0) {
337
+ ctx.beginPath();
338
+ const spread = this.shadowSpread;
339
+ this.roundRect(
340
+ ctx,
341
+ this.x - spread / 2,
342
+ this.y - spread / 2,
343
+ this.width + spread,
344
+ this.height + spread,
345
+ this.borderRadius + spread / 2
346
+ );
347
+ ctx.fill();
348
+ } else {
349
+ const spread = this.shadowSpread;
350
+ ctx.fillRect(
351
+ this.x - spread / 2,
352
+ this.y - spread / 2,
353
+ this.width + spread,
354
+ this.height + spread
355
+ );
356
+ }
357
+
358
+ ctx.restore();
359
+ }
360
+
361
+ /**
362
+ * Dessine la navigation Material
363
+ * @private
364
+ */
365
+ drawMaterialNavigation(ctx, navY) {
366
+ // Background navigation
367
+ ctx.fillStyle = this.navColor;
368
+ ctx.fillRect(this.x, navY, this.width, this.navHeight);
369
+
370
+ // Ligne de séparation
371
+ ctx.strokeStyle = this.borderColor;
372
+ ctx.lineWidth = 1;
373
+ ctx.beginPath();
374
+ ctx.moveTo(this.x, navY);
375
+ ctx.lineTo(this.x + this.width, navY);
376
+ ctx.stroke();
377
+
378
+ // Boutons navigation
379
+ this.drawMaterialButtons(ctx, navY);
380
+
381
+ // Compteur de pages (centré)
382
+ ctx.fillStyle = this.inactiveColor;
383
+ ctx.font = '14px Roboto, sans-serif';
384
+ ctx.textAlign = 'center';
385
+ ctx.textBaseline = 'middle';
386
+ ctx.fillText(
387
+ `Page ${this.currentPage + 1} / ${this.totalPages}`,
388
+ this.x + this.width / 2,
389
+ navY + this.navHeight / 2
390
+ );
391
+ }
392
+
393
+ /**
394
+ * Dessine les boutons Material avec effet ripple (taille doublée)
395
+ * @private
396
+ */
397
+ drawMaterialButtons(ctx, navY) {
398
+ const now = Date.now();
399
+
400
+ for (const btn of this.navButtons) {
401
+ // Ne dessiner que les boutons de cette barre de navigation
402
+ const isThisNav = Math.abs(btn.y - (navY + (this.navHeight - btn.size) / 2)) < 1;
403
+ if (!isThisNav) continue;
404
+
405
+ const isHovered = this.hoveredButton === btn.action;
406
+ const isPressed = this.rippleButton === btn.action;
407
+
408
+ ctx.save();
409
+
410
+ // Centre du bouton
411
+ const centerX = btn.x + btn.size / 2;
412
+ const centerY = btn.y + btn.size / 2;
413
+ const buttonRadius = btn.size / 2;
414
+
415
+ // Masque circulaire (garde le ripple à l'intérieur du bouton)
416
+ ctx.beginPath();
417
+ ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
418
+ ctx.clip();
419
+
420
+ // Background du bouton
421
+ if (btn.disabled) {
422
+ ctx.fillStyle = this.buttonDisabledColor;
423
+ } else {
424
+ ctx.fillStyle = this.buttonBgColor;
425
+ }
426
+ ctx.beginPath();
427
+ ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
428
+ ctx.fill();
429
+
430
+ // EFFET RIPPLE - TAILLE DOUBLÉE
431
+ if (isPressed && !btn.disabled && this.rippleStartTime) {
432
+ const elapsed = now - this.rippleStartTime;
433
+
434
+ if (elapsed < 300) { // Pendant les 300ms
435
+ // Progression de l'animation (0 à 1)
436
+ const progress = elapsed / 300;
437
+
438
+ // 🌟 TAILLE DOUBLÉE: le ripple atteint le DIAMÈTRE complet (btn.size)
439
+ // Au lieu de rayon (btn.size/2), on utilise le diamètre
440
+ const maxRippleRadius = btn.size; // Double de la taille normale
441
+ const rippleRadius = maxRippleRadius * Math.min(progress, 1);
442
+
443
+ // Opacité: forte au début, faible à la fin
444
+ const opacity = 0.5 * (1 - progress * 0.7);
445
+
446
+ // Cercle ripple (plus grand que le bouton, mais clipé)
447
+ ctx.fillStyle = this.hexToRgba(this.activeColor, opacity);
448
+ ctx.beginPath();
449
+ ctx.arc(centerX, centerY, rippleRadius, 0, Math.PI * 2);
450
+ ctx.fill();
451
+
452
+ // Forcer un redessin
453
+ if (this.framework && this.framework.redraw) {
454
+ setTimeout(() => {
455
+ this.framework.redraw();
456
+ }, 16);
457
+ }
458
+ }
459
+ }
460
+
461
+ // Effet hover
462
+ if (isHovered && !btn.disabled && !isPressed) {
463
+ ctx.fillStyle = this.hexToRgba(this.activeColor, 0.1);
464
+ ctx.beginPath();
465
+ ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
466
+ ctx.fill();
467
+ }
468
+
469
+ ctx.restore();
470
+
471
+ // Bordure et icône (sans clip)
472
+ ctx.save();
473
+
474
+ // Bordure
475
+ ctx.strokeStyle = btn.disabled ? '#E0E0E0' : this.borderColor;
476
+ ctx.lineWidth = 1;
477
+ ctx.beginPath();
478
+ ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
479
+ ctx.stroke();
480
+
481
+ // Icône
482
+ ctx.fillStyle = btn.disabled ? '#BDBDBD' : this.activeColor;
483
+ ctx.font = '28px Roboto, sans-serif';
484
+ ctx.textAlign = 'center';
485
+ ctx.textBaseline = 'middle';
486
+ ctx.fillText(
487
+ btn.action === 'prev' ? '‹' : '›',
488
+ centerX,
489
+ centerY
490
+ );
491
+
492
+ ctx.restore();
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Dessine la navigation Cupertino (iOS)
498
+ * @private
499
+ */
500
+ drawCupertinoNavigation(ctx, navY) {
501
+ // Background
502
+ ctx.fillStyle = this.navColor;
503
+ ctx.fillRect(this.x, navY, this.width, this.navHeight);
504
+
505
+ // Ligne de séparation
506
+ ctx.strokeStyle = this.borderColor;
507
+ ctx.lineWidth = 0.5;
508
+ ctx.beginPath();
509
+ ctx.moveTo(this.x, navY);
510
+ ctx.lineTo(this.x + this.width, navY);
511
+ ctx.stroke();
512
+
513
+ // Boutons navigation
514
+ this.drawCupertinoButtons(ctx, navY);
515
+
516
+ // Compteur de pages (centré)
517
+ ctx.fillStyle = this.inactiveColor;
518
+ ctx.font = '15px -apple-system, BlinkMacSystemFont, sans-serif';
519
+ ctx.textAlign = 'center';
520
+ ctx.textBaseline = 'middle';
521
+ ctx.fillText(
522
+ `Page ${this.currentPage + 1} / ${this.totalPages}`,
523
+ this.x + this.width / 2,
524
+ navY + this.navHeight / 2
525
+ );
526
+ }
527
+
528
+ /**
529
+ * Dessine les boutons Cupertino (iOS)
530
+ * @private
531
+ */
532
+ drawCupertinoButtons(ctx, navY) {
533
+ for (const btn of this.navButtons) {
534
+ // Ne dessiner que les boutons de cette barre de navigation
535
+ const isThisNav = Math.abs(btn.y - (navY + (this.navHeight - btn.size) / 2)) < 1;
536
+ if (!isThisNav) continue;
537
+
538
+ const isPressed = this.pressedButton === btn.action;
539
+
540
+ ctx.save();
541
+
542
+ // Background iOS style
543
+ if (btn.disabled) {
544
+ ctx.fillStyle = this.buttonDisabledColor;
545
+ } else {
546
+ ctx.fillStyle = this.buttonBgColor;
547
+ }
548
+
549
+ // Rectangle arrondi (iOS style)
550
+ const padding = 2;
551
+ ctx.beginPath();
552
+ this.roundRect(
553
+ ctx,
554
+ btn.x + padding,
555
+ btn.y + padding,
556
+ btn.size - padding * 2,
557
+ btn.size - padding * 2,
558
+ (btn.size - padding * 2) / 2
559
+ );
560
+ ctx.fill();
561
+
562
+ // Bordure légère
563
+ if (!btn.disabled) {
564
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
565
+ ctx.lineWidth = 0.5;
566
+ ctx.stroke();
567
+ }
568
+
569
+ // Effet pressed
570
+ if (isPressed && !btn.disabled) {
571
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
572
+ ctx.fill();
573
+ }
574
+
575
+ // Icône chevron iOS
576
+ const chevronColor = btn.disabled ? this.inactiveColor : this.activeColor;
577
+ this.drawChevron(
578
+ ctx,
579
+ btn.x + btn.size / 2,
580
+ btn.y + btn.size / 2,
581
+ btn.action === 'prev' ? 'left' : 'right',
582
+ chevronColor
583
+ );
584
+
585
+ ctx.restore();
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Dessine un chevron iOS
591
+ * @private
592
+ */
593
+ drawChevron(ctx, cx, cy, direction, color) {
594
+ const size = 10;
595
+
596
+ ctx.save();
597
+ ctx.strokeStyle = color;
598
+ ctx.lineWidth = 2.5;
599
+ ctx.lineCap = 'round';
600
+ ctx.lineJoin = 'round';
601
+
602
+ ctx.beginPath();
603
+ if (direction === 'left') {
604
+ ctx.moveTo(cx + size / 2, cy - size / 2);
605
+ ctx.lineTo(cx - size / 2, cy);
606
+ ctx.lineTo(cx + size / 2, cy + size / 2);
607
+ } else {
608
+ ctx.moveTo(cx - size / 2, cy - size / 2);
609
+ ctx.lineTo(cx + size / 2, cy);
610
+ ctx.lineTo(cx - size / 2, cy + size / 2);
611
+ }
612
+ ctx.stroke();
613
+ ctx.restore();
614
+ }
615
+
616
+ /**
617
+ * Dessine le composant - UNIQUEMENT LA PAGE COURANTE
618
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
619
+ */
620
+ draw(ctx) {
621
+ ctx.save();
622
+
623
+ // Ombre
624
+ if (this.platform === 'material') {
625
+ this.drawShadow(ctx);
626
+ }
627
+
628
+ // Background principal
629
+ ctx.fillStyle = this.bgColor;
630
+ if (this.borderRadius > 0) {
631
+ ctx.beginPath();
632
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
633
+ ctx.fill();
634
+ } else {
635
+ ctx.fillRect(this.x, this.y, this.width, this.height);
636
+ }
637
+
638
+ // Navigation du haut
639
+ if (this.showNavigation && (this.navPosition === 'top' || this.navPosition === 'both')) {
640
+ if (this.platform === 'material') {
641
+ this.drawMaterialNavigation(ctx, this.y);
642
+ } else {
643
+ this.drawCupertinoNavigation(ctx, this.y);
644
+ }
645
+ }
646
+
647
+ // Zone de contenu avec clipping
648
+ const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
649
+ const pageY = this.y + navTopHeight;
650
+
651
+ ctx.save();
652
+ ctx.beginPath();
653
+ ctx.rect(this.x, pageY, this.width, this.pageHeight);
654
+ ctx.clip();
655
+
656
+ // Dessiner UNIQUEMENT les enfants de la page courante
657
+ if (this.currentPage < this.pages.length) {
658
+ const currentPageChildren = this.pages[this.currentPage];
659
+ for (const child of currentPageChildren) {
660
+ if (child.visible !== false && child.draw) {
661
+ child.draw(ctx);
662
+ }
663
+ }
664
+ }
665
+
666
+ ctx.restore();
667
+
668
+ // Navigation du bas
669
+ if (this.showNavigation && (this.navPosition === 'bottom' || this.navPosition === 'both')) {
670
+ const navBottomY = this.y + navTopHeight + this.pageHeight;
671
+ if (this.platform === 'material') {
672
+ this.drawMaterialNavigation(ctx, navBottomY);
673
+ } else {
674
+ this.drawCupertinoNavigation(ctx, navBottomY);
675
+ }
676
+ }
677
+
678
+ ctx.restore();
679
+ }
680
+
681
+ /**
682
+ * Gère le survol (pour hover effect)
683
+ * @param {number} x - Coordonnée X
684
+ * @param {number} y - Coordonnée Y
685
+ */
686
+ handleHover(x, y) {
687
+ if (!this.showNavigation) return;
688
+
689
+ let newHovered = null;
690
+
691
+ for (const btn of this.navButtons) {
692
+ if (btn.disabled) continue;
693
+
694
+ const margin = 10;
695
+ if (x >= (btn.x - margin) &&
696
+ x <= (btn.x + btn.size + margin) &&
697
+ y >= (btn.y - margin) &&
698
+ y <= (btn.y + btn.size + margin)) {
699
+ newHovered = btn.action;
700
+ break;
701
+ }
702
+ }
703
+
704
+ if (newHovered !== this.hoveredButton) {
705
+ this.hoveredButton = newHovered;
706
+ if (this.framework && this.framework.redraw) {
707
+ this.framework.redraw();
708
+ }
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Override setPosition pour mettre à jour les enfants
714
+ */
715
+ setPosition(x, y) {
716
+ super.setPosition(x, y);
717
+ this.updateChildrenPositions();
718
+ this.updateNavButtons();
719
+ }
720
+
721
+ /**
722
+ * Utilitaire: Convertit hex en rgba
723
+ * @private
724
+ */
725
+ hexToRgba(hex, alpha) {
726
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
727
+ if (result) {
728
+ return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
729
+ }
730
+ return `rgba(0, 0, 0, ${alpha})`;
731
+ }
732
+
733
+ roundRect(ctx, x, y, width, height, radius) {
734
+ ctx.beginPath();
735
+ ctx.moveTo(x + radius, y);
736
+ ctx.lineTo(x + width - radius, y);
737
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
738
+ ctx.lineTo(x + width, y + height - radius);
739
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
740
+ ctx.lineTo(x + radius, y + height);
741
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
742
+ ctx.lineTo(x, y + radius);
743
+ ctx.quadraticCurveTo(x, y, x + radius, y);
744
+ ctx.closePath();
745
+ }
746
+
747
+ isPointInside(x, y) {
748
+ const isInside = x >= this.x &&
749
+ x <= this.x + this.width &&
750
+ y >= this.y &&
751
+ y <= this.y + this.height;
752
+
753
+ if (isInside) {
754
+ if (this.navButtons && this.navButtons.length >= 2) {
755
+ const leftButton = this.navButtons[0];
756
+ const rightButton = this.navButtons[1];
757
+
758
+ const tolerance = 40;
759
+
760
+ // Bouton gauche
761
+ if (Math.abs(x - leftButton.x) < tolerance &&
762
+ Math.abs(y - leftButton.y) < tolerance) {
763
+
764
+ if (!leftButton.disabled) {
765
+ console.log("🎯 Ripple sur bouton gauche"); // LOG DE DEBUG
766
+
767
+ // EFFET RIPPLE
768
+ this.rippleButton = 'prev';
769
+ this.rippleStartTime = Date.now();
770
+
771
+ // Redessiner immédiatement
772
+ if (this.framework && this.framework.redraw) {
773
+ this.framework.redraw();
774
+ }
775
+
776
+ // Action après un petit délai
777
+ setTimeout(() => {
778
+ if (this.currentPage > 0) {
779
+ this.currentPage--;
780
+ this.updateChildrenPositions();
781
+ this.updateNavButtons();
782
+ }
783
+ }, 100);
784
+
785
+ // Reset ripple après animation
786
+ setTimeout(() => {
787
+ this.rippleButton = null;
788
+ this.rippleStartTime = null;
789
+ if (this.framework && this.framework.redraw) {
790
+ this.framework.redraw();
791
+ }
792
+ }, 350);
793
+ }
794
+ return true;
795
+ }
796
+
797
+ // Bouton droit
798
+ if (Math.abs(x - rightButton.x) < tolerance &&
799
+ Math.abs(y - rightButton.y) < tolerance) {
800
+
801
+ if (!rightButton.disabled) {
802
+ console.log("🎯 Ripple sur bouton droit"); // LOG DE DEBUG
803
+
804
+ // EFFET RIPPLE
805
+ this.rippleButton = 'next';
806
+ this.rippleStartTime = Date.now();
807
+
808
+ // Redessiner immédiatement
809
+ if (this.framework && this.framework.redraw) {
810
+ this.framework.redraw();
811
+ }
812
+
813
+ // Action après un petit délai
814
+ setTimeout(() => {
815
+ if (this.currentPage < this.totalPages - 1) {
816
+ this.currentPage++;
817
+ this.updateChildrenPositions();
818
+ this.updateNavButtons();
819
+ }
820
+ }, 100);
821
+
822
+ // Reset ripple après animation
823
+ setTimeout(() => {
824
+ this.rippleButton = null;
825
+ this.rippleStartTime = null;
826
+ if (this.framework && this.framework.redraw) {
827
+ this.framework.redraw();
828
+ }
829
+ }, 350);
830
+ }
831
+ return true;
832
+ }
833
+ }
834
+ }
835
+
836
+ return isInside;
837
+ }
838
+ }
839
+
840
+ export default PaginatedContainer;
@@ -60,6 +60,7 @@ import FloatedCamera from '../components/FloatedCamera.js';
60
60
  import TimePicker from '../components/TimePicker.js';
61
61
  import QRCodeReader from '../components/QRCodeReader.js';
62
62
  import QRCodeGenerator from '../components/QRCodeGenerator.js';
63
+ import PaginatedContainer from '../components/PaginatedContainer.js';
63
64
 
64
65
  // Utils
65
66
  import SafeArea from '../utils/SafeArea.js';
package/core/UIBuilder.js CHANGED
@@ -60,6 +60,7 @@ import FloatedCamera from '../components/FloatedCamera.js';
60
60
  import TimePicker from '../components/TimePicker.js';
61
61
  import QRCodeReader from '../components/QRCodeReader.js';
62
62
  import QRCodeGenerator from '../components/QRCodeGenerator.js';
63
+ import PaginatedContainer from '../components/PaginatedContainer.js';
63
64
 
64
65
  // Features
65
66
  import PullToRefresh from '../features/PullToRefresh.js';
@@ -145,6 +146,7 @@ const Components = {
145
146
  Column,
146
147
  Positioned,
147
148
  Banner,
149
+ PaginatedContainer,
148
150
  Chart,
149
151
  QRCodeGenerator,
150
152
  Stack
package/index.js CHANGED
@@ -67,6 +67,7 @@ export { default as FloatedCamera } from './components/FloatedCamera.js';
67
67
  export { default as TimePicker } from './components/TimePicker.js';
68
68
  export { default as QRCodeReader } from './components/QRCodeReader.js';
69
69
  export { default as QRCodeGenerator } from './components/QRCodeGenerator.js';
70
+ export { default as PaginatedContainer } from './components/PaginatedContainer.js';
70
71
 
71
72
  // Utils
72
73
  export { default as SafeArea } from './utils/SafeArea.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.58",
3
+ "version": "0.5.59",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"