canvasframework 0.5.18 → 0.5.20
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 +30 -0
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -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 +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -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 +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -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 +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -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/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -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/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- package/dist/canvasframework.js.LICENSE.txt +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classe de base pour tous les composants
|
|
3
|
+
* @class
|
|
4
|
+
* @abstract
|
|
5
|
+
* @property {CanvasFramework} framework - Référence au framework parent
|
|
6
|
+
* @property {number} x - Position X
|
|
7
|
+
* @property {number} y - Position Y
|
|
8
|
+
* @property {number} width - Largeur
|
|
9
|
+
* @property {number} height - Hauteur
|
|
10
|
+
* @property {boolean} visible - Visibilité du composant
|
|
11
|
+
* @property {boolean} pressed - État pressé
|
|
12
|
+
* @property {boolean} hovered - État survolé
|
|
13
|
+
* @property {Function} onClick - Callback au clic
|
|
14
|
+
* @property {Function} onPress - Callback à la pression
|
|
15
|
+
*/
|
|
16
|
+
class Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de Component
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {number} [options.x=0] - Position X
|
|
22
|
+
* @param {number} [options.y=0] - Position Y
|
|
23
|
+
* @param {number} [options.width=100] - Largeur
|
|
24
|
+
* @param {number} [options.height=50] - Hauteur
|
|
25
|
+
* @param {boolean} [options.visible=true] - Visibilité
|
|
26
|
+
* @param {Function} [options.onClick] - Callback au clic
|
|
27
|
+
* @param {Function} [options.onPress] - Callback à la pression
|
|
28
|
+
*/
|
|
29
|
+
constructor(framework, options = {}) {
|
|
30
|
+
this.framework = framework;
|
|
31
|
+
this.x = options.x || 0;
|
|
32
|
+
this.y = options.y || 0;
|
|
33
|
+
this.width = options.width || 100;
|
|
34
|
+
this.height = options.height || 50;
|
|
35
|
+
this.visible = options.visible !== false;
|
|
36
|
+
this.pressed = false;
|
|
37
|
+
this.hovered = false;
|
|
38
|
+
this.onClick = options.onClick;
|
|
39
|
+
this.onPress = options.onPress;
|
|
40
|
+
|
|
41
|
+
// Système dirty simple (optionnel)
|
|
42
|
+
this._dirty = true;
|
|
43
|
+
|
|
44
|
+
// Lifecycle
|
|
45
|
+
this._mounted = false;
|
|
46
|
+
|
|
47
|
+
// Pour détecter les updates
|
|
48
|
+
this._prevProps = { ...options };
|
|
49
|
+
|
|
50
|
+
// Système de listeners
|
|
51
|
+
this._listeners = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ajoute un listener pour un événement
|
|
56
|
+
* @param {string} event - Nom de l'événement
|
|
57
|
+
* @param {Function} handler - Fonction callback
|
|
58
|
+
* @returns {Component} - Pour le chaînage
|
|
59
|
+
*/
|
|
60
|
+
on(event, handler) {
|
|
61
|
+
if (!this._listeners.has(event)) {
|
|
62
|
+
this._listeners.set(event, []);
|
|
63
|
+
}
|
|
64
|
+
this._listeners.get(event).push(handler);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retire un listener
|
|
70
|
+
* @param {string} event - Nom de l'événement
|
|
71
|
+
* @param {Function} handler - Fonction à retirer
|
|
72
|
+
* @returns {Component}
|
|
73
|
+
*/
|
|
74
|
+
off(event, handler) {
|
|
75
|
+
if (!this._listeners.has(event)) return this;
|
|
76
|
+
|
|
77
|
+
const handlers = this._listeners.get(event);
|
|
78
|
+
const index = handlers.indexOf(handler);
|
|
79
|
+
if (index > -1) {
|
|
80
|
+
handlers.splice(index, 1);
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ajoute un listener qui s'exécute une seule fois
|
|
87
|
+
* @param {string} event - Nom de l'événement
|
|
88
|
+
* @param {Function} handler - Fonction callback
|
|
89
|
+
* @returns {Component}
|
|
90
|
+
*/
|
|
91
|
+
once(event, handler) {
|
|
92
|
+
const wrapper = (...args) => {
|
|
93
|
+
handler(...args);
|
|
94
|
+
this.off(event, wrapper);
|
|
95
|
+
};
|
|
96
|
+
return this.on(event, wrapper);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Émet un événement
|
|
101
|
+
* @param {string} event - Nom de l'événement
|
|
102
|
+
* @param {...any} args - Arguments à passer aux handlers
|
|
103
|
+
* @returns {Component}
|
|
104
|
+
*/
|
|
105
|
+
emit(event, ...args) {
|
|
106
|
+
if (!this._listeners.has(event)) return this;
|
|
107
|
+
|
|
108
|
+
const handlers = this._listeners.get(event);
|
|
109
|
+
for (let handler of handlers) {
|
|
110
|
+
try {
|
|
111
|
+
handler(...args);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Error in ${event} handler:`, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retire tous les listeners d'un événement (ou tous)
|
|
121
|
+
* @param {string} [event] - Nom de l'événement (optionnel)
|
|
122
|
+
* @returns {Component}
|
|
123
|
+
*/
|
|
124
|
+
removeAllListeners(event) {
|
|
125
|
+
if (event) {
|
|
126
|
+
this._listeners.delete(event);
|
|
127
|
+
} else {
|
|
128
|
+
this._listeners.clear();
|
|
129
|
+
}
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Retourne le nombre de listeners pour un événement
|
|
135
|
+
* @param {string} event - Nom de l'événement
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
listenerCount(event) {
|
|
139
|
+
return this._listeners.has(event) ? this._listeners.get(event).length : 0;
|
|
140
|
+
}
|
|
141
|
+
/* =======================
|
|
142
|
+
LIFECYCLE HOOKS
|
|
143
|
+
======================= */
|
|
144
|
+
|
|
145
|
+
onMount() {}
|
|
146
|
+
onUnmount() {}
|
|
147
|
+
onUpdate(prevProps) {}
|
|
148
|
+
onResize(width, height) {}
|
|
149
|
+
|
|
150
|
+
/* ======================= */
|
|
151
|
+
|
|
152
|
+
_mount() {
|
|
153
|
+
if (!this._mounted) {
|
|
154
|
+
this._mounted = true;
|
|
155
|
+
this.onMount();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_unmount() {
|
|
160
|
+
if (this._mounted) {
|
|
161
|
+
this.onUnmount();
|
|
162
|
+
this._mounted = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_update(newProps) {
|
|
167
|
+
this.onUpdate(this._prevProps);
|
|
168
|
+
this._prevProps = { ...newProps };
|
|
169
|
+
this.markDirty();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_resize(width, height) {
|
|
173
|
+
this.onResize(width, height);
|
|
174
|
+
this.markDirty();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setProps(newProps = {}) {
|
|
178
|
+
const changed = Object.keys(newProps).some(
|
|
179
|
+
key => this[key] !== newProps[key]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!changed) return;
|
|
183
|
+
|
|
184
|
+
Object.assign(this, newProps);
|
|
185
|
+
this._update(newProps);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
measure(constraints) {
|
|
189
|
+
return {
|
|
190
|
+
width: this.width,
|
|
191
|
+
height: this.height
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Marque le composant pour redessin
|
|
197
|
+
* Appelez cette méthode après avoir modifié une propriété
|
|
198
|
+
*/
|
|
199
|
+
markDirty() {
|
|
200
|
+
this._dirty = true;
|
|
201
|
+
if (this.framework && this.framework.markComponentDirty) {
|
|
202
|
+
this.framework.markComponentDirty(this);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Marque le composant comme propre (appelé automatiquement après draw)
|
|
208
|
+
*/
|
|
209
|
+
markClean() {
|
|
210
|
+
this._dirty = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Vérifie si le composant est dirty
|
|
215
|
+
*/
|
|
216
|
+
isDirty() {
|
|
217
|
+
return this._dirty;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Vérifie si un point est dans les limites du composant
|
|
222
|
+
*/
|
|
223
|
+
isPointInside(x, y) {
|
|
224
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
225
|
+
y >= this.y && y <= this.y + this.height;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Méthode de dessin (à implémenter par les sous-classes)
|
|
230
|
+
*/
|
|
231
|
+
draw(ctx) {
|
|
232
|
+
// À implémenter par les sous-classes
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
export default Component;
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gestionnaire de thèmes indépendant et fiable
|
|
3
|
+
* Gère automatiquement le light/dark mode avec persistance
|
|
4
|
+
*/
|
|
5
|
+
class ThemeManager {
|
|
6
|
+
constructor(framework, options = {}) {
|
|
7
|
+
this.framework = framework;
|
|
8
|
+
|
|
9
|
+
// Thèmes par défaut (peuvent être overridés)
|
|
10
|
+
this.themes = {
|
|
11
|
+
light: options.lightTheme || {
|
|
12
|
+
// Couleurs de base
|
|
13
|
+
background: '#FFFFFF',
|
|
14
|
+
surface: '#F5F5F5',
|
|
15
|
+
surfaceVariant: '#E7E0EC',
|
|
16
|
+
|
|
17
|
+
// Texte
|
|
18
|
+
text: '#1C1B1F',
|
|
19
|
+
textSecondary: '#49454F',
|
|
20
|
+
textDisabled: '#79747E',
|
|
21
|
+
|
|
22
|
+
// Primaire
|
|
23
|
+
primary: '#6750A4',
|
|
24
|
+
onPrimary: '#FFFFFF',
|
|
25
|
+
primaryContainer: '#EADDFF',
|
|
26
|
+
onPrimaryContainer: '#21005D',
|
|
27
|
+
|
|
28
|
+
// Secondaire
|
|
29
|
+
secondary: '#625B71',
|
|
30
|
+
onSecondary: '#FFFFFF',
|
|
31
|
+
secondaryContainer: '#E8DEF8',
|
|
32
|
+
onSecondaryContainer: '#1D192B',
|
|
33
|
+
|
|
34
|
+
// Tertiaire
|
|
35
|
+
tertiary: '#7D5260',
|
|
36
|
+
onTertiary: '#FFFFFF',
|
|
37
|
+
tertiaryContainer: '#FFD8E4',
|
|
38
|
+
onTertiaryContainer: '#31111D',
|
|
39
|
+
|
|
40
|
+
// Erreur
|
|
41
|
+
error: '#B3261E',
|
|
42
|
+
onError: '#FFFFFF',
|
|
43
|
+
errorContainer: '#F9DEDC',
|
|
44
|
+
onErrorContainer: '#410E0B',
|
|
45
|
+
|
|
46
|
+
// Bordures et dividers
|
|
47
|
+
border: '#CAC4D0',
|
|
48
|
+
divider: '#E0E0E0',
|
|
49
|
+
outline: '#79747E',
|
|
50
|
+
outlineVariant: '#CAC4D0',
|
|
51
|
+
|
|
52
|
+
// États
|
|
53
|
+
hover: 'rgba(103, 80, 164, 0.08)',
|
|
54
|
+
pressed: 'rgba(103, 80, 164, 0.12)',
|
|
55
|
+
focus: 'rgba(103, 80, 164, 0.12)',
|
|
56
|
+
disabled: 'rgba(28, 27, 31, 0.12)',
|
|
57
|
+
|
|
58
|
+
// Ombres
|
|
59
|
+
shadow: 'rgba(0, 0, 0, 0.2)',
|
|
60
|
+
elevation1: 'rgba(0, 0, 0, 0.05)',
|
|
61
|
+
elevation2: 'rgba(0, 0, 0, 0.08)',
|
|
62
|
+
elevation3: 'rgba(0, 0, 0, 0.12)',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
dark: options.darkTheme || {
|
|
66
|
+
// Couleurs de base
|
|
67
|
+
background: '#1C1B1F',
|
|
68
|
+
surface: '#2B2930',
|
|
69
|
+
surfaceVariant: '#49454F',
|
|
70
|
+
|
|
71
|
+
// Texte
|
|
72
|
+
text: '#E6E1E5',
|
|
73
|
+
textSecondary: '#CAC4D0',
|
|
74
|
+
textDisabled: '#938F99',
|
|
75
|
+
|
|
76
|
+
// Primaire
|
|
77
|
+
primary: '#D0BCFF',
|
|
78
|
+
onPrimary: '#381E72',
|
|
79
|
+
primaryContainer: '#4F378B',
|
|
80
|
+
onPrimaryContainer: '#EADDFF',
|
|
81
|
+
|
|
82
|
+
// Secondaire
|
|
83
|
+
secondary: '#CCC2DC',
|
|
84
|
+
onSecondary: '#332D41',
|
|
85
|
+
secondaryContainer: '#4A4458',
|
|
86
|
+
onSecondaryContainer: '#E8DEF8',
|
|
87
|
+
|
|
88
|
+
// Tertiaire
|
|
89
|
+
tertiary: '#EFB8C8',
|
|
90
|
+
onTertiary: '#492532',
|
|
91
|
+
tertiaryContainer: '#633B48',
|
|
92
|
+
onTertiaryContainer: '#FFD8E4',
|
|
93
|
+
|
|
94
|
+
// Erreur
|
|
95
|
+
error: '#F2B8B5',
|
|
96
|
+
onError: '#601410',
|
|
97
|
+
errorContainer: '#8C1D18',
|
|
98
|
+
onErrorContainer: '#F9DEDC',
|
|
99
|
+
|
|
100
|
+
// Bordures et dividers
|
|
101
|
+
border: '#938F99',
|
|
102
|
+
divider: '#3D3D3D',
|
|
103
|
+
outline: '#938F99',
|
|
104
|
+
outlineVariant: '#49454F',
|
|
105
|
+
|
|
106
|
+
// États
|
|
107
|
+
hover: 'rgba(208, 188, 255, 0.08)',
|
|
108
|
+
pressed: 'rgba(208, 188, 255, 0.12)',
|
|
109
|
+
focus: 'rgba(208, 188, 255, 0.12)',
|
|
110
|
+
disabled: 'rgba(230, 225, 229, 0.12)',
|
|
111
|
+
|
|
112
|
+
// Ombres
|
|
113
|
+
shadow: 'rgba(0, 0, 0, 0.8)',
|
|
114
|
+
elevation1: 'rgba(0, 0, 0, 0.3)',
|
|
115
|
+
elevation2: 'rgba(0, 0, 0, 0.4)',
|
|
116
|
+
elevation3: 'rgba(0, 0, 0, 0.5)',
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// État actuel
|
|
121
|
+
this.currentMode = null; // 'light', 'dark', ou null (system)
|
|
122
|
+
this.currentTheme = null;
|
|
123
|
+
|
|
124
|
+
// Callbacks
|
|
125
|
+
this.listeners = [];
|
|
126
|
+
|
|
127
|
+
// Storage key
|
|
128
|
+
this.storageKey = options.storageKey || 'app-theme-mode';
|
|
129
|
+
|
|
130
|
+
// Initialisation
|
|
131
|
+
this.init();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialise le ThemeManager
|
|
136
|
+
*/
|
|
137
|
+
init() {
|
|
138
|
+
// 1. Charger la préférence sauvegardée
|
|
139
|
+
const savedMode = this.loadPreference();
|
|
140
|
+
|
|
141
|
+
// 2. Écouter les changements système
|
|
142
|
+
this.setupSystemListener();
|
|
143
|
+
|
|
144
|
+
// 3. Appliquer le thème
|
|
145
|
+
if (savedMode) {
|
|
146
|
+
this.setMode(savedMode, false); // false = ne pas re-sauvegarder
|
|
147
|
+
} else {
|
|
148
|
+
this.setMode('system', false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Charge la préférence depuis le localStorage
|
|
154
|
+
*/
|
|
155
|
+
loadPreference() {
|
|
156
|
+
try {
|
|
157
|
+
const saved = localStorage.getItem(this.storageKey);
|
|
158
|
+
if (saved && ['light', 'dark', 'system'].includes(saved)) {
|
|
159
|
+
return saved;
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.warn('Impossible de charger les préférences de thème:', e);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sauvegarde la préférence dans le localStorage
|
|
169
|
+
*/
|
|
170
|
+
savePreference(mode) {
|
|
171
|
+
try {
|
|
172
|
+
localStorage.setItem(this.storageKey, mode);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn('Impossible de sauvegarder les préférences de thème:', e);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Configure l'écoute des changements système
|
|
180
|
+
*/
|
|
181
|
+
setupSystemListener() {
|
|
182
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
183
|
+
|
|
184
|
+
const handleChange = (e) => {
|
|
185
|
+
// Ne réagir que si on est en mode 'system'
|
|
186
|
+
if (this.currentMode === 'system') {
|
|
187
|
+
this.applySystemTheme();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Méthode moderne
|
|
192
|
+
if (mediaQuery.addEventListener) {
|
|
193
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
194
|
+
} else {
|
|
195
|
+
// Fallback pour anciens navigateurs
|
|
196
|
+
mediaQuery.addListener(handleChange);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Sauvegarder pour cleanup
|
|
200
|
+
this.systemMediaQuery = mediaQuery;
|
|
201
|
+
this.systemChangeHandler = handleChange;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Applique le thème système
|
|
206
|
+
*/
|
|
207
|
+
applySystemTheme() {
|
|
208
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
209
|
+
const theme = prefersDark ? this.themes.dark : this.themes.light;
|
|
210
|
+
this.applyTheme(theme);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Définit le mode de thème
|
|
215
|
+
* @param {'light'|'dark'|'system'} mode
|
|
216
|
+
* @param {boolean} save - Sauvegarder la préférence
|
|
217
|
+
*/
|
|
218
|
+
setMode(mode, save = true) {
|
|
219
|
+
if (!['light', 'dark', 'system'].includes(mode)) {
|
|
220
|
+
console.warn(`Mode invalide: ${mode}. Utilisez 'light', 'dark' ou 'system'.`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.currentMode = mode;
|
|
225
|
+
|
|
226
|
+
// Sauvegarder si demandé
|
|
227
|
+
if (save) {
|
|
228
|
+
this.savePreference(mode);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Appliquer le thème correspondant
|
|
232
|
+
if (mode === 'system') {
|
|
233
|
+
this.applySystemTheme();
|
|
234
|
+
} else {
|
|
235
|
+
const theme = this.themes[mode];
|
|
236
|
+
this.applyTheme(theme);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Applique un thème au framework
|
|
242
|
+
*/
|
|
243
|
+
applyTheme(theme) {
|
|
244
|
+
this.currentTheme = theme;
|
|
245
|
+
|
|
246
|
+
// Mettre à jour le framework
|
|
247
|
+
if (this.framework) {
|
|
248
|
+
this.framework.theme = theme;
|
|
249
|
+
|
|
250
|
+
// Forcer le rendu de tous les composants
|
|
251
|
+
if (this.framework.components) {
|
|
252
|
+
this.framework.components.forEach(comp => {
|
|
253
|
+
if (comp.markDirty) {
|
|
254
|
+
comp.markDirty();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Notifier les listeners
|
|
261
|
+
this.notifyListeners(theme);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Obtient le mode actuel
|
|
266
|
+
*/
|
|
267
|
+
getMode() {
|
|
268
|
+
return this.currentMode;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Obtient le thème actuel
|
|
273
|
+
*/
|
|
274
|
+
getTheme() {
|
|
275
|
+
return this.currentTheme;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Obtient une couleur spécifique
|
|
280
|
+
*/
|
|
281
|
+
getColor(colorName) {
|
|
282
|
+
return this.currentTheme?.[colorName] || '#000000';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Bascule entre light et dark
|
|
287
|
+
*/
|
|
288
|
+
toggle() {
|
|
289
|
+
if (this.currentMode === 'system') {
|
|
290
|
+
// Si en mode system, basculer vers le mode opposé
|
|
291
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
292
|
+
this.setMode(isDark ? 'light' : 'dark');
|
|
293
|
+
} else {
|
|
294
|
+
// Basculer entre light et dark
|
|
295
|
+
this.setMode(this.currentMode === 'light' ? 'dark' : 'light');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Ajoute un listener de changement de thème
|
|
301
|
+
*/
|
|
302
|
+
addListener(callback) {
|
|
303
|
+
if (typeof callback === 'function') {
|
|
304
|
+
this.listeners.push(callback);
|
|
305
|
+
// Appeler immédiatement avec le thème actuel
|
|
306
|
+
callback(this.currentTheme);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Retire un listener
|
|
312
|
+
*/
|
|
313
|
+
removeListener(callback) {
|
|
314
|
+
const index = this.listeners.indexOf(callback);
|
|
315
|
+
if (index > -1) {
|
|
316
|
+
this.listeners.splice(index, 1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Notifie tous les listeners
|
|
322
|
+
*/
|
|
323
|
+
notifyListeners(theme) {
|
|
324
|
+
this.listeners.forEach(callback => {
|
|
325
|
+
try {
|
|
326
|
+
callback(theme);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
console.error('Erreur dans un listener de thème:', e);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Définit un thème personnalisé
|
|
335
|
+
*/
|
|
336
|
+
setCustomTheme(name, theme) {
|
|
337
|
+
this.themes[name] = theme;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Nettoie les ressources
|
|
342
|
+
*/
|
|
343
|
+
destroy() {
|
|
344
|
+
// Retirer l'écoute système
|
|
345
|
+
if (this.systemMediaQuery && this.systemChangeHandler) {
|
|
346
|
+
if (this.systemMediaQuery.removeEventListener) {
|
|
347
|
+
this.systemMediaQuery.removeEventListener('change', this.systemChangeHandler);
|
|
348
|
+
} else {
|
|
349
|
+
this.systemMediaQuery.removeListener(this.systemChangeHandler);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Vider les listeners
|
|
354
|
+
this.listeners = [];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export default ThemeManager;
|