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,255 @@
1
+ import Modal from '../components/Modal.js';
2
+ /**
3
+ * Modal pour la sélection d'une option parmi une liste
4
+ * @class
5
+ * @extends Modal
6
+ * @param {Framework} framework - Instance du framework
7
+ * @param {Object} [options={}] - Options de configuration
8
+ * @param {string} [options.title='Sélectionner une option'] - Titre du modal
9
+ * @param {string[]} [options.options=[]] - Liste des options
10
+ * @param {number} [options.selectedIndex=0] - Index de l'option sélectionnée par défaut
11
+ * @param {Function} [options.onSelect] - Callback lors de la sélection
12
+ * @example
13
+ * const dialog = new SelectDialog(framework, {
14
+ * title: 'Choisir une couleur',
15
+ * options: ['Rouge', 'Vert', 'Bleu'],
16
+ * selectedIndex: 1,
17
+ * onSelect: (index, value) => console.log('Selected:', value)
18
+ * });
19
+ */
20
+ class SelectDialog extends Modal {
21
+ /**
22
+ * @constructs SelectDialog
23
+ */
24
+ constructor(framework, options = {}) {
25
+ // Calculer la hauteur en fonction du nombre d'options
26
+ const optionsCount = options.options?.length || 0;
27
+ const itemHeight = 50;
28
+ const dialogHeight = Math.min(
29
+ 400, // Hauteur max
30
+ Math.max(200, optionsCount * itemHeight + 100) // Hauteur min + espace pour titre
31
+ );
32
+
33
+ // Appeler le constructeur parent
34
+ super(framework, {
35
+ title: options.title || 'Sélectionner une option',
36
+ width: Math.min(350, framework.width - 40),
37
+ height: dialogHeight,
38
+ showCloseButton: true,
39
+ closeOnOverlayClick: true,
40
+ padding: 0, // Pas de padding, on gère nous-même
41
+ ...options
42
+ });
43
+
44
+ /** @type {string[]} */
45
+ this.options = options.options || [];
46
+ /** @type {number} */
47
+ this.selectedIndex = options.selectedIndex || 0;
48
+ /** @type {Function|undefined} */
49
+ this.onSelect = options.onSelect;
50
+ /** @type {number} */
51
+ this.itemHeight = itemHeight;
52
+ /** @type {number} */
53
+ this.hoveredIndex = -1;
54
+
55
+ // Désactiver les animations
56
+ this.opacity = 1;
57
+ this.scale = 1;
58
+ this.isVisible = true;
59
+ this.visible = true;
60
+
61
+ // AJOUTER: Définir onPress et onMove pour que le framework les appelle
62
+ this.onPress = this.handlePress.bind(this);
63
+ this.onMove = this.handleMove.bind(this);
64
+ }
65
+
66
+ /**
67
+ * Dessine le modal de sélection
68
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
69
+ */
70
+ draw(ctx) {
71
+ if (!this.isVisible) return;
72
+
73
+ ctx.save();
74
+ ctx.globalAlpha = this.opacity;
75
+
76
+ // Overlay sombre
77
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
78
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
79
+
80
+ // Calculer la position du modal (centré)
81
+ const modalX = (this.framework.width - this.modalWidth) / 2;
82
+ const modalY = (this.framework.height - this.modalHeight) / 2;
83
+
84
+ // Fond du modal
85
+ ctx.fillStyle = '#FFFFFF';
86
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
87
+ ctx.shadowBlur = 20;
88
+ ctx.shadowOffsetY = 10;
89
+
90
+ ctx.beginPath();
91
+ this.roundRect(ctx, modalX, modalY, this.modalWidth, this.modalHeight, 12);
92
+ ctx.fill();
93
+
94
+ ctx.shadowColor = 'transparent';
95
+
96
+ // Titre
97
+ if (this.title) {
98
+ ctx.fillStyle = '#000000';
99
+ ctx.font = 'bold 18px -apple-system, sans-serif';
100
+ ctx.textAlign = 'center';
101
+ ctx.textBaseline = 'middle';
102
+ ctx.fillText(this.title, modalX + this.modalWidth / 2, modalY + 30);
103
+
104
+ // Ligne de séparation sous le titre
105
+ ctx.strokeStyle = '#E0E0E0';
106
+ ctx.lineWidth = 1;
107
+ ctx.beginPath();
108
+ ctx.moveTo(modalX, modalY + 50);
109
+ ctx.lineTo(modalX + this.modalWidth, modalY + 50);
110
+ ctx.stroke();
111
+ }
112
+
113
+ // Zone de contenu (avec scroll si nécessaire)
114
+ const contentX = modalX;
115
+ const contentY = modalY + 55; // Après le titre et la ligne
116
+ const contentHeight = this.modalHeight - 55;
117
+
118
+ ctx.save();
119
+ ctx.beginPath();
120
+ ctx.rect(contentX, contentY, this.modalWidth, contentHeight);
121
+ ctx.clip();
122
+
123
+ // Options
124
+ for (let i = 0; i < this.options.length; i++) {
125
+ const optionY = contentY + i * this.itemHeight;
126
+
127
+ // Si l'option est en dehors de la zone visible, passer à la suivante
128
+ if (optionY + this.itemHeight < contentY || optionY > contentY + contentHeight) {
129
+ continue;
130
+ }
131
+
132
+ // Option sélectionnée
133
+ if (i === this.selectedIndex) {
134
+ ctx.fillStyle = this.framework.platform === 'material' ? 'rgba(98, 0, 238, 0.1)' : 'rgba(0, 122, 255, 0.1)';
135
+ ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
136
+ }
137
+
138
+ // Effet hover
139
+ if (this.hoveredIndex === i) {
140
+ ctx.fillStyle = '#F5F5F5';
141
+ ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
142
+ }
143
+
144
+ // Texte de l'option
145
+ ctx.fillStyle = i === this.selectedIndex ?
146
+ (this.framework.platform === 'material' ? '#6200EE' : '#007AFF') :
147
+ '#000000';
148
+ ctx.font = i === this.selectedIndex ? 'bold 16px -apple-system, sans-serif' : '16px -apple-system, sans-serif';
149
+ ctx.textAlign = 'left';
150
+ ctx.textBaseline = 'middle';
151
+ ctx.fillText(this.options[i], contentX + 20, optionY + this.itemHeight / 2);
152
+
153
+ // Divider entre les options
154
+ if (i < this.options.length - 1) {
155
+ ctx.strokeStyle = '#E0E0E0';
156
+ ctx.lineWidth = 1;
157
+ ctx.beginPath();
158
+ ctx.moveTo(contentX + 20, optionY + this.itemHeight);
159
+ ctx.lineTo(contentX + this.modalWidth - 20, optionY + this.itemHeight);
160
+ ctx.stroke();
161
+ }
162
+ }
163
+
164
+ ctx.restore();
165
+ ctx.restore();
166
+ }
167
+
168
+ /**
169
+ * Gère le clic dans le modal
170
+ * @param {number} x - Position X du clic
171
+ * @param {number} y - Position Y du clic
172
+ */
173
+ handlePress(x, y) {
174
+ // POUR LES MODALS: utiliser les coordonnées brutes car le modal est un composant FIXE
175
+ // Les modals ne sont pas affectés par le scroll
176
+
177
+ const modalX = (this.framework.width - this.modalWidth) / 2;
178
+ const modalY = (this.framework.height - this.modalHeight) / 2;
179
+ const contentY = modalY + 55;
180
+
181
+ // Vérifier si une option a été cliquée
182
+ for (let i = 0; i < this.options.length; i++) {
183
+ const optionY = contentY + i * this.itemHeight;
184
+
185
+ // IMPORTANT: Pas d'ajustement de scroll pour les modals
186
+ if (y >= optionY && y <= optionY + this.itemHeight &&
187
+ x >= modalX && x <= modalX + this.modalWidth) {
188
+
189
+ this.selectedIndex = i;
190
+ if (this.onSelect) {
191
+ this.onSelect(i, this.options[i]);
192
+ }
193
+ this.hide();
194
+ return;
195
+ }
196
+ }
197
+
198
+ // Sinon, laisser le parent gérer (bouton de fermeture, overlay)
199
+ super.handlePress(x, y);
200
+ }
201
+
202
+ /**
203
+ * Gère le survol dans le modal
204
+ * @param {number} x - Position X actuelle
205
+ * @param {number} y - Position Y actuelle
206
+ */
207
+ handleMove(x, y) {
208
+ const modalX = (this.framework.width - this.modalWidth) / 2;
209
+ const modalY = (this.framework.height - this.modalHeight) / 2;
210
+ const contentY = modalY + 55;
211
+
212
+ this.hoveredIndex = -1;
213
+
214
+ // Vérifier si on survole une option
215
+ for (let i = 0; i < this.options.length; i++) {
216
+ const optionY = contentY + i * this.itemHeight;
217
+
218
+ // IMPORTANT: Pas d'ajustement de scroll pour les modals
219
+ if (y >= optionY && y <= optionY + this.itemHeight &&
220
+ x >= modalX && x <= modalX + this.modalWidth) {
221
+ this.hoveredIndex = i;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Vérifie si un point est à l'intérieur du modal
229
+ * @param {number} x - Position X
230
+ * @param {number} y - Position Y
231
+ * @returns {boolean} True si le point est à l'intérieur du modal
232
+ */
233
+ isPointInside(x, y) {
234
+ const modalX = (this.framework.width - this.modalWidth) / 2;
235
+ const modalY = (this.framework.height - this.modalHeight) / 2;
236
+
237
+ // Les modals sont des composants fixes, donc pas d'ajustement de scroll
238
+ return x >= modalX &&
239
+ x <= modalX + this.modalWidth &&
240
+ y >= modalY &&
241
+ y <= modalY + this.modalHeight;
242
+ }
243
+
244
+ /**
245
+ * Affiche le modal
246
+ */
247
+ show() {
248
+ this.isVisible = true;
249
+ this.visible = true;
250
+ this.opacity = 1;
251
+ this.scale = 1;
252
+ }
253
+ }
254
+
255
+ export default SelectDialog;
@@ -0,0 +1,113 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ class Slider extends Component {
4
+ constructor(framework, options = {}) {
5
+ super(framework, options);
6
+ this.min = options.min || 0;
7
+ this.max = options.max || 100;
8
+ this.value = options.value || 50;
9
+ this.platform = framework.platform;
10
+ this.onChange = options.onChange;
11
+ this.dragging = false;
12
+
13
+ // Animation state
14
+ this.thumbScale = 1.0;
15
+ this.targetScale = 1.0;
16
+
17
+ this.onPress = this.handlePress.bind(this);
18
+ this.onMove = this.handleMove.bind(this);
19
+ this.onClick = this.handleClick.bind(this);
20
+ }
21
+
22
+ handlePress(x, y) {
23
+ this.dragging = true;
24
+ this.targetScale = 1.5; // Agrandissement de 50%
25
+ this.updateValue(x);
26
+ return true;
27
+ }
28
+
29
+ handleMove(x, y) {
30
+ if (this.dragging) {
31
+ this.updateValue(x);
32
+ }
33
+ }
34
+
35
+ handleClick() {
36
+ this.dragging = false;
37
+ this.targetScale = 1.0; // Retour à la taille normale
38
+ }
39
+
40
+ updateValue(x) {
41
+ const relativeX = Math.max(0, Math.min(this.width, x - this.x));
42
+ const newValue = this.min + (relativeX / this.width) * (this.max - this.min);
43
+
44
+ if (newValue !== this.value) {
45
+ this.value = newValue;
46
+ if (this.onChange) this.onChange(this.value);
47
+ }
48
+ }
49
+
50
+ draw(ctx) {
51
+ ctx.save();
52
+
53
+ // Animation du scale
54
+ this.thumbScale += (this.targetScale - this.thumbScale) * 0.2;
55
+
56
+ const progress = (this.value - this.min) / (this.max - this.min);
57
+ const thumbX = this.x + progress * this.width;
58
+
59
+ // Track
60
+ ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#C7C7CC';
61
+ ctx.lineWidth = 2;
62
+ ctx.beginPath();
63
+ ctx.moveTo(this.x, this.y + this.height / 2);
64
+ ctx.lineTo(this.x + this.width, this.y + this.height / 2);
65
+ ctx.stroke();
66
+
67
+ // Track rempli
68
+ const trackColor = this.platform === 'material' ? '#6200EE' : '#007AFF';
69
+ ctx.strokeStyle = trackColor;
70
+ ctx.lineWidth = 2;
71
+ ctx.beginPath();
72
+ ctx.moveTo(this.x, this.y + this.height / 2);
73
+ ctx.lineTo(thumbX, this.y + this.height / 2);
74
+ ctx.stroke();
75
+
76
+ // Curseur avec animation
77
+ const baseRadius = 8;
78
+ const currentRadius = baseRadius * this.thumbScale;
79
+
80
+ // Effet d'ombre pendant le drag
81
+ if (this.dragging) {
82
+ ctx.shadowColor = trackColor;
83
+ ctx.shadowBlur = 10;
84
+ }
85
+
86
+ ctx.fillStyle = trackColor;
87
+ ctx.beginPath();
88
+ ctx.arc(thumbX, this.y + this.height / 2, currentRadius, 0, Math.PI * 2);
89
+ ctx.fill();
90
+
91
+ if (this.dragging) {
92
+ ctx.shadowColor = 'transparent';
93
+ ctx.shadowBlur = 0;
94
+ }
95
+
96
+ ctx.restore();
97
+ }
98
+
99
+ isPointInside(x, y) {
100
+ const progress = (this.value - this.min) / (this.max - this.min);
101
+ const thumbX = this.x + progress * this.width;
102
+ const thumbY = this.y + this.height / 2;
103
+ const maxRadius = 8 * 1.5 + 5; // Rayon max + marge
104
+
105
+ // Zone de clic plus large pour faciliter l'utilisation
106
+ const distance = Math.sqrt((x - thumbX) ** 2 + (y - thumbY) ** 2);
107
+ return distance <= maxRadius ||
108
+ (x >= this.x && x <= this.x + this.width &&
109
+ y >= this.y && y <= this.y + this.height);
110
+ }
111
+ }
112
+
113
+ export default Slider;
@@ -0,0 +1,243 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Snackbar (notification avec action)
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} message - Message
7
+ * @property {string|null} actionText - Texte de l'action
8
+ * @property {Function} onAction - Callback de l'action
9
+ * @property {number} duration - Durée d'affichage
10
+ * @property {string} platform - Plateforme
11
+ * @property {number} padding - Padding interne
12
+ * @property {number} minWidth - Largeur minimale
13
+ * @property {number} maxWidth - Largeur maximale
14
+ * @property {number} targetY - Position Y cible
15
+ * @property {number} opacity - Opacité
16
+ * @property {boolean} isVisible - Visibilité
17
+ * @property {Object|null} actionRect - Rectangle de l'action
18
+ * @property {boolean} actionHovered - Action survolée
19
+ */
20
+ class Snackbar extends Component {
21
+ /**
22
+ * Crée une instance de Snackbar
23
+ * @param {CanvasFramework} framework - Framework parent
24
+ * @param {Object} [options={}] - Options de configuration
25
+ * @param {string} [options.message=''] - Message
26
+ * @param {string} [options.actionText] - Texte de l'action
27
+ * @param {Function} [options.onAction] - Callback de l'action
28
+ * @param {number} [options.duration=4000] - Durée en ms
29
+ */
30
+ constructor(framework, options = {}) {
31
+ super(framework, options);
32
+ this.message = options.message || '';
33
+ this.actionText = options.actionText || null;
34
+ this.onAction = options.onAction;
35
+ this.duration = options.duration || 4000;
36
+ this.platform = framework.platform;
37
+
38
+ // Dimensions
39
+ this.height = 48;
40
+ this.padding = 16;
41
+ this.minWidth = 344;
42
+ this.maxWidth = Math.min(672, framework.width - 32);
43
+
44
+ // Calculer la largeur
45
+ const ctx = framework.ctx;
46
+ ctx.font = '14px -apple-system, Roboto, sans-serif';
47
+ const messageWidth = ctx.measureText(this.message).width;
48
+ const actionWidth = this.actionText ? ctx.measureText(this.actionText).width + 40 : 0;
49
+ this.width = Math.min(this.maxWidth, Math.max(this.minWidth, messageWidth + actionWidth + this.padding * 3));
50
+
51
+ // Position (centré en bas)
52
+ this.x = (framework.width - this.width) / 2;
53
+ this.y = framework.height; // Commence hors écran
54
+ this.targetY = framework.height - this.height - 37;
55
+
56
+ this.opacity = 0;
57
+ this.isVisible = false;
58
+
59
+ // Zone du bouton d'action
60
+ this.actionRect = null;
61
+
62
+ this.onPress = this.handlePress.bind(this);
63
+ }
64
+
65
+ /**
66
+ * Affiche la snackbar
67
+ */
68
+ show() {
69
+ this.isVisible = true;
70
+ this.visible = true;
71
+ this.animateIn();
72
+
73
+ // Auto-hide après duration
74
+ setTimeout(() => {
75
+ if (this.isVisible) {
76
+ this.hide();
77
+ }
78
+ }, this.duration);
79
+ }
80
+
81
+ /**
82
+ * Cache la snackbar
83
+ */
84
+ hide() {
85
+ this.animateOut();
86
+ }
87
+
88
+ /**
89
+ * Anime l'entrée
90
+ * @private
91
+ */
92
+ animateIn() {
93
+ const animate = () => {
94
+ if (this.y > this.targetY) {
95
+ this.y -= (this.y - this.targetY) * 0.2;
96
+ this.opacity = Math.min(1, this.opacity + 0.1);
97
+ requestAnimationFrame(animate);
98
+ } else {
99
+ this.y = this.targetY;
100
+ this.opacity = 1;
101
+ }
102
+ };
103
+ animate();
104
+ }
105
+
106
+ /**
107
+ * Anime la sortie
108
+ * @private
109
+ */
110
+ animateOut() {
111
+ const animate = () => {
112
+ if (this.opacity > 0) {
113
+ this.y += 5;
114
+ this.opacity -= 0.1;
115
+ requestAnimationFrame(animate);
116
+ } else {
117
+ this.isVisible = false;
118
+ this.visible = false;
119
+ this.framework.remove(this);
120
+ }
121
+ };
122
+ animate();
123
+ }
124
+
125
+ /**
126
+ * Dessine la snackbar
127
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
128
+ */
129
+ draw(ctx) {
130
+ if (!this.isVisible || this.opacity <= 0) return;
131
+
132
+ ctx.save();
133
+ ctx.globalAlpha = this.opacity;
134
+
135
+ // Background
136
+ ctx.fillStyle = this.platform === 'material' ? '#323232' : 'rgba(0, 0, 0, 0.9)';
137
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
138
+ ctx.shadowBlur = 8;
139
+ ctx.shadowOffsetY = 4;
140
+
141
+ ctx.beginPath();
142
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
143
+ ctx.fill();
144
+
145
+ ctx.shadowColor = 'transparent';
146
+
147
+ // Message
148
+ ctx.fillStyle = '#FFFFFF';
149
+ ctx.font = '14px -apple-system, Roboto, sans-serif';
150
+ ctx.textAlign = 'left';
151
+ ctx.textBaseline = 'middle';
152
+ ctx.fillText(this.message, this.x + this.padding, this.y + this.height / 2);
153
+
154
+ // Bouton d'action
155
+ if (this.actionText) {
156
+ const ctx2 = this.framework.ctx;
157
+ ctx2.font = '14px -apple-system, Roboto, sans-serif';
158
+ const actionWidth = ctx2.measureText(this.actionText).width;
159
+ const actionX = this.x + this.width - actionWidth - this.padding * 2;
160
+ const actionY = this.y;
161
+
162
+ this.actionRect = {
163
+ x: actionX,
164
+ y: actionY,
165
+ width: actionWidth + this.padding * 2,
166
+ height: this.height
167
+ };
168
+
169
+ // Highlight si hover
170
+ if (this.actionHovered) {
171
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
172
+ ctx.fillRect(actionX, actionY, actionWidth + this.padding * 2, this.height);
173
+ }
174
+
175
+ // Texte du bouton
176
+ const actionColor = this.platform === 'material' ? '#BB86FC' : '#0A84FF';
177
+ ctx.fillStyle = actionColor;
178
+ ctx.font = 'bold 14px -apple-system, Roboto, sans-serif';
179
+ ctx.textAlign = 'center';
180
+ ctx.fillText(this.actionText, actionX + (actionWidth + this.padding * 2) / 2, this.y + this.height / 2);
181
+ }
182
+
183
+ ctx.restore();
184
+ }
185
+
186
+ /**
187
+ * Gère la pression (clic)
188
+ * @param {number} x - Coordonnée X
189
+ * @param {number} y - Coordonnée Y
190
+ * @private
191
+ */
192
+ handlePress(x, y) {
193
+ if (this.actionRect && this.actionText) {
194
+ if (x >= this.actionRect.x &&
195
+ x <= this.actionRect.x + this.actionRect.width &&
196
+ y >= this.actionRect.y &&
197
+ y <= this.actionRect.y + this.actionRect.height) {
198
+ if (this.onAction) {
199
+ this.onAction();
200
+ }
201
+ this.hide();
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Dessine un rectangle avec coins arrondis
208
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
209
+ * @param {number} x - Position X
210
+ * @param {number} y - Position Y
211
+ * @param {number} width - Largeur
212
+ * @param {number} height - Hauteur
213
+ * @param {number} radius - Rayon des coins
214
+ * @private
215
+ */
216
+ roundRect(ctx, x, y, width, height, radius) {
217
+ ctx.beginPath();
218
+ ctx.moveTo(x + radius, y);
219
+ ctx.lineTo(x + width - radius, y);
220
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
221
+ ctx.lineTo(x + width, y + height - radius);
222
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
223
+ ctx.lineTo(x + radius, y + height);
224
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
225
+ ctx.lineTo(x, y + radius);
226
+ ctx.quadraticCurveTo(x, y, x + radius, y);
227
+ ctx.closePath();
228
+ }
229
+
230
+ /**
231
+ * Vérifie si un point est dans les limites
232
+ * @param {number} x - Coordonnée X
233
+ * @param {number} y - Coordonnée Y
234
+ * @returns {boolean} True si le point est dans la snackbar
235
+ */
236
+ isPointInside(x, y) {
237
+ return this.isVisible &&
238
+ x >= this.x && x <= this.x + this.width &&
239
+ y >= this.y && y <= this.y + this.height;
240
+ }
241
+ }
242
+
243
+ export default Snackbar;