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,281 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Stepper (incrémenteur/décrémenteur)
4
+ * @class
5
+ * @extends Component
6
+ * @property {number} value - Valeur
7
+ * @property {number} min - Minimum
8
+ * @property {number} max - Maximum
9
+ * @property {number} step - Pas d'incrémentation
10
+ * @property {string} platform - Plateforme
11
+ * @property {Function} onChange - Callback au changement
12
+ * @property {number} buttonWidth - Largeur des boutons
13
+ * @property {boolean} decrementPressed - Bouton décrémenter pressé
14
+ * @property {boolean} incrementPressed - Bouton incrémenter pressé
15
+ */
16
+ class Stepper extends Component {
17
+ /**
18
+ * Crée une instance de Stepper
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {number} [options.value=0] - Valeur initiale
22
+ * @param {number} [options.min=0] - Minimum
23
+ * @param {number} [options.max=100] - Maximum
24
+ * @param {number} [options.step=1] - Pas
25
+ * @param {Function} [options.onChange] - Callback au changement
26
+ * @param {number} [options.width=120] - Largeur
27
+ * @param {number} [options.height=40] - Hauteur
28
+ */
29
+ constructor(framework, options = {}) {
30
+ super(framework, options);
31
+ this.value = options.value || 0;
32
+ this.min = options.min !== undefined ? options.min : 0;
33
+ this.max = options.max !== undefined ? options.max : 100;
34
+ this.step = options.step || 1;
35
+ this.platform = framework.platform;
36
+ this.onChange = options.onChange;
37
+ this.width = options.width || 120;
38
+ this.height = options.height || 40;
39
+ this.buttonWidth = this.height;
40
+
41
+ this.decrementPressed = false;
42
+ this.incrementPressed = false;
43
+
44
+ this.onPress = this.handlePress.bind(this);
45
+ }
46
+
47
+ /**
48
+ * Incrémente la valeur
49
+ */
50
+ increment() {
51
+ if (this.value + this.step <= this.max) {
52
+ this.value += this.step;
53
+ if (this.onChange) this.onChange(this.value);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Décrémente la valeur
59
+ */
60
+ decrement() {
61
+ if (this.value - this.step >= this.min) {
62
+ this.value -= this.step;
63
+ if (this.onChange) this.onChange(this.value);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Définit la valeur
69
+ * @param {number} value - Nouvelle valeur
70
+ */
71
+ setValue(value) {
72
+ this.value = Math.max(this.min, Math.min(this.max, value));
73
+ if (this.onChange) this.onChange(this.value);
74
+ }
75
+
76
+ /**
77
+ * Dessine le stepper
78
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
79
+ */
80
+ draw(ctx) {
81
+ ctx.save();
82
+
83
+ if (this.platform === 'material') {
84
+ // Material Design Stepper
85
+
86
+ // Bouton décrement (-)
87
+ const canDecrement = this.value > this.min;
88
+ ctx.fillStyle = this.decrementPressed ? '#E0E0E0' : '#F5F5F5';
89
+ ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
90
+ ctx.lineWidth = 2;
91
+
92
+ ctx.beginPath();
93
+ this.roundRect(ctx, this.x, this.y, this.buttonWidth, this.height, 4);
94
+ ctx.fill();
95
+ ctx.stroke();
96
+
97
+ // Icône -
98
+ ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
99
+ ctx.lineWidth = 2;
100
+ ctx.lineCap = 'round';
101
+ ctx.beginPath();
102
+ ctx.moveTo(this.x + this.buttonWidth / 2 - 8, this.y + this.height / 2);
103
+ ctx.lineTo(this.x + this.buttonWidth / 2 + 8, this.y + this.height / 2);
104
+ ctx.stroke();
105
+
106
+ // Zone centrale (valeur)
107
+ ctx.fillStyle = '#FFFFFF';
108
+ ctx.fillRect(this.x + this.buttonWidth, this.y,
109
+ this.width - (this.buttonWidth * 2), this.height);
110
+
111
+ ctx.strokeStyle = '#E0E0E0';
112
+ ctx.lineWidth = 1;
113
+ ctx.strokeRect(this.x + this.buttonWidth, this.y,
114
+ this.width - (this.buttonWidth * 2), this.height);
115
+
116
+ // Valeur
117
+ ctx.fillStyle = '#000000';
118
+ ctx.font = 'bold 16px Roboto, sans-serif';
119
+ ctx.textAlign = 'center';
120
+ ctx.textBaseline = 'middle';
121
+ ctx.fillText(this.value.toString(), this.x + this.width / 2, this.y + this.height / 2);
122
+
123
+ // Bouton increment (+)
124
+ const canIncrement = this.value < this.max;
125
+ ctx.fillStyle = this.incrementPressed ? '#E0E0E0' : '#F5F5F5';
126
+ ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
127
+ ctx.lineWidth = 2;
128
+
129
+ ctx.beginPath();
130
+ this.roundRect(ctx, this.x + this.width - this.buttonWidth, this.y,
131
+ this.buttonWidth, this.height, 4);
132
+ ctx.fill();
133
+ ctx.stroke();
134
+
135
+ // Icône +
136
+ ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
137
+ ctx.lineWidth = 2;
138
+ ctx.lineCap = 'round';
139
+ const plusX = this.x + this.width - this.buttonWidth / 2;
140
+ const plusY = this.y + this.height / 2;
141
+
142
+ ctx.beginPath();
143
+ ctx.moveTo(plusX - 8, plusY);
144
+ ctx.lineTo(plusX + 8, plusY);
145
+ ctx.stroke();
146
+
147
+ ctx.beginPath();
148
+ ctx.moveTo(plusX, plusY - 8);
149
+ ctx.lineTo(plusX, plusY + 8);
150
+ ctx.stroke();
151
+
152
+ } else {
153
+ // Cupertino (iOS) Stepper
154
+
155
+ // Container
156
+ ctx.strokeStyle = '#C7C7CC';
157
+ ctx.lineWidth = 1;
158
+ ctx.beginPath();
159
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.height / 2);
160
+ ctx.stroke();
161
+
162
+ // Divider central
163
+ ctx.beginPath();
164
+ ctx.moveTo(this.x + this.width / 2, this.y);
165
+ ctx.lineTo(this.x + this.width / 2, this.y + this.height);
166
+ ctx.stroke();
167
+
168
+ // Bouton décrement (-)
169
+ const canDecrement = this.value > this.min;
170
+ if (this.decrementPressed) {
171
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
172
+ ctx.beginPath();
173
+ ctx.arc(this.x + this.width / 4, this.y + this.height / 2,
174
+ this.height / 2 - 2, Math.PI / 2, Math.PI * 1.5);
175
+ ctx.lineTo(this.x + this.width / 2, this.y);
176
+ ctx.lineTo(this.x + this.width / 2, this.y + this.height);
177
+ ctx.closePath();
178
+ ctx.fill();
179
+ }
180
+
181
+ ctx.strokeStyle = canDecrement ? '#007AFF' : '#C7C7CC';
182
+ ctx.lineWidth = 2;
183
+ ctx.lineCap = 'round';
184
+ ctx.beginPath();
185
+ ctx.moveTo(this.x + this.width / 4 - 10, this.y + this.height / 2);
186
+ ctx.lineTo(this.x + this.width / 4 + 10, this.y + this.height / 2);
187
+ ctx.stroke();
188
+
189
+ // Bouton increment (+)
190
+ const canIncrement = this.value < this.max;
191
+ if (this.incrementPressed) {
192
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
193
+ ctx.beginPath();
194
+ ctx.arc(this.x + (this.width * 3 / 4), this.y + this.height / 2,
195
+ this.height / 2 - 2, Math.PI * 1.5, Math.PI / 2);
196
+ ctx.lineTo(this.x + this.width / 2, this.y + this.height);
197
+ ctx.lineTo(this.x + this.width / 2, this.y);
198
+ ctx.closePath();
199
+ ctx.fill();
200
+ }
201
+
202
+ ctx.strokeStyle = canIncrement ? '#007AFF' : '#C7C7CC';
203
+ ctx.lineWidth = 2;
204
+ ctx.lineCap = 'round';
205
+ const plusX = this.x + (this.width * 3 / 4);
206
+ const plusY = this.y + this.height / 2;
207
+
208
+ ctx.beginPath();
209
+ ctx.moveTo(plusX - 10, plusY);
210
+ ctx.lineTo(plusX + 10, plusY);
211
+ ctx.stroke();
212
+
213
+ ctx.beginPath();
214
+ ctx.moveTo(plusX, plusY - 10);
215
+ ctx.lineTo(plusX, plusY + 10);
216
+ ctx.stroke();
217
+ }
218
+
219
+ ctx.restore();
220
+ }
221
+
222
+ /**
223
+ * Gère la pression (clic)
224
+ * @param {number} x - Coordonnée X
225
+ * @param {number} y - Coordonnée Y
226
+ * @private
227
+ */
228
+ handlePress(x, y) {
229
+ const adjustedY = y - this.framework.scrollOffset;
230
+
231
+ // Bouton décrement (gauche)
232
+ if (x >= this.x && x <= this.x + this.buttonWidth) {
233
+ this.decrementPressed = true;
234
+ this.decrement();
235
+ setTimeout(() => { this.decrementPressed = false; }, 150);
236
+ }
237
+ // Bouton increment (droite)
238
+ else if (x >= this.x + this.width - this.buttonWidth && x <= this.x + this.width) {
239
+ this.incrementPressed = true;
240
+ this.increment();
241
+ setTimeout(() => { this.incrementPressed = false; }, 150);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Dessine un rectangle avec coins arrondis
247
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
248
+ * @param {number} x - Position X
249
+ * @param {number} y - Position Y
250
+ * @param {number} width - Largeur
251
+ * @param {number} height - Hauteur
252
+ * @param {number} radius - Rayon des coins
253
+ * @private
254
+ */
255
+ roundRect(ctx, x, y, width, height, radius) {
256
+ ctx.beginPath();
257
+ ctx.moveTo(x + radius, y);
258
+ ctx.lineTo(x + width - radius, y);
259
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
260
+ ctx.lineTo(x + width, y + height - radius);
261
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
262
+ ctx.lineTo(x + radius, y + height);
263
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
264
+ ctx.lineTo(x, y + radius);
265
+ ctx.quadraticCurveTo(x, y, x + radius, y);
266
+ ctx.closePath();
267
+ }
268
+
269
+ /**
270
+ * Vérifie si un point est dans les limites
271
+ * @param {number} x - Coordonnée X
272
+ * @param {number} y - Coordonnée Y
273
+ * @returns {boolean} True si le point est dans le stepper
274
+ */
275
+ isPointInside(x, y) {
276
+ return x >= this.x && x <= this.x + this.width &&
277
+ y >= this.y && y <= this.y + this.height;
278
+ }
279
+ }
280
+
281
+ export default Stepper;
@@ -0,0 +1,179 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Élément de liste avec support swipe et actions
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} title - Titre
8
+ * @property {string} subtitle - Sous-titre
9
+ * @property {Array<{icon?: string, text: string, color: string, onClick: Function}>} leftActions - Actions swipe gauche
10
+ * @property {Array<{icon?: string, text: string, color: string, onClick: Function}>} rightActions - Actions swipe droite
11
+ * @property {string} bgColor - Couleur de fond
12
+ * @property {string} platform - Plateforme ('material' ou 'cupertino')
13
+ * @property {number} height - Hauteur de l'item
14
+ */
15
+ class SwipeableListItem extends Component {
16
+ constructor(framework, options = {}) {
17
+ super(framework, options);
18
+
19
+ this.title = options.title || '';
20
+ this.subtitle = options.subtitle || '';
21
+ this.height = options.height || (this.subtitle ? 72 : 56);
22
+ this.width = options.width || framework.width;
23
+ this.bgColor = options.bgColor || '#FFFFFF';
24
+ this.platform = framework.platform;
25
+
26
+ this.leftActions = options.leftActions || [];
27
+ this.rightActions = options.rightActions || [];
28
+
29
+ this.dragOffset = 0;
30
+ this.dragging = false;
31
+ this.startX = 0;
32
+
33
+ this.ripples = [];
34
+ this.onPress = this.handlePress.bind(this);
35
+ }
36
+
37
+ /**
38
+ * Début du swipe
39
+ * @param {number} x
40
+ * @param {number} y
41
+ */
42
+ onDragStart(x, y) {
43
+ if (this.isPointInside(x, y)) {
44
+ this.dragging = true;
45
+ this.startX = x;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Déplacement pendant swipe
51
+ * @param {number} x
52
+ */
53
+ onDragMove(x) {
54
+ if (this.dragging) {
55
+ this.dragOffset = x - this.startX;
56
+ const maxOffset = Math.max(this.leftActions.length, this.rightActions.length) * 80;
57
+ this.dragOffset = Math.min(Math.max(this.dragOffset, -maxOffset), maxOffset);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Fin du swipe
63
+ */
64
+ onDragEnd() {
65
+ if (this.dragging) {
66
+ if (this.dragOffset > 80 && this.leftActions[0]) {
67
+ this.leftActions[0].onClick?.();
68
+ } else if (this.dragOffset < -80 && this.rightActions[0]) {
69
+ this.rightActions[0].onClick?.();
70
+ }
71
+ this.dragOffset = 0;
72
+ this.dragging = false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Gestion du press pour ripple Material
78
+ * @param {number} x
79
+ * @param {number} y
80
+ */
81
+ handlePress(x, y) {
82
+ if (this.platform === 'material') {
83
+ const adjustedY = y - this.framework.scrollOffset;
84
+ this.ripples.push({
85
+ x: x - this.x,
86
+ y: adjustedY - this.y,
87
+ radius: 0,
88
+ maxRadius: Math.max(this.width, this.height) * 1.5,
89
+ opacity: 1
90
+ });
91
+ this.animateRipple();
92
+ }
93
+ }
94
+
95
+ animateRipple() {
96
+ const animate = () => {
97
+ let hasActive = false;
98
+ for (let ripple of this.ripples) {
99
+ if (ripple.radius < ripple.maxRadius) {
100
+ ripple.radius += ripple.maxRadius / 15;
101
+ hasActive = true;
102
+ }
103
+ if (ripple.radius >= ripple.maxRadius * 0.5) ripple.opacity -= 0.05;
104
+ }
105
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
106
+ if (hasActive) requestAnimationFrame(animate);
107
+ };
108
+ animate();
109
+ }
110
+
111
+ /**
112
+ * Dessine l'item
113
+ * @param {CanvasRenderingContext2D} ctx
114
+ */
115
+ draw(ctx) {
116
+ ctx.save();
117
+
118
+ // Actions swipe
119
+ if (this.dragOffset > 0 && this.leftActions[0]) {
120
+ ctx.fillStyle = this.leftActions[0].color || '#388E3C';
121
+ ctx.fillRect(this.x, this.y, this.dragOffset, this.height);
122
+ ctx.fillStyle = '#FFF';
123
+ ctx.font = '14px sans-serif';
124
+ ctx.textAlign = 'center';
125
+ ctx.textBaseline = 'middle';
126
+ ctx.fillText(this.leftActions[0].text, this.x + this.dragOffset / 2, this.y + this.height / 2);
127
+ } else if (this.dragOffset < 0 && this.rightActions[0]) {
128
+ const offset = -this.dragOffset;
129
+ ctx.fillStyle = this.rightActions[0].color || '#D32F2F';
130
+ ctx.fillRect(this.x + this.width - offset, this.y, offset, this.height);
131
+ ctx.fillStyle = '#FFF';
132
+ ctx.textAlign = 'center';
133
+ ctx.fillText(this.rightActions[0].text, this.x + this.width - offset / 2, this.y + this.height / 2);
134
+ }
135
+
136
+ ctx.translate(this.dragOffset, 0);
137
+
138
+ // Background
139
+ ctx.fillStyle = this.bgColor;
140
+ ctx.fillRect(this.x, this.y, this.width, this.height);
141
+
142
+ // Ripple
143
+ if (this.platform === 'material') {
144
+ ctx.save();
145
+ ctx.beginPath();
146
+ ctx.rect(this.x, this.y, this.width, this.height);
147
+ ctx.clip();
148
+ for (let ripple of this.ripples) {
149
+ ctx.globalAlpha = ripple.opacity;
150
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
151
+ ctx.beginPath();
152
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
153
+ ctx.fill();
154
+ }
155
+ ctx.restore();
156
+ }
157
+
158
+ // Texte
159
+ ctx.fillStyle = '#000';
160
+ ctx.font = '16px sans-serif';
161
+ ctx.textAlign = 'left';
162
+ ctx.textBaseline = 'middle';
163
+ ctx.fillText(this.title, this.x + 16, this.y + this.height / 2);
164
+ if (this.subtitle) {
165
+ ctx.fillStyle = '#757575';
166
+ ctx.font = '14px sans-serif';
167
+ ctx.fillText(this.subtitle, this.x + 16, this.y + this.height / 2 + 12);
168
+ }
169
+
170
+ ctx.restore();
171
+ }
172
+
173
+ isPointInside(x, y) {
174
+ return x >= this.x && x <= this.x + this.width &&
175
+ y >= this.y && y <= this.y + this.height;
176
+ }
177
+ }
178
+
179
+ export default SwipeableListItem;
@@ -0,0 +1,147 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Interrupteur (toggle)
4
+ * @class
5
+ * @extends Component
6
+ * @property {boolean} checked - État activé
7
+ * @property {string} platform - Plateforme
8
+ * @property {number} animProgress - Progression de l'animation
9
+ * @property {boolean} isAnimating - En cours d'animation
10
+ * @property {Function} onChange - Callback au changement
11
+ */
12
+ class Switch extends Component {
13
+ /**
14
+ * Crée une instance de Switch
15
+ * @param {CanvasFramework} framework - Framework parent
16
+ * @param {Object} [options={}] - Options de configuration
17
+ * @param {boolean} [options.checked=false] - État initial
18
+ * @param {Function} [options.onChange] - Callback au changement
19
+ */
20
+ constructor(framework, options = {}) {
21
+ super(framework, options);
22
+ this.checked = options.checked || false;
23
+ this.platform = framework.platform;
24
+ this.width = 51;
25
+ this.height = 31;
26
+ this.onChange = options.onChange;
27
+ this.animProgress = this.checked ? 1 : 0;
28
+ this.isAnimating = false;
29
+
30
+ // S'assurer que le Switch est cliquable
31
+ this.onClick = this.handleClick.bind(this);
32
+ }
33
+
34
+ /**
35
+ * Gère le clic sur le switch
36
+ * @private
37
+ */
38
+ handleClick() {
39
+ console.log('Switch clicked!');
40
+ this.checked = !this.checked;
41
+ if (this.onChange) this.onChange(this.checked);
42
+ this.animate();
43
+ }
44
+
45
+ /**
46
+ * Anime le toggle
47
+ * @private
48
+ */
49
+ animate() {
50
+ if (this.isAnimating) return;
51
+
52
+ this.isAnimating = true;
53
+ const target = this.checked ? 1 : 0;
54
+ const step = 0.1;
55
+ const interval = setInterval(() => {
56
+ if (Math.abs(this.animProgress - target) < step) {
57
+ this.animProgress = target;
58
+ clearInterval(interval);
59
+ this.isAnimating = false;
60
+ } else {
61
+ this.animProgress += this.animProgress < target ? step : -step;
62
+ }
63
+ }, 16);
64
+ }
65
+
66
+ /**
67
+ * Dessine le switch
68
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
69
+ */
70
+ draw(ctx) {
71
+ ctx.save();
72
+
73
+ // Déplacer le contexte avec le scroll
74
+ const adjustedY = this.y;
75
+
76
+ if (this.platform === 'material') {
77
+ // Material Design Switch
78
+ const trackColor = this.checked ? 'rgba(98, 0, 238, 0.5)' : 'rgba(0, 0, 0, 0.38)';
79
+ const thumbColor = this.checked ? '#6200EE' : '#FAFAFA';
80
+
81
+ // Track
82
+ ctx.fillStyle = trackColor;
83
+ ctx.beginPath();
84
+ ctx.arc(this.x + 15.5, adjustedY + 15.5, 7, Math.PI / 2, Math.PI * 1.5);
85
+ ctx.arc(this.x + 35.5, adjustedY + 15.5, 7, Math.PI * 1.5, Math.PI / 2);
86
+ ctx.closePath();
87
+ ctx.fill();
88
+
89
+ // Thumb
90
+ const thumbX = this.x + 15.5 + (this.animProgress * 20);
91
+ ctx.fillStyle = thumbColor;
92
+ ctx.beginPath();
93
+ ctx.arc(thumbX, adjustedY + 15.5, 10, 0, Math.PI * 2);
94
+ ctx.fill();
95
+
96
+ // Shadow for unchecked state
97
+ if (!this.checked) {
98
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.12)';
99
+ ctx.lineWidth = 1;
100
+ ctx.stroke();
101
+ }
102
+ } else {
103
+ // Cupertino (iOS) Switch
104
+ const bgColor = this.checked ? '#34C759' : '#E9E9EA';
105
+
106
+ // Track
107
+ ctx.fillStyle = bgColor;
108
+ ctx.beginPath();
109
+ ctx.arc(this.x + 15.5, adjustedY + 15.5, 15.5, Math.PI / 2, Math.PI * 1.5);
110
+ ctx.arc(this.x + 35.5, adjustedY + 15.5, 15.5, Math.PI * 1.5, Math.PI / 2);
111
+ ctx.closePath();
112
+ ctx.fill();
113
+
114
+ // Thumb with shadow
115
+ const thumbX = this.x + 15.5 + (this.animProgress * 20);
116
+ ctx.fillStyle = '#FFFFFF';
117
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
118
+ ctx.shadowBlur = 4;
119
+ ctx.shadowOffsetY = 2;
120
+ ctx.beginPath();
121
+ ctx.arc(thumbX, adjustedY + 15.5, 13.5, 0, Math.PI * 2);
122
+ ctx.fill();
123
+
124
+ // Reset shadow
125
+ ctx.shadowColor = 'transparent';
126
+ ctx.shadowBlur = 0;
127
+ ctx.shadowOffsetY = 0;
128
+ }
129
+
130
+ ctx.restore();
131
+ }
132
+
133
+ /**
134
+ * Vérifie si un point est dans les limites
135
+ * @param {number} x - Coordonnée X
136
+ * @param {number} y - Coordonnée Y
137
+ * @returns {boolean} True si le point est dans le switch
138
+ */
139
+ isPointInside(x, y) {
140
+ return x >= this.x &&
141
+ x <= this.x + this.width &&
142
+ y >= this.y &&
143
+ y <= this.y + this.height;
144
+ }
145
+ }
146
+
147
+ export default Switch;