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,252 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Accordéon (section extensible)
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} title - Titre
7
+ * @property {string} content - Contenu
8
+ * @property {string|null} icon - Icône
9
+ * @property {boolean} expanded - Déplié
10
+ * @property {string} platform - Plateforme
11
+ * @property {number} headerHeight - Hauteur de l'en-tête
12
+ * @property {number} contentPadding - Padding du contenu
13
+ * @property {string} bgColor - Couleur de fond
14
+ * @property {string} borderColor - Couleur de la bordure
15
+ * @property {Function} onToggle - Callback au toggle
16
+ * @property {boolean} animating - En cours d'animation
17
+ * @property {number} animProgress - Progression de l'animation
18
+ * @property {number} contentHeight - Hauteur du contenu
19
+ */
20
+ class Accordion extends Component {
21
+ /**
22
+ * Crée une instance de Accordion
23
+ * @param {CanvasFramework} framework - Framework parent
24
+ * @param {Object} [options={}] - Options de configuration
25
+ * @param {string} [options.title=''] - Titre
26
+ * @param {string} [options.content=''] - Contenu
27
+ * @param {string} [options.icon] - Icône
28
+ * @param {boolean} [options.expanded=false] - Déplié initialement
29
+ * @param {Function} [options.onToggle] - Callback au toggle
30
+ * @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
31
+ * @param {string} [options.borderColor='#E0E0E0'] - Couleur de bordure
32
+ */
33
+ constructor(framework, options = {}) {
34
+ super(framework, options);
35
+ this.title = options.title || '';
36
+ this.content = options.content || '';
37
+ this.icon = options.icon || null;
38
+ this.expanded = options.expanded || false;
39
+ this.platform = framework.platform;
40
+ this.headerHeight = 56;
41
+ this.contentPadding = 16;
42
+ this.bgColor = options.bgColor || '#FFFFFF';
43
+ this.borderColor = options.borderColor || '#E0E0E0';
44
+ this.onToggle = options.onToggle;
45
+ this.animating = false;
46
+ this.animProgress = this.expanded ? 1 : 0;
47
+
48
+ // Calculer la hauteur du contenu
49
+ this.calculateContentHeight();
50
+ this.height = this.headerHeight + (this.expanded ? this.contentHeight : 0);
51
+
52
+ // CORRECTION: Bloquer les clics pendant l'animation
53
+ this.onClick = () => {
54
+ // Ignorer les clics pendant l'animation
55
+ if (this.animating) {
56
+ return;
57
+ }
58
+ this.toggle();
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Calcule la hauteur du contenu
64
+ * @private
65
+ */
66
+ calculateContentHeight() {
67
+ const ctx = this.framework.ctx;
68
+ ctx.save();
69
+ ctx.font = '14px -apple-system, sans-serif';
70
+
71
+ // Diviser le contenu en lignes
72
+ const maxWidth = this.width - (this.contentPadding * 2);
73
+ const lines = this.wrapText(ctx, this.content, maxWidth);
74
+ const lineHeight = 20;
75
+
76
+ ctx.restore();
77
+ this.contentHeight = lines.length * lineHeight + (this.contentPadding * 2);
78
+ }
79
+
80
+ /**
81
+ * Divise le texte en lignes
82
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
83
+ * @param {string} text - Texte
84
+ * @param {number} maxWidth - Largeur maximale
85
+ * @returns {string[]} Tableau de lignes
86
+ * @private
87
+ */
88
+ wrapText(ctx, text, maxWidth) {
89
+ const words = text.split(' ');
90
+ const lines = [];
91
+ let currentLine = words[0] || '';
92
+
93
+ for (let i = 1; i < words.length; i++) {
94
+ const word = words[i];
95
+ const width = ctx.measureText(currentLine + " " + word).width;
96
+ if (width < maxWidth) {
97
+ currentLine += " " + word;
98
+ } else {
99
+ lines.push(currentLine);
100
+ currentLine = word;
101
+ }
102
+ }
103
+ lines.push(currentLine);
104
+ return lines;
105
+ }
106
+
107
+ /**
108
+ * Alterne l'état déplié/replié
109
+ */
110
+ toggle() {
111
+ // Empêcher les toggles multiples pendant l'animation
112
+ if (this.animating) return;
113
+
114
+ this.expanded = !this.expanded;
115
+ if (this.onToggle) this.onToggle(this.expanded);
116
+ this.animate();
117
+ }
118
+
119
+ /**
120
+ * Anime le toggle
121
+ * @private
122
+ */
123
+ animate() {
124
+ if (this.animating) return;
125
+ this.animating = true;
126
+
127
+ const target = this.expanded ? 1 : 0;
128
+ const step = 0.1;
129
+
130
+ const doAnimate = () => {
131
+ if (Math.abs(this.animProgress - target) < 0.01) {
132
+ this.animProgress = target;
133
+ this.height = this.headerHeight + (this.contentHeight * this.animProgress);
134
+ this.animating = false;
135
+ return;
136
+ }
137
+
138
+ this.animProgress += this.animProgress < target ? step : -step;
139
+ this.height = this.headerHeight + (this.contentHeight * this.animProgress);
140
+ requestAnimationFrame(doAnimate);
141
+ };
142
+
143
+ doAnimate();
144
+ }
145
+
146
+ /**
147
+ * Dessine l'accordéon
148
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
149
+ */
150
+ draw(ctx) {
151
+ ctx.save();
152
+
153
+ // Background
154
+ ctx.fillStyle = this.bgColor;
155
+ ctx.fillRect(this.x, this.y, this.width, this.height);
156
+
157
+ // Bordure
158
+ ctx.strokeStyle = this.borderColor;
159
+ ctx.lineWidth = 1;
160
+ ctx.strokeRect(this.x, this.y, this.width, this.height);
161
+
162
+ // Header
163
+ // Icône (si présente)
164
+ if (this.icon) {
165
+ ctx.font = '20px sans-serif';
166
+ ctx.fillStyle = '#666666';
167
+ ctx.textAlign = 'left';
168
+ ctx.textBaseline = 'middle';
169
+ ctx.fillText(this.icon, this.x + 16, this.y + this.headerHeight / 2);
170
+ }
171
+
172
+ // Titre
173
+ ctx.fillStyle = '#000000';
174
+ ctx.font = 'bold 16px -apple-system, sans-serif';
175
+ ctx.textAlign = 'left';
176
+ ctx.textBaseline = 'middle';
177
+ const titleX = this.x + (this.icon ? 56 : 16);
178
+ ctx.fillText(this.title, titleX, this.y + this.headerHeight / 2);
179
+
180
+ // Chevron (flèche)
181
+ const chevronX = this.x + this.width - 30;
182
+ const chevronY = this.y + this.headerHeight / 2;
183
+ const chevronRotation = this.animProgress * Math.PI;
184
+
185
+ ctx.save();
186
+ ctx.translate(chevronX, chevronY);
187
+ ctx.rotate(chevronRotation);
188
+
189
+ ctx.strokeStyle = '#666666';
190
+ ctx.lineWidth = 2;
191
+ ctx.lineCap = 'round';
192
+ ctx.lineJoin = 'round';
193
+ ctx.beginPath();
194
+ ctx.moveTo(-6, -3);
195
+ ctx.lineTo(0, 3);
196
+ ctx.lineTo(6, -3);
197
+ ctx.stroke();
198
+
199
+ ctx.restore();
200
+
201
+ // Contenu (si expanded ou en train d'animer)
202
+ if (this.animProgress > 0) {
203
+ ctx.save();
204
+
205
+ // Clipping pour l'animation
206
+ ctx.beginPath();
207
+ ctx.rect(this.x, this.y + this.headerHeight, this.width, this.contentHeight * this.animProgress);
208
+ ctx.clip();
209
+
210
+ // Divider
211
+ ctx.strokeStyle = this.borderColor;
212
+ ctx.lineWidth = 1;
213
+ ctx.beginPath();
214
+ ctx.moveTo(this.x, this.y + this.headerHeight);
215
+ ctx.lineTo(this.x + this.width, this.y + this.headerHeight);
216
+ ctx.stroke();
217
+
218
+ // Texte du contenu
219
+ ctx.fillStyle = '#666666';
220
+ ctx.font = '14px -apple-system, sans-serif';
221
+ ctx.textAlign = 'left';
222
+ ctx.textBaseline = 'top';
223
+
224
+ const contentX = this.x + this.contentPadding;
225
+ const contentY = this.y + this.headerHeight + this.contentPadding;
226
+ const maxWidth = this.width - (this.contentPadding * 2);
227
+ const lines = this.wrapText(ctx, this.content, maxWidth);
228
+ const lineHeight = 20;
229
+
230
+ lines.forEach((line, index) => {
231
+ ctx.fillText(line, contentX, contentY + (index * lineHeight));
232
+ });
233
+
234
+ ctx.restore();
235
+ }
236
+
237
+ ctx.restore();
238
+ }
239
+
240
+ /**
241
+ * Vérifie si un point est dans les limites
242
+ * @param {number} x - Coordonnée X
243
+ * @param {number} y - Coordonnée Y
244
+ * @returns {boolean} True si le point est dans l'en-tête
245
+ */
246
+ isPointInside(x, y) {
247
+ return x >= this.x && x <= this.x + this.width &&
248
+ y >= this.y && y <= this.y + this.headerHeight;
249
+ }
250
+ }
251
+
252
+ export default Accordion;
@@ -0,0 +1,398 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Dialog de sélection de date Android
4
+ * @class
5
+ * @extends Component
6
+ * @property {Date} selectedDate - Date sélectionnée
7
+ * @property {Function} onChange - Callback au changement
8
+ * @property {number} currentMonth - Mois courant
9
+ * @property {number} currentYear - Année courante
10
+ * @property {number|null} hoveredDay - Jour survolé
11
+ * @property {number} dialogWidth - Largeur du dialog
12
+ * @property {number} dialogHeight - Hauteur du dialog
13
+ * @property {number} headerHeight - Hauteur de l'en-tête
14
+ * @property {number} daySize - Taille d'un jour
15
+ * @property {number} opacity - Opacité
16
+ * @property {boolean} isVisible - Visibilité
17
+ */
18
+ class AndroidDatePickerDialog extends Component {
19
+ /**
20
+ * Crée une instance de AndroidDatePickerDialog
21
+ * @param {CanvasFramework} framework - Framework parent
22
+ * @param {Object} [options={}] - Options de configuration
23
+ * @param {Date} [options.selectedDate=new Date()] - Date initiale
24
+ * @param {Function} [options.onChange] - Callback au changement
25
+ */
26
+ constructor(framework, options = {}) {
27
+ super(framework, {
28
+ x: 0,
29
+ y: 0,
30
+ width: framework.width,
31
+ height: framework.height,
32
+ visible: false
33
+ });
34
+
35
+ this.selectedDate = options.selectedDate || new Date();
36
+ this.onChange = options.onChange;
37
+ this.currentMonth = this.selectedDate.getMonth();
38
+ this.currentYear = this.selectedDate.getFullYear();
39
+ this.hoveredDay = null;
40
+
41
+ this.dialogWidth = Math.min(320, framework.width - 40);
42
+ this.dialogHeight = 420;
43
+ this.headerHeight = 100;
44
+ this.daySize = (this.dialogWidth - 40) / 7;
45
+
46
+ this.opacity = 0;
47
+ this.isVisible = false;
48
+
49
+ this.onPress = this.handlePress.bind(this);
50
+ }
51
+
52
+ /**
53
+ * Affiche le dialog
54
+ */
55
+ show() {
56
+ this.isVisible = true;
57
+ this.visible = true;
58
+ const fadeIn = () => {
59
+ this.opacity += 0.1;
60
+ if (this.opacity < 1) requestAnimationFrame(fadeIn);
61
+ };
62
+ fadeIn();
63
+ }
64
+
65
+ /**
66
+ * Cache le dialog
67
+ */
68
+ hide() {
69
+ const fadeOut = () => {
70
+ this.opacity -= 0.1;
71
+ if (this.opacity > 0) {
72
+ requestAnimationFrame(fadeOut);
73
+ } else {
74
+ this.isVisible = false;
75
+ this.visible = false;
76
+ this.framework.remove(this);
77
+ }
78
+ };
79
+ fadeOut();
80
+ }
81
+
82
+ /**
83
+ * Obtient le nombre de jours dans un mois
84
+ * @param {number} month - Mois (0-11)
85
+ * @param {number} year - Année
86
+ * @returns {number} Nombre de jours
87
+ * @private
88
+ */
89
+ getDaysInMonth(month, year) {
90
+ return new Date(year, month + 1, 0).getDate();
91
+ }
92
+
93
+ /**
94
+ * Obtient le premier jour de la semaine d'un mois
95
+ * @param {number} month - Mois (0-11)
96
+ * @param {number} year - Année
97
+ * @returns {number} Jour de la semaine (0-6)
98
+ * @private
99
+ */
100
+ getFirstDayOfMonth(month, year) {
101
+ return new Date(year, month, 1).getDay();
102
+ }
103
+
104
+ /**
105
+ * Passe au mois précédent
106
+ * @private
107
+ */
108
+ previousMonth() {
109
+ this.currentMonth--;
110
+ if (this.currentMonth < 0) {
111
+ this.currentMonth = 11;
112
+ this.currentYear--;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Passe au mois suivant
118
+ * @private
119
+ */
120
+ nextMonth() {
121
+ this.currentMonth++;
122
+ if (this.currentMonth > 11) {
123
+ this.currentMonth = 0;
124
+ this.currentYear++;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Sélectionne un jour
130
+ * @param {number} day - Jour à sélectionner
131
+ * @private
132
+ */
133
+ selectDate(day) {
134
+ this.selectedDate = new Date(this.currentYear, this.currentMonth, day);
135
+ }
136
+
137
+ /**
138
+ * Dessine le dialog
139
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
140
+ */
141
+ draw(ctx) {
142
+ if (this.opacity <= 0 || !this.isVisible) return;
143
+
144
+ ctx.save();
145
+ ctx.globalAlpha = this.opacity;
146
+
147
+ // Overlay
148
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
149
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
150
+
151
+ const dialogX = (this.framework.width - this.dialogWidth) / 2;
152
+ const dialogY = (this.framework.height - this.dialogHeight) / 2;
153
+
154
+ // Dialog background
155
+ ctx.fillStyle = '#FFFFFF';
156
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
157
+ ctx.shadowBlur = 20;
158
+ ctx.beginPath();
159
+ this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.dialogHeight, 4);
160
+ ctx.fill();
161
+
162
+ ctx.shadowColor = 'transparent';
163
+
164
+ // Header coloré
165
+ ctx.fillStyle = '#6200EE';
166
+ ctx.beginPath();
167
+ this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.headerHeight, 4);
168
+ ctx.rect(dialogX, dialogY + this.headerHeight - 4, this.dialogWidth, 4);
169
+ ctx.fill();
170
+
171
+ // Année (petit)
172
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
173
+ ctx.font = '16px Roboto, sans-serif';
174
+ ctx.textAlign = 'left';
175
+ ctx.textBaseline = 'top';
176
+ ctx.fillText(this.currentYear.toString(), dialogX + 20, dialogY + 20);
177
+
178
+ // Date sélectionnée (grand)
179
+ const monthNames = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin',
180
+ 'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
181
+ const dayNames = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
182
+ const selectedDay = dayNames[this.selectedDate.getDay()];
183
+ const selectedMonth = monthNames[this.selectedDate.getMonth()];
184
+ const selectedDayNum = this.selectedDate.getDate();
185
+
186
+ ctx.fillStyle = '#FFFFFF';
187
+ ctx.font = 'bold 32px Roboto, sans-serif';
188
+ ctx.textBaseline = 'middle';
189
+ ctx.fillText(`${selectedDay}, ${selectedMonth} ${selectedDayNum}`,
190
+ dialogX + 20, dialogY + this.headerHeight / 2 + 15);
191
+
192
+ // Navigation mois
193
+ const navY = dialogY + this.headerHeight + 20;
194
+
195
+ // Bouton mois précédent
196
+ ctx.fillStyle = '#666666';
197
+ ctx.font = '20px sans-serif';
198
+ ctx.textAlign = 'center';
199
+ ctx.fillText('◀', dialogX + 30, navY);
200
+
201
+ // Mois et année actuel
202
+ const monthNamesLong = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
203
+ 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
204
+ ctx.font = 'bold 16px Roboto, sans-serif';
205
+ ctx.fillText(`${monthNamesLong[this.currentMonth]} ${this.currentYear}`,
206
+ dialogX + this.dialogWidth / 2, navY);
207
+
208
+ // Bouton mois suivant
209
+ ctx.fillText('▶', dialogX + this.dialogWidth - 30, navY);
210
+
211
+ // Jours de la semaine
212
+ const dayNamesShort = ['D', 'L', 'M', 'M', 'J', 'V', 'S'];
213
+ ctx.fillStyle = '#666666';
214
+ ctx.font = 'bold 12px Roboto, sans-serif';
215
+
216
+ for (let i = 0; i < 7; i++) {
217
+ const dayX = dialogX + 20 + i * this.daySize + this.daySize / 2;
218
+ const dayY = navY + 40;
219
+ ctx.fillText(dayNamesShort[i], dayX, dayY);
220
+ }
221
+
222
+ // Grille de jours
223
+ const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
224
+ const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
225
+
226
+ let dayNumber = 1;
227
+ const startY = navY + 60;
228
+
229
+ for (let week = 0; week < 6; week++) {
230
+ for (let day = 0; day < 7; day++) {
231
+ const index = week * 7 + day;
232
+
233
+ if (index >= firstDay && dayNumber <= daysInMonth) {
234
+ const dayX = dialogX + 20 + day * this.daySize;
235
+ const dayY = startY + week * this.daySize;
236
+
237
+ const isSelected = this.selectedDate.getDate() === dayNumber &&
238
+ this.selectedDate.getMonth() === this.currentMonth &&
239
+ this.selectedDate.getFullYear() === this.currentYear;
240
+
241
+ const today = new Date();
242
+ const isToday = dayNumber === today.getDate() &&
243
+ this.currentMonth === today.getMonth() &&
244
+ this.currentYear === today.getFullYear();
245
+
246
+ // Cercle sélectionné
247
+ if (isSelected) {
248
+ ctx.fillStyle = '#6200EE';
249
+ ctx.beginPath();
250
+ ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
251
+ this.daySize / 2 - 2, 0, Math.PI * 2);
252
+ ctx.fill();
253
+ }
254
+
255
+ // Cercle aujourd'hui
256
+ if (isToday && !isSelected) {
257
+ ctx.strokeStyle = '#6200EE';
258
+ ctx.lineWidth = 2;
259
+ ctx.beginPath();
260
+ ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
261
+ this.daySize / 2 - 2, 0, Math.PI * 2);
262
+ ctx.stroke();
263
+ }
264
+
265
+ // Numéro
266
+ ctx.fillStyle = isSelected ? '#FFFFFF' : '#000000';
267
+ ctx.font = '14px Roboto, sans-serif';
268
+ ctx.textAlign = 'center';
269
+ ctx.textBaseline = 'middle';
270
+ ctx.fillText(dayNumber.toString(), dayX + this.daySize / 2, dayY + this.daySize / 2);
271
+
272
+ dayNumber++;
273
+ }
274
+ }
275
+
276
+ if (dayNumber > daysInMonth) break;
277
+ }
278
+
279
+ // Boutons d'action
280
+ const btnY = dialogY + this.dialogHeight - 30;
281
+
282
+ // Annuler
283
+ ctx.fillStyle = '#6200EE';
284
+ ctx.font = 'bold 14px Roboto, sans-serif';
285
+ ctx.textAlign = 'right';
286
+ ctx.fillText('ANNULER', dialogX + this.dialogWidth - 120, btnY);
287
+
288
+ // OK
289
+ ctx.fillText('OK', dialogX + this.dialogWidth - 20, btnY);
290
+
291
+ ctx.restore();
292
+ }
293
+
294
+ /**
295
+ * Gère la pression (clic)
296
+ * @param {number} x - Coordonnée X
297
+ * @param {number} y - Coordonnée Y
298
+ * @private
299
+ */
300
+ handlePress(x, y) {
301
+ const dialogX = (this.framework.width - this.dialogWidth) / 2;
302
+ const dialogY = (this.framework.height - this.dialogHeight) / 2;
303
+
304
+ // Boutons navigation
305
+ const navY = dialogY + this.headerHeight + 20;
306
+
307
+ if (y >= navY - 15 && y <= navY + 15) {
308
+ if (x >= dialogX + 10 && x <= dialogX + 50) {
309
+ this.previousMonth();
310
+ return;
311
+ }
312
+ if (x >= dialogX + this.dialogWidth - 50 && x <= dialogX + this.dialogWidth - 10) {
313
+ this.nextMonth();
314
+ return;
315
+ }
316
+ }
317
+
318
+ // Sélection d'un jour
319
+ const startY = navY + 60;
320
+ const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
321
+ const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
322
+
323
+ let dayNumber = 1;
324
+ for (let week = 0; week < 6; week++) {
325
+ for (let day = 0; day < 7; day++) {
326
+ const index = week * 7 + day;
327
+ if (index >= firstDay && dayNumber <= daysInMonth) {
328
+ const dayX = dialogX + 20 + day * this.daySize;
329
+ const dayY = startY + week * this.daySize;
330
+
331
+ if (x >= dayX && x <= dayX + this.daySize &&
332
+ y >= dayY && y <= dayY + this.daySize) {
333
+ this.selectDate(dayNumber);
334
+ return;
335
+ }
336
+ dayNumber++;
337
+ }
338
+ }
339
+ if (dayNumber > daysInMonth) break;
340
+ }
341
+
342
+ // Boutons d'action
343
+ const btnY = dialogY + this.dialogHeight - 30;
344
+ if (y >= btnY - 20 && y <= btnY + 20) {
345
+ // Annuler
346
+ if (x >= dialogX + this.dialogWidth - 180 && x <= dialogX + this.dialogWidth - 80) {
347
+ this.hide();
348
+ return;
349
+ }
350
+ // OK
351
+ if (x >= dialogX + this.dialogWidth - 70 && x <= dialogX + this.dialogWidth) {
352
+ if (this.onChange) this.onChange(this.selectedDate);
353
+ this.hide();
354
+ return;
355
+ }
356
+ }
357
+
358
+ // Clic sur overlay pour fermer
359
+ if (x < dialogX || x > dialogX + this.dialogWidth ||
360
+ y < dialogY || y > dialogY + this.dialogHeight) {
361
+ this.hide();
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Dessine un rectangle avec coins arrondis
367
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
368
+ * @param {number} x - Position X
369
+ * @param {number} y - Position Y
370
+ * @param {number} width - Largeur
371
+ * @param {number} height - Hauteur
372
+ * @param {number} radius - Rayon des coins
373
+ * @private
374
+ */
375
+ roundRect(ctx, x, y, width, height, radius) {
376
+ ctx.beginPath();
377
+ ctx.moveTo(x + radius, y);
378
+ ctx.lineTo(x + width - radius, y);
379
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
380
+ ctx.lineTo(x + width, y + height - radius);
381
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
382
+ ctx.lineTo(x + radius, y + height);
383
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
384
+ ctx.lineTo(x, y + radius);
385
+ ctx.quadraticCurveTo(x, y, x + radius, y);
386
+ ctx.closePath();
387
+ }
388
+
389
+ /**
390
+ * Vérifie si un point est dans les limites
391
+ * @returns {boolean} True si visible
392
+ */
393
+ isPointInside() {
394
+ return this.isVisible;
395
+ }
396
+ }
397
+
398
+ export default AndroidDatePickerDialog;