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,141 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Composant texte
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} text - Texte à afficher
7
+ * @property {number} fontSize - Taille de police
8
+ * @property {string} color - Couleur
9
+ * @property {string} align - Alignement ('left', 'center', 'right')
10
+ * @property {boolean} bold - Gras
11
+ * @property {number|null} maxWidth - Largeur maximale
12
+ * @property {boolean} wrap - Retour à la ligne
13
+ * @property {number} lineHeight - Hauteur de ligne
14
+ * @property {string[]|null} wrappedLines - Lignes après wrap
15
+ */
16
+ class Text extends Component {
17
+ /**
18
+ * Crée une instance de Text
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {string} [options.text=''] - Texte
22
+ * @param {number} [options.fontSize=16] - Taille de police
23
+ * @param {string} [options.color='#000000'] - Couleur
24
+ * @param {string} [options.align='left'] - Alignement
25
+ * @param {boolean} [options.bold=false] - Gras
26
+ * @param {number} [options.maxWidth] - Largeur maximale
27
+ * @param {boolean} [options.wrap=false] - Retour à la ligne
28
+ * @param {number} [options.lineHeight] - Hauteur de ligne
29
+ */
30
+ constructor(framework, options = {}) {
31
+ super(framework, options);
32
+ this.text = options.text || '';
33
+ this.fontSize = options.fontSize || 16;
34
+ this.color = options.color || '#000000';
35
+ this.align = options.align || 'left';
36
+ this.bold = options.bold || false;
37
+ this.maxWidth = options.maxWidth || null; // Nouvelle option: largeur maximale
38
+ this.wrap = options.wrap || false; // Nouvelle option: retour à la ligne
39
+ this.lineHeight = options.lineHeight || this.fontSize * 1.2;
40
+
41
+ // Calculer la hauteur en fonction du texte
42
+ if (this.wrap && this.maxWidth && this.text) {
43
+ this.calculateWrappedHeight();
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Calcule la hauteur avec wrap
49
+ * @private
50
+ */
51
+ calculateWrappedHeight() {
52
+ // Cette méthode sera appelée dans draw quand on a le contexte
53
+ // Pour l'instant, on initialise juste
54
+ this.wrappedLines = null;
55
+ }
56
+
57
+ /**
58
+ * Dessine le texte
59
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
60
+ */
61
+ draw(ctx) {
62
+ ctx.save();
63
+ ctx.fillStyle = this.color;
64
+ ctx.font = `${this.bold ? 'bold ' : ''}${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
65
+ ctx.textAlign = this.align;
66
+ ctx.textBaseline = 'top';
67
+
68
+ let lines = [this.text];
69
+
70
+ // Si wrap est activé et on a une largeur max, on divise le texte
71
+ if (this.wrap && this.maxWidth && this.text) {
72
+ lines = this.wrapText(ctx, this.text, this.maxWidth);
73
+ } else if (this.maxWidth && this.text) {
74
+ // Sinon, on tronque le texte avec des points de suspension
75
+ const ellipsis = '...';
76
+ let text = this.text;
77
+ while (ctx.measureText(text).width > this.maxWidth && text.length > 3) {
78
+ text = text.substring(0, text.length - 1);
79
+ }
80
+ if (text !== this.text && text.length > 3) {
81
+ text = text.substring(0, text.length - 3) + ellipsis;
82
+ }
83
+ lines = [text];
84
+ }
85
+
86
+ // Calculer la position x en fonction de l'alignement
87
+ const x = this.align === 'center' ? this.x + (this.maxWidth || this.width) / 2 :
88
+ this.align === 'right' ? this.x + (this.maxWidth || this.width) : this.x;
89
+
90
+ // Dessiner chaque ligne
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i];
93
+ const y = this.y + (i * this.lineHeight);
94
+ ctx.fillText(line, x, y);
95
+ }
96
+
97
+ // Ajuster la hauteur si on a plusieurs lignes
98
+ if (lines.length > 1) {
99
+ this.height = lines.length * this.lineHeight;
100
+ }
101
+
102
+ ctx.restore();
103
+ }
104
+
105
+ /**
106
+ * Divise le texte en plusieurs lignes
107
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
108
+ * @param {string} text - Texte à diviser
109
+ * @param {number} maxWidth - Largeur maximale
110
+ * @returns {string[]} Tableau de lignes
111
+ * @private
112
+ */
113
+ wrapText(ctx, text, maxWidth) {
114
+ const words = text.split(' ');
115
+ const lines = [];
116
+ let currentLine = words[0];
117
+
118
+ for (let i = 1; i < words.length; i++) {
119
+ const word = words[i];
120
+ const width = ctx.measureText(currentLine + " " + word).width;
121
+ if (width < maxWidth) {
122
+ currentLine += " " + word;
123
+ } else {
124
+ lines.push(currentLine);
125
+ currentLine = word;
126
+ }
127
+ }
128
+ lines.push(currentLine);
129
+ return lines;
130
+ }
131
+
132
+ /**
133
+ * Vérifie si un point est dans les limites
134
+ * @returns {boolean} False (non cliquable)
135
+ */
136
+ isPointInside() {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ export default Text;
@@ -0,0 +1,331 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Champ de texte avancé avec label flottant, validation et messages d'erreur
4
+ * @class
5
+ * @extends Component
6
+ * @param {Framework} framework - Instance du framework
7
+ * @param {Object} [options={}] - Options de configuration
8
+ * @param {string} [options.label=''] - Label du champ
9
+ * @param {string} [options.value=''] - Valeur initiale
10
+ * @param {string} [options.placeholder=''] - Placeholder
11
+ * @param {string} [options.helperText=''] - Texte d'aide
12
+ * @param {string} [options.errorText=''] - Texte d'erreur
13
+ * @param {boolean} [options.error=false] - État d'erreur
14
+ * @param {number} [options.fontSize=16] - Taille de police
15
+ * @param {Function} [options.onChange] - Callback lors du changement
16
+ * @param {number} [options.height=80] - Hauteur totale (inclut label + input + helper)
17
+ * @example
18
+ * const textField = new TextField(framework, {
19
+ * label: 'Email',
20
+ * placeholder: 'Entrez votre email',
21
+ * helperText: 'Nous ne partagerons jamais votre email',
22
+ * onChange: (value) => validateEmail(value)
23
+ * });
24
+ */
25
+ class TextField extends Component {
26
+ /**
27
+ * @constructs TextField
28
+ */
29
+ constructor(framework, options = {}) {
30
+ super(framework, options);
31
+ /** @type {string} */
32
+ this.label = options.label || '';
33
+ /** @type {string} */
34
+ this.value = options.value || '';
35
+ /** @type {string} */
36
+ this.placeholder = options.placeholder || '';
37
+ /** @type {string} */
38
+ this.helperText = options.helperText || '';
39
+ /** @type {string} */
40
+ this.errorText = options.errorText || '';
41
+ /** @type {boolean} */
42
+ this.error = options.error || false;
43
+ /** @type {string} */
44
+ this.platform = framework.platform;
45
+ /** @type {boolean} */
46
+ this.focused = false;
47
+ /** @type {number} */
48
+ this.fontSize = options.fontSize || 16;
49
+ /** @type {Function|undefined} */
50
+ this.onChange = options.onChange;
51
+ /** @type {number} */
52
+ this.labelY = this.value ? -10 : 20; // Position du label
53
+ /** @type {number} */
54
+ this.labelFontSize = this.value ? 12 : 16;
55
+ /** @type {boolean} */
56
+ this.cursorVisible = true;
57
+
58
+ // Hauteur pour inclure label + input + helper
59
+ this.height = options.height || 80;
60
+
61
+ this.onFocus = this.handleFocus.bind(this);
62
+ this.onBlur = this.handleBlur.bind(this);
63
+
64
+ this.setupHiddenInput();
65
+
66
+ // Animation du curseur
67
+ /** @type {number} */
68
+ this.cursorInterval = setInterval(() => {
69
+ if (this.focused) this.cursorVisible = !this.cursorVisible;
70
+ }, 500);
71
+ }
72
+
73
+ /**
74
+ * Configure l'input caché dans le DOM
75
+ * @private
76
+ */
77
+ setupHiddenInput() {
78
+ let hiddenInput = document.getElementById('hidden-textfield-input');
79
+ if (!hiddenInput) {
80
+ hiddenInput = document.createElement('input');
81
+ hiddenInput.id = 'hidden-textfield-input';
82
+ hiddenInput.type = 'text';
83
+ hiddenInput.style.position = 'fixed';
84
+ hiddenInput.style.opacity = '0';
85
+ hiddenInput.style.pointerEvents = 'none';
86
+ hiddenInput.style.top = '-100px';
87
+ document.body.appendChild(hiddenInput);
88
+
89
+ hiddenInput.addEventListener('input', (e) => {
90
+ if (this.focused) {
91
+ this.value = e.target.value;
92
+ if (this.onChange) this.onChange(this.value);
93
+ this.animateLabel();
94
+ }
95
+ });
96
+
97
+ hiddenInput.addEventListener('blur', () => {
98
+ this.handleBlur();
99
+ });
100
+ }
101
+ /** @type {HTMLInputElement} */
102
+ this.hiddenInput = hiddenInput;
103
+ }
104
+
105
+ /**
106
+ * Anime le label (flottant)
107
+ * @private
108
+ */
109
+ animateLabel() {
110
+ const shouldFloat = this.focused || this.value;
111
+ const targetY = shouldFloat ? -10 : 20;
112
+ const targetSize = shouldFloat ? 12 : 16;
113
+
114
+ const animate = () => {
115
+ const diffY = targetY - this.labelY;
116
+ const diffSize = targetSize - this.labelFontSize;
117
+
118
+ if (Math.abs(diffY) < 0.5 && Math.abs(diffSize) < 0.5) {
119
+ this.labelY = targetY;
120
+ this.labelFontSize = targetSize;
121
+ return;
122
+ }
123
+
124
+ this.labelY += diffY * 0.2;
125
+ this.labelFontSize += diffSize * 0.2;
126
+
127
+ requestAnimationFrame(animate);
128
+ };
129
+
130
+ animate();
131
+ }
132
+
133
+ /**
134
+ * Gère le focus sur le champ
135
+ */
136
+ handleFocus() {
137
+ this.focused = true;
138
+ this.cursorVisible = true;
139
+ if (this.hiddenInput) {
140
+ this.hiddenInput.value = this.value;
141
+ const adjustedY = this.y + this.framework.scrollOffset;
142
+ this.hiddenInput.style.top = `${adjustedY}px`;
143
+ this.hiddenInput.focus();
144
+ }
145
+ this.animateLabel();
146
+ }
147
+
148
+ /**
149
+ * Gère la perte de focus
150
+ */
151
+ handleBlur() {
152
+ this.focused = false;
153
+ this.cursorVisible = false;
154
+ this.animateLabel();
155
+ }
156
+
157
+ /**
158
+ * Gère le clic sur le champ
159
+ */
160
+ onClick() {
161
+ this.handleFocus();
162
+ }
163
+
164
+ /**
165
+ * Dessine le champ de texte
166
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
167
+ */
168
+ draw(ctx) {
169
+ ctx.save();
170
+
171
+ const inputY = this.y + 30;
172
+ const inputHeight = 40;
173
+
174
+ if (this.platform === 'material') {
175
+ // Material Design TextField
176
+
177
+ // Label flottant
178
+ const labelColor = this.error ? '#B00020' :
179
+ (this.focused ? '#6200EE' : '#757575');
180
+ ctx.fillStyle = labelColor;
181
+ ctx.font = `${this.labelFontSize}px Roboto, sans-serif`;
182
+ ctx.textAlign = 'left';
183
+ ctx.textBaseline = 'middle';
184
+ ctx.fillText(this.label, this.x, this.y + 20 + this.labelY);
185
+
186
+ // Ligne de soulignement
187
+ const lineColor = this.error ? '#B00020' :
188
+ (this.focused ? '#6200EE' : '#CCCCCC');
189
+ ctx.strokeStyle = lineColor;
190
+ ctx.lineWidth = this.focused ? 2 : 1;
191
+ ctx.beginPath();
192
+ ctx.moveTo(this.x, inputY + inputHeight);
193
+ ctx.lineTo(this.x + this.width, inputY + inputHeight);
194
+ ctx.stroke();
195
+
196
+ // Valeur ou placeholder
197
+ const displayText = this.value || (this.focused ? '' : this.placeholder);
198
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
199
+ ctx.font = `${this.fontSize}px Roboto, sans-serif`;
200
+ ctx.textBaseline = 'middle';
201
+ ctx.fillText(displayText, this.x, inputY + inputHeight / 2);
202
+
203
+ // Curseur
204
+ if (this.focused && this.cursorVisible) {
205
+ const textWidth = ctx.measureText(this.value).width;
206
+ ctx.fillStyle = '#6200EE';
207
+ ctx.fillRect(this.x + textWidth + 2, inputY + 10, 2, inputHeight - 20);
208
+ }
209
+
210
+ // Helper text ou error text
211
+ const helperColor = this.error ? '#B00020' : '#757575';
212
+ const helperMessage = this.error ? this.errorText : this.helperText;
213
+
214
+ if (helperMessage) {
215
+ ctx.fillStyle = helperColor;
216
+ ctx.font = '12px Roboto, sans-serif';
217
+ ctx.fillText(helperMessage, this.x, inputY + inputHeight + 20);
218
+ }
219
+
220
+ } else {
221
+ // Cupertino style (label au-dessus)
222
+
223
+ if (this.label) {
224
+ ctx.fillStyle = '#000000';
225
+ ctx.font = 'bold 14px -apple-system, sans-serif';
226
+ ctx.textAlign = 'left';
227
+ ctx.textBaseline = 'top';
228
+ ctx.fillText(this.label, this.x, this.y);
229
+ }
230
+
231
+ // Input box
232
+ ctx.strokeStyle = this.error ? '#FF3B30' :
233
+ (this.focused ? '#007AFF' : '#C7C7CC');
234
+ ctx.lineWidth = 1;
235
+ ctx.beginPath();
236
+ this.roundRect(ctx, this.x, inputY, this.width, inputHeight, 8);
237
+ ctx.stroke();
238
+
239
+ // Valeur ou placeholder
240
+ const displayText = this.value || this.placeholder;
241
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
242
+ ctx.font = `${this.fontSize}px -apple-system, sans-serif`;
243
+ ctx.textAlign = 'left';
244
+ ctx.textBaseline = 'middle';
245
+ ctx.fillText(displayText, this.x + 10, inputY + inputHeight / 2);
246
+
247
+ // Curseur
248
+ if (this.focused && this.cursorVisible) {
249
+ const textWidth = ctx.measureText(this.value).width;
250
+ ctx.fillStyle = '#007AFF';
251
+ ctx.fillRect(this.x + 10 + textWidth + 2, inputY + 10, 2, inputHeight - 20);
252
+ }
253
+
254
+ // Helper/Error text
255
+ if (this.error && this.errorText) {
256
+ ctx.fillStyle = '#FF3B30';
257
+ ctx.font = '12px -apple-system, sans-serif';
258
+ ctx.fillText(this.errorText, this.x, inputY + inputHeight + 8);
259
+ } else if (this.helperText) {
260
+ ctx.fillStyle = '#8E8E93';
261
+ ctx.font = '12px -apple-system, sans-serif';
262
+ ctx.fillText(this.helperText, this.x, inputY + inputHeight + 8);
263
+ }
264
+ }
265
+
266
+ ctx.restore();
267
+ }
268
+
269
+ /**
270
+ * Dessine un rectangle avec des coins arrondis
271
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
272
+ * @param {number} x - Position X
273
+ * @param {number} y - Position Y
274
+ * @param {number} width - Largeur
275
+ * @param {number} height - Hauteur
276
+ * @param {number} radius - Rayon des coins
277
+ * @private
278
+ */
279
+ roundRect(ctx, x, y, width, height, radius) {
280
+ ctx.beginPath();
281
+ ctx.moveTo(x + radius, y);
282
+ ctx.lineTo(x + width - radius, y);
283
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
284
+ ctx.lineTo(x + width, y + height - radius);
285
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
286
+ ctx.lineTo(x + radius, y + height);
287
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
288
+ ctx.lineTo(x, y + radius);
289
+ ctx.quadraticCurveTo(x, y, x + radius, y);
290
+ ctx.closePath();
291
+ }
292
+
293
+ /**
294
+ * Vérifie si un point est à l'intérieur du composant
295
+ * @param {number} x - Position X
296
+ * @param {number} y - Position Y
297
+ * @returns {boolean} True si le point est à l'intérieur
298
+ */
299
+ isPointInside(x, y) {
300
+ return x >= this.x && x <= this.x + this.width &&
301
+ y >= this.y && y <= this.y + this.height;
302
+ }
303
+
304
+ /**
305
+ * Définit une erreur sur le champ
306
+ * @param {string} errorText - Texte d'erreur à afficher
307
+ */
308
+ setError(errorText) {
309
+ this.error = true;
310
+ this.errorText = errorText;
311
+ }
312
+
313
+ /**
314
+ * Efface l'erreur du champ
315
+ */
316
+ clearError() {
317
+ this.error = false;
318
+ this.errorText = '';
319
+ }
320
+
321
+ /**
322
+ * Nettoie les ressources (arrête l'animation du curseur)
323
+ */
324
+ destroy() {
325
+ if (this.cursorInterval) {
326
+ clearInterval(this.cursorInterval);
327
+ }
328
+ }
329
+ }
330
+
331
+ export default TextField;
@@ -0,0 +1,236 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Toast (notification temporaire)
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} text - Message
7
+ * @property {number} duration - Durée d'affichage
8
+ * @property {number} fontSize - Taille de police
9
+ * @property {number} padding - Padding interne
10
+ * @property {number} opacity - Opacité
11
+ * @property {string} platform - Plateforme
12
+ * @property {boolean} isVisible - Visibilité
13
+ * @property {number} targetY - Position Y cible
14
+ * @property {number} minWidth - Largeur minimale
15
+ * @property {number} maxWidth - Largeur maximale
16
+ * @property {boolean} animating - En cours d'animation
17
+ */
18
+ class Toast extends Component {
19
+ /**
20
+ * Crée une instance de Toast
21
+ * @param {CanvasFramework} framework - Framework parent
22
+ * @param {Object} [options={}] - Options de configuration
23
+ * @param {string} [options.text=''] - Message
24
+ * @param {number} [options.duration=3000] - Durée en ms
25
+ * @param {number} [options.x] - Position X (auto-centré)
26
+ * @param {number} [options.y] - Position Y (en bas)
27
+ */
28
+ constructor(framework, options = {}) {
29
+ super(framework, {
30
+ x: 0,
31
+ y: framework.height, // Commence hors écran en bas
32
+ width: framework.width,
33
+ height: 60, // Hauteur fixe pour le toast
34
+ ...options
35
+ });
36
+
37
+ this.text = options.text || '';
38
+ this.duration = options.duration || 3000;
39
+ this.fontSize = 16;
40
+ this.padding = 20;
41
+ this.opacity = 0;
42
+ this.platform = framework.platform;
43
+ this.isVisible = false;
44
+
45
+ // Position cible (en bas, légèrement remonté)
46
+ this.targetY = framework.height - 100;
47
+
48
+ // Calculer la largeur minimale
49
+ this.minWidth = 200;
50
+ this.maxWidth = Math.min(600, framework.width - 40);
51
+
52
+ // Animation
53
+ this.animating = false;
54
+
55
+ // NE PAS appeler show() ici - laissé à l'appelant
56
+ }
57
+
58
+ /**
59
+ * Affiche le toast
60
+ */
61
+ show() {
62
+ this.isVisible = true;
63
+ this.visible = true;
64
+ this.animateIn();
65
+
66
+ // Auto-dismiss après la durée
67
+ setTimeout(() => {
68
+ if (this.isVisible) {
69
+ this.hide();
70
+ }
71
+ }, this.duration);
72
+ }
73
+
74
+ /**
75
+ * Cache le toast
76
+ */
77
+ hide() {
78
+ this.animateOut();
79
+ }
80
+
81
+ /**
82
+ * Anime l'entrée
83
+ * @private
84
+ */
85
+ animateIn() {
86
+ if (this.animating) return;
87
+ this.animating = true;
88
+
89
+ const animate = () => {
90
+ this.opacity += 0.1;
91
+ this.y -= (this.y - this.targetY) * 0.2;
92
+
93
+ if (this.opacity >= 1 && Math.abs(this.y - this.targetY) < 1) {
94
+ this.opacity = 1;
95
+ this.y = this.targetY;
96
+ this.animating = false;
97
+ return;
98
+ }
99
+
100
+ requestAnimationFrame(animate);
101
+ };
102
+
103
+ animate();
104
+ }
105
+
106
+ /**
107
+ * Anime la sortie
108
+ * @private
109
+ */
110
+ animateOut() {
111
+ if (this.animating) return;
112
+ this.animating = true;
113
+
114
+ const animate = () => {
115
+ this.opacity -= 0.1;
116
+ this.y += 5;
117
+
118
+ if (this.opacity <= 0) {
119
+ this.opacity = 0;
120
+ this.isVisible = false;
121
+ this.visible = false;
122
+ this.animating = false;
123
+ this.framework.remove(this);
124
+ return;
125
+ }
126
+
127
+ requestAnimationFrame(animate);
128
+ };
129
+
130
+ animate();
131
+ }
132
+
133
+ /**
134
+ * Dessine le toast
135
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
136
+ */
137
+ draw(ctx) {
138
+ if (!this.isVisible || this.opacity <= 0) return;
139
+
140
+ ctx.save();
141
+ ctx.globalAlpha = this.opacity;
142
+
143
+ // Calculer la largeur en fonction du texte
144
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
145
+ const textWidth = ctx.measureText(this.text).width;
146
+ const toastWidth = Math.min(
147
+ this.maxWidth,
148
+ Math.max(this.minWidth, textWidth + this.padding * 2)
149
+ );
150
+
151
+ // Position centrée horizontalement
152
+ const toastX = (this.framework.width - toastWidth) / 2;
153
+ const toastY = this.y;
154
+
155
+ if (this.platform === 'material') {
156
+ // Material Toast
157
+ ctx.fillStyle = '#323232';
158
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
159
+ ctx.shadowBlur = 15;
160
+ ctx.shadowOffsetY = 4;
161
+ ctx.beginPath();
162
+ this.roundRect(ctx, toastX, toastY, toastWidth, this.height, 8);
163
+ ctx.fill();
164
+ } else {
165
+ // Cupertino Toast
166
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
167
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
168
+ ctx.shadowBlur = 20;
169
+ ctx.shadowOffsetY = 4;
170
+ ctx.beginPath();
171
+ this.roundRect(ctx, toastX, toastY, toastWidth, this.height, 14);
172
+ ctx.fill();
173
+ }
174
+
175
+ ctx.shadowColor = 'transparent';
176
+ ctx.shadowBlur = 0;
177
+
178
+ // Texte
179
+ ctx.fillStyle = '#FFFFFF';
180
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
181
+ ctx.textAlign = 'center';
182
+ ctx.textBaseline = 'middle';
183
+
184
+ // Tronquer le texte si nécessaire
185
+ let displayText = this.text;
186
+ if (textWidth > toastWidth - this.padding * 2) {
187
+ // Trouver où couper le texte
188
+ let truncated = this.text;
189
+ for (let i = this.text.length; i > 0; i--) {
190
+ truncated = this.text.substring(0, i) + '...';
191
+ if (ctx.measureText(truncated).width <= toastWidth - this.padding * 2) {
192
+ displayText = truncated;
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ ctx.fillText(displayText, toastX + toastWidth / 2, toastY + this.height / 2);
199
+
200
+ ctx.restore();
201
+ }
202
+
203
+ /**
204
+ * Dessine un rectangle avec coins arrondis
205
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
206
+ * @param {number} x - Position X
207
+ * @param {number} y - Position Y
208
+ * @param {number} width - Largeur
209
+ * @param {number} height - Hauteur
210
+ * @param {number} radius - Rayon des coins
211
+ * @private
212
+ */
213
+ roundRect(ctx, x, y, width, height, radius) {
214
+ ctx.beginPath();
215
+ ctx.moveTo(x + radius, y);
216
+ ctx.lineTo(x + width - radius, y);
217
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
218
+ ctx.lineTo(x + width, y + height - radius);
219
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
220
+ ctx.lineTo(x + radius, y + height);
221
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
222
+ ctx.lineTo(x, y + radius);
223
+ ctx.quadraticCurveTo(x, y, x + radius, y);
224
+ ctx.closePath();
225
+ }
226
+
227
+ /**
228
+ * Vérifie si un point est dans les limites
229
+ * @returns {boolean} False (non cliquable)
230
+ */
231
+ isPointInside() {
232
+ return false; // Non cliquable
233
+ }
234
+ }
235
+
236
+ export default Toast;