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.
- package/README.md +554 -0
- package/components/Accordion.js +252 -0
- package/components/AndroidDatePickerDialog.js +398 -0
- package/components/AppBar.js +225 -0
- package/components/Avatar.js +202 -0
- package/components/BottomNavigationBar.js +205 -0
- package/components/BottomSheet.js +374 -0
- package/components/Button.js +225 -0
- package/components/Card.js +193 -0
- package/components/Checkbox.js +180 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +143 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +257 -0
- package/components/Dialog.js +367 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +261 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/IOSDatePickerWheel.js +268 -0
- package/components/ImageCarousel.js +193 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +309 -0
- package/components/List.js +94 -0
- package/components/ListItem.js +223 -0
- package/components/Modal.js +364 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/ProgressBar.js +88 -0
- package/components/RadioButton.js +142 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +202 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/Snackbar.js +243 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +179 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +125 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +331 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +1271 -0
- package/core/CanvasWork.js +32 -0
- package/core/Component.js +153 -0
- package/core/LogicWorker.js +25 -0
- package/core/WebGLCanvasAdapter.js +1369 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +84 -0
- package/features/Stack.js +21 -0
- package/index.js +101 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +28 -0
- package/utils/AnimationEngine.js +428 -0
- package/utils/DataStore.js +403 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- 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;
|