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,309 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Champ de saisie texte
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} placeholder - Texte d'indication
8
+ * @property {string} value - Valeur
9
+ * @property {number} fontSize - Taille de police
10
+ * @property {boolean} focused - Focus actif
11
+ * @property {string} platform - Plateforme
12
+ * @property {boolean} cursorVisible - Curseur visible
13
+ * @property {number} cursorPosition - Position du curseur
14
+ * @property {HTMLInputElement} hiddenInput - Input HTML caché
15
+ */
16
+ class Input extends Component {
17
+ static activeInput = null;
18
+ static allInputs = new Set();
19
+ static globalClickHandler = null;
20
+
21
+ /**
22
+ * Crée une instance de Input
23
+ * @param {CanvasFramework} framework - Framework parent
24
+ * @param {Object} [options={}] - Options de configuration
25
+ * @param {string} [options.placeholder=''] - Texte d'indication
26
+ * @param {string} [options.value=''] - Valeur initiale
27
+ * @param {number} [options.fontSize=16] - Taille de police
28
+ * @param {Function} [options.onFocus] - Callback au focus
29
+ * @param {Function} [options.onBlur] - Callback au blur
30
+ */
31
+ constructor(framework, options = {}) {
32
+ super(framework, options);
33
+ this.placeholder = options.placeholder || '';
34
+ this.value = options.value || '';
35
+ this.fontSize = options.fontSize || 16;
36
+ this.focused = false;
37
+ this.platform = framework.platform;
38
+ this.cursorVisible = true;
39
+ this.cursorPosition = this.value.length;
40
+
41
+ // Gestion du focus
42
+ this.onFocus = this.onFocus.bind(this);
43
+ this.onBlur = this.onBlur.bind(this);
44
+
45
+ // Enregistrer cet input
46
+ Input.allInputs.add(this);
47
+
48
+ // Animation du curseur
49
+ this.cursorInterval = setInterval(() => {
50
+ if (this.focused) this.cursorVisible = !this.cursorVisible;
51
+ }, 500);
52
+
53
+ // Écouter les clics partout pour détecter quand on clique ailleurs
54
+ this.setupGlobalClickHandler();
55
+ }
56
+
57
+ /**
58
+ * Écoute les clics globaux pour détecter les clics hors input
59
+ */
60
+ setupGlobalClickHandler() {
61
+ // On crée un gestionnaire unique pour tous les inputs
62
+ if (!Input.globalClickHandler) {
63
+ Input.globalClickHandler = (e) => {
64
+ // Vérifier si on a cliqué en dehors de TOUS les inputs
65
+ let clickedOnInput = false;
66
+
67
+ for (let input of Input.allInputs) {
68
+ if (input.hiddenInput && e.target === input.hiddenInput) {
69
+ clickedOnInput = true;
70
+ break;
71
+ }
72
+ }
73
+
74
+ // Si on n'a pas cliqué sur un input, détruire tous les inputs HTML
75
+ if (!clickedOnInput) {
76
+ Input.removeAllHiddenInputs();
77
+ }
78
+ };
79
+
80
+ // Attacher l'écouteur avec capture pour qu'il se déclenche tôt
81
+ document.addEventListener('click', Input.globalClickHandler, true);
82
+ document.addEventListener('touchstart', Input.globalClickHandler, true);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Configure l'input HTML caché
88
+ * @private
89
+ */
90
+ setupHiddenInput() {
91
+ if (this.hiddenInput) return;
92
+
93
+ // Créer un input HTML caché unique pour cette instance
94
+ this.hiddenInput = document.createElement('input');
95
+ this.hiddenInput.style.position = 'fixed';
96
+ this.hiddenInput.style.opacity = '0';
97
+ this.hiddenInput.style.pointerEvents = 'none';
98
+ this.hiddenInput.style.top = '-100px';
99
+ this.hiddenInput.style.zIndex = '9999';
100
+ document.body.appendChild(this.hiddenInput);
101
+
102
+ this.hiddenInput.addEventListener('input', (e) => {
103
+ if (this.focused) {
104
+ this.value = e.target.value;
105
+ this.cursorPosition = this.value.length;
106
+ }
107
+ });
108
+
109
+ this.hiddenInput.addEventListener('blur', () => {
110
+ this.focused = false;
111
+ this.cursorVisible = false;
112
+
113
+ // Détruire l'input HTML après un court délai
114
+ setTimeout(() => {
115
+ this.destroyHiddenInput();
116
+ }, 100);
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Gère le focus
122
+ */
123
+ onFocus() {
124
+ // Si c'est déjà l'input actif, ne rien faire
125
+ if (Input.activeInput === this) {
126
+ return;
127
+ }
128
+
129
+ // D'abord, détruire TOUS les autres inputs HTML
130
+ Input.removeAllHiddenInputs();
131
+
132
+ // Désactiver tous les autres inputs visuellement
133
+ for (let input of Input.allInputs) {
134
+ if (input !== this) {
135
+ input.focused = false;
136
+ input.cursorVisible = false;
137
+ }
138
+ }
139
+
140
+ // Activer celui-ci
141
+ this.focused = true;
142
+ this.cursorVisible = true;
143
+ Input.activeInput = this;
144
+
145
+ // Créer l'input HTML si nécessaire
146
+ this.setupHiddenInput();
147
+
148
+ if (this.hiddenInput) {
149
+ this.hiddenInput.value = this.value;
150
+ // Positionner l'input au bon endroit pour le scroll du clavier
151
+ const adjustedY = this.y + this.framework.scrollOffset;
152
+ this.hiddenInput.style.top = `${adjustedY}px`;
153
+
154
+ // Focus avec un petit délai
155
+ setTimeout(() => {
156
+ if (this.hiddenInput && this.focused) {
157
+ this.hiddenInput.focus();
158
+ // Positionner le curseur à la fin
159
+ this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
160
+ }
161
+ }, 50);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Gère le blur
167
+ */
168
+ onBlur() {
169
+ this.focused = false;
170
+ this.cursorVisible = false;
171
+ }
172
+
173
+ /**
174
+ * Détruit l'input HTML
175
+ */
176
+ destroyHiddenInput() {
177
+ if (this.hiddenInput && this.hiddenInput.parentNode) {
178
+ this.hiddenInput.parentNode.removeChild(this.hiddenInput);
179
+ this.hiddenInput = null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Gère le clic
185
+ */
186
+ onClick() {
187
+ this.onFocus();
188
+ }
189
+
190
+ /**
191
+ * Méthode statique pour détruire tous les inputs HTML
192
+ */
193
+ static removeAllHiddenInputs() {
194
+ // Désactiver tous les inputs visuels
195
+ for (let input of Input.allInputs) {
196
+ input.focused = false;
197
+ input.cursorVisible = false;
198
+
199
+ // Détruire l'input HTML
200
+ if (input.hiddenInput && input.hiddenInput.parentNode) {
201
+ input.hiddenInput.parentNode.removeChild(input.hiddenInput);
202
+ input.hiddenInput = null;
203
+ }
204
+ }
205
+
206
+ Input.activeInput = null;
207
+ }
208
+
209
+ /**
210
+ * Vérifie si un point est dans les limites
211
+ * @param {number} x - Coordonnée X
212
+ * @param {number} y - Coordonnée Y
213
+ * @returns {boolean} True si le point est dans l'input
214
+ */
215
+ isPointInside(x, y) {
216
+ return x >= this.x &&
217
+ x <= this.x + this.width &&
218
+ y >= this.y &&
219
+ y <= this.y + this.height;
220
+ }
221
+
222
+ /**
223
+ * Dessine l'input
224
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
225
+ */
226
+ draw(ctx) {
227
+ ctx.save();
228
+
229
+ if (this.platform === 'material') {
230
+ // Material Design Input
231
+ ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
232
+ ctx.lineWidth = this.focused ? 2 : 1;
233
+ ctx.beginPath();
234
+ ctx.moveTo(this.x, this.y + this.height);
235
+ ctx.lineTo(this.x + this.width, this.y + this.height);
236
+ ctx.stroke();
237
+ } else {
238
+ // Cupertino Input
239
+ ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
240
+ ctx.lineWidth = 1;
241
+ ctx.beginPath();
242
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
243
+ ctx.stroke();
244
+ }
245
+
246
+ // Texte
247
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
248
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
249
+ ctx.textAlign = 'left';
250
+ ctx.textBaseline = 'middle';
251
+ const displayText = this.value || this.placeholder;
252
+ ctx.fillText(displayText, this.x + 10, this.y + this.height / 2);
253
+
254
+ // Curseur
255
+ if (this.focused && this.cursorVisible && this.value) {
256
+ const textWidth = ctx.measureText(this.value).width;
257
+ ctx.fillStyle = '#000000';
258
+ ctx.fillRect(this.x + 10 + textWidth, this.y + 10, 2, this.height - 20);
259
+ }
260
+
261
+ ctx.restore();
262
+ }
263
+
264
+ /**
265
+ * Dessine un rectangle avec coins arrondis
266
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
267
+ * @param {number} x - Position X
268
+ * @param {number} y - Position Y
269
+ * @param {number} width - Largeur
270
+ * @param {number} height - Hauteur
271
+ * @param {number} radius - Rayon des coins
272
+ * @private
273
+ */
274
+ roundRect(ctx, x, y, width, height, radius) {
275
+ ctx.moveTo(x + radius, y);
276
+ ctx.lineTo(x + width - radius, y);
277
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
278
+ ctx.lineTo(x + width, y + height - radius);
279
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
280
+ ctx.lineTo(x + radius, y + height);
281
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
282
+ ctx.lineTo(x, y + radius);
283
+ ctx.quadraticCurveTo(x, y, x + radius, y);
284
+ }
285
+
286
+ /**
287
+ * Nettoie les ressources
288
+ */
289
+ destroy() {
290
+ this.destroyHiddenInput();
291
+
292
+ if (this.cursorInterval) {
293
+ clearInterval(this.cursorInterval);
294
+ }
295
+
296
+ Input.allInputs.delete(this);
297
+
298
+ // Si c'était le dernier input, retirer le gestionnaire global
299
+ if (Input.allInputs.size === 0 && Input.globalClickHandler) {
300
+ document.removeEventListener('click', Input.globalClickHandler, true);
301
+ document.removeEventListener('touchstart', Input.globalClickHandler, true);
302
+ Input.globalClickHandler = null;
303
+ }
304
+
305
+ super.destroy && super.destroy();
306
+ }
307
+ }
308
+
309
+ export default Input;
@@ -0,0 +1,94 @@
1
+ import Component from '../core/Component.js';
2
+ import ListItem from '../components/ListItem.js';
3
+ /**
4
+ * Conteneur pour les éléments de liste (ListItems) avec défilement automatique
5
+ * @class
6
+ * @extends Component
7
+ * @param {Framework} framework - Instance du framework
8
+ * @param {Object} [options={}] - Options de configuration
9
+ * @param {number} [options.itemHeight=56] - Hauteur de chaque item en pixels
10
+ * @param {Function} [options.onItemClick] - Callback appelé lors du clic sur un item
11
+ * @param {number} [options.y=0] - Position Y de départ
12
+ * @example
13
+ * const list = new List(framework, {
14
+ * itemHeight: 64,
15
+ * onItemClick: (index, itemOptions) => console.log('Item clicked:', index)
16
+ * });
17
+ */
18
+ class List extends Component {
19
+ /**
20
+ * @constructs List
21
+ */
22
+ constructor(framework, options = {}) {
23
+ super(framework, options);
24
+ /** @type {ListItem[]} */
25
+ this.items = [];
26
+ /** @type {number} */
27
+ this.itemHeight = options.itemHeight || 56;
28
+ /** @type {Function|undefined} */
29
+ this.onItemClick = options.onItemClick;
30
+ /** @type {number} */
31
+ this.y = options.y || 0; // Position Y de départ
32
+ }
33
+
34
+ /**
35
+ * Ajoute un item à la liste
36
+ * @param {Object} itemOptions - Options pour l'item
37
+ * @param {string} itemOptions.text - Texte à afficher
38
+ * @param {Function} [itemOptions.onClick] - Callback spécifique à l'item
39
+ * @param {Object} [itemOptions.style] - Style optionnel pour l'item
40
+ * @returns {ListItem} L'item créé
41
+ */
42
+ addItem(itemOptions) {
43
+ const item = new ListItem(this.framework, {
44
+ ...itemOptions,
45
+ x: this.x,
46
+ y: this.y + (this.items.length * this.itemHeight),
47
+ width: this.width,
48
+ height: this.itemHeight, // IMPORTANT: définir la hauteur
49
+ onClick: () => {
50
+ if (this.onItemClick) {
51
+ this.onItemClick(this.items.length, itemOptions);
52
+ }
53
+ if (itemOptions.onClick) {
54
+ itemOptions.onClick();
55
+ }
56
+ }
57
+ });
58
+
59
+ this.items.push(item);
60
+ this.framework.add(item); // Ajouter chaque item au framework
61
+ this.height = this.items.length * this.itemHeight; // Mettre à jour la hauteur totale
62
+
63
+ return item;
64
+ }
65
+
66
+ /**
67
+ * Vide la liste et supprime tous les items du framework
68
+ */
69
+ clear() {
70
+ for (let item of this.items) {
71
+ this.framework.remove(item);
72
+ }
73
+ this.items = [];
74
+ this.height = 0;
75
+ }
76
+
77
+ /**
78
+ * Dessine le composant (les items se dessinent eux-mêmes)
79
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
80
+ */
81
+ draw(ctx) {
82
+ // Les items se dessinent eux-mêmes
83
+ }
84
+
85
+ /**
86
+ * Vérifie si un point est à l'intérieur du composant
87
+ * @returns {boolean} Toujours false (les ListItems gèrent leurs propres clics)
88
+ */
89
+ isPointInside() {
90
+ return false; // Les ListItems gèrent leurs propres clics
91
+ }
92
+ }
93
+
94
+ export default List;
@@ -0,0 +1,223 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Élément de liste
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} title - Titre
7
+ * @property {string} subtitle - Sous-titre
8
+ * @property {string|null} leftIcon - Icône gauche
9
+ * @property {string|null} leftImage - Image gauche (URL)
10
+ * @property {string|null} rightIcon - Icône droite
11
+ * @property {string|null} rightText - Texte droite
12
+ * @property {boolean} divider - Afficher un diviseur
13
+ * @property {string} platform - Plateforme
14
+ * @property {string} bgColor - Couleur de fond
15
+ * @property {Array} ripples - Effets ripple (Material)
16
+ */
17
+ class ListItem extends Component {
18
+ /**
19
+ * Crée une instance de ListItem
20
+ * @param {CanvasFramework} framework - Framework parent
21
+ * @param {Object} [options={}] - Options de configuration
22
+ * @param {string} [options.title=''] - Titre
23
+ * @param {string} [options.subtitle=''] - Sous-titre
24
+ * @param {string} [options.leftIcon] - Icône gauche
25
+ * @param {string} [options.leftImage] - URL image gauche
26
+ * @param {string} [options.rightIcon] - Icône droite
27
+ * @param {string} [options.rightText] - Texte droite
28
+ * @param {boolean} [options.divider=true] - Diviseur
29
+ * @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
30
+ * @param {number} [options.height] - Hauteur (auto selon contenu)
31
+ */
32
+ constructor(framework, options = {}) {
33
+ super(framework, options);
34
+ this.title = options.title || '';
35
+ this.subtitle = options.subtitle || '';
36
+ this.leftIcon = options.leftIcon || null;
37
+ this.leftImage = options.leftImage || null; // URL d'image
38
+ this.rightIcon = options.rightIcon || null;
39
+ this.rightText = options.rightText || '';
40
+ this.divider = options.divider !== false;
41
+ this.platform = framework.platform;
42
+ this.height = options.height || (this.subtitle ? 72 : 56);
43
+ this.width = options.width || framework.width;
44
+ this.bgColor = options.bgColor || '#FFFFFF';
45
+ this.ripples = []; // Pour l'effet ripple Material
46
+
47
+ this.onPress = this.handlePress.bind(this);
48
+ }
49
+
50
+ /**
51
+ * Gère la pression (clic)
52
+ * @param {number} x - Coordonnée X
53
+ * @param {number} y - Coordonnée Y
54
+ * @private
55
+ */
56
+ handlePress(x, y) {
57
+ if (this.platform === 'material') {
58
+ const adjustedY = y - this.framework.scrollOffset;
59
+ this.ripples.push({
60
+ x: x - this.x,
61
+ y: adjustedY - this.y,
62
+ radius: 0,
63
+ maxRadius: Math.max(this.width, this.height) * 1.5,
64
+ opacity: 1
65
+ });
66
+ this.animateRipple();
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Anime les effets ripple
72
+ * @private
73
+ */
74
+ animateRipple() {
75
+ const animate = () => {
76
+ let hasActiveRipples = false;
77
+
78
+ for (let ripple of this.ripples) {
79
+ if (ripple.radius < ripple.maxRadius) {
80
+ ripple.radius += ripple.maxRadius / 15;
81
+ hasActiveRipples = true;
82
+ }
83
+
84
+ if (ripple.radius >= ripple.maxRadius * 0.5) {
85
+ ripple.opacity -= 0.05;
86
+ }
87
+ }
88
+
89
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
90
+
91
+ if (hasActiveRipples) {
92
+ requestAnimationFrame(animate);
93
+ }
94
+ };
95
+
96
+ animate();
97
+ }
98
+
99
+ /**
100
+ * Dessine l'élément de liste
101
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
102
+ */
103
+ draw(ctx) {
104
+ ctx.save();
105
+
106
+ // Background
107
+ ctx.fillStyle = this.pressed ? '#F5F5F5' : this.bgColor;
108
+ ctx.fillRect(this.x, this.y, this.width, this.height);
109
+
110
+ // Ripple effect (Material)
111
+ if (this.platform === 'material' && this.ripples.length > 0) {
112
+ ctx.save();
113
+ ctx.beginPath();
114
+ ctx.rect(this.x, this.y, this.width, this.height);
115
+ ctx.clip();
116
+
117
+ for (let ripple of this.ripples) {
118
+ ctx.globalAlpha = ripple.opacity;
119
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
120
+ ctx.beginPath();
121
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
122
+ ctx.fill();
123
+ }
124
+
125
+ ctx.restore();
126
+ }
127
+
128
+ let leftOffset = 16;
129
+
130
+ // Left Icon ou Image
131
+ if (this.leftIcon) {
132
+ ctx.fillStyle = '#757575';
133
+ ctx.font = '24px sans-serif';
134
+ ctx.textAlign = 'left';
135
+ ctx.textBaseline = 'middle';
136
+ ctx.fillText(this.leftIcon, this.x + leftOffset, this.y + this.height / 2);
137
+ leftOffset += 48;
138
+ } else if (this.leftImage) {
139
+ // Circle pour l'avatar
140
+ ctx.fillStyle = '#E0E0E0';
141
+ ctx.beginPath();
142
+ ctx.arc(this.x + leftOffset + 20, this.y + this.height / 2, 20, 0, Math.PI * 2);
143
+ ctx.fill();
144
+
145
+ // TODO: Charger vraie image
146
+ ctx.fillStyle = '#757575';
147
+ ctx.font = '14px sans-serif';
148
+ ctx.textAlign = 'center';
149
+ ctx.textBaseline = 'middle';
150
+ ctx.fillText('👤', this.x + leftOffset + 20, this.y + this.height / 2);
151
+
152
+ leftOffset += 56;
153
+ }
154
+
155
+ // Title et Subtitle
156
+ const textX = this.x + leftOffset;
157
+ const centerY = this.y + this.height / 2;
158
+
159
+ if (this.subtitle) {
160
+ // Title
161
+ ctx.fillStyle = '#000000';
162
+ ctx.font = '16px -apple-system, Roboto, sans-serif';
163
+ ctx.textAlign = 'left';
164
+ ctx.textBaseline = 'bottom';
165
+ ctx.fillText(this.title, textX, centerY - 2);
166
+
167
+ // Subtitle
168
+ ctx.fillStyle = '#757575';
169
+ ctx.font = '14px -apple-system, Roboto, sans-serif';
170
+ ctx.textBaseline = 'top';
171
+ ctx.fillText(this.subtitle, textX, centerY + 2);
172
+ } else {
173
+ // Title seul (centré verticalement)
174
+ ctx.fillStyle = '#000000';
175
+ ctx.font = '16px -apple-system, Roboto, sans-serif';
176
+ ctx.textAlign = 'left';
177
+ ctx.textBaseline = 'middle';
178
+ ctx.fillText(this.title, textX, centerY);
179
+ }
180
+
181
+ // Right Text ou Icon
182
+ if (this.rightText) {
183
+ ctx.fillStyle = '#757575';
184
+ ctx.font = '14px -apple-system, Roboto, sans-serif';
185
+ ctx.textAlign = 'right';
186
+ ctx.textBaseline = 'middle';
187
+ ctx.fillText(this.rightText, this.x + this.width - 16, centerY);
188
+ } else if (this.rightIcon) {
189
+ ctx.fillStyle = '#757575';
190
+ ctx.font = '20px sans-serif';
191
+ ctx.textAlign = 'right';
192
+ ctx.textBaseline = 'middle';
193
+ ctx.fillText(this.rightIcon, this.x + this.width - 16, centerY);
194
+ }
195
+
196
+ // Divider
197
+ if (this.divider) {
198
+ ctx.strokeStyle = '#E0E0E0';
199
+ ctx.lineWidth = 1;
200
+ ctx.beginPath();
201
+ ctx.moveTo(this.x + leftOffset, this.y + this.height);
202
+ ctx.lineTo(this.x + this.width, this.y + this.height);
203
+ ctx.stroke();
204
+ }
205
+
206
+ ctx.restore();
207
+ }
208
+
209
+ /**
210
+ * Vérifie si un point est dans les limites
211
+ * @param {number} x - Coordonnée X
212
+ * @param {number} y - Coordonnée Y
213
+ * @returns {boolean} True si le point est dans l'élément
214
+ */
215
+ isPointInside(x, y) {
216
+ return x >= this.x &&
217
+ x <= this.x + this.width &&
218
+ y >= this.y &&
219
+ y <= this.y + this.height;
220
+ }
221
+ }
222
+
223
+ export default ListItem;