canvasframework 0.5.18 → 0.5.19

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 (112) hide show
  1. package/components/Accordion.js +265 -0
  2. package/components/AndroidDatePickerDialog.js +406 -0
  3. package/components/AppBar.js +398 -0
  4. package/components/AudioPlayer.js +611 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/Banner.js +342 -0
  7. package/components/BottomNavigationBar.js +433 -0
  8. package/components/BottomSheet.js +234 -0
  9. package/components/Button.js +358 -0
  10. package/components/Camera.js +644 -0
  11. package/components/Card.js +193 -0
  12. package/components/Chart.js +700 -0
  13. package/components/Checkbox.js +166 -0
  14. package/components/Chip.js +212 -0
  15. package/components/CircularProgress.js +327 -0
  16. package/components/ContextMenu.js +116 -0
  17. package/components/DatePicker.js +298 -0
  18. package/components/Dialog.js +337 -0
  19. package/components/Divider.js +125 -0
  20. package/components/Drawer.js +276 -0
  21. package/components/FAB.js +270 -0
  22. package/components/FileUpload.js +315 -0
  23. package/components/FloatedCamera.js +644 -0
  24. package/components/IOSDatePickerWheel.js +430 -0
  25. package/components/ImageCarousel.js +219 -0
  26. package/components/ImageComponent.js +223 -0
  27. package/components/Input.js +831 -0
  28. package/components/InputDatalist.js +723 -0
  29. package/components/InputTags.js +624 -0
  30. package/components/List.js +95 -0
  31. package/components/ListItem.js +269 -0
  32. package/components/Modal.js +364 -0
  33. package/components/MorphingFAB.js +428 -0
  34. package/components/MultiSelectDialog.js +206 -0
  35. package/components/NumberInput.js +271 -0
  36. package/components/PasswordInput.js +462 -0
  37. package/components/ProgressBar.js +88 -0
  38. package/components/QRCodeReader.js +539 -0
  39. package/components/RadioButton.js +151 -0
  40. package/components/SearchInput.js +315 -0
  41. package/components/SegmentedControl.js +357 -0
  42. package/components/Select.js +199 -0
  43. package/components/SelectDialog.js +255 -0
  44. package/components/Slider.js +113 -0
  45. package/components/SliverAppBar.js +139 -0
  46. package/components/Snackbar.js +243 -0
  47. package/components/SpeedDialFAB.js +397 -0
  48. package/components/Stepper.js +281 -0
  49. package/components/SwipeableListItem.js +327 -0
  50. package/components/Switch.js +147 -0
  51. package/components/Table.js +492 -0
  52. package/components/Tabs.js +423 -0
  53. package/components/Text.js +141 -0
  54. package/components/TextField.js +151 -0
  55. package/components/TimePicker.js +934 -0
  56. package/components/Toast.js +236 -0
  57. package/components/TreeView.js +420 -0
  58. package/components/Video.js +397 -0
  59. package/components/View.js +140 -0
  60. package/components/VirtualList.js +120 -0
  61. package/core/CanvasFramework.js +3045 -0
  62. package/core/Component.js +243 -0
  63. package/core/ThemeManager.js +358 -0
  64. package/core/UIBuilder.js +267 -0
  65. package/core/WebGLCanvasAdapter.js +782 -0
  66. package/features/Column.js +43 -0
  67. package/features/Grid.js +47 -0
  68. package/features/LayoutComponent.js +43 -0
  69. package/features/OpenStreetMap.js +310 -0
  70. package/features/Positioned.js +33 -0
  71. package/features/PullToRefresh.js +328 -0
  72. package/features/Row.js +40 -0
  73. package/features/SignaturePad.js +257 -0
  74. package/features/Skeleton.js +193 -0
  75. package/features/Stack.js +21 -0
  76. package/index.js +119 -0
  77. package/manager/AccessibilityManager.js +107 -0
  78. package/manager/ErrorHandler.js +59 -0
  79. package/manager/FeatureFlags.js +60 -0
  80. package/manager/MemoryManager.js +107 -0
  81. package/manager/PerformanceMonitor.js +84 -0
  82. package/manager/SecurityManager.js +54 -0
  83. package/package.json +22 -16
  84. package/utils/AnimationEngine.js +734 -0
  85. package/utils/CryptoManager.js +303 -0
  86. package/utils/DataStore.js +403 -0
  87. package/utils/DevTools.js +1618 -0
  88. package/utils/DevToolsConsole.js +201 -0
  89. package/utils/EventBus.js +407 -0
  90. package/utils/FetchClient.js +74 -0
  91. package/utils/FirebaseAuth.js +653 -0
  92. package/utils/FirebaseCore.js +246 -0
  93. package/utils/FirebaseFirestore.js +581 -0
  94. package/utils/FirebaseFunctions.js +97 -0
  95. package/utils/FirebaseRealtimeDB.js +498 -0
  96. package/utils/FirebaseStorage.js +612 -0
  97. package/utils/FormValidator.js +355 -0
  98. package/utils/GeoLocationService.js +62 -0
  99. package/utils/I18n.js +207 -0
  100. package/utils/IndexedDBManager.js +273 -0
  101. package/utils/InspectionOverlay.js +308 -0
  102. package/utils/NotificationManager.js +60 -0
  103. package/utils/OfflineSyncManager.js +342 -0
  104. package/utils/PayPalPayment.js +678 -0
  105. package/utils/QueryBuilder.js +478 -0
  106. package/utils/SafeArea.js +64 -0
  107. package/utils/SecureStorage.js +289 -0
  108. package/utils/StateManager.js +207 -0
  109. package/utils/StripePayment.js +552 -0
  110. package/utils/WebSocketClient.js +66 -0
  111. package/dist/canvasframework.js +0 -2
  112. package/dist/canvasframework.js.LICENSE.txt +0 -1
