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,315 @@
1
+ import Input from './Input.js';
2
+
3
+ /**
4
+ * Champ de saisie pour la recherche
5
+ * @class
6
+ * @extends Input
7
+ * @property {Function} onSearch - Callback lors de la recherche
8
+ * @property {Function} onClear - Callback lors de l'effacement
9
+ * @property {boolean} showClearButton - Afficher le bouton d'effacement
10
+ */
11
+ class SearchInput extends Input {
12
+ /**
13
+ * Crée une instance de SearchInput
14
+ * @param {CanvasFramework} framework - Framework parent
15
+ * @param {Object} [options={}] - Options de configuration
16
+ * @param {Function} [options.onSearch] - Callback lors de la recherche
17
+ * @param {Function} [options.onClear] - Callback lors de l'effacement
18
+ * @param {string} [options.searchIcon] - Icône de recherche
19
+ */
20
+ constructor(framework, options = {}) {
21
+ super(framework, {
22
+ placeholder: options.placeholder || 'Rechercher...',
23
+ ...options
24
+ });
25
+
26
+ this.onSearch = options.onSearch;
27
+ this.onClear = options.onClear;
28
+ this.searchIcon = options.searchIcon || '🔍';
29
+ this.clearIcon = '×';
30
+ this.showClearButton = false;
31
+
32
+ // Bind des méthodes supplémentaires
33
+ this.handleKeyDown = this.handleKeyDown.bind(this);
34
+ this.handleClearClick = this.handleClearClick.bind(this);
35
+ }
36
+
37
+ /**
38
+ * Configure l'input HTML caché (surcharge)
39
+ */
40
+ setupHiddenInput() {
41
+ if (this.hiddenInput) return;
42
+
43
+ // Appeler la méthode parent
44
+ super.setupHiddenInput();
45
+
46
+ if (this.hiddenInput) {
47
+ // Ajouter l'événement keydown pour la touche Entrée
48
+ this.hiddenInput.addEventListener('keydown', this.handleKeyDown);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Gère la pression des touches
54
+ * @param {KeyboardEvent} e - Événement clavier
55
+ * @private
56
+ */
57
+ handleKeyDown(e) {
58
+ if (e.key === 'Enter' && this.onSearch) {
59
+ this.onSearch(this.value);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Gère le clic sur le bouton d'effacement
65
+ * @private
66
+ */
67
+ handleClearClick() {
68
+ const hadValue = this.value.length > 0;
69
+ this.value = '';
70
+
71
+ if (this.hiddenInput) {
72
+ this.hiddenInput.value = '';
73
+ }
74
+
75
+ // Appeler les callbacks
76
+ if (hadValue && this.onClear) {
77
+ this.onClear();
78
+ }
79
+
80
+ // Mettre à jour l'affichage
81
+ this.showClearButton = false;
82
+
83
+ // Redonner le focus
84
+ this.onFocus();
85
+ }
86
+
87
+ /**
88
+ * Gère la saisie (surcharge)
89
+ */
90
+ onInputUpdate() {
91
+ // Mettre à jour l'affichage du bouton d'effacement
92
+ this.showClearButton = this.value.length > 0;
93
+
94
+ // Appeler le callback de recherche au fur et à mesure (optionnel)
95
+ if (this.value.length > 0 && this.onSearch) {
96
+ // Délai pour éviter de déclencher à chaque frappe
97
+ clearTimeout(this._searchTimeout);
98
+ this._searchTimeout = setTimeout(() => {
99
+ if (this.onSearch) {
100
+ this.onSearch(this.value);
101
+ }
102
+ }, 300);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Gère le clic (surcharge)
108
+ */
109
+ onClick() {
110
+ // Vérifier si on a cliqué sur le bouton d'effacement
111
+ if (this.showClearButton && this.isPointInClearButton(this.lastClickX, this.lastClickY)) {
112
+ this.handleClearClick();
113
+ return;
114
+ }
115
+
116
+ // Sinon, focus normal
117
+ super.onFocus();
118
+ }
119
+
120
+ /**
121
+ * Gère le focus (surcharge)
122
+ */
123
+ onFocus() {
124
+ super.onFocus();
125
+
126
+ // Mettre à jour l'état du bouton d'effacement
127
+ this.showClearButton = this.value.length > 0;
128
+ }
129
+
130
+ /**
131
+ * Gère la pression (surcharge pour capturer les coordonnées)
132
+ * @param {number} x - Coordonnée X
133
+ * @param {number} y - Coordonnée Y
134
+ */
135
+ onPress(x, y) {
136
+ // Stocker les coordonnées du dernier clic
137
+ this.lastClickX = x;
138
+ this.lastClickY = y;
139
+
140
+ // Appeler la méthode parent
141
+ super.onPress(x, y);
142
+ }
143
+
144
+ /**
145
+ * Vérifie si un point est dans le bouton d'effacement
146
+ * @param {number} x - Coordonnée X
147
+ * @param {number} y - Coordonnée Y
148
+ * @returns {boolean} True si le point est dans le bouton
149
+ */
150
+ isPointInClearButton(x, y) {
151
+ if (!this.showClearButton) return false;
152
+
153
+ // Position du bouton d'effacement (à droite)
154
+ const clearButtonSize = this.height * 0.6;
155
+ const clearButtonX = this.x + this.width - clearButtonSize - 10;
156
+ const clearButtonY = this.y + (this.height - clearButtonSize) / 2;
157
+
158
+ return x >= clearButtonX &&
159
+ x <= clearButtonX + clearButtonSize &&
160
+ y >= clearButtonY &&
161
+ y <= clearButtonY + clearButtonSize;
162
+ }
163
+
164
+ /**
165
+ * Dessine le champ de recherche (surcharge)
166
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
167
+ */
168
+ draw(ctx) {
169
+ ctx.save();
170
+
171
+ // Style selon la plateforme
172
+ if (this.platform === 'material') {
173
+ ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
174
+ ctx.lineWidth = this.focused ? 2 : 1;
175
+ ctx.beginPath();
176
+ ctx.moveTo(this.x, this.y + this.height);
177
+ ctx.lineTo(this.x + this.width, this.y + this.height);
178
+ ctx.stroke();
179
+ } else {
180
+ // iOS style avec coins arrondis
181
+ ctx.fillStyle = this.focused ? '#FFFFFF' : '#F2F2F7';
182
+ ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
183
+ ctx.lineWidth = 1;
184
+
185
+ // Fond
186
+ ctx.beginPath();
187
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
188
+ ctx.fill();
189
+
190
+ // Bordure
191
+ ctx.stroke();
192
+ }
193
+
194
+ // Icône de recherche (à gauche)
195
+ ctx.fillStyle = '#999999';
196
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
197
+ ctx.textAlign = 'center';
198
+ ctx.textBaseline = 'middle';
199
+ ctx.fillText(this.searchIcon, this.x + 25, this.y + this.height / 2);
200
+
201
+ // Texte
202
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
203
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
204
+ ctx.textAlign = 'left';
205
+ ctx.textBaseline = 'middle';
206
+
207
+ // Calculer la largeur disponible pour le texte
208
+ const leftPadding = 50; // Pour l'icône de recherche
209
+ const rightPadding = this.showClearButton ? 50 : 20; // Pour le bouton d'effacement
210
+ const maxTextWidth = this.width - leftPadding - rightPadding;
211
+
212
+ const displayText = this.value || this.placeholder;
213
+
214
+ // Tronquer le texte si nécessaire
215
+ let textToShow = displayText;
216
+ let textWidth = ctx.measureText(textToShow).width;
217
+
218
+ if (textWidth > maxTextWidth) {
219
+ // Ajouter "..." à la fin
220
+ while (textWidth > maxTextWidth && textToShow.length > 0) {
221
+ textToShow = textToShow.substring(0, textToShow.length - 1);
222
+ textWidth = ctx.measureText(textToShow + '...').width;
223
+ }
224
+ if (textToShow.length > 0) {
225
+ textToShow += '...';
226
+ }
227
+ }
228
+
229
+ // Couleur différente pour le placeholder
230
+ if (!this.value) {
231
+ ctx.fillStyle = '#999999';
232
+ }
233
+
234
+ ctx.fillText(textToShow, this.x + leftPadding, this.y + this.height / 2);
235
+
236
+ // Bouton d'effacement (si il y a du texte)
237
+ if (this.showClearButton) {
238
+ const clearButtonSize = this.height * 0.6;
239
+ const clearButtonX = this.x + this.width - clearButtonSize - 15;
240
+ const clearButtonY = this.y + (this.height - clearButtonSize) / 2;
241
+
242
+ // Cercle de fond
243
+ ctx.fillStyle = '#E0E0E0';
244
+ ctx.beginPath();
245
+ ctx.arc(
246
+ clearButtonX + clearButtonSize / 2,
247
+ clearButtonY + clearButtonSize / 2,
248
+ clearButtonSize / 2,
249
+ 0,
250
+ Math.PI * 2
251
+ );
252
+ ctx.fill();
253
+
254
+ // Croix
255
+ ctx.fillStyle = '#666666';
256
+ ctx.font = `bold ${clearButtonSize * 0.6}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
257
+ ctx.textAlign = 'center';
258
+ ctx.textBaseline = 'middle';
259
+ ctx.fillText(
260
+ this.clearIcon,
261
+ clearButtonX + clearButtonSize / 2,
262
+ clearButtonY + clearButtonSize / 2
263
+ );
264
+ }
265
+
266
+ // Curseur (si focus et a du texte)
267
+ if (this.focused && this.cursorVisible && this.value) {
268
+ const textWidth = ctx.measureText(this.value).width;
269
+ const cursorX = this.x + leftPadding + Math.min(textWidth, maxTextWidth);
270
+ ctx.fillStyle = '#000000';
271
+ ctx.fillRect(cursorX, this.y + 10, 2, this.height - 20);
272
+ }
273
+
274
+ ctx.restore();
275
+ }
276
+
277
+ /**
278
+ * Vérifie si un point est dans les limites (surcharge)
279
+ * @param {number} x - Coordonnée X
280
+ * @param {number} y - Coordonnée Y
281
+ * @returns {boolean} True si le point est dans l'input
282
+ */
283
+ isPointInside(x, y) {
284
+ // Vérifier l'input principal
285
+ const inInput = x >= this.x &&
286
+ x <= this.x + this.width &&
287
+ y >= this.y &&
288
+ y <= this.y + this.height;
289
+
290
+ // Vérifier aussi le bouton d'effacement
291
+ const inClearButton = this.isPointInClearButton(x, y);
292
+
293
+ return inInput || inClearButton;
294
+ }
295
+
296
+ /**
297
+ * Nettoie les ressources (surcharge)
298
+ */
299
+ destroy() {
300
+ // Nettoyer le timeout de recherche
301
+ if (this._searchTimeout) {
302
+ clearTimeout(this._searchTimeout);
303
+ }
304
+
305
+ // Retirer l'écouteur keydown
306
+ if (this.hiddenInput) {
307
+ this.hiddenInput.removeEventListener('keydown', this.handleKeyDown);
308
+ }
309
+
310
+ // Appeler la méthode parent
311
+ super.destroy();
312
+ }
313
+ }
314
+
315
+ export default SearchInput;
@@ -0,0 +1,357 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Segmented Control (Material + Cupertino)
5
+ * @class
6
+ * @extends Component
7
+ * @param {CanvasFramework} framework - Instance du framework
8
+ * @param {Object} [options={}] - Options de configuration
9
+ * @param {Array<{text: string, onClick?: Function}>} [options.buttons] - Liste des segments
10
+ * @param {number} [options.selectedIndex=0] - Segment sélectionné par défaut
11
+ * @param {number} [options.height=40] - Hauteur du contrôle
12
+ * @param {number} [options.spacing=1] - Espacement entre les segments
13
+ */
14
+ class SegmentedControl extends Component {
15
+ constructor(framework, options = {}) {
16
+ super(framework, options);
17
+
18
+ this.platform = framework.platform;
19
+ this.buttons = options.buttons || [{ text: 'One' }, { text: 'Two' }, { text: 'Three' }];
20
+ this.selectedIndex = options.selectedIndex || 0;
21
+ this.height = options.height || 40;
22
+ this.spacing = options.spacing || 1;
23
+
24
+ // IMPORTANT: Lier les handlers d'événements
25
+ this.onPress = this.handlePress.bind(this);
26
+ this.onRelease = this.handleRelease.bind(this);
27
+
28
+ // État pour les animations
29
+ this.ripples = [];
30
+ this.pressedIndex = null;
31
+ this._isAnimating = false;
32
+
33
+ }
34
+
35
+ /**
36
+ * Gère la pression sur un segment
37
+ * @param {number} x - Coordonnée X du clic
38
+ * @param {number} y - Coordonnée Y du clic
39
+ * @returns {boolean} True si un segment a été cliqué
40
+ */
41
+ handlePress(x, y) {
42
+ const index = this.getButtonIndexAt(x, y);
43
+
44
+ if (index !== null) {
45
+ // Sauvegarder l'index pressé pour l'animation
46
+ this.pressedIndex = index;
47
+
48
+ // Pour Material: créer un ripple
49
+ if (this.platform === 'material') {
50
+ const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
51
+ const btnX = this.x + index * (btnWidth + this.spacing);
52
+
53
+ this.ripples.push({
54
+ x: x - btnX, // Position relative au bouton
55
+ y: y - this.y, // Position relative au bouton
56
+ index: index,
57
+ radius: 0,
58
+ maxRadius: Math.max(btnWidth, this.height) * 1.5,
59
+ opacity: 0.3,
60
+ startTime: Date.now()
61
+ });
62
+
63
+ // Démarrer l'animation si pas déjà en cours
64
+ if (!this._isAnimating) {
65
+ this._animate();
66
+ }
67
+ }
68
+
69
+ // Sélectionner le segment
70
+ this.selectedIndex = index;
71
+
72
+ // Forcer le redessin immédiat
73
+ this._requestRedraw();
74
+
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * Gère le relâchement
83
+ * @param {number} x - Coordonnée X
84
+ * @param {number} y - Coordonnée Y
85
+ */
86
+ handleRelease(x, y) {
87
+ const index = this.getButtonIndexAt(x, y);
88
+
89
+ if (index !== null && index === this.pressedIndex) {
90
+ // Appeler le callback si défini
91
+ if (this.buttons[index].onClick) {
92
+ this.buttons[index].onClick(index);
93
+ }
94
+ }
95
+
96
+ // Réinitialiser l'index pressé
97
+ this.pressedIndex = null;
98
+
99
+ // Forcer le redessin
100
+ this._requestRedraw();
101
+ }
102
+
103
+ /**
104
+ * Anime les ripples Material
105
+ * @private
106
+ */
107
+ _animate() {
108
+ this._isAnimating = true;
109
+
110
+ const animateFrame = () => {
111
+ let hasActiveRipples = false;
112
+ const now = Date.now();
113
+
114
+ // Mettre à jour tous les ripples
115
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
116
+ const ripple = this.ripples[i];
117
+ const elapsed = now - ripple.startTime;
118
+
119
+ // Animation sur 600ms
120
+ const progress = Math.min(elapsed / 600, 1);
121
+
122
+ // Équation d'easing
123
+ const easedProgress = 1 - Math.pow(1 - progress, 3);
124
+
125
+ // Mettre à jour le rayon
126
+ ripple.radius = ripple.maxRadius * easedProgress;
127
+
128
+ // Diminuer l'opacité après 50% de progression
129
+ if (progress > 0.5) {
130
+ ripple.opacity = 0.3 * (1 - (progress - 0.5) * 2);
131
+ }
132
+
133
+ // Supprimer les ripples terminés
134
+ if (progress >= 1) {
135
+ this.ripples.splice(i, 1);
136
+ } else {
137
+ hasActiveRipples = true;
138
+ }
139
+ }
140
+
141
+ // Redessiner si il y a des ripples actifs
142
+ if (hasActiveRipples) {
143
+ this._requestRedraw();
144
+ requestAnimationFrame(animateFrame);
145
+ } else {
146
+ this._isAnimating = false;
147
+ }
148
+ };
149
+
150
+ requestAnimationFrame(animateFrame);
151
+ }
152
+
153
+ /**
154
+ * Retourne l'index du bouton sous un point donné
155
+ * @param {number} x - Coordonnée X
156
+ * @param {number} y - Coordonnée Y
157
+ * @returns {number|null} Index du segment ou null
158
+ * @private
159
+ */
160
+ getButtonIndexAt(x, y) {
161
+ // Vérifier si dans les limites verticales
162
+ if (y < this.y || y > this.y + this.height) {
163
+ return null;
164
+ }
165
+
166
+ const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
167
+
168
+ for (let i = 0; i < this.buttons.length; i++) {
169
+ const btnX = this.x + i * (btnWidth + this.spacing);
170
+
171
+ // Vérifier si dans les limites horizontales du bouton
172
+ if (x >= btnX && x <= btnX + btnWidth) {
173
+ return i;
174
+ }
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Force le redessin du composant
182
+ * @private
183
+ */
184
+ _requestRedraw() {
185
+ if (this.framework && this.framework.markComponentDirty) {
186
+ this.framework.markComponentDirty(this);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Dessine le SegmentedControl
192
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
193
+ */
194
+ draw(ctx) {
195
+ ctx.save();
196
+
197
+ const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
198
+ const radius = this.height / 2;
199
+
200
+ // Dessiner tous les boutons
201
+ for (let i = 0; i < this.buttons.length; i++) {
202
+ const btn = this.buttons[i];
203
+ const btnX = this.x + i * (btnWidth + this.spacing);
204
+
205
+ // Couleurs selon la plateforme et l'état
206
+ let backgroundColor;
207
+ let textColor;
208
+
209
+ if (this.platform === 'material') {
210
+ // Material Design
211
+ if (this.selectedIndex === i) {
212
+ backgroundColor = '#6200EE'; // Violet Material
213
+ textColor = '#FFFFFF';
214
+ } else {
215
+ backgroundColor = '#E0E0E0'; // Gris clair
216
+ textColor = '#000000';
217
+ }
218
+
219
+ // Si pressé (mais pas encore sélectionné)
220
+ if (this.pressedIndex === i && this.pressedIndex !== this.selectedIndex) {
221
+ backgroundColor = 'rgba(98, 0, 238, 0.12)'; // Violet très transparent
222
+ }
223
+ } else {
224
+ // iOS/Cupertino
225
+ if (this.selectedIndex === i) {
226
+ backgroundColor = '#007AFF'; // Bleu iOS
227
+ textColor = '#FFFFFF';
228
+ } else {
229
+ backgroundColor = '#F0F0F0'; // Gris très clair iOS
230
+ textColor = '#000000';
231
+ }
232
+
233
+ // Si pressé
234
+ if (this.pressedIndex === i) {
235
+ backgroundColor = this.selectedIndex === i ? '#0056CC' : '#D9D9D9';
236
+ }
237
+ }
238
+
239
+ // Dessiner le fond du bouton
240
+ ctx.fillStyle = backgroundColor;
241
+
242
+ // Coins arrondis selon la position
243
+ ctx.beginPath();
244
+
245
+ if (i === 0 && i === this.buttons.length - 1) {
246
+ // Un seul bouton - tous les coins arrondis
247
+ ctx.moveTo(btnX + radius, this.y);
248
+ ctx.lineTo(btnX + btnWidth - radius, this.y);
249
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
250
+ ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
251
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
252
+ ctx.lineTo(btnX + radius, this.y + this.height);
253
+ ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
254
+ ctx.lineTo(btnX, this.y + radius);
255
+ ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
256
+ } else if (i === 0) {
257
+ // Premier bouton - coins gauche arrondis
258
+ ctx.moveTo(btnX + radius, this.y);
259
+ ctx.lineTo(btnX + btnWidth, this.y);
260
+ ctx.lineTo(btnX + btnWidth, this.y + this.height);
261
+ ctx.lineTo(btnX + radius, this.y + this.height);
262
+ ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
263
+ ctx.lineTo(btnX, this.y + radius);
264
+ ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
265
+ } else if (i === this.buttons.length - 1) {
266
+ // Dernier bouton - coins droit arrondis
267
+ ctx.moveTo(btnX, this.y);
268
+ ctx.lineTo(btnX + btnWidth - radius, this.y);
269
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
270
+ ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
271
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
272
+ ctx.lineTo(btnX, this.y + this.height);
273
+ } else {
274
+ // Bouton du milieu - coins carrés
275
+ ctx.rect(btnX, this.y, btnWidth, this.height);
276
+ }
277
+
278
+ ctx.closePath();
279
+ ctx.fill();
280
+
281
+ // Dessiner les ripples Material
282
+ if (this.platform === 'material') {
283
+ for (const ripple of this.ripples) {
284
+ if (ripple.index === i) {
285
+ ctx.save();
286
+
287
+ // Clip sur le bouton pour que le ripple ne dépasse pas
288
+ ctx.beginPath();
289
+ if (i === 0) {
290
+ ctx.moveTo(btnX + radius, this.y);
291
+ ctx.lineTo(btnX + btnWidth, this.y);
292
+ ctx.lineTo(btnX + btnWidth, this.y + this.height);
293
+ ctx.lineTo(btnX + radius, this.y + this.height);
294
+ ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
295
+ ctx.lineTo(btnX, this.y + radius);
296
+ ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
297
+ } else if (i === this.buttons.length - 1) {
298
+ ctx.moveTo(btnX, this.y);
299
+ ctx.lineTo(btnX + btnWidth - radius, this.y);
300
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
301
+ ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
302
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
303
+ ctx.lineTo(btnX, this.y + this.height);
304
+ } else {
305
+ ctx.rect(btnX, this.y, btnWidth, this.height);
306
+ }
307
+ ctx.closePath();
308
+ ctx.clip();
309
+
310
+ // Dessiner le ripple
311
+ ctx.fillStyle = `rgba(255, 255, 255, ${ripple.opacity})`;
312
+ ctx.beginPath();
313
+ ctx.arc(btnX + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
314
+ ctx.fill();
315
+
316
+ ctx.restore();
317
+ }
318
+ }
319
+ }
320
+
321
+ // Dessiner le texte
322
+ ctx.fillStyle = textColor;
323
+ ctx.font = `500 ${this.height / 2.5}px -apple-system, Roboto, sans-serif`;
324
+ ctx.textAlign = 'center';
325
+ ctx.textBaseline = 'middle';
326
+ ctx.fillText(btn.text || `Button ${i + 1}`, btnX + btnWidth / 2, this.y + this.height / 2);
327
+ }
328
+
329
+ // Dessiner les séparateurs (pour iOS)
330
+ if (this.platform === 'cupertino' && this.buttons.length > 1) {
331
+ ctx.strokeStyle = '#C7C7CC';
332
+ ctx.lineWidth = 1;
333
+
334
+ for (let i = 1; i < this.buttons.length; i++) {
335
+ const separatorX = this.x + i * btnWidth + (i - 1) * this.spacing;
336
+ ctx.beginPath();
337
+ ctx.moveTo(separatorX, this.y + 8);
338
+ ctx.lineTo(separatorX, this.y + this.height - 8);
339
+ ctx.stroke();
340
+ }
341
+ }
342
+
343
+ ctx.restore();
344
+ }
345
+
346
+ /**
347
+ * Vérifie si un point est dans le contrôle
348
+ * @param {number} x - Coordonnée X
349
+ * @param {number} y - Coordonnée Y
350
+ * @returns {boolean} True si un segment est touché
351
+ */
352
+ isPointInside(x, y) {
353
+ return this.getButtonIndexAt(x, y) !== null;
354
+ }
355
+ }
356
+
357
+ export default SegmentedControl;