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,225 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Barre d'application supérieure
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} title - Titre
7
+ * @property {string|null} leftIcon - Icône gauche ('menu' ou 'back')
8
+ * @property {string|null} rightIcon - Icône droite ('search' ou 'more')
9
+ * @property {Function} onLeftClick - Callback au clic gauche
10
+ * @property {Function} onRightClick - Callback au clic droit
11
+ * @property {string} platform - Plateforme
12
+ * @property {string} bgColor - Couleur de fond
13
+ * @property {string} textColor - Couleur du texte
14
+ * @property {number} elevation - Élévation (ombre)
15
+ */
16
+ class AppBar extends Component {
17
+ /**
18
+ * Crée une instance de AppBar
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {string} [options.title=''] - Titre
22
+ * @param {string} [options.leftIcon] - Icône gauche
23
+ * @param {string} [options.rightIcon] - Icône droite
24
+ * @param {Function} [options.onLeftClick] - Callback gauche
25
+ * @param {Function} [options.onRightClick] - Callback droit
26
+ * @param {number} [options.height] - Hauteur (auto selon platform)
27
+ * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
28
+ * @param {string} [options.textColor] - Couleur du texte (auto selon platform)
29
+ * @param {number} [options.elevation=4] - Élévation (Material)
30
+ */
31
+ constructor(framework, options = {}) {
32
+ super(framework, {
33
+ x: 0,
34
+ y: 0,
35
+ width: framework.width,
36
+ height: options.height || (framework.platform === 'material' ? 56 : 44),
37
+ ...options
38
+ });
39
+ this.title = options.title || '';
40
+ this.leftIcon = options.leftIcon || null;
41
+ this.rightIcon = options.rightIcon || null;
42
+ this.onLeftClick = options.onLeftClick;
43
+ this.onRightClick = options.onRightClick;
44
+ this.platform = framework.platform;
45
+ this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6200EE' : '#F8F8F8');
46
+ this.textColor = options.textColor || (framework.platform === 'material' ? '#FFFFFF' : '#000000');
47
+ this.elevation = options.elevation !== undefined ? options.elevation : 4;
48
+
49
+ // IMPORTANT: Définir onPress pour que le framework appelle handlePress
50
+ this.onPress = this.handlePress.bind(this);
51
+ }
52
+
53
+ /**
54
+ * Dessine l'AppBar
55
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
56
+ */
57
+ draw(ctx) {
58
+ ctx.save();
59
+
60
+ // Ombre (Material uniquement)
61
+ if (this.platform === 'material' && this.elevation > 0) {
62
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
63
+ ctx.shadowBlur = this.elevation * 2;
64
+ ctx.shadowOffsetY = this.elevation / 2;
65
+ }
66
+
67
+ // Background
68
+ ctx.fillStyle = this.bgColor;
69
+ ctx.fillRect(this.x, this.y, this.width, this.height);
70
+
71
+ ctx.shadowColor = 'transparent';
72
+
73
+ // Bordure inférieure (iOS uniquement)
74
+ if (this.platform === 'cupertino') {
75
+ ctx.strokeStyle = '#C6C6C8';
76
+ ctx.lineWidth = 0.5;
77
+ ctx.beginPath();
78
+ ctx.moveTo(this.x, this.y + this.height);
79
+ ctx.lineTo(this.x + this.width, this.y + this.height);
80
+ ctx.stroke();
81
+ }
82
+
83
+ // Titre
84
+ ctx.fillStyle = this.textColor;
85
+ ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
86
+ ctx.textAlign = 'center';
87
+ ctx.textBaseline = 'middle';
88
+ ctx.fillText(this.title, this.width / 2, this.y + this.height / 2);
89
+
90
+ // Icône gauche (hamburger menu ou back)
91
+ if (this.leftIcon === 'menu') {
92
+ this.drawMenuIcon(ctx, 16, this.y + this.height / 2);
93
+ } else if (this.leftIcon === 'back') {
94
+ this.drawBackIcon(ctx, 16, this.y + this.height / 2);
95
+ }
96
+
97
+ // Icône droite
98
+ if (this.rightIcon === 'search') {
99
+ this.drawSearchIcon(ctx, this.width - 36, this.y + this.height / 2);
100
+ } else if (this.rightIcon === 'more') {
101
+ this.drawMoreIcon(ctx, this.width - 36, this.y + this.height / 2);
102
+ }
103
+
104
+ ctx.restore();
105
+ }
106
+
107
+ /**
108
+ * Dessine l'icône menu
109
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
110
+ * @param {number} x - Position X
111
+ * @param {number} y - Position Y
112
+ * @private
113
+ */
114
+ drawMenuIcon(ctx, x, y) {
115
+ ctx.strokeStyle = this.textColor;
116
+ ctx.lineWidth = 2;
117
+ ctx.lineCap = 'round';
118
+ for (let i = 0; i < 3; i++) {
119
+ ctx.beginPath();
120
+ ctx.moveTo(x, y - 8 + i * 8);
121
+ ctx.lineTo(x + 24, y - 8 + i * 8);
122
+ ctx.stroke();
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Dessine l'icône retour
128
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
129
+ * @param {number} x - Position X
130
+ * @param {number} y - Position Y
131
+ * @private
132
+ */
133
+ drawBackIcon(ctx, x, y) {
134
+ ctx.strokeStyle = this.textColor;
135
+ ctx.lineWidth = 2;
136
+ ctx.lineCap = 'round';
137
+ ctx.lineJoin = 'round';
138
+ ctx.beginPath();
139
+ ctx.moveTo(x + 16, y - 10);
140
+ ctx.lineTo(x + 6, y);
141
+ ctx.lineTo(x + 16, y + 10);
142
+ ctx.stroke();
143
+ }
144
+
145
+ /**
146
+ * Dessine l'icône recherche
147
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
148
+ * @param {number} x - Position X
149
+ * @param {number} y - Position Y
150
+ * @private
151
+ */
152
+ drawSearchIcon(ctx, x, y) {
153
+ ctx.strokeStyle = this.textColor;
154
+ ctx.lineWidth = 2;
155
+ ctx.beginPath();
156
+ ctx.arc(x + 8, y - 2, 8, 0, Math.PI * 2);
157
+ ctx.stroke();
158
+ ctx.beginPath();
159
+ ctx.moveTo(x + 14, y + 4);
160
+ ctx.lineTo(x + 20, y + 10);
161
+ ctx.stroke();
162
+ }
163
+
164
+ /**
165
+ * Dessine l'icône plus
166
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
167
+ * @param {number} x - Position X
168
+ * @param {number} y - Position Y
169
+ * @private
170
+ */
171
+ drawMoreIcon(ctx, x, y) {
172
+ ctx.fillStyle = this.textColor;
173
+ for (let i = 0; i < 3; i++) {
174
+ ctx.beginPath();
175
+ ctx.arc(x + 12, y - 10 + i * 10, 2, 0, Math.PI * 2);
176
+ ctx.fill();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Vérifie si un point est dans les zones cliquables
182
+ * @param {number} x - Coordonnée X
183
+ * @param {number} y - Coordonnée Y
184
+ * @returns {boolean} True si le point est dans une zone cliquable
185
+ */
186
+ isPointInside(x, y) {
187
+ // Zones cliquables pour les icônes
188
+ if (y >= this.y && y <= this.y + this.height) {
189
+ if (this.leftIcon && x >= 0 && x <= 56) {
190
+ return true;
191
+ }
192
+ if (this.rightIcon && x >= this.width - 56 && x <= this.width) {
193
+ return true;
194
+ }
195
+ }
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Gère la pression (clic)
201
+ * @param {number} x - Coordonnée X
202
+ * @param {number} y - Coordonnée Y
203
+ * @returns {boolean} True si un clic a été traité
204
+ * @private
205
+ */
206
+ handlePress(x, y) {
207
+ // Ajuster y avec le scrollOffset si nécessaire
208
+ const adjustedY = y;
209
+
210
+ // Détecter quelle zone a été cliquée
211
+ if (adjustedY >= this.y && adjustedY <= this.y + this.height) {
212
+ if (this.leftIcon && x >= 0 && x <= 56) {
213
+ if (this.onLeftClick) this.onLeftClick();
214
+ return true;
215
+ }
216
+ if (this.rightIcon && x >= this.width - 56 && x <= this.width) {
217
+ if (this.onRightClick) this.onRightClick();
218
+ return true;
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+ }
224
+
225
+ export default AppBar;
@@ -0,0 +1,202 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Avatar (photo de profil)
4
+ * @class
5
+ * @extends Component
6
+ * @property {string|null} imageUrl - URL de l'image
7
+ * @property {string} initials - Initiales
8
+ * @property {number} size - Taille
9
+ * @property {string} bgColor - Couleur de fond
10
+ * @property {string} textColor - Couleur du texte
11
+ * @property {number} borderWidth - Épaisseur de la bordure
12
+ * @property {string} borderColor - Couleur de la bordure
13
+ * @property {string|null} status - Statut ('online', 'offline', 'away', 'busy')
14
+ * @property {ImageComponent|null} imageComponent - Composant image interne
15
+ */
16
+ class Avatar extends Component {
17
+ /**
18
+ * Crée une instance de Avatar
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {string} [options.imageUrl] - URL de l'image
22
+ * @param {string} [options.initials='??'] - Initiales
23
+ * @param {number} [options.size=48] - Taille
24
+ * @param {string} [options.bgColor] - Couleur de fond (auto générée)
25
+ * @param {string} [options.textColor='#FFFFFF'] - Couleur du texte
26
+ * @param {number} [options.borderWidth=0] - Épaisseur de la bordure
27
+ * @param {string} [options.borderColor='#FFFFFF'] - Couleur de la bordure
28
+ * @param {string} [options.status] - Statut
29
+ */
30
+ constructor(framework, options = {}) {
31
+ super(framework, options);
32
+ this.imageUrl = options.imageUrl || null;
33
+ this.initials = options.initials || '??';
34
+ this.size = options.size || 48;
35
+ this.bgColor = options.bgColor || this.generateColor(this.initials);
36
+ this.textColor = options.textColor || '#FFFFFF';
37
+ this.borderWidth = options.borderWidth || 0;
38
+ this.borderColor = options.borderColor || '#FFFFFF';
39
+ this.status = options.status || null; // 'online', 'offline', 'away', 'busy'
40
+
41
+ this.width = this.size;
42
+ this.height = this.size;
43
+
44
+ // Image component interne
45
+ this.imageComponent = null;
46
+ if (this.imageUrl) {
47
+ this.imageComponent = new ImageComponent(framework, {
48
+ x: this.x,
49
+ y: this.y,
50
+ width: this.size,
51
+ height: this.size,
52
+ src: this.imageUrl,
53
+ fit: 'cover',
54
+ borderRadius: this.size / 2
55
+ });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Génère une couleur basée sur le texte
61
+ * @param {string} text - Texte pour générer la couleur
62
+ * @returns {string} Couleur hexadécimale
63
+ * @private
64
+ */
65
+ generateColor(text) {
66
+ // Générer une couleur basée sur le texte (pour cohérence)
67
+ let hash = 0;
68
+ for (let i = 0; i < text.length; i++) {
69
+ hash = text.charCodeAt(i) + ((hash << 5) - hash);
70
+ }
71
+
72
+ const colors = [
73
+ '#E91E63', '#9C27B0', '#673AB7', '#3F51B5',
74
+ '#2196F3', '#00BCD4', '#009688', '#4CAF50',
75
+ '#FF9800', '#FF5722', '#795548', '#607D8B'
76
+ ];
77
+
78
+ return colors[Math.abs(hash) % colors.length];
79
+ }
80
+
81
+ /**
82
+ * Dessine l'avatar
83
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
84
+ */
85
+ draw(ctx) {
86
+ ctx.save();
87
+
88
+ const centerX = this.x + this.size / 2;
89
+ const centerY = this.y + this.size / 2;
90
+ const radius = this.size / 2;
91
+
92
+ // Bordure
93
+ if (this.borderWidth > 0) {
94
+ ctx.fillStyle = this.borderColor;
95
+ ctx.beginPath();
96
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
97
+ ctx.fill();
98
+ }
99
+
100
+ // Clipping circulaire
101
+ ctx.save();
102
+ ctx.beginPath();
103
+ ctx.arc(centerX, centerY, radius - this.borderWidth, 0, Math.PI * 2);
104
+ ctx.clip();
105
+
106
+ if (this.imageComponent && this.imageComponent.loaded) {
107
+ // Dessiner l'image
108
+ this.imageComponent.x = this.x + this.borderWidth;
109
+ this.imageComponent.y = this.y + this.borderWidth;
110
+ this.imageComponent.width = this.size - this.borderWidth * 2;
111
+ this.imageComponent.height = this.size - this.borderWidth * 2;
112
+ this.imageComponent.draw(ctx);
113
+ } else {
114
+ // Background coloré
115
+ ctx.fillStyle = this.bgColor;
116
+ ctx.fillRect(this.x, this.y, this.size, this.size);
117
+
118
+ // Initiales
119
+ ctx.fillStyle = this.textColor;
120
+ ctx.font = `bold ${this.size / 2.5}px -apple-system, sans-serif`;
121
+ ctx.textAlign = 'center';
122
+ ctx.textBaseline = 'middle';
123
+ ctx.fillText(this.initials.toUpperCase(), centerX, centerY);
124
+ }
125
+
126
+ ctx.restore();
127
+
128
+ // Indicateur de statut
129
+ if (this.status) {
130
+ const statusSize = this.size / 4;
131
+ const statusX = this.x + this.size - statusSize;
132
+ const statusY = this.y + this.size - statusSize;
133
+
134
+ let statusColor;
135
+ switch (this.status) {
136
+ case 'online': statusColor = '#4CAF50'; break;
137
+ case 'offline': statusColor = '#9E9E9E'; break;
138
+ case 'away': statusColor = '#FF9800'; break;
139
+ case 'busy': statusColor = '#F44336'; break;
140
+ default: statusColor = '#9E9E9E';
141
+ }
142
+
143
+ // Bordure blanche autour du statut
144
+ ctx.fillStyle = '#FFFFFF';
145
+ ctx.beginPath();
146
+ ctx.arc(statusX, statusY, statusSize / 2 + 1, 0, Math.PI * 2);
147
+ ctx.fill();
148
+
149
+ // Cercle de statut
150
+ ctx.fillStyle = statusColor;
151
+ ctx.beginPath();
152
+ ctx.arc(statusX, statusY, statusSize / 2, 0, Math.PI * 2);
153
+ ctx.fill();
154
+ }
155
+
156
+ ctx.restore();
157
+ }
158
+
159
+ /**
160
+ * Change l'image de l'avatar
161
+ * @param {string} url - Nouvelle URL d'image
162
+ */
163
+ setImage(url) {
164
+ this.imageUrl = url;
165
+ if (url) {
166
+ this.imageComponent = new ImageComponent(this.framework, {
167
+ x: this.x,
168
+ y: this.y,
169
+ width: this.size,
170
+ height: this.size,
171
+ src: url,
172
+ fit: 'cover',
173
+ borderRadius: this.size / 2
174
+ });
175
+ } else {
176
+ this.imageComponent = null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Change le statut
182
+ * @param {string} status - Nouveau statut
183
+ */
184
+ setStatus(status) {
185
+ this.status = status;
186
+ }
187
+
188
+ /**
189
+ * Vérifie si un point est dans les limites
190
+ * @param {number} x - Coordonnée X
191
+ * @param {number} y - Coordonnée Y
192
+ * @returns {boolean} True si le point est dans l'avatar
193
+ */
194
+ isPointInside(x, y) {
195
+ const adjustedY = y - this.framework.scrollOffset;
196
+ const dx = x - (this.x + this.size / 2);
197
+ const dy = adjustedY - (this.y + this.size / 2);
198
+ return Math.sqrt(dx * dx + dy * dy) <= this.size / 2;
199
+ }
200
+ }
201
+
202
+ export default Avatar;
@@ -0,0 +1,205 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Barre de navigation inférieure
4
+ * @class
5
+ * @extends Component
6
+ * @property {Array} items - Items de navigation
7
+ * @property {number} selectedIndex - Index sélectionné
8
+ * @property {Function} onChange - Callback au changement
9
+ * @property {string} platform - Plateforme
10
+ * @property {string} bgColor - Couleur de fond
11
+ * @property {string} selectedColor - Couleur sélectionnée
12
+ * @property {string} unselectedColor - Couleur non sélectionnée
13
+ */
14
+ class BottomNavigationBar extends Component {
15
+ /**
16
+ * Crée une instance de BottomNavigationBar
17
+ * @param {CanvasFramework} framework - Framework parent
18
+ * @param {Object} [options={}] - Options de configuration
19
+ * @param {Array} [options.items=[]] - Items [{icon, label}]
20
+ * @param {number} [options.selectedIndex=0] - Index sélectionné
21
+ * @param {Function} [options.onChange] - Callback au changement
22
+ * @param {number} [options.height] - Hauteur (auto selon platform)
23
+ * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
24
+ * @param {string} [options.selectedColor] - Couleur sélectionnée (auto selon platform)
25
+ * @param {string} [options.unselectedColor='#757575'] - Couleur non sélectionnée
26
+ */
27
+ constructor(framework, options = {}) {
28
+ super(framework, {
29
+ x: 0,
30
+ y: framework.height - (options.height || 56),
31
+ width: framework.width,
32
+ height: options.height || 56,
33
+ ...options
34
+ });
35
+ this.items = options.items || [];
36
+ this.selectedIndex = options.selectedIndex || 0;
37
+ this.onChange = options.onChange;
38
+ this.platform = framework.platform;
39
+ this.bgColor = options.bgColor || '#FFFFFF';
40
+ this.selectedColor = options.selectedColor || (framework.platform === 'material' ? '#6200EE' : '#007AFF');
41
+ this.unselectedColor = options.unselectedColor || '#757575';
42
+
43
+ // IMPORTANT: Définir onPress pour que le framework l'appelle
44
+ this.onPress = this.handlePress.bind(this);
45
+ }
46
+
47
+ /**
48
+ * Dessine la barre de navigation
49
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
50
+ */
51
+ draw(ctx) {
52
+ ctx.save();
53
+
54
+ // Ombre/bordure supérieure
55
+ if (this.platform === 'material') {
56
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
57
+ ctx.shadowBlur = 8;
58
+ ctx.shadowOffsetY = -2;
59
+ } else {
60
+ ctx.strokeStyle = '#C6C6C8';
61
+ ctx.lineWidth = 0.5;
62
+ ctx.beginPath();
63
+ ctx.moveTo(this.x, this.y);
64
+ ctx.lineTo(this.x + this.width, this.y);
65
+ ctx.stroke();
66
+ }
67
+
68
+ // Background
69
+ ctx.fillStyle = this.bgColor;
70
+ ctx.fillRect(this.x, this.y, this.width, this.height);
71
+
72
+ ctx.shadowColor = 'transparent';
73
+
74
+ // Items
75
+ const itemWidth = this.width / this.items.length;
76
+ for (let i = 0; i < this.items.length; i++) {
77
+ const item = this.items[i];
78
+ const itemX = this.x + i * itemWidth;
79
+ const isSelected = i === this.selectedIndex;
80
+ const color = isSelected ? this.selectedColor : this.unselectedColor;
81
+
82
+ // Icône
83
+ this.drawIcon(ctx, item.icon, itemX + itemWidth / 2, this.y + 16, color);
84
+
85
+ // Label
86
+ ctx.fillStyle = color;
87
+ ctx.font = `${isSelected ? 'bold ' : ''}12px -apple-system, Roboto, sans-serif`;
88
+ ctx.textAlign = 'center';
89
+ ctx.textBaseline = 'top';
90
+ ctx.fillText(item.label, itemX + itemWidth / 2, this.y + 36);
91
+ }
92
+
93
+ ctx.restore();
94
+ }
95
+
96
+ /**
97
+ * Dessine une icône
98
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
99
+ * @param {string} icon - Type d'icône
100
+ * @param {number} x - Position X
101
+ * @param {number} y - Position Y
102
+ * @param {string} color - Couleur
103
+ * @private
104
+ */
105
+ drawIcon(ctx, icon, x, y, color) {
106
+ ctx.strokeStyle = color;
107
+ ctx.fillStyle = color;
108
+ ctx.lineWidth = 2;
109
+ ctx.lineCap = 'round';
110
+ ctx.lineJoin = 'round';
111
+
112
+ switch(icon) {
113
+ case 'home':
114
+ ctx.beginPath();
115
+ ctx.moveTo(x, y);
116
+ ctx.lineTo(x - 10, y + 8);
117
+ ctx.lineTo(x - 10, y + 16);
118
+ ctx.lineTo(x + 10, y + 16);
119
+ ctx.lineTo(x + 10, y + 8);
120
+ ctx.closePath();
121
+ ctx.stroke();
122
+ break;
123
+
124
+ case 'search':
125
+ ctx.beginPath();
126
+ ctx.arc(x - 2, y + 4, 6, 0, Math.PI * 2);
127
+ ctx.stroke();
128
+ ctx.beginPath();
129
+ ctx.moveTo(x + 3, y + 9);
130
+ ctx.lineTo(x + 8, y + 14);
131
+ ctx.stroke();
132
+ break;
133
+
134
+ case 'favorite':
135
+ ctx.beginPath();
136
+ ctx.moveTo(x, y + 2);
137
+ ctx.lineTo(x + 6, y + 14);
138
+ ctx.lineTo(x - 6, y + 14);
139
+ ctx.closePath();
140
+ ctx.beginPath();
141
+ ctx.moveTo(x, y + 14);
142
+ ctx.lineTo(x + 10, y + 6);
143
+ ctx.lineTo(x + 4, y + 6);
144
+ ctx.lineTo(x, y);
145
+ ctx.lineTo(x - 4, y + 6);
146
+ ctx.lineTo(x - 10, y + 6);
147
+ ctx.closePath();
148
+ ctx.fill();
149
+ break;
150
+
151
+ case 'person':
152
+ ctx.beginPath();
153
+ ctx.arc(x, y + 4, 5, 0, Math.PI * 2);
154
+ ctx.stroke();
155
+ ctx.beginPath();
156
+ ctx.arc(x, y + 16, 8, Math.PI, 0, true);
157
+ ctx.stroke();
158
+ break;
159
+
160
+ case 'settings':
161
+ ctx.beginPath();
162
+ ctx.arc(x, y + 8, 4, 0, Math.PI * 2);
163
+ ctx.stroke();
164
+ for (let i = 0; i < 4; i++) {
165
+ const angle = (i * Math.PI / 2) - Math.PI / 4;
166
+ ctx.beginPath();
167
+ ctx.moveTo(x + Math.cos(angle) * 6, y + 8 + Math.sin(angle) * 6);
168
+ ctx.lineTo(x + Math.cos(angle) * 10, y + 8 + Math.sin(angle) * 10);
169
+ ctx.stroke();
170
+ }
171
+ break;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Vérifie si un point est dans les limites
177
+ * @param {number} x - Coordonnée X
178
+ * @param {number} y - Coordonnée Y
179
+ * @returns {boolean} True si le point est dans la barre
180
+ */
181
+ isPointInside(x, y) {
182
+ return y >= this.y && y <= this.y + this.height;
183
+ }
184
+
185
+ /**
186
+ * Gère la pression (clic)
187
+ * @param {number} x - Coordonnée X
188
+ * @param {number} y - Coordonnée Y
189
+ * @private
190
+ */
191
+ handlePress(x, y) {
192
+ // Calculer quel item a été cliqué
193
+ const itemWidth = this.width / this.items.length;
194
+ const index = Math.floor(x / itemWidth);
195
+
196
+ if (index >= 0 && index < this.items.length && index !== this.selectedIndex) {
197
+ this.selectedIndex = index;
198
+ if (this.onChange) {
199
+ this.onChange(index, this.items[index]);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ export default BottomNavigationBar;