canvasframework 0.4.5 → 0.4.7

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.
@@ -81,7 +81,6 @@ import DevTools from '../utils/DevTools.js';
81
81
  import InspectionOverlay from '../utils/InspectionOverlay.js';
82
82
  import DevToolsConsole from '../utils/DevToolsConsole.js';
83
83
 
84
-
85
84
  // Features
86
85
  import PullToRefresh from '../features/PullToRefresh.js';
87
86
  import Skeleton from '../features/Skeleton.js';
@@ -105,27 +104,8 @@ import FeatureFlags from '../manager/FeatureFlags.js';
105
104
  // WebGL Adapter
106
105
  import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
107
106
  import ui, { createRef } from './UIBuilder.js';
107
+ import ThemeManager from './ThemeManager.js';
108
108
 
109
- // theme
110
- export const lightTheme = {
111
- background: '#FFFFFF',
112
- text: '#000000',
113
- primary: '#6200EE',
114
- secondary: '#03DAC6',
115
- buttonText: '#FFFFFF',
116
- buttonBackground: '#6200EE',
117
- border: '#E0E0E0'
118
- };
119
-
120
- export const darkTheme = {
121
- background: '#121212',
122
- text: '#FFFFFF',
123
- primary: '#BB86FC',
124
- secondary: '#03DAC6',
125
- buttonText: '#000000',
126
- buttonBackground: '#BB86FC',
127
- border: '#333333'
128
- };
129
109
 
130
110
  const FIXED_COMPONENT_TYPES = new Set([
131
111
  AppBar,
@@ -179,9 +159,7 @@ class CanvasFramework {
179
159
 
180
160
  this.platform = this.detectPlatform();
181
161
 
182
- // Thèmes
183
- this.lightTheme = lightTheme;
184
- this.darkTheme = darkTheme;
162
+
185
163
  // État actuel + préférence
186
164
  this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
187
165
  this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
@@ -197,7 +175,6 @@ class CanvasFramework {
197
175
  }
198
176
 
199
177
  this.components = [];
200
- this.theme = lightTheme; // thème par défaut
201
178
  // ✅ AJOUTER ICI :
202
179
  this._cachedMaxScroll = 0;
203
180
  this._maxScrollDirty = true;
@@ -287,30 +264,18 @@ class CanvasFramework {
287
264
  this.enableDevTools();
288
265
  console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
289
266
  }
267
+
268
+ // Initialiser le ThemeManager
269
+ this.themeManager = new ThemeManager(this, {
270
+ lightTheme: options.lightTheme,
271
+ darkTheme: options.darkTheme,
272
+ storageKey: options.themeStorageKey || 'app-theme-mode'
273
+ });
274
+
275
+ // Raccourci pour accéder au thème actuel
276
+ this.theme = this.themeManager.getTheme();
290
277
  }
291
278
 
292
- /**
293
- * Détecte le thème système et applique si mode = 'system'
294
- */
295
- applyThemeFromSystem() {
296
- // ✅ Vérifier que tout est initialisé
297
- if (!this.lightTheme || !this.darkTheme) {
298
- console.warn('Thèmes non initialisés');
299
- return;
300
- }
301
-
302
- if (this.themeMode === 'system') {
303
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
304
- const newTheme = prefersDark ? this.darkTheme : this.lightTheme;
305
- this.setTheme(newTheme);
306
- } else {
307
- // Mode forcé
308
- this.setTheme(
309
- this.themeMode === 'dark' ? this.darkTheme : this.lightTheme
310
- );
311
- }
312
- }
313
-
314
279
  /**
315
280
  * Écoute les changements système (ex: utilisateur bascule dark mode)
316
281
  */
@@ -339,24 +304,34 @@ class CanvasFramework {
339
304
  * @param {'light'|'dark'|'system'} mode - Mode à appliquer
340
305
  * @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
341
306
  */
342
- setThemeMode(mode, save = true) {
343
- if (!['light', 'dark', 'system'].includes(mode)) {
344
- console.warn('Mode invalide, valeurs acceptées: light, dark, system');
345
- return;
346
- }
347
-
348
- this.themeMode = mode;
349
-
350
- if (save && mode !== 'system') {
351
- this.userThemeOverride = mode;
352
- // Sauvegarde (ex: localStorage ou ton SecureStorage)
353
- localStorage.setItem('themeOverride', mode);
354
- } else if (mode === 'system') {
355
- this.userThemeOverride = null;
356
- localStorage.removeItem('themeOverride');
357
- }
358
-
359
- this.applyThemeFromSystem();
307
+ setThemeMode(mode) {
308
+ this.themeManager.setMode(mode);
309
+ this.theme = this.themeManager.getTheme();
310
+ }
311
+
312
+ /**
313
+ * Obtient une couleur du thème
314
+ */
315
+ getColor(colorName) {
316
+ return this.themeManager.getColor(colorName);
317
+ }
318
+
319
+ /**
320
+ * Ajoute un listener de changement de thème
321
+ */
322
+ onThemeChange(callback) {
323
+ this.themeManager.addListener((theme) => {
324
+ this.theme = theme;
325
+ callback(theme);
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Bascule entre light et dark
331
+ */
332
+ toggleTheme() {
333
+ this.themeManager.toggle();
334
+ this.theme = this.themeManager.getTheme();
360
335
  }
361
336
 
362
337
  /**
@@ -687,6 +662,9 @@ class CanvasFramework {
687
662
  beforeEnter: options.beforeEnter,
688
663
  afterEnter: options.afterEnter,
689
664
  beforeLeave: options.beforeLeave,
665
+ afterLeave: options.afterLeave, // ✅ NOUVEAU
666
+ onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
667
+ onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
690
668
  transition: options.transition || 'slide'
691
669
  };
692
670
 
@@ -783,28 +761,55 @@ class CanvasFramework {
783
761
 
784
762
  const { route, params, query, pathname } = match;
785
763
 
786
- // Hook beforeLeave de la route actuelle
764
+ // ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
765
+
766
+ // Hook beforeLeave de la route actuelle (peut bloquer la navigation)
787
767
  const currentRouteData = this.routes.get(this.currentRoute);
788
768
  if (currentRouteData?.beforeLeave) {
789
769
  const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
790
- if (canLeave === false) return;
770
+ if (canLeave === false) {
771
+ console.log('Navigation cancelled by beforeLeave hook');
772
+ return;
773
+ }
774
+ }
775
+
776
+ // ✅ NOUVEAU : Hook onLeave (alias plus intuitif de beforeLeave, mais ne bloque pas)
777
+ if (currentRouteData?.onLeave) {
778
+ await currentRouteData.onLeave(this.currentParams, this.currentQuery);
791
779
  }
792
780
 
793
- // Hook beforeEnter de la nouvelle route
781
+ // ===== LIFECYCLE: AVANT D'ENTRER DANS LA NOUVELLE ROUTE =====
782
+
783
+ // Hook beforeEnter de la nouvelle route (peut bloquer la navigation)
794
784
  if (route.beforeEnter) {
795
785
  const canEnter = await route.beforeEnter(params, query);
796
- if (canEnter === false) return;
786
+ if (canEnter === false) {
787
+ console.log('Navigation cancelled by beforeEnter hook');
788
+ return;
789
+ }
790
+ }
791
+
792
+ // ✅ NOUVEAU : Hook onEnter (appelé juste avant de créer les composants)
793
+ if (route.onEnter) {
794
+ await route.onEnter(params, query);
797
795
  }
798
796
 
799
- // Sauvegarder l'ancienne route pour l'animation
797
+ // ===== SAUVEGARDER L'ÉTAT ACTUEL =====
798
+
799
+ // Sauvegarder l'ancienne route pour l'animation et les hooks
800
800
  const oldComponents = [...this.components];
801
+ const oldRoute = this.currentRoute;
802
+ const oldParams = { ...this.currentParams };
803
+ const oldQuery = { ...this.currentQuery };
801
804
 
802
- // Mettre à jour l'état
805
+ // ===== METTRE À JOUR L'ÉTAT =====
806
+
803
807
  this.currentRoute = pathname;
804
808
  this.currentParams = params;
805
809
  this.currentQuery = query;
806
810
 
807
- // Gérer l'historique
811
+ // ===== GÉRER L'HISTORIQUE =====
812
+
808
813
  if (!replace) {
809
814
  this.historyIndex++;
810
815
  this.history = this.history.slice(0, this.historyIndex);
@@ -819,28 +824,48 @@ class CanvasFramework {
819
824
  } else {
820
825
  this.history[this.historyIndex] = { path, params, query, state };
821
826
  window.history.replaceState(
822
- { route: path, params, query, state },
827
+ { route: path, params, query, state },
823
828
  '',
824
829
  path
825
830
  );
826
831
  }
827
832
 
828
- // Créer les nouveaux composants
833
+ // ===== CRÉER LES NOUVEAUX COMPOSANTS =====
834
+
829
835
  this.components = [];
830
836
  if (typeof route.component === 'function') {
831
837
  route.component(this, params, query);
832
838
  }
833
839
 
834
- // Lancer l'animation de transition
840
+ // ===== LANCER L'ANIMATION DE TRANSITION =====
841
+
835
842
  if (animate && !this.transitionState.isTransitioning) {
836
843
  const transitionType = transition || route.transition || 'slide';
837
844
  this.startTransition(oldComponents, this.components, transitionType, direction);
838
845
  }
839
846
 
840
- // Hook afterEnter
847
+ // ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
848
+
849
+ // Hook afterEnter (appelé immédiatement après la création des composants)
841
850
  if (route.afterEnter) {
842
851
  route.afterEnter(params, query);
843
852
  }
853
+
854
+ // ✅ NOUVEAU : Hook afterLeave de l'ancienne route (après transition complète)
855
+ if (currentRouteData?.afterLeave) {
856
+ // Si animation, attendre la fin de la transition
857
+ if (animate && this.transitionState.isTransitioning) {
858
+ setTimeout(() => {
859
+ currentRouteData.afterLeave(oldParams, oldQuery);
860
+ }, this.transitionState.duration || 300);
861
+ } else {
862
+ // Pas d'animation, appeler immédiatement
863
+ currentRouteData.afterLeave(oldParams, oldQuery);
864
+ }
865
+ }
866
+
867
+ // ✅ OPTIONNEL : Marquer les composants comme "dirty" pour forcer le rendu
868
+ this._maxScrollDirty = true;
844
869
  }
845
870
 
846
871
  /**
@@ -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;
package/index.js CHANGED
@@ -93,7 +93,6 @@ export { default as FirebaseRealtimeDB } from './utils/FirebaseRealtimeDB.js';
93
93
  export { default as PayPalPayment } from './utils/PayPalPayment.js';
94
94
  export { default as StripePayment } from './utils/StripePayment.js';
95
95
 
96
-
97
96
  // Features
98
97
  export { default as PullToRefresh } from './features/PullToRefresh.js';
99
98
  export { default as Skeleton } from './features/Skeleton.js';
@@ -116,7 +115,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
116
115
 
117
116
  // Version du framework
118
117
 
119
- export const VERSION = '0.4.4';
118
+ export const VERSION = '0.4.7';
120
119
 
121
120
 
122
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",