canvasframework 0.3.6

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 (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,261 @@
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
+ }
49
+
50
+ /**
51
+ * Ouvre le drawer
52
+ */
53
+ open() {
54
+ this.visible = true;
55
+ this.targetX = 0;
56
+ this.animate();
57
+ }
58
+
59
+ /**
60
+ * Ferme le drawer
61
+ */
62
+ close() {
63
+ this.targetX = -this.width;
64
+ this.animate();
65
+ }
66
+
67
+ /**
68
+ * Anime le drawer
69
+ * @private
70
+ */
71
+ animate() {
72
+ if (this.animating) return;
73
+ this.animating = true;
74
+
75
+ const step = () => {
76
+ const diff = this.targetX - this.x;
77
+ if (Math.abs(diff) < 1) {
78
+ this.x = this.targetX;
79
+ this.animating = false;
80
+ if (this.targetX < 0) {
81
+ this.visible = false;
82
+ }
83
+ return;
84
+ }
85
+ this.x += diff * 0.2;
86
+ requestAnimationFrame(step);
87
+ };
88
+ step();
89
+ }
90
+
91
+ /**
92
+ * Vérifie dans quelle zone se trouve un point
93
+ * @param {number} x - Coordonnée X
94
+ * @param {number} y - Coordonnée Y
95
+ * @returns {string|null} Zone ('overlay', 'item', 'drawer', null si en dehors)
96
+ * @private
97
+ */
98
+ getZoneAtPoint(x, y) {
99
+ if (!this.visible) return null;
100
+
101
+ // Vérifier si le point est dans l'overlay (toute la zone de l'écran)
102
+ // Mais on ne veut pas capturer les clics sur le drawer lui-même pour les items
103
+ if (x >= this.x && x <= this.x + this.width) {
104
+ // Le point est dans le drawer
105
+ const startY = this.header ? 150 : 0;
106
+ const index = Math.floor((y - startY) / 56);
107
+ if (index >= 0 && index < this.items.length) {
108
+ const itemY = startY + index * 56;
109
+ if (y >= itemY && y <= itemY + 56) {
110
+ return 'item';
111
+ }
112
+ }
113
+ return 'drawer';
114
+ }
115
+
116
+ // Le point est dans l'overlay (zone sombre autour du drawer)
117
+ return 'overlay';
118
+ }
119
+
120
+ /**
121
+ * Gère la pression (clic)
122
+ * @param {number} x - Coordonnée X
123
+ * @param {number} y - Coordonnée Y
124
+ * @private
125
+ */
126
+ handlePress(x, y) {
127
+ const zone = this.getZoneAtPoint(x, y);
128
+
129
+ if (zone === 'overlay') {
130
+ // Clic sur l'overlay - fermer le drawer
131
+ this.close();
132
+ return true; // On a géré le clic
133
+ } else if (zone === 'item') {
134
+ // Clic sur un item
135
+ const startY = this.header ? 150 : 0;
136
+ const index = Math.floor((y - startY) / 56);
137
+ if (index >= 0 && index < this.items.length) {
138
+ if (this.onItemClick) {
139
+ this.onItemClick(index, this.items[index]);
140
+ }
141
+ this.close();
142
+ }
143
+ return true; // On a géré le clic
144
+ }
145
+
146
+ // Clic sur le drawer (mais pas sur un item) - on ne fait rien mais on capture le clic
147
+ return true;
148
+ }
149
+
150
+ /**
151
+ * Gère le mouvement (hover)
152
+ * @param {number} x - Coordonnée X
153
+ * @param {number} y - Coordonnée Y
154
+ * @private
155
+ */
156
+ handleMove(x, y) {
157
+ if (!this.visible) return;
158
+
159
+ const zone = this.getZoneAtPoint(x, y);
160
+ if (zone === 'item') {
161
+ const startY = this.header ? 150 : 0;
162
+ const index = Math.floor((y - startY) / 56);
163
+ this.hoveredIndex = index;
164
+ } else {
165
+ this.hoveredIndex = -1;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Vérifie si un point est dans les limites du drawer (inclut l'overlay)
171
+ * @param {number} x - Coordonnée X
172
+ * @param {number} y - Coordonnée Y
173
+ * @returns {boolean} True si le point est dans le drawer ou l'overlay
174
+ */
175
+ isPointInside(x, y) {
176
+ if (!this.visible) return false;
177
+
178
+ // Quand le drawer est ouvert, il capture TOUS les clics sur l'écran
179
+ // car il a un overlay qui couvre tout
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Dessine le drawer
185
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
186
+ */
187
+ draw(ctx) {
188
+ if (!this.visible) return;
189
+
190
+ ctx.save();
191
+
192
+ // Overlay sombre avec opacité progressive
193
+ const overlayOpacity = Math.min(0.5, (this.x + this.width) / this.width * 0.5);
194
+ ctx.fillStyle = `rgba(0, 0, 0, ${overlayOpacity})`;
195
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
196
+
197
+ // Drawer
198
+ ctx.fillStyle = '#FFFFFF';
199
+ ctx.fillRect(this.x, this.y, this.width, this.height);
200
+
201
+ // Ombre droite
202
+ const gradient = ctx.createLinearGradient(this.x + this.width, 0, this.x + this.width + 10, 0);
203
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 0.2)');
204
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
205
+ ctx.fillStyle = gradient;
206
+ ctx.fillRect(this.x + this.width, 0, 10, this.height);
207
+
208
+ // Header
209
+ if (this.header) {
210
+ ctx.fillStyle = this.platform === 'material' ? '#6200EE' : '#F8F8F8';
211
+ ctx.fillRect(this.x, this.y, this.width, 150);
212
+
213
+ ctx.fillStyle = this.platform === 'material' ? '#FFFFFF' : '#000000';
214
+ ctx.font = 'bold 24px -apple-system, Roboto, sans-serif';
215
+ ctx.textAlign = 'left';
216
+ ctx.textBaseline = 'bottom';
217
+ ctx.fillText(this.header.title || '', this.x + 20, this.y + 130);
218
+ }
219
+
220
+ // Items
221
+ const startY = this.header ? 150 : 0;
222
+ for (let i = 0; i < this.items.length; i++) {
223
+ const item = this.items[i];
224
+ const itemY = this.y + startY + i * 56;
225
+
226
+ // Hover effect
227
+ if (this.hoveredIndex === i) {
228
+ ctx.fillStyle = '#F5F5F5';
229
+ ctx.fillRect(this.x, itemY, this.width, 56);
230
+ }
231
+
232
+ // Icon
233
+ if (item.icon) {
234
+ ctx.fillStyle = '#757575';
235
+ ctx.font = '20px -apple-system, Roboto, sans-serif';
236
+ ctx.textAlign = 'left';
237
+ ctx.textBaseline = 'middle';
238
+ ctx.fillText(item.icon, this.x + 20, itemY + 28);
239
+ }
240
+
241
+ // Label
242
+ ctx.fillStyle = '#000000';
243
+ ctx.font = '16px -apple-system, Roboto, sans-serif';
244
+ ctx.fillText(item.label, this.x + (item.icon ? 72 : 20), itemY + 28);
245
+
246
+ // Divider
247
+ if (item.divider) {
248
+ ctx.strokeStyle = '#E0E0E0';
249
+ ctx.lineWidth = 1;
250
+ ctx.beginPath();
251
+ ctx.moveTo(this.x, itemY + 56);
252
+ ctx.lineTo(this.x + this.width, itemY + 56);
253
+ ctx.stroke();
254
+ }
255
+ }
256
+
257
+ ctx.restore();
258
+ }
259
+ }
260
+
261
+ 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;