canvasframework 0.5.18 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +30 -0
  2. package/components/Accordion.js +265 -0
  3. package/components/AndroidDatePickerDialog.js +406 -0
  4. package/components/AppBar.js +398 -0
  5. package/components/AudioPlayer.js +611 -0
  6. package/components/Avatar.js +202 -0
  7. package/components/Banner.js +342 -0
  8. package/components/BottomNavigationBar.js +433 -0
  9. package/components/BottomSheet.js +234 -0
  10. package/components/Button.js +358 -0
  11. package/components/Camera.js +644 -0
  12. package/components/Card.js +193 -0
  13. package/components/Chart.js +700 -0
  14. package/components/Checkbox.js +166 -0
  15. package/components/Chip.js +212 -0
  16. package/components/CircularProgress.js +327 -0
  17. package/components/ContextMenu.js +116 -0
  18. package/components/DatePicker.js +298 -0
  19. package/components/Dialog.js +337 -0
  20. package/components/Divider.js +125 -0
  21. package/components/Drawer.js +276 -0
  22. package/components/FAB.js +270 -0
  23. package/components/FileUpload.js +315 -0
  24. package/components/FloatedCamera.js +644 -0
  25. package/components/IOSDatePickerWheel.js +430 -0
  26. package/components/ImageCarousel.js +219 -0
  27. package/components/ImageComponent.js +223 -0
  28. package/components/Input.js +831 -0
  29. package/components/InputDatalist.js +723 -0
  30. package/components/InputTags.js +624 -0
  31. package/components/List.js +95 -0
  32. package/components/ListItem.js +269 -0
  33. package/components/Modal.js +364 -0
  34. package/components/MorphingFAB.js +428 -0
  35. package/components/MultiSelectDialog.js +206 -0
  36. package/components/NumberInput.js +271 -0
  37. package/components/PasswordInput.js +462 -0
  38. package/components/ProgressBar.js +88 -0
  39. package/components/QRCodeReader.js +539 -0
  40. package/components/RadioButton.js +151 -0
  41. package/components/SearchInput.js +315 -0
  42. package/components/SegmentedControl.js +357 -0
  43. package/components/Select.js +199 -0
  44. package/components/SelectDialog.js +255 -0
  45. package/components/Slider.js +113 -0
  46. package/components/SliverAppBar.js +139 -0
  47. package/components/Snackbar.js +243 -0
  48. package/components/SpeedDialFAB.js +397 -0
  49. package/components/Stepper.js +281 -0
  50. package/components/SwipeableListItem.js +327 -0
  51. package/components/Switch.js +147 -0
  52. package/components/Table.js +492 -0
  53. package/components/Tabs.js +423 -0
  54. package/components/Text.js +141 -0
  55. package/components/TextField.js +151 -0
  56. package/components/TimePicker.js +934 -0
  57. package/components/Toast.js +236 -0
  58. package/components/TreeView.js +420 -0
  59. package/components/Video.js +397 -0
  60. package/components/View.js +140 -0
  61. package/components/VirtualList.js +120 -0
  62. package/core/CanvasFramework.js +3045 -0
  63. package/core/Component.js +243 -0
  64. package/core/ThemeManager.js +358 -0
  65. package/core/UIBuilder.js +267 -0
  66. package/core/WebGLCanvasAdapter.js +782 -0
  67. package/features/Column.js +43 -0
  68. package/features/Grid.js +47 -0
  69. package/features/LayoutComponent.js +43 -0
  70. package/features/OpenStreetMap.js +310 -0
  71. package/features/Positioned.js +33 -0
  72. package/features/PullToRefresh.js +328 -0
  73. package/features/Row.js +40 -0
  74. package/features/SignaturePad.js +257 -0
  75. package/features/Skeleton.js +193 -0
  76. package/features/Stack.js +21 -0
  77. package/index.js +119 -0
  78. package/manager/AccessibilityManager.js +107 -0
  79. package/manager/ErrorHandler.js +59 -0
  80. package/manager/FeatureFlags.js +60 -0
  81. package/manager/MemoryManager.js +107 -0
  82. package/manager/PerformanceMonitor.js +84 -0
  83. package/manager/SecurityManager.js +54 -0
  84. package/package.json +22 -16
  85. package/utils/AnimationEngine.js +734 -0
  86. package/utils/CryptoManager.js +303 -0
  87. package/utils/DataStore.js +403 -0
  88. package/utils/DevTools.js +1618 -0
  89. package/utils/DevToolsConsole.js +201 -0
  90. package/utils/EventBus.js +407 -0
  91. package/utils/FetchClient.js +74 -0
  92. package/utils/FirebaseAuth.js +653 -0
  93. package/utils/FirebaseCore.js +246 -0
  94. package/utils/FirebaseFirestore.js +581 -0
  95. package/utils/FirebaseFunctions.js +97 -0
  96. package/utils/FirebaseRealtimeDB.js +498 -0
  97. package/utils/FirebaseStorage.js +612 -0
  98. package/utils/FormValidator.js +355 -0
  99. package/utils/GeoLocationService.js +62 -0
  100. package/utils/I18n.js +207 -0
  101. package/utils/IndexedDBManager.js +273 -0
  102. package/utils/InspectionOverlay.js +308 -0
  103. package/utils/NotificationManager.js +60 -0
  104. package/utils/OfflineSyncManager.js +342 -0
  105. package/utils/PayPalPayment.js +678 -0
  106. package/utils/QueryBuilder.js +478 -0
  107. package/utils/SafeArea.js +64 -0
  108. package/utils/SecureStorage.js +289 -0
  109. package/utils/StateManager.js +207 -0
  110. package/utils/StripePayment.js +552 -0
  111. package/utils/WebSocketClient.js +66 -0
  112. package/dist/canvasframework.js +0 -2
  113. package/dist/canvasframework.js.LICENSE.txt +0 -1
@@ -0,0 +1,276 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Tiroir latéral (navigation)
5
+ * @class
6
+ * @extends Component
7
+ * @property {number} targetX - Position X cible
8
+ * @property {Array} items - Items du drawer
9
+ * @property {Object|null} header - En-tête
10
+ * @property {Function} onItemClick - Callback au clic sur item
11
+ * @property {string} platform - Plateforme
12
+ * @property {boolean} animating - En cours d'animation
13
+ * @property {number} hoveredIndex - Index survolé
14
+ */
15
+ class Drawer extends Component {
16
+ /**
17
+ * Crée une instance de Drawer
18
+ * @param {CanvasFramework} framework - Framework parent
19
+ * @param {Object} [options={}] - Options de configuration
20
+ * @param {Array} [options.items=[]] - Items [{label, icon, divider}]
21
+ * @param {Object} [options.header] - En-tête {title}
22
+ * @param {Function} [options.onItemClick] - Callback au clic sur item
23
+ */
24
+ constructor(framework, options = {}) {
25
+ super(framework, {
26
+ x: -framework.width * 0.8,
27
+ y: 0,
28
+ width: framework.width * 0.8,
29
+ height: framework.height,
30
+ visible: false,
31
+ ...options
32
+ });
33
+ this.targetX = -this.width;
34
+ this.items = options.items || [];
35
+ this.header = options.header || null;
36
+ this.onItemClick = options.onItemClick;
37
+ this.platform = framework.platform;
38
+ this.animating = false;
39
+ this.hoveredIndex = -1;
40
+
41
+ // Bind des méthodes
42
+ this.handlePress = this.handlePress.bind(this);
43
+ this.handleMove = this.handleMove.bind(this);
44
+
45
+ // IMPORTANT: Définir les callbacks
46
+ this.onPress = this.handlePress;
47
+ this.onMove = this.handleMove;
48
+ // ✅ Se mettre automatiquement au-dessus de tous les composants
49
+ this.bringToFront();
50
+ }
51
+
52
+ /**
53
+ * Met le drawer au-dessus de tous les composants
54
+ * @private
55
+ */
56
+ bringToFront() {
57
+ const index = this.framework.components.indexOf(this);
58
+ if (index > -1) {
59
+ this.framework.components.splice(index, 1);
60
+ this.framework.components.push(this);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Ouvre le drawer
66
+ */
67
+ open() {
68
+ this.bringToFront(); // ✅ Se remettre au-dessus à chaque ouverture
69
+ this.visible = true;
70
+ this.targetX = 0;
71
+ this.animate();
72
+ }
73
+
74
+ /**
75
+ * Ferme le drawer
76
+ */
77
+ close() {
78
+ this.targetX = -this.width;
79
+ this.animate();
80
+ }
81
+
82
+ /**
83
+ * Anime le drawer
84
+ * @private
85
+ */
86
+ animate() {
87
+ if (this.animating) return;
88
+ this.animating = true;
89
+
90
+ const step = () => {
91
+ const diff = this.targetX - this.x;
92
+ if (Math.abs(diff) < 1) {
93
+ this.x = this.targetX;
94
+ this.animating = false;
95
+ if (this.targetX < 0) {
96
+ this.visible = false;
97
+ }
98
+ return;
99
+ }
100
+ this.x += diff * 0.2;
101
+ requestAnimationFrame(step);
102
+ };
103
+ step();
104
+ }
105
+
106
+ /**
107
+ * Vérifie dans quelle zone se trouve un point
108
+ * @param {number} x - Coordonnée X
109
+ * @param {number} y - Coordonnée Y
110
+ * @returns {string|null} Zone ('overlay', 'item', 'drawer', null si en dehors)
111
+ * @private
112
+ */
113
+ getZoneAtPoint(x, y) {
114
+ if (!this.visible) return null;
115
+
116
+ // Vérifier si le point est dans l'overlay (toute la zone de l'écran)
117
+ // Mais on ne veut pas capturer les clics sur le drawer lui-même pour les items
118
+ if (x >= this.x && x <= this.x + this.width) {
119
+ // Le point est dans le drawer
120
+ const startY = this.header ? 150 : 0;
121
+ const index = Math.floor((y - startY) / 56);
122
+ if (index >= 0 && index < this.items.length) {
123
+ const itemY = startY + index * 56;
124
+ if (y >= itemY && y <= itemY + 56) {
125
+ return 'item';
126
+ }
127
+ }
128
+ return 'drawer';
129
+ }
130
+
131
+ // Le point est dans l'overlay (zone sombre autour du drawer)
132
+ return 'overlay';
133
+ }
134
+
135
+ /**
136
+ * Gère la pression (clic)
137
+ * @param {number} x - Coordonnée X
138
+ * @param {number} y - Coordonnée Y
139
+ * @private
140
+ */
141
+ handlePress(x, y) {
142
+ const zone = this.getZoneAtPoint(x, y);
143
+
144
+ if (zone === 'overlay') {
145
+ // Clic sur l'overlay - fermer le drawer
146
+ this.close();
147
+ return true; // On a géré le clic
148
+ } else if (zone === 'item') {
149
+ // Clic sur un item
150
+ const startY = this.header ? 150 : 0;
151
+ const index = Math.floor((y - startY) / 56);
152
+ if (index >= 0 && index < this.items.length) {
153
+ if (this.onItemClick) {
154
+ this.onItemClick(index, this.items[index]);
155
+ }
156
+ this.close();
157
+ }
158
+ return true; // On a géré le clic
159
+ }
160
+
161
+ // Clic sur le drawer (mais pas sur un item) - on ne fait rien mais on capture le clic
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Gère le mouvement (hover)
167
+ * @param {number} x - Coordonnée X
168
+ * @param {number} y - Coordonnée Y
169
+ * @private
170
+ */
171
+ handleMove(x, y) {
172
+ if (!this.visible) return;
173
+
174
+ const zone = this.getZoneAtPoint(x, y);
175
+ if (zone === 'item') {
176
+ const startY = this.header ? 150 : 0;
177
+ const index = Math.floor((y - startY) / 56);
178
+ this.hoveredIndex = index;
179
+ } else {
180
+ this.hoveredIndex = -1;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Vérifie si un point est dans les limites du drawer (inclut l'overlay)
186
+ * @param {number} x - Coordonnée X
187
+ * @param {number} y - Coordonnée Y
188
+ * @returns {boolean} True si le point est dans le drawer ou l'overlay
189
+ */
190
+ isPointInside(x, y) {
191
+ if (!this.visible) return false;
192
+
193
+ // Quand le drawer est ouvert, il capture TOUS les clics sur l'écran
194
+ // car il a un overlay qui couvre tout
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Dessine le drawer
200
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
201
+ */
202
+ draw(ctx) {
203
+ if (!this.visible) return;
204
+
205
+ ctx.save();
206
+
207
+ // Overlay sombre avec opacité progressive
208
+ const overlayOpacity = Math.min(0.5, (this.x + this.width) / this.width * 0.5);
209
+ ctx.fillStyle = `rgba(0, 0, 0, ${overlayOpacity})`;
210
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
211
+
212
+ // Drawer
213
+ ctx.fillStyle = '#FFFFFF';
214
+ ctx.fillRect(this.x, this.y, this.width, this.height);
215
+
216
+ // Ombre droite
217
+ const gradient = ctx.createLinearGradient(this.x + this.width, 0, this.x + this.width + 10, 0);
218
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 0.2)');
219
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
220
+ ctx.fillStyle = gradient;
221
+ ctx.fillRect(this.x + this.width, 0, 10, this.height);
222
+
223
+ // Header
224
+ if (this.header) {
225
+ ctx.fillStyle = this.platform === 'material' ? '#6200EE' : '#F8F8F8';
226
+ ctx.fillRect(this.x, this.y, this.width, 150);
227
+
228
+ ctx.fillStyle = this.platform === 'material' ? '#FFFFFF' : '#000000';
229
+ ctx.font = 'bold 24px -apple-system, Roboto, sans-serif';
230
+ ctx.textAlign = 'left';
231
+ ctx.textBaseline = 'bottom';
232
+ ctx.fillText(this.header.title || '', this.x + 20, this.y + 130);
233
+ }
234
+
235
+ // Items
236
+ const startY = this.header ? 150 : 0;
237
+ for (let i = 0; i < this.items.length; i++) {
238
+ const item = this.items[i];
239
+ const itemY = this.y + startY + i * 56;
240
+
241
+ // Hover effect
242
+ if (this.hoveredIndex === i) {
243
+ ctx.fillStyle = '#F5F5F5';
244
+ ctx.fillRect(this.x, itemY, this.width, 56);
245
+ }
246
+
247
+ // Icon
248
+ if (item.icon) {
249
+ ctx.fillStyle = '#757575';
250
+ ctx.font = '20px -apple-system, Roboto, sans-serif';
251
+ ctx.textAlign = 'left';
252
+ ctx.textBaseline = 'middle';
253
+ ctx.fillText(item.icon, this.x + 20, itemY + 28);
254
+ }
255
+
256
+ // Label
257
+ ctx.fillStyle = '#000000';
258
+ ctx.font = '16px -apple-system, Roboto, sans-serif';
259
+ ctx.fillText(item.label, this.x + (item.icon ? 72 : 20), itemY + 28);
260
+
261
+ // Divider
262
+ if (item.divider) {
263
+ ctx.strokeStyle = '#E0E0E0';
264
+ ctx.lineWidth = 1;
265
+ ctx.beginPath();
266
+ ctx.moveTo(this.x, itemY + 56);
267
+ ctx.lineTo(this.x + this.width, itemY + 56);
268
+ ctx.stroke();
269
+ }
270
+ }
271
+
272
+ ctx.restore();
273
+ }
274
+ }
275
+
276
+ export default Drawer;
@@ -0,0 +1,270 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Bouton d'action flottant (Material Design 3)
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} icon - Icône du bouton
8
+ * @property {boolean} extended - Mode étendu (avec texte)
9
+ * @property {string} text - Texte (en mode étendu)
10
+ * @property {string} platform - Plateforme
11
+ * @property {string} variant - Variante Material 3: 'small', 'medium', 'large', 'extended'
12
+ * @property {number} size - Taille du bouton
13
+ * @property {string} bgColor - Couleur de fond
14
+ * @property {string} iconColor - Couleur de l'icône
15
+ * @property {Array} ripples - Effets ripple
16
+ */
17
+ class FAB extends Component {
18
+ /**
19
+ * Crée une instance de FAB
20
+ * @param {CanvasFramework} framework - Framework parent
21
+ * @param {Object} [options={}] - Options de configuration
22
+ * @param {string} [options.icon='+'] - Icône
23
+ * @param {boolean} [options.extended=false] - Mode étendu
24
+ * @param {string} [options.text=''] - Texte (mode étendu)
25
+ * @param {string} [options.variant='medium'] - Variante: 'small', 'medium', 'large', 'extended'
26
+ * @param {string} [options.bgColor] - Couleur (auto selon platform)
27
+ * @param {string} [options.iconColor='#FFFFFF'] - Couleur de l'icône
28
+ */
29
+ constructor(framework, options = {}) {
30
+ super(framework, options);
31
+
32
+ this.icon = options.icon || '+';
33
+ this.extended = options.extended || false;
34
+ this.text = options.text || '';
35
+ this.platform = framework.platform;
36
+ this.variant = options.variant || 'medium';
37
+
38
+ // Tailles selon Material Design 3
39
+ const sizes = {
40
+ small: 40,
41
+ medium: 56,
42
+ large: 96
43
+ };
44
+
45
+ this.size = options.size || sizes[this.variant] || 56;
46
+
47
+ // Couleurs Material 3
48
+ this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
49
+ this.iconColor = options.iconColor || '#FFFFFF';
50
+
51
+ // Border radius selon Material 3 (pas circulaire!)
52
+ this.borderRadius = {
53
+ small: 12,
54
+ medium: 16,
55
+ large: 28,
56
+ extended: 16
57
+ }[this.variant] || 16;
58
+
59
+ // Position par défaut en bas à droite
60
+ this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
61
+ this.y = options.y !== undefined ? options.y : framework.height - this.size - 80;
62
+
63
+ // Si extended, ajuster la largeur
64
+ if (this.extended && this.text) {
65
+ const ctx = framework.ctx;
66
+ ctx.save();
67
+ ctx.font = 'bold 14px -apple-system, sans-serif';
68
+ const textWidth = ctx.measureText(this.text).width;
69
+ ctx.restore();
70
+ this.width = this.size + textWidth + 24;
71
+ this.borderRadius = 16;
72
+ } else {
73
+ this.width = this.size;
74
+ }
75
+ this.height = this.size;
76
+
77
+ // Effet ripple
78
+ this.ripples = [];
79
+
80
+ // ✅ CORRECTION : Binder onPress comme dans Button
81
+ this.onPress = this.handlePress.bind(this);
82
+ }
83
+
84
+ /**
85
+ * Gère la pression sur le FAB
86
+ * @param {number} x - Coordonnée X
87
+ * @param {number} y - Coordonnée Y
88
+ * @private
89
+ */
90
+ handlePress(x, y) {
91
+ // Créer un ripple au point de clic (Material uniquement)
92
+ if (this.platform === 'material') {
93
+ const adjustedY = y - this.framework.scrollOffset;
94
+ this.ripples.push({
95
+ x: x - this.x,
96
+ y: adjustedY - this.y,
97
+ radius: 0,
98
+ maxRadius: Math.max(this.width, this.height) * 1.5,
99
+ opacity: 1
100
+ });
101
+ this.animateRipple();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Anime l'effet ripple
107
+ * @private
108
+ */
109
+ animateRipple() {
110
+ const animate = () => {
111
+ let hasActiveRipples = false;
112
+
113
+ for (let ripple of this.ripples) {
114
+ if (ripple.radius < ripple.maxRadius) {
115
+ ripple.radius += ripple.maxRadius / 15;
116
+ hasActiveRipples = true;
117
+ }
118
+
119
+ // Fade out après 50% de l'expansion
120
+ if (ripple.radius >= ripple.maxRadius * 0.5) {
121
+ ripple.opacity -= 0.05;
122
+ }
123
+ }
124
+
125
+ // Nettoyer les ripples terminés
126
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
127
+
128
+ if (hasActiveRipples) {
129
+ requestAnimationFrame(animate);
130
+ }
131
+ };
132
+
133
+ animate();
134
+ }
135
+
136
+ /**
137
+ * Dessine le FAB
138
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
139
+ */
140
+ draw(ctx) {
141
+ ctx.save();
142
+
143
+ // Ombre (elevation)
144
+ if (!this.pressed) {
145
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
146
+ ctx.shadowBlur = this.platform === 'material' ? 8 : 12;
147
+ ctx.shadowOffsetY = this.platform === 'material' ? 4 : 6;
148
+ }
149
+
150
+ // Background - Material 3: rectangles arrondis, pas cercles!
151
+ ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
152
+ ctx.beginPath();
153
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
154
+ ctx.fill();
155
+
156
+ ctx.shadowColor = 'transparent';
157
+ ctx.shadowBlur = 0;
158
+ ctx.shadowOffsetY = 0;
159
+
160
+ // Clipping pour les ripples (Material uniquement)
161
+ if (this.platform === 'material') {
162
+ ctx.save();
163
+ ctx.beginPath();
164
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
165
+ ctx.clip();
166
+
167
+ // Dessiner les ripples
168
+ for (let ripple of this.ripples) {
169
+ ctx.globalAlpha = ripple.opacity;
170
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
171
+ ctx.beginPath();
172
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
173
+ ctx.fill();
174
+ }
175
+
176
+ ctx.restore();
177
+ }
178
+
179
+ // Overlay si pressed (iOS)
180
+ if (this.pressed && this.platform === 'cupertino') {
181
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
182
+ ctx.beginPath();
183
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
184
+ ctx.fill();
185
+ }
186
+
187
+ // Icône
188
+ ctx.fillStyle = this.iconColor;
189
+ const iconSize = this.variant === 'large' ? 36 : 24;
190
+ ctx.font = `bold ${iconSize}px sans-serif`;
191
+ ctx.textAlign = 'center';
192
+ ctx.textBaseline = 'middle';
193
+
194
+ if (this.extended && this.text) {
195
+ // Icône à gauche
196
+ ctx.fillText(this.icon, this.x + this.size / 2, this.y + this.size / 2);
197
+
198
+ // Texte à droite
199
+ ctx.font = 'bold 14px -apple-system, sans-serif';
200
+ ctx.fillText(this.text, this.x + this.size + 12, this.y + this.size / 2);
201
+ } else {
202
+ // Icône centrée
203
+ ctx.fillText(this.icon, this.x + this.width / 2, this.y + this.height / 2);
204
+ }
205
+
206
+ ctx.restore();
207
+ }
208
+
209
+ /**
210
+ * Dessine un rectangle avec coins arrondis
211
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
212
+ * @param {number} x - Position X
213
+ * @param {number} y - Position Y
214
+ * @param {number} width - Largeur
215
+ * @param {number} height - Hauteur
216
+ * @param {number} radius - Rayon des coins
217
+ * @private
218
+ */
219
+ roundRect(ctx, x, y, width, height, radius) {
220
+ ctx.moveTo(x + radius, y);
221
+ ctx.lineTo(x + width - radius, y);
222
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
223
+ ctx.lineTo(x + width, y + height - radius);
224
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
225
+ ctx.lineTo(x + radius, y + height);
226
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
227
+ ctx.lineTo(x, y + radius);
228
+ ctx.quadraticCurveTo(x, y, x + radius, y);
229
+ }
230
+
231
+ /**
232
+ * Assombrit une couleur
233
+ * @param {string} color - Couleur hexadécimale
234
+ * @returns {string} Couleur assombrie
235
+ * @private
236
+ */
237
+ darkenColor(color) {
238
+ const rgb = this.hexToRgb(color);
239
+ return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
240
+ }
241
+
242
+ /**
243
+ * Convertit une couleur hex en RGB
244
+ * @param {string} hex - Couleur hexadécimale
245
+ * @returns {{r: number, g: number, b: number}} Objet RGB
246
+ * @private
247
+ */
248
+ hexToRgb(hex) {
249
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
250
+ return result ? {
251
+ r: parseInt(result[1], 16),
252
+ g: parseInt(result[2], 16),
253
+ b: parseInt(result[3], 16)
254
+ } : { r: 0, g: 0, b: 0 };
255
+ }
256
+
257
+ /**
258
+ * Vérifie si un point est dans les limites
259
+ * @param {number} x - Coordonnée X
260
+ * @param {number} y - Coordonnée Y
261
+ * @returns {boolean} True si le point est dans le FAB
262
+ */
263
+ isPointInside(x, y) {
264
+ // Material 3: toujours des rectangles arrondis, plus de cercles
265
+ return x >= this.x && x <= this.x + this.width &&
266
+ y >= this.y && y <= this.y + this.height;
267
+ }
268
+ }
269
+
270
+ export default FAB;