@@ -0,0 +1,265 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Accordion (section extensible) avec styles Material & Cupertino + Ripple centré Android
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class Accordion extends Component {
9
+ constructor(framework, options = {}) {
10
+ super(framework, options);
11
+ this.title = options.title || '';
12
+ this.content = options.content || '';
13
+ this.icon = options.icon || null;
14
+ this.expanded = options.expanded || false;
15
+ this.platform = framework.platform;
16
+ this.headerHeight = 56;
17
+ this.contentPadding = 16;
18
+ this.bgColor = options.bgColor || '#FFFFFF';
19
+ this.borderColor = options.borderColor || '#E0E0E0';
20
+ this.onToggle = options.onToggle;
21
+ this.animating = false;
22
+ this.animProgress = this.expanded ? 1 : 0;
23
+
24
+ this.calculateContentHeight();
25
+ this.height = this.headerHeight + (this.expanded ? this.contentHeight : 0);
26
+
27
+ // Pour les ripples Material
28
+ this.ripples = [];
29
+ this.rippleColor = 'rgba(1,0,0,0.2)';
30
+
31
+ // Clic
32
+ this.onClick = () => {
33
+ if (this.animating) return;
34
+
35
+ // Ripple centré Material
36
+ if (this.platform === 'material') {
37
+ this.addRipple();
38
+ }
39
+
40
+ this.toggle();
41
+ };
42
+ }
43
+
44
+ calculateContentHeight() {
45
+ const ctx = this.framework.ctx;
46
+ ctx.save();
47
+ ctx.font = '14px -apple-system, sans-serif';
48
+ const maxWidth = this.width - this.contentPadding * 2;
49
+ const lines = this.wrapText(ctx, this.content, maxWidth);
50
+ ctx.restore();
51
+ const lineHeight = 20;
52
+ this.contentHeight = lines.length * lineHeight + this.contentPadding * 2;
53
+ }
54
+
55
+ wrapText(ctx, text, maxWidth) {
56
+ const words = text.split(' ');
57
+ const lines = [];
58
+ let currentLine = words[0] || '';
59
+ for (let i = 1; i < words.length; i++) {
60
+ const word = words[i];
61
+ const width = ctx.measureText(currentLine + ' ' + word).width;
62
+ if (width < maxWidth) currentLine += ' ' + word;
63
+ else {
64
+ lines.push(currentLine);
65
+ currentLine = word;
66
+ }
67
+ }
68
+ lines.push(currentLine);
69
+ return lines;
70
+ }
71
+
72
+ toggle() {
73
+ if (this.animating) return;
74
+ this.expanded = !this.expanded;
75
+ if (this.onToggle) this.onToggle(this.expanded);
76
+ this.animate();
77
+ }
78
+
79
+ animate() {
80
+ if (this.animating) return;
81
+ this.animating = true;
82
+ const target = this.expanded ? 1 : 0;
83
+ const step = 0.1;
84
+
85
+ const doAnimate = () => {
86
+ if (Math.abs(this.animProgress - target) < 0.01) {
87
+ this.animProgress = target;
88
+ this.height = this.headerHeight + this.contentHeight * this.animProgress;
89
+ this.animating = false;
90
+ return;
91
+ }
92
+ this.animProgress += this.animProgress < target ? step : -step;
93
+ this.height = this.headerHeight + this.contentHeight * this.animProgress;
94
+ requestAnimationFrame(doAnimate);
95
+ };
96
+ doAnimate();
97
+ }
98
+
99
+ addRipple() {
100
+ const ripple = {
101
+ x: this.width / 2,
102
+ y: this.headerHeight / 2,
103
+ radius: 0,
104
+ maxRadius: Math.max(this.width, this.headerHeight) * 1.5,
105
+ opacity: 0.3
106
+ };
107
+ this.ripples.push(ripple);
108
+ this.animateRipples();
109
+ }
110
+
111
+ animateRipples() {
112
+ const animate = () => {
113
+ let active = false;
114
+ for (let ripple of this.ripples) {
115
+ if (ripple.radius < ripple.maxRadius) {
116
+ ripple.radius += ripple.maxRadius / 15;
117
+ ripple.opacity -= 0.03;
118
+ active = true;
119
+ }
120
+ }
121
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
122
+ if (active) requestAnimationFrame(animate);
123
+ };
124
+ animate();
125
+ }
126
+
127
+ draw(ctx) {
128
+ ctx.save();
129
+
130
+ let headerBg = '#FFFFFF';
131
+ let headerTextColor = '#000000';
132
+ let borderColor = this.borderColor;
133
+ let shadowBlur = 0;
134
+ let chevronWidth = 2;
135
+
136
+ if (this.platform === 'material') {
137
+ headerBg = '#F5F5F5';
138
+ headerTextColor = '#212121';
139
+ shadowBlur = 4;
140
+ chevronWidth = 3;
141
+ } else if (this.platform === 'cupertino') {
142
+ headerBg = '#FFFFFF';
143
+ headerTextColor = '#000000';
144
+ borderColor = '#C7C7CC';
145
+ chevronWidth = 1.5;
146
+ }
147
+
148
+ // Ombre Material
149
+ if (shadowBlur > 0) {
150
+ ctx.shadowColor = 'rgba(0,0,0,0.2)';
151
+ ctx.shadowBlur = shadowBlur;
152
+ ctx.shadowOffsetX = 0;
153
+ ctx.shadowOffsetY = 2;
154
+ }
155
+
156
+ // Background
157
+ ctx.fillStyle = this.bgColor;
158
+ ctx.fillRect(this.x, this.y, this.width, this.height);
159
+
160
+ // Bordure Cupertino
161
+ if (this.platform === 'cupertino') {
162
+ ctx.strokeStyle = borderColor;
163
+ ctx.lineWidth = 1;
164
+ ctx.strokeRect(this.x, this.y, this.width, this.height);
165
+ }
166
+
167
+ // Header
168
+ ctx.fillStyle = headerBg;
169
+ ctx.fillRect(this.x, this.y, this.width, this.headerHeight);
170
+
171
+ // Ripple centré Material
172
+ if (this.platform === 'material' && this.ripples.length) {
173
+ ctx.save();
174
+ ctx.beginPath();
175
+ ctx.rect(this.x, this.y, this.width, this.headerHeight);
176
+ ctx.clip();
177
+ for (let ripple of this.ripples) {
178
+ ctx.globalAlpha = ripple.opacity;
179
+ ctx.fillStyle = this.rippleColor;
180
+ ctx.beginPath();
181
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
182
+ ctx.fill();
183
+ }
184
+ ctx.restore();
185
+ ctx.globalAlpha = 1;
186
+ }
187
+
188
+ // Icône
189
+ if (this.icon) {
190
+ ctx.font = '20px sans-serif';
191
+ ctx.fillStyle = '#666666';
192
+ ctx.textAlign = 'left';
193
+ ctx.textBaseline = 'middle';
194
+ ctx.fillText(this.icon, this.x + 16, this.y + this.headerHeight / 2);
195
+ }
196
+
197
+ // Titre
198
+ ctx.fillStyle = headerTextColor;
199
+ ctx.font =
200
+ this.platform === 'material'
201
+ ? 'bold 16px Roboto, sans-serif'
202
+ : 'bold 16px -apple-system, sans-serif';
203
+ ctx.textAlign = 'left';
204
+ ctx.textBaseline = 'middle';
205
+ const titleX = this.icon ? this.x + 56 : this.x + 16;
206
+ ctx.fillText(this.title, titleX, this.y + this.headerHeight / 2);
207
+
208
+ // Chevron
209
+ const chevronX = this.x + this.width - 30;
210
+ const chevronY = this.y + this.headerHeight / 2;
211
+ const chevronRotation = this.animProgress * Math.PI;
212
+ ctx.save();
213
+ ctx.translate(chevronX, chevronY);
214
+ ctx.rotate(chevronRotation);
215
+ ctx.strokeStyle = '#666666';
216
+ ctx.lineWidth = chevronWidth;
217
+ ctx.lineCap = 'round';
218
+ ctx.lineJoin = 'round';
219
+ ctx.beginPath();
220
+ ctx.moveTo(-6, -3);
221
+ ctx.lineTo(0, 3);
222
+ ctx.lineTo(6, -3);
223
+ ctx.stroke();
224
+ ctx.restore();
225
+
226
+ // Contenu
227
+ if (this.animProgress > 0) {
228
+ ctx.save();
229
+ ctx.beginPath();
230
+ ctx.rect(this.x, this.y + this.headerHeight, this.width, this.contentHeight * this.animProgress);
231
+ ctx.clip();
232
+
233
+ ctx.strokeStyle = borderColor;
234
+ ctx.lineWidth = 1;
235
+ ctx.beginPath();
236
+ ctx.moveTo(this.x, this.y + this.headerHeight);
237
+ ctx.lineTo(this.x + this.width, this.y + this.headerHeight);
238
+ ctx.stroke();
239
+
240
+ ctx.fillStyle = '#666666';
241
+ ctx.font = '14px -apple-system, sans-serif';
242
+ ctx.textAlign = 'left';
243
+ ctx.textBaseline = 'top';
244
+ const contentX = this.x + this.contentPadding;
245
+ const contentY = this.y + this.headerHeight + this.contentPadding;
246
+ const maxWidth = this.width - this.contentPadding * 2;
247
+ const lines = this.wrapText(ctx, this.content, maxWidth);
248
+ const lineHeight = 20;
249
+ lines.forEach((line, index) => {
250
+ ctx.fillText(line, contentX, contentY + index * lineHeight);
251
+ });
252
+
253
+ ctx.restore();
254
+ }
255
+
256
+ ctx.restore();
257
+ }
258
+
259
+ isPointInside(x, y) {
260
+ return x >= this.x && x <= this.x + this.width &&
261
+ y >= this.y && y <= this.y + this.headerHeight;
262
+ }
263
+ }
264
+
265
+ export default Accordion;
@@ -0,0 +1,406 @@
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
+ // Options de personnalisation
42
+ this.headerBgColor = options.headerBgColor || '#6200EE';
43
+ this.selectedColor = options.selectedColor || '#6200EE';
44
+ this.buttonColor = options.buttonColor || '#6200EE';
45
+ this.todayColor = options.todayColor || '#6200EE';
46
+ this.textColor = options.textColor || '#000000';
47
+ this.dayNamesColor = options.dayNamesColor || '#666666';
48
+
49
+ this.dialogWidth = Math.min(320, framework.width - 40);
50
+ this.dialogHeight = 420;
51
+ this.headerHeight = 100;
52
+ this.daySize = (this.dialogWidth - 40) / 7;
53
+
54
+ this.opacity = 0;
55
+ this.isVisible = false;
56
+
57
+ this.onPress = this.handlePress.bind(this);
58
+ }
59
+
60
+ /**
61
+ * Affiche le dialog
62
+ */
63
+ show() {
64
+ this.isVisible = true;
65
+ this.visible = true;
66
+ const fadeIn = () => {
67
+ this.opacity += 0.1;
68
+ if (this.opacity < 1) requestAnimationFrame(fadeIn);
69
+ };
70
+ fadeIn();
71
+ }
72
+
73
+ /**
74
+ * Cache le dialog
75
+ */
76
+ hide() {
77
+ const fadeOut = () => {
78
+ this.opacity -= 0.1;
79
+ if (this.opacity > 0) {
80
+ requestAnimationFrame(fadeOut);
81
+ } else {
82
+ this.isVisible = false;
83
+ this.visible = false;
84
+ this.framework.remove(this);
85
+ }
86
+ };
87
+ fadeOut();
88
+ }
89
+
90
+ /**
91
+ * Obtient le nombre de jours dans un mois
92
+ * @param {number} month - Mois (0-11)
93
+ * @param {number} year - Année
94
+ * @returns {number} Nombre de jours
95
+ * @private
96
+ */
97
+ getDaysInMonth(month, year) {
98
+ return new Date(year, month + 1, 0).getDate();
99
+ }
100
+
101
+ /**
102
+ * Obtient le premier jour de la semaine d'un mois
103
+ * @param {number} month - Mois (0-11)
104
+ * @param {number} year - Année
105
+ * @returns {number} Jour de la semaine (0-6)
106
+ * @private
107
+ */
108
+ getFirstDayOfMonth(month, year) {
109
+ return new Date(year, month, 1).getDay();
110
+ }
111
+
112
+ /**
113
+ * Passe au mois précédent
114
+ * @private
115
+ */
116
+ previousMonth() {
117
+ this.currentMonth--;
118
+ if (this.currentMonth < 0) {
119
+ this.currentMonth = 11;
120
+ this.currentYear--;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Passe au mois suivant
126
+ * @private
127
+ */
128
+ nextMonth() {
129
+ this.currentMonth++;
130
+ if (this.currentMonth > 11) {
131
+ this.currentMonth = 0;
132
+ this.currentYear++;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Sélectionne un jour
138
+ * @param {number} day - Jour à sélectionner
139
+ * @private
140
+ */
141
+ selectDate(day) {
142
+ this.selectedDate = new Date(this.currentYear, this.currentMonth, day);
143
+ }
144
+
145
+ /**
146
+ * Dessine le dialog
147
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
148
+ */
149
+ draw(ctx) {
150
+ if (this.opacity <= 0 || !this.isVisible) return;
151
+
152
+ ctx.save();
153
+ ctx.globalAlpha = this.opacity;
154
+
155
+ // Overlay
156
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
157
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
158
+
159
+ const dialogX = (this.framework.width - this.dialogWidth) / 2;
160
+ const dialogY = (this.framework.height - this.dialogHeight) / 2;
161
+
162
+ // Dialog background
163
+ ctx.fillStyle = '#FFFFFF';
164
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
165
+ ctx.shadowBlur = 20;
166
+ ctx.beginPath();
167
+ this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.dialogHeight, 4);
168
+ ctx.fill();
169
+
170
+ ctx.shadowColor = 'transparent';
171
+
172
+ // Header coloré
173
+ ctx.fillStyle = this.headerBgColor;
174
+ ctx.beginPath();
175
+ this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.headerHeight, 4);
176
+ ctx.rect(dialogX, dialogY + this.headerHeight - 4, this.dialogWidth, 4);
177
+ ctx.fill();
178
+
179
+ // Année (petit)
180
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
181
+ ctx.font = '16px Roboto, sans-serif';
182
+ ctx.textAlign = 'left';
183
+ ctx.textBaseline = 'top';
184
+ ctx.fillText(this.currentYear.toString(), dialogX + 20, dialogY + 20);
185
+
186
+ // Date sélectionnée (grand)
187
+ const monthNames = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin',
188
+ 'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
189
+ const dayNames = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
190
+ const selectedDay = dayNames[this.selectedDate.getDay()];
191
+ const selectedMonth = monthNames[this.selectedDate.getMonth()];
192
+ const selectedDayNum = this.selectedDate.getDate();
193
+
194
+ ctx.fillStyle = '#ffffff';
195
+ ctx.font = 'bold 32px Roboto, sans-serif';
196
+ ctx.textBaseline = 'middle';
197
+ ctx.fillText(`${selectedDay}, ${selectedMonth} ${selectedDayNum}`,
198
+ dialogX + 20, dialogY + this.headerHeight / 2 + 15);
199
+
200
+ // Navigation mois
201
+ const navY = dialogY + this.headerHeight + 20;
202
+
203
+ // Bouton mois précédent
204
+ ctx.fillStyle = '#666666';
205
+ ctx.font = '20px sans-serif';
206
+ ctx.textAlign = 'center';
207
+ ctx.fillText('◀', dialogX + 30, navY);
208
+
209
+ // Mois et année actuel
210
+ const monthNamesLong = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
211
+ 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
212
+ ctx.font = 'bold 16px Roboto, sans-serif';
213
+ ctx.fillText(`${monthNamesLong[this.currentMonth]} ${this.currentYear}`,
214
+ dialogX + this.dialogWidth / 2, navY);
215
+
216
+ // Bouton mois suivant
217
+ ctx.fillText('▶', dialogX + this.dialogWidth - 30, navY);
218
+
219
+ // Jours de la semaine
220
+ const dayNamesShort = ['D', 'L', 'M', 'M', 'J', 'V', 'S'];
221
+ ctx.fillStyle = '#666666';
222
+ ctx.font = 'bold 12px Roboto, sans-serif';
223
+
224
+ for (let i = 0; i < 7; i++) {
225
+ const dayX = dialogX + 20 + i * this.daySize + this.daySize / 2;
226
+ const dayY = navY + 40;
227
+ ctx.fillText(dayNamesShort[i], dayX, dayY);
228
+ }
229
+
230
+ // Grille de jours
231
+ const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
232
+ const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
233
+
234
+ let dayNumber = 1;
235
+ const startY = navY + 60;
236
+
237
+ for (let week = 0; week < 6; week++) {
238
+ for (let day = 0; day < 7; day++) {
239
+ const index = week * 7 + day;
240
+
241
+ if (index >= firstDay && dayNumber <= daysInMonth) {
242
+ const dayX = dialogX + 20 + day * this.daySize;
243
+ const dayY = startY + week * this.daySize;
244
+
245
+ const isSelected = this.selectedDate.getDate() === dayNumber &&
246
+ this.selectedDate.getMonth() === this.currentMonth &&
247
+ this.selectedDate.getFullYear() === this.currentYear;
248
+
249
+ const today = new Date();
250
+ const isToday = dayNumber === today.getDate() &&
251
+ this.currentMonth === today.getMonth() &&
252
+ this.currentYear === today.getFullYear();
253
+
254
+ // Cercle sélectionné
255
+ if (isSelected) {
256
+ ctx.fillStyle = '#6200EE';
257
+ ctx.beginPath();
258
+ ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
259
+ this.daySize / 2 - 2, 0, Math.PI * 2);
260
+ ctx.fill();
261
+ }
262
+
263
+ // Cercle aujourd'hui
264
+ if (isToday && !isSelected) {
265
+ ctx.strokeStyle = '#6200EE';
266
+ ctx.lineWidth = 2;
267
+ ctx.beginPath();
268
+ ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
269
+ this.daySize / 2 - 2, 0, Math.PI * 2);
270
+ ctx.stroke();
271
+ }
272
+
273
+ // Numéro
274
+ ctx.fillStyle = isSelected ? '#FFFFFF' : '#212121';
275
+ ctx.font = '14px Roboto, sans-serif';
276
+ ctx.textAlign = 'center';
277
+ ctx.textBaseline = 'middle';
278
+ ctx.fillText(dayNumber.toString(), dayX + this.daySize / 2, dayY + this.daySize / 2);
279
+
280
+ dayNumber++;
281
+ }
282
+ }
283
+
284
+ if (dayNumber > daysInMonth) break;
285
+ }
286
+
287
+ // Boutons d'action
288
+ const btnY = dialogY + this.dialogHeight - 30;
289
+
290
+ // Annuler
291
+ ctx.fillStyle = '#6200EE';
292
+ ctx.font = 'bold 14px Roboto, sans-serif';
293
+ ctx.textAlign = 'right';
294
+ ctx.fillText('ANNULER', dialogX + this.dialogWidth - 120, btnY);
295
+
296
+ // OK
297
+ ctx.fillText('OK', dialogX + this.dialogWidth - 20, btnY);
298
+
299
+ ctx.restore();
300
+ }
301
+
302
+ /**
303
+ * Gère la pression (clic)
304
+ * @param {number} x - Coordonnée X
305
+ * @param {number} y - Coordonnée Y
306
+ * @private
307
+ */
308
+ handlePress(x, y) {
309
+ const dialogX = (this.framework.width - this.dialogWidth) / 2;
310
+ const dialogY = (this.framework.height - this.dialogHeight) / 2;
311
+
312
+ // Boutons navigation
313
+ const navY = dialogY + this.headerHeight + 20;
314
+
315
+ if (y >= navY - 15 && y <= navY + 15) {
316
+ if (x >= dialogX + 10 && x <= dialogX + 50) {
317
+ this.previousMonth();
318
+ return;
319
+ }
320
+ if (x >= dialogX + this.dialogWidth - 50 && x <= dialogX + this.dialogWidth - 10) {
321
+ this.nextMonth();
322
+ return;
323
+ }
324
+ }
325
+
326
+ // Sélection d'un jour
327
+ const startY = navY + 60;
328
+ const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
329
+ const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
330
+
331
+ let dayNumber = 1;
332
+ for (let week = 0; week < 6; week++) {
333
+ for (let day = 0; day < 7; day++) {
334
+ const index = week * 7 + day;
335
+ if (index >= firstDay && dayNumber <= daysInMonth) {
336
+ const dayX = dialogX + 20 + day * this.daySize;
337
+ const dayY = startY + week * this.daySize;
338
+
339
+ if (x >= dayX && x <= dayX + this.daySize &&
340
+ y >= dayY && y <= dayY + this.daySize) {
341
+ this.selectDate(dayNumber);
342
+ return;
343
+ }
344
+ dayNumber++;
345
+ }
346
+ }
347
+ if (dayNumber > daysInMonth) break;
348
+ }
349
+
350
+ // Boutons d'action
351
+ const btnY = dialogY + this.dialogHeight - 30;
352
+ if (y >= btnY - 20 && y <= btnY + 20) {
353
+ // Annuler
354
+ if (x >= dialogX + this.dialogWidth - 180 && x <= dialogX + this.dialogWidth - 80) {
355
+ this.hide();
356
+ return;
357
+ }
358
+ // OK
359
+ if (x >= dialogX + this.dialogWidth - 70 && x <= dialogX + this.dialogWidth) {
360
+ if (this.onChange) this.onChange(this.selectedDate);
361
+ this.hide();
362
+ return;
363
+ }
364
+ }
365
+
366
+ // Clic sur overlay pour fermer
367
+ if (x < dialogX || x > dialogX + this.dialogWidth ||
368
+ y < dialogY || y > dialogY + this.dialogHeight) {
369
+ this.hide();
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Dessine un rectangle avec coins arrondis
375
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
376
+ * @param {number} x - Position X
377
+ * @param {number} y - Position Y
378
+ * @param {number} width - Largeur
379
+ * @param {number} height - Hauteur
380
+ * @param {number} radius - Rayon des coins
381
+ * @private
382
+ */
383
+ roundRect(ctx, x, y, width, height, radius) {
384
+ ctx.beginPath();
385
+ ctx.moveTo(x + radius, y);
386
+ ctx.lineTo(x + width - radius, y);
387
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
388
+ ctx.lineTo(x + width, y + height - radius);
389
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
390
+ ctx.lineTo(x + radius, y + height);
391
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
392
+ ctx.lineTo(x, y + radius);
393
+ ctx.quadraticCurveTo(x, y, x + radius, y);
394
+ ctx.closePath();
395
+ }
396
+
397
+ /**
398
+ * Vérifie si un point est dans les limites
399
+ * @returns {boolean} True si visible
400
+ */
401
+ isPointInside() {
402
+ return this.isVisible;
403
+ }
404
+ }
405
+
406
+ export default AndroidDatePickerDialog;