canvasframework 0.7.0 → 0.7.1

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.
@@ -1,347 +1,333 @@
1
1
  /**
2
- * Gestionnaire de thèmes indépendant et fiable
3
- * Gère automatiquement le light/dark mode avec persistance
2
+ * Gestionnaire de thèmes indépendant et fiable.
3
+ * Gère automatiquement le light/dark mode avec persistance localStorage.
4
+ *
5
+ * IMPORTANT : Ce gestionnaire est la seule source de vérité pour le thème.
6
+ * Ne pas dupliquer la logique themeMode / setupSystemThemeListener dans
7
+ * CanvasFramework — déléguer entièrement à cette classe.
4
8
  */
5
9
  class ThemeManager {
6
10
  constructor(framework, options = {}) {
7
11
  this.framework = framework;
8
-
9
- // Thèmes par défaut (peuvent être overridés)
12
+
13
+ // Thèmes par défaut (peuvent être overridés via options)
10
14
  this.themes = {
11
15
  light: options.lightTheme || {
12
16
  // Couleurs de base
13
17
  background: '#FFFFFF',
14
18
  surface: '#F5F5F5',
15
19
  surfaceVariant: '#E7E0EC',
16
-
20
+
17
21
  // Texte
18
22
  text: '#1C1B1F',
19
23
  textSecondary: '#49454F',
20
24
  textDisabled: '#79747E',
21
-
25
+
22
26
  // Primaire
23
27
  primary: '#6750A4',
24
28
  onPrimary: '#FFFFFF',
25
29
  primaryContainer: '#EADDFF',
26
30
  onPrimaryContainer: '#21005D',
27
-
31
+
28
32
  // Secondaire
29
33
  secondary: '#625B71',
30
34
  onSecondary: '#FFFFFF',
31
35
  secondaryContainer: '#E8DEF8',
32
36
  onSecondaryContainer: '#1D192B',
33
-
37
+
34
38
  // Tertiaire
35
39
  tertiary: '#7D5260',
36
40
  onTertiary: '#FFFFFF',
37
41
  tertiaryContainer: '#FFD8E4',
38
42
  onTertiaryContainer: '#31111D',
39
-
43
+
40
44
  // Erreur
41
45
  error: '#B3261E',
42
46
  onError: '#FFFFFF',
43
47
  errorContainer: '#F9DEDC',
44
48
  onErrorContainer: '#410E0B',
45
-
49
+
46
50
  // Bordures et dividers
47
51
  border: '#CAC4D0',
48
52
  divider: '#E0E0E0',
49
53
  outline: '#79747E',
50
54
  outlineVariant: '#CAC4D0',
51
-
55
+
52
56
  // États
53
57
  hover: 'rgba(103, 80, 164, 0.08)',
54
58
  pressed: 'rgba(103, 80, 164, 0.12)',
55
59
  focus: 'rgba(103, 80, 164, 0.12)',
56
60
  disabled: 'rgba(28, 27, 31, 0.12)',
57
-
61
+
58
62
  // Ombres
59
63
  shadow: 'rgba(0, 0, 0, 0.2)',
60
64
  elevation1: 'rgba(0, 0, 0, 0.05)',
61
65
  elevation2: 'rgba(0, 0, 0, 0.08)',
62
66
  elevation3: 'rgba(0, 0, 0, 0.12)',
63
67
  },
64
-
68
+
65
69
  dark: options.darkTheme || {
66
70
  // Couleurs de base
67
71
  background: '#1C1B1F',
68
72
  surface: '#2B2930',
69
73
  surfaceVariant: '#49454F',
70
-
74
+
71
75
  // Texte
72
76
  text: '#E6E1E5',
73
77
  textSecondary: '#CAC4D0',
74
78
  textDisabled: '#938F99',
75
-
79
+
76
80
  // Primaire
77
81
  primary: '#D0BCFF',
78
82
  onPrimary: '#381E72',
79
83
  primaryContainer: '#4F378B',
80
84
  onPrimaryContainer: '#EADDFF',
81
-
85
+
82
86
  // Secondaire
83
87
  secondary: '#CCC2DC',
84
88
  onSecondary: '#332D41',
85
89
  secondaryContainer: '#4A4458',
86
90
  onSecondaryContainer: '#E8DEF8',
87
-
91
+
88
92
  // Tertiaire
89
93
  tertiary: '#EFB8C8',
90
94
  onTertiary: '#492532',
91
95
  tertiaryContainer: '#633B48',
92
96
  onTertiaryContainer: '#FFD8E4',
93
-
97
+
94
98
  // Erreur
95
99
  error: '#F2B8B5',
96
100
  onError: '#601410',
97
101
  errorContainer: '#8C1D18',
98
102
  onErrorContainer: '#F9DEDC',
99
-
103
+
100
104
  // Bordures et dividers
101
105
  border: '#938F99',
102
106
  divider: '#3D3D3D',
103
107
  outline: '#938F99',
104
108
  outlineVariant: '#49454F',
105
-
109
+
106
110
  // États
107
111
  hover: 'rgba(208, 188, 255, 0.08)',
108
112
  pressed: 'rgba(208, 188, 255, 0.12)',
109
113
  focus: 'rgba(208, 188, 255, 0.12)',
110
114
  disabled: 'rgba(230, 225, 229, 0.12)',
111
-
115
+
112
116
  // Ombres
113
117
  shadow: 'rgba(0, 0, 0, 0.8)',
114
118
  elevation1: 'rgba(0, 0, 0, 0.3)',
115
119
  elevation2: 'rgba(0, 0, 0, 0.4)',
116
120
  elevation3: 'rgba(0, 0, 0, 0.5)',
117
- }
121
+ },
118
122
  };
119
-
123
+
120
124
  // État actuel
121
- this.currentMode = null; // 'light', 'dark', ou null (system)
125
+ this.currentMode = null; // 'light' | 'dark' | 'system'
122
126
  this.currentTheme = null;
123
-
124
- // Callbacks
127
+
128
+ // Callbacks enregistrés via addListener()
125
129
  this.listeners = [];
126
-
127
- // Storage key
130
+
131
+ // Clé de persistence localStorage
128
132
  this.storageKey = options.storageKey || 'app-theme-mode';
129
-
130
- // Initialisation
131
- this.init();
133
+
134
+ // Références pour cleanup
135
+ this.systemMediaQuery = null;
136
+ this.systemChangeHandler = null;
137
+
138
+ this._init();
132
139
  }
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
- }
140
+
141
+ // ─────────────────────────────────────────
142
+ // INITIALISATION
143
+ // ─────────────────────────────────────────
144
+
145
+ /** @private */
146
+ _init() {
147
+ const savedMode = this._loadPreference();
148
+ this._setupSystemListener();
149
+ // false = ne pas re-sauvegarder lors de l'init
150
+ this.setMode(savedMode || 'system', false);
150
151
  }
151
-
152
- /**
153
- * Charge la préférence depuis le localStorage
154
- */
155
- loadPreference() {
152
+
153
+ /** @private */
154
+ _loadPreference() {
156
155
  try {
157
156
  const saved = localStorage.getItem(this.storageKey);
158
- if (saved && ['light', 'dark', 'system'].includes(saved)) {
159
- return saved;
160
- }
157
+ if (saved && ['light', 'dark', 'system'].includes(saved)) return saved;
161
158
  } catch (e) {
162
- console.warn('Impossible de charger les préférences de thème:', e);
159
+ console.warn('[ThemeManager] Impossible de charger les préférences :', e);
163
160
  }
164
161
  return null;
165
162
  }
166
-
167
- /**
168
- * Sauvegarde la préférence dans le localStorage
169
- */
170
- savePreference(mode) {
163
+
164
+ /** @private */
165
+ _savePreference(mode) {
171
166
  try {
172
167
  localStorage.setItem(this.storageKey, mode);
173
168
  } catch (e) {
174
- console.warn('Impossible de sauvegarder les préférences de thème:', e);
169
+ console.warn('[ThemeManager] Impossible de sauvegarder les préférences :', e);
175
170
  }
176
171
  }
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
- }
172
+
173
+ /** @private */
174
+ _setupSystemListener() {
175
+ this.systemMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
176
+ this.systemChangeHandler = () => {
177
+ if (this.currentMode === 'system') this._applySystemTheme();
189
178
  };
190
-
191
- // Méthode moderne
192
- if (mediaQuery.addEventListener) {
193
- mediaQuery.addEventListener('change', handleChange);
179
+
180
+ if (this.systemMediaQuery.addEventListener) {
181
+ this.systemMediaQuery.addEventListener('change', this.systemChangeHandler);
194
182
  } else {
195
- // Fallback pour anciens navigateurs
196
- mediaQuery.addListener(handleChange);
183
+ // Fallback anciens navigateurs
184
+ this.systemMediaQuery.addListener(this.systemChangeHandler);
197
185
  }
198
-
199
- // Sauvegarder pour cleanup
200
- this.systemMediaQuery = mediaQuery;
201
- this.systemChangeHandler = handleChange;
202
186
  }
203
-
204
- /**
205
- * Applique le thème système
206
- */
207
- applySystemTheme() {
187
+
188
+ /** @private */
189
+ _applySystemTheme() {
208
190
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
209
- const theme = prefersDark ? this.themes.dark : this.themes.light;
210
- this.applyTheme(theme);
191
+ this._applyTheme(prefersDark ? this.themes.dark : this.themes.light);
211
192
  }
212
-
193
+
194
+ // ─────────────────────────────────────────
195
+ // API PUBLIQUE
196
+ // ─────────────────────────────────────────
197
+
213
198
  /**
214
- * Définit le mode de thème
199
+ * Définit le mode de thème.
215
200
  * @param {'light'|'dark'|'system'} mode
216
- * @param {boolean} save - Sauvegarder la préférence
201
+ * @param {boolean} [save=true] - Persister la préférence
217
202
  */
218
203
  setMode(mode, save = true) {
219
204
  if (!['light', 'dark', 'system'].includes(mode)) {
220
- console.warn(`Mode invalide: ${mode}. Utilisez 'light', 'dark' ou 'system'.`);
205
+ console.warn(`[ThemeManager] Mode invalide : "${mode}". Utiliser 'light', 'dark' ou 'system'.`);
221
206
  return;
222
207
  }
223
-
208
+
224
209
  this.currentMode = mode;
225
-
226
- // Sauvegarder si demandé
227
- if (save) {
228
- this.savePreference(mode);
229
- }
230
-
231
- // Appliquer le thème correspondant
210
+ if (save) this._savePreference(mode);
211
+
232
212
  if (mode === 'system') {
233
- this.applySystemTheme();
213
+ this._applySystemTheme();
234
214
  } else {
235
- const theme = this.themes[mode];
236
- this.applyTheme(theme);
215
+ this._applyTheme(this.themes[mode]);
237
216
  }
238
217
  }
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
- */
218
+
219
+ /** Retourne le mode actuel : 'light', 'dark' ou 'system'. */
267
220
  getMode() {
268
221
  return this.currentMode;
269
222
  }
270
-
223
+
271
224
  /**
272
- * Obtient le thème actuel
225
+ * Retourne le mode effectif résolu ('light' ou 'dark').
226
+ * Utile quand currentMode === 'system'.
227
+ * @returns {'light'|'dark'}
273
228
  */
229
+ getResolvedMode() {
230
+ if (this.currentMode !== 'system') return this.currentMode;
231
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
232
+ }
233
+
234
+ /** Retourne le thème courant (objet de couleurs). */
274
235
  getTheme() {
275
236
  return this.currentTheme;
276
237
  }
277
-
238
+
278
239
  /**
279
- * Obtient une couleur spécifique
240
+ * Retourne une couleur spécifique du thème courant.
241
+ * @param {string} colorName
242
+ * @returns {string}
280
243
  */
281
244
  getColor(colorName) {
282
- return this.currentTheme?.[colorName] || '#000000';
245
+ return this.currentTheme?.[colorName] ?? '#000000';
283
246
  }
284
-
285
- /**
286
- * Bascule entre light et dark
287
- */
247
+
248
+ /** Bascule entre light et dark (ou quitte system). */
288
249
  toggle() {
289
250
  if (this.currentMode === 'system') {
290
- // Si en mode system, basculer vers le mode opposé
291
251
  const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
292
252
  this.setMode(isDark ? 'light' : 'dark');
293
253
  } else {
294
- // Basculer entre light et dark
295
254
  this.setMode(this.currentMode === 'light' ? 'dark' : 'light');
296
255
  }
297
256
  }
298
-
257
+
299
258
  /**
300
- * Ajoute un listener de changement de thème
259
+ * Enregistre un callback appelé à chaque changement de thème.
260
+ * Le callback est appelé immédiatement avec le thème courant,
261
+ * SEULEMENT si celui-ci est déjà initialisé (évite les appels avec null).
262
+ *
263
+ * @param {Function} callback - Reçoit le thème courant en argument
301
264
  */
302
265
  addListener(callback) {
303
- if (typeof callback === 'function') {
304
- this.listeners.push(callback);
305
- // Appeler immédiatement avec le thème actuel
266
+ if (typeof callback !== 'function') return;
267
+ this.listeners.push(callback);
268
+ // Appel immédiat uniquement si le thème est déjà prêt
269
+ if (this.currentTheme !== null) {
306
270
  callback(this.currentTheme);
307
271
  }
308
272
  }
309
-
273
+
310
274
  /**
311
- * Retire un listener
275
+ * Supprime un callback enregistré.
276
+ * @param {Function} callback
312
277
  */
313
278
  removeListener(callback) {
314
279
  const index = this.listeners.indexOf(callback);
315
- if (index > -1) {
316
- this.listeners.splice(index, 1);
317
- }
280
+ if (index > -1) this.listeners.splice(index, 1);
318
281
  }
319
-
282
+
320
283
  /**
321
- * Notifie tous les listeners
284
+ * Enregistre un thème personnalisé sous un nom donné.
285
+ * @param {string} name - Ex : 'high-contrast'
286
+ * @param {Object} theme - Objet de couleurs
322
287
  */
323
- notifyListeners(theme) {
324
- this.listeners.forEach(callback => {
288
+ setCustomTheme(name, theme) {
289
+ this.themes[name] = theme;
290
+ }
291
+
292
+ // ─────────────────────────────────────────
293
+ // INTERNE
294
+ // ─────────────────────────────────────────
295
+
296
+ /** @private */
297
+ _applyTheme(theme) {
298
+ this.currentTheme = theme;
299
+
300
+ if (this.framework) {
301
+ this.framework.theme = theme;
302
+
303
+ // Invalider tous les composants pour forcer un redessin
304
+ if (this.framework.components) {
305
+ for (const comp of this.framework.components) {
306
+ comp.markDirty?.();
307
+ }
308
+ }
309
+ }
310
+
311
+ this._notifyListeners(theme);
312
+ }
313
+
314
+ /** @private */
315
+ _notifyListeners(theme) {
316
+ for (const callback of this.listeners) {
325
317
  try {
326
318
  callback(theme);
327
319
  } catch (e) {
328
- console.error('Erreur dans un listener de thème:', e);
320
+ console.error('[ThemeManager] Erreur dans un listener de thème :', e);
329
321
  }
330
- });
331
- }
332
-
333
- /**
334
- * Définit un thème personnalisé
335
- */
336
- setCustomTheme(name, theme) {
337
- this.themes[name] = theme;
322
+ }
338
323
  }
339
-
340
- /**
341
- * Nettoie les ressources
342
- */
324
+
325
+ // ─────────────────────────────────────────
326
+ // DESTROY
327
+ // ─────────────────────────────────────────
328
+
329
+ /** Nettoie les ressources (event listeners système, callbacks). */
343
330
  destroy() {
344
- // Retirer l'écoute système
345
331
  if (this.systemMediaQuery && this.systemChangeHandler) {
346
332
  if (this.systemMediaQuery.removeEventListener) {
347
333
  this.systemMediaQuery.removeEventListener('change', this.systemChangeHandler);
@@ -349,9 +335,9 @@ class ThemeManager {
349
335
  this.systemMediaQuery.removeListener(this.systemChangeHandler);
350
336
  }
351
337
  }
352
-
353
- // Vider les listeners
354
338
  this.listeners = [];
339
+ this.systemMediaQuery = null;
340
+ this.systemChangeHandler = null;
355
341
  }
356
342
  }
357
343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"