canvasframework 0.5.57 → 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';
@@ -374,7 +375,12 @@ class CanvasFramework {
374
375
  };
375
376
  this._firstRenderDone = false;
376
377
  this._startupStartTime = startTime;
378
+ // Dans le constructeur, après this.scrollFriction = 0.95;
379
+ this.scrollFriction = 0.95;
377
380
 
381
+ // ✅ AJOUTER
382
+ this.overscrollDistance = 0;
383
+ this.isOverscrollAnimating = false;
378
384
  // ✅ Créer automatiquement le canvas
379
385
  this.canvas = document.createElement('canvas');
380
386
  this.canvas.id = canvasId || `canvas-${Date.now()}`;
@@ -620,6 +626,7 @@ class CanvasFramework {
620
626
  if (this.optimizations.useSpatialPartitioning) {
621
627
  this._initSpatialPartitioning();
622
628
  }
629
+
623
630
  }
624
631
 
625
632
  /**
@@ -688,7 +695,7 @@ class CanvasFramework {
688
695
  * Crée le Worker pour le calcul du scroll
689
696
  */
690
697
  createScrollWorker() {
691
- const workerCode = `
698
+ const workerCode = `
692
699
  let state = {
693
700
  scrollOffset: 0,
694
701
  scrollVelocity: 0,
@@ -697,7 +704,10 @@ class CanvasFramework {
697
704
  maxScroll: 0,
698
705
  height: 0,
699
706
  lastTouchY: 0,
700
- components: []
707
+ components: [],
708
+ overscrollDistance: 0,
709
+ maxOverscroll: 150, // ✅ Limite maximale
710
+ overscrollResistance: 0.3
701
711
  };
702
712
 
703
713
  const FIXED_COMPONENT_TYPES = [
@@ -708,13 +718,11 @@ class CanvasFramework {
708
718
 
709
719
  const calculateMaxScroll = () => {
710
720
  let maxY = 0;
711
-
712
721
  for (const comp of state.components) {
713
722
  if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
714
723
  const bottom = comp.y + comp.height;
715
724
  if (bottom > maxY) maxY = bottom;
716
725
  }
717
-
718
726
  return Math.max(0, maxY - state.height + 50);
719
727
  };
720
728
 
@@ -722,7 +730,29 @@ class CanvasFramework {
722
730
  if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
723
731
  state.scrollOffset += state.scrollVelocity;
724
732
  state.maxScroll = calculateMaxScroll();
725
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
733
+
734
+ // ✅ CORRIGER: Limiter l'overscroll pendant l'inertie
735
+ if (state.scrollOffset > 0) {
736
+ // Limiter à maxOverscroll
737
+ if (state.scrollOffset > state.maxOverscroll) {
738
+ state.scrollOffset = state.maxOverscroll;
739
+ state.scrollVelocity = 0;
740
+ }
741
+ state.overscrollDistance = state.scrollOffset;
742
+ state.scrollVelocity *= 0.85;
743
+ } else if (state.scrollOffset < -state.maxScroll) {
744
+ const excess = Math.abs(state.scrollOffset + state.maxScroll);
745
+ // Limiter à maxOverscroll
746
+ if (excess > state.maxOverscroll) {
747
+ state.scrollOffset = -state.maxScroll - state.maxOverscroll;
748
+ state.scrollVelocity = 0;
749
+ }
750
+ state.overscrollDistance = state.scrollOffset + state.maxScroll;
751
+ state.scrollVelocity *= 0.85;
752
+ } else {
753
+ state.overscrollDistance = 0;
754
+ }
755
+
726
756
  state.scrollVelocity *= state.scrollFriction;
727
757
  } else {
728
758
  state.scrollVelocity = 0;
@@ -731,20 +761,82 @@ class CanvasFramework {
731
761
  return {
732
762
  scrollOffset: state.scrollOffset,
733
763
  scrollVelocity: state.scrollVelocity,
734
- maxScroll: state.maxScroll
764
+ maxScroll: state.maxScroll,
765
+ overscrollDistance: state.overscrollDistance
735
766
  };
736
767
  };
737
768
 
738
769
  const handleTouchMove = (deltaY) => {
739
770
  if (state.isDragging) {
740
- state.scrollOffset += deltaY;
741
771
  state.maxScroll = calculateMaxScroll();
742
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
772
+ const wouldBeOffset = state.scrollOffset + deltaY;
773
+
774
+ let actualDelta = deltaY;
775
+
776
+ // ✅ CORRIGER: Appliquer les limites d'overscroll
777
+ if (wouldBeOffset > 0) {
778
+ // Overscroll en haut
779
+ const currentOverscroll = state.scrollOffset > 0 ? state.scrollOffset : 0;
780
+
781
+ // Si on a déjà atteint la limite, ne plus bouger
782
+ if (currentOverscroll >= state.maxOverscroll) {
783
+ actualDelta = 0;
784
+ state.overscrollDistance = state.maxOverscroll;
785
+ } else {
786
+ // Calculer la résistance progressive
787
+ const overscrollRatio = currentOverscroll / state.maxOverscroll;
788
+ const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
789
+
790
+ actualDelta = deltaY * resistance;
791
+
792
+ // S'assurer qu'on ne dépasse pas la limite
793
+ const newOverscroll = currentOverscroll + actualDelta;
794
+ if (newOverscroll > state.maxOverscroll) {
795
+ actualDelta = state.maxOverscroll - currentOverscroll;
796
+ }
797
+
798
+ state.overscrollDistance = currentOverscroll + actualDelta;
799
+ }
800
+ } else if (wouldBeOffset < -state.maxScroll) {
801
+ // Overscroll en bas
802
+ const currentOverscroll = state.scrollOffset < -state.maxScroll
803
+ ? Math.abs(state.scrollOffset + state.maxScroll)
804
+ : 0;
805
+
806
+ // Si on a déjà atteint la limite, ne plus bouger
807
+ if (currentOverscroll >= state.maxOverscroll) {
808
+ actualDelta = 0;
809
+ state.overscrollDistance = -state.maxOverscroll;
810
+ } else {
811
+ // Calculer la résistance progressive
812
+ const overscrollRatio = currentOverscroll / state.maxOverscroll;
813
+ const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
814
+
815
+ actualDelta = deltaY * resistance;
816
+
817
+ // S'assurer qu'on ne dépasse pas la limite
818
+ const newOverscroll = currentOverscroll + Math.abs(actualDelta);
819
+ if (newOverscroll > state.maxOverscroll) {
820
+ actualDelta = deltaY > 0
821
+ ? state.maxOverscroll - currentOverscroll
822
+ : -(state.maxOverscroll - currentOverscroll);
823
+ }
824
+
825
+ state.overscrollDistance = -(currentOverscroll + Math.abs(actualDelta));
826
+ }
827
+ } else {
828
+ // Scroll normal
829
+ state.overscrollDistance = 0;
830
+ }
831
+
832
+ state.scrollOffset += actualDelta;
743
833
  state.scrollVelocity = deltaY;
834
+
744
835
  return {
745
836
  scrollOffset: state.scrollOffset,
746
837
  scrollVelocity: state.scrollVelocity,
747
- maxScroll: state.maxScroll
838
+ maxScroll: state.maxScroll,
839
+ overscrollDistance: state.overscrollDistance
748
840
  };
749
841
  }
750
842
  return null;
@@ -755,16 +847,14 @@ class CanvasFramework {
755
847
 
756
848
  switch (type) {
757
849
  case 'INIT':
758
- state = {
759
- ...state,
760
- ...payload
761
- };
850
+ state = { ...state, ...payload };
762
851
  state.maxScroll = calculateMaxScroll();
763
852
  self.postMessage({
764
853
  type: 'INITIALIZED',
765
854
  payload: {
766
855
  scrollOffset: state.scrollOffset,
767
- maxScroll: state.maxScroll
856
+ maxScroll: state.maxScroll,
857
+ overscrollDistance: 0
768
858
  }
769
859
  });
770
860
  break;
@@ -817,12 +907,43 @@ class CanvasFramework {
817
907
  case 'SET_SCROLL_OFFSET':
818
908
  state.scrollOffset = payload.scrollOffset;
819
909
  state.maxScroll = calculateMaxScroll();
820
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
910
+ state.overscrollDistance = 0;
911
+ self.postMessage({
912
+ type: 'SCROLL_UPDATED',
913
+ payload: {
914
+ scrollOffset: state.scrollOffset,
915
+ maxScroll: state.maxScroll,
916
+ overscrollDistance: 0
917
+ }
918
+ });
919
+ break;
920
+
921
+ case 'ANIMATE_RETURN':
922
+ state.maxScroll = calculateMaxScroll();
923
+
924
+ if (state.scrollOffset > 0) {
925
+ state.scrollOffset *= 0.75;
926
+ state.overscrollDistance = state.scrollOffset;
927
+ } else if (state.scrollOffset < -state.maxScroll) {
928
+ const diff = state.scrollOffset + state.maxScroll;
929
+ state.scrollOffset = -state.maxScroll + (diff * 0.75);
930
+ state.overscrollDistance = diff * 0.75;
931
+ }
932
+
933
+ const shouldContinue = Math.abs(state.overscrollDistance) > 1;
934
+
935
+ if (!shouldContinue) {
936
+ state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
937
+ state.overscrollDistance = 0;
938
+ }
939
+
821
940
  self.postMessage({
822
941
  type: 'SCROLL_UPDATED',
823
942
  payload: {
824
943
  scrollOffset: state.scrollOffset,
825
- maxScroll: state.maxScroll
944
+ maxScroll: state.maxScroll,
945
+ overscrollDistance: state.overscrollDistance,
946
+ shouldContinue
826
947
  }
827
948
  });
828
949
  break;
@@ -834,7 +955,8 @@ class CanvasFramework {
834
955
  scrollOffset: state.scrollOffset,
835
956
  scrollVelocity: state.scrollVelocity,
836
957
  maxScroll: state.maxScroll,
837
- isDragging: state.isDragging
958
+ isDragging: state.isDragging,
959
+ overscrollDistance: state.overscrollDistance
838
960
  }
839
961
  });
840
962
  break;
@@ -842,64 +964,71 @@ class CanvasFramework {
842
964
  };
843
965
  `;
844
966
 
845
- const blob = new Blob([workerCode], {
846
- type: 'application/javascript'
847
- });
848
- return new Worker(URL.createObjectURL(blob));
849
- }
967
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
968
+ return new Worker(URL.createObjectURL(blob));
969
+ }
850
970
 
851
971
  /**
852
972
  * Gère les messages du Scroll Worker
853
973
  */
854
974
  handleScrollWorkerMessage(e) {
855
- const {
856
- type,
857
- payload
858
- } = e.data;
975
+ const { type, payload } = e.data;
859
976
 
860
- switch (type) {
861
- case 'SCROLL_UPDATED':
862
- this.scrollOffset = payload.scrollOffset;
863
- this.scrollVelocity = payload.scrollVelocity;
864
- // ✅ CORRECTION IMPORTANTE : Vider le cache dirty pendant le scroll
865
- if (Math.abs(payload.scrollVelocity) > 0.5) {
866
- this.dirtyComponents.clear();
867
- }
868
- // Mettre à jour le cache
869
- this._cachedMaxScroll = payload.maxScroll;
870
- this._maxScrollDirty = false;
871
-
872
- // Marquer les composants comme sales pour mise à jour visuelle
873
- if (Math.abs(payload.scrollVelocity) > 0) {
874
- this.components.forEach(comp => {
875
- if (!this.isFixedComponent(comp)) {
876
- this.markComponentDirty(comp);
877
- }
878
- });
879
- }
880
- break;
977
+ switch (type) {
978
+ case 'SCROLL_UPDATED':
979
+ this.scrollOffset = payload.scrollOffset;
980
+ this.scrollVelocity = payload.scrollVelocity;
981
+
982
+ // AJOUTER
983
+ this.overscrollDistance = payload.overscrollDistance || 0;
984
+
985
+ if (Math.abs(payload.scrollVelocity) > 0.5) {
986
+ this.dirtyComponents.clear();
987
+ }
988
+
989
+ this._cachedMaxScroll = payload.maxScroll;
990
+ this._maxScrollDirty = false;
881
991
 
882
- case 'MAX_SCROLL_UPDATED':
883
- this._cachedMaxScroll = payload.maxScroll;
884
- this._maxScrollDirty = false;
885
- break;
992
+ if (Math.abs(payload.scrollVelocity) > 0) {
993
+ this.components.forEach(comp => {
994
+ if (!this.isFixedComponent(comp)) {
995
+ this.markComponentDirty(comp);
996
+ }
997
+ });
998
+ }
999
+
1000
+ // ✅ AJOUTER: Continuer l'animation de retour si nécessaire
1001
+ if (payload.shouldContinue !== undefined && payload.shouldContinue) {
1002
+ requestAnimationFrame(() => {
1003
+ this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
1004
+ });
1005
+ } else if (payload.shouldContinue === false) {
1006
+ this.isOverscrollAnimating = false;
1007
+ }
1008
+ break;
886
1009
 
887
- case 'INITIALIZED':
888
- this.scrollOffset = payload.scrollOffset;
889
- this._cachedMaxScroll = payload.maxScroll;
890
- this._maxScrollDirty = false;
891
- break;
1010
+ case 'MAX_SCROLL_UPDATED':
1011
+ this._cachedMaxScroll = payload.maxScroll;
1012
+ this._maxScrollDirty = false;
1013
+ break;
892
1014
 
893
- case 'STATE':
894
- // Synchroniser l'état local
895
- this.scrollOffset = payload.scrollOffset;
896
- this.scrollVelocity = payload.scrollVelocity;
897
- this.isDragging = payload.isDragging;
898
- this._cachedMaxScroll = payload.maxScroll;
899
- this._maxScrollDirty = false;
900
- break;
901
- }
1015
+ case 'INITIALIZED':
1016
+ this.scrollOffset = payload.scrollOffset;
1017
+ this._cachedMaxScroll = payload.maxScroll;
1018
+ this._maxScrollDirty = false;
1019
+ this.overscrollDistance = 0; // ✅ AJOUTER
1020
+ break;
1021
+
1022
+ case 'STATE':
1023
+ this.scrollOffset = payload.scrollOffset;
1024
+ this.scrollVelocity = payload.scrollVelocity;
1025
+ this.isDragging = payload.isDragging;
1026
+ this._cachedMaxScroll = payload.maxScroll;
1027
+ this._maxScrollDirty = false;
1028
+ this.overscrollDistance = payload.overscrollDistance || 0; // ✅ AJOUTER
1029
+ break;
902
1030
  }
1031
+ }
903
1032
 
904
1033
  /**
905
1034
  * Initialise le Scroll Worker avec les données actuelles
@@ -2311,23 +2440,29 @@ class CanvasFramework {
2311
2440
  }
2312
2441
 
2313
2442
  handleTouchEnd(e) {
2314
- e.preventDefault();
2315
- const touch = e.changedTouches[0];
2316
- const pos = this.getTouchPos(touch);
2443
+ e.preventDefault();
2444
+ const touch = e.changedTouches[0];
2445
+ const pos = this.getTouchPos(touch);
2317
2446
 
2318
- if (!this.isDragging) {
2319
- this.checkComponentsAtPosition(pos.x, pos.y, 'end');
2320
- } else {
2321
- this.isDragging = false;
2322
- this.scrollWorker.postMessage({
2323
- type: 'SET_DRAGGING',
2324
- payload: {
2325
- isDragging: false,
2326
- lastVelocity: this.scrollVelocity
2327
- }
2328
- });
2329
- }
2330
- }
2447
+ if (!this.isDragging) {
2448
+ this.checkComponentsAtPosition(pos.x, pos.y, 'end');
2449
+ } else {
2450
+ this.isDragging = false;
2451
+ this.scrollWorker.postMessage({
2452
+ type: 'SET_DRAGGING',
2453
+ payload: {
2454
+ isDragging: false,
2455
+ lastVelocity: this.scrollVelocity
2456
+ }
2457
+ });
2458
+
2459
+ // ✅ AJOUTER: Démarrer l'animation de retour
2460
+ if (!this.isOverscrollAnimating) {
2461
+ this.isOverscrollAnimating = true;
2462
+ this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
2463
+ }
2464
+ }
2465
+ }
2331
2466
 
2332
2467
  handleMouseDown(e) {
2333
2468
  this.isDragging = false;
@@ -2375,21 +2510,26 @@ class CanvasFramework {
2375
2510
  }
2376
2511
  }
2377
2512
 
2378
- handleMouseUp(e) {
2379
- if (!this.isDragging) {
2380
- this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
2381
- } else {
2382
- this.isDragging = false;
2383
- this.scrollWorker.postMessage({
2384
- type: 'SET_DRAGGING',
2385
- payload: {
2386
- isDragging: false,
2387
- lastVelocity: this.scrollVelocity
2388
- }
2389
- });
2513
+ handleMouseUp(e) {
2514
+ if (!this.isDragging) {
2515
+ this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
2516
+ } else {
2517
+ this.isDragging = false;
2518
+ this.scrollWorker.postMessage({
2519
+ type: 'SET_DRAGGING',
2520
+ payload: {
2521
+ isDragging: false,
2522
+ lastVelocity: this.scrollVelocity
2523
+ }
2524
+ });
2525
+
2526
+ // ✅ AJOUTER: Démarrer l'animation de retour
2527
+ if (!this.isOverscrollAnimating) {
2528
+ this.isOverscrollAnimating = true;
2529
+ this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
2390
2530
  }
2391
2531
  }
2392
-
2532
+ }
2393
2533
 
2394
2534
  getTouchPos(touch) {
2395
2535
  const rect = this.canvas.getBoundingClientRect();
@@ -2988,6 +3128,46 @@ class CanvasFramework {
2988
3128
  this.renderFull();
2989
3129
  }
2990
3130
  }
3131
+
3132
+ /**
3133
+ * Dessine l'effet d'overscroll (overlay gris)
3134
+ */
3135
+ drawOverscrollEffect() {console.log('dessine');
3136
+ if (Math.abs(this.overscrollDistance) < 1) return;
3137
+
3138
+ const ctx = this.ctx;
3139
+ ctx.save();
3140
+
3141
+ // Calculer l'opacité (max 0.4 pour Android)
3142
+ const maxOverscroll = 150;
3143
+ const opacity = Math.min(Math.abs(this.overscrollDistance) / maxOverscroll, 1) * 0.4;
3144
+
3145
+ // Hauteur de l'overlay
3146
+ const overlayHeight = Math.min(Math.abs(this.overscrollDistance) * 1.2, 250);
3147
+
3148
+ let gradient;
3149
+
3150
+ // Overscroll en haut
3151
+ if (this.overscrollDistance > 0) {
3152
+ gradient = ctx.createLinearGradient(0, 0, 0, overlayHeight);
3153
+ gradient.addColorStop(0, `rgba(100, 100, 100, ${opacity})`);
3154
+ gradient.addColorStop(1, 'rgba(100, 100, 100, 0)');
3155
+
3156
+ ctx.fillStyle = gradient;
3157
+ ctx.fillRect(0, 0, this.width, overlayHeight);
3158
+ }
3159
+ // Overscroll en bas
3160
+ else if (this.overscrollDistance < 0) {
3161
+ gradient = ctx.createLinearGradient(0, this.height - overlayHeight, 0, this.height);
3162
+ gradient.addColorStop(0, 'rgba(100, 100, 100, 0)');
3163
+ gradient.addColorStop(1, `rgba(100, 100, 100, ${opacity})`);
3164
+
3165
+ ctx.fillStyle = gradient;
3166
+ ctx.fillRect(0, this.height - overlayHeight, this.width, overlayHeight);
3167
+ }
3168
+
3169
+ ctx.restore();
3170
+ }
2991
3171
 
2992
3172
  /**
2993
3173
  * Rendu normal (sans transition)
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.57",
3
+ "version": "0.5.59",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"