@design.estate/dees-domtools 5.0.1 → 5.1.0

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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-domtools',
6
- version: '5.0.1',
6
+ version: '5.1.0',
7
7
  description: 'A package providing tools to simplify complex CSS structures and web development tasks, featuring TypeScript support and integration with various web technologies.'
8
8
  }
@@ -1,28 +1,73 @@
1
1
  import { DomTools } from './domtools.classes.domtools.js';
2
2
  import * as plugins from './domtools.plugins.js';
3
3
 
4
+ /**
5
+ * Which theme the reader asked for: `'system'` follows `prefers-color-scheme`, `'bright'` and
6
+ * `'dark'` hold that theme whatever the system does.
7
+ */
8
+ export type TThemePreference = 'system' | 'bright' | 'dark';
9
+
10
+ const themePreferences: readonly TThemePreference[] = ['system', 'bright', 'dark'];
11
+
12
+ const isThemePreference = (valueArg: unknown): valueArg is TThemePreference => (
13
+ themePreferences.includes(valueArg as TThemePreference)
14
+ );
15
+
4
16
  export class ThemeManager {
17
+ /**
18
+ * The `localStorage` key a picked preference is kept under, per browser and origin. `'system'` is
19
+ * the absence of the key.
20
+ */
21
+ public static readonly preferenceStorageKey = 'dees-domtools:theme-preference';
22
+
5
23
  public domtoolsRef: DomTools;
6
24
 
7
25
  public goBrightBoolean = false;
26
+ /** The reader's preference; the theme follows the system only while it is `'system'`. */
27
+ public preference: TThemePreference = 'system';
8
28
  public preferredColorSchemeMediaMatch = window.matchMedia('(prefers-color-scheme: light)');
9
29
 
10
30
  public themeObservable = new plugins.smartrx.rxjs.ReplaySubject<boolean>(1);
11
31
  private automaticGlobalThemeChangeSubscription: plugins.smartrx.rxjs.Subscription | null = null;
12
32
  private readonly preferredColorSchemeChangeHandler = (eventArg: MediaQueryListEvent) => {
33
+ if (this.preference !== 'system') return;
13
34
  this.goBrightBoolean = eventArg.matches;
14
35
  this.updateAllConnectedElements();
15
36
  };
37
+ /** Another tab of this origin picked a preference; this page follows it. */
38
+ private readonly storageHandler = (eventArg: StorageEvent) => {
39
+ if (eventArg.storageArea !== window.localStorage) return;
40
+ if (eventArg.key !== ThemeManager.preferenceStorageKey && eventArg.key !== null) return;
41
+ // A cleared storage (`key` null) forgets the pick as well.
42
+ const value = eventArg.key === null ? null : eventArg.newValue;
43
+ this.applyPreference(isThemePreference(value) ? value : 'system');
44
+ };
16
45
 
17
46
  constructor(domtoolsRefArg: DomTools) {
18
47
  this.domtoolsRef = domtoolsRefArg;
19
48
 
20
- // lets care
21
- this.goBrightBoolean = this.preferredColorSchemeMediaMatch.matches;
49
+ // The stored preference is read before the first emission, so a page never flashes the system theme.
50
+ this.preference = ThemeManager.storedPreference();
51
+ this.goBrightBoolean = this.resolveBright();
22
52
  this.preferredColorSchemeMediaMatch.addEventListener('change', this.preferredColorSchemeChangeHandler);
53
+ window.addEventListener('storage', this.storageHandler);
23
54
  this.updateAllConnectedElements();
24
55
  }
25
56
 
57
+ /**
58
+ * The preference kept in this browser. A browser that does not let the page read its storage, or a
59
+ * value this version does not know, keeps no preference: the theme follows the system.
60
+ */
61
+ private static storedPreference(): TThemePreference {
62
+ let stored: string | null;
63
+ try {
64
+ stored = window.localStorage.getItem(ThemeManager.preferenceStorageKey);
65
+ } catch {
66
+ return 'system';
67
+ }
68
+ return isThemePreference(stored) ? stored : 'system';
69
+ }
70
+
26
71
  public async enableAutomaticGlobalThemeChange() {
27
72
  await this.domtoolsRef.domReady.promise;
28
73
  if (!this.automaticGlobalThemeChangeSubscription) {
@@ -41,8 +86,43 @@ export class ThemeManager {
41
86
  this.themeObservable.next(this.goBrightBoolean);
42
87
  }
43
88
 
89
+ private resolveBright(): boolean {
90
+ return this.preference === 'system'
91
+ ? this.preferredColorSchemeMediaMatch.matches
92
+ : this.preference === 'bright';
93
+ }
94
+
95
+ private applyPreference(preferenceArg: TThemePreference): void {
96
+ this.preference = preferenceArg;
97
+ const goBright = this.resolveBright();
98
+ if (goBright === this.goBrightBoolean) return;
99
+ this.goBrightBoolean = goBright;
100
+ this.updateAllConnectedElements();
101
+ }
102
+
103
+ /**
104
+ * Picks the theme and keeps the pick in this browser (`'system'` forgets it), so the next page
105
+ * load starts in it. While the preference is `'bright'` or `'dark'` the system's colour scheme no
106
+ * longer moves the theme. Returns whether the pick was kept: false when the browser refuses the
107
+ * page its storage, in which case it holds for this page only. Throws on a value that is no
108
+ * `TThemePreference`.
109
+ */
110
+ public setPreference(preferenceArg: TThemePreference): boolean {
111
+ if (!isThemePreference(preferenceArg)) {
112
+ throw new Error(`Unknown theme preference "${String(preferenceArg)}".`);
113
+ }
114
+ this.applyPreference(preferenceArg);
115
+ try {
116
+ if (preferenceArg === 'system') window.localStorage.removeItem(ThemeManager.preferenceStorageKey);
117
+ else window.localStorage.setItem(ThemeManager.preferenceStorageKey, preferenceArg);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
44
124
  /**
45
- * set the theme of the website to bright
125
+ * set the theme of the website to bright, for now: the preference is unchanged
46
126
  */
47
127
  public goBright() {
48
128
  this.goBrightBoolean = true;
@@ -50,7 +130,7 @@ export class ThemeManager {
50
130
  }
51
131
 
52
132
  /**
53
- * set the theme of the website to dark
133
+ * set the theme of the website to dark, for now: the preference is unchanged
54
134
  */
55
135
  public goDark() {
56
136
  this.goBrightBoolean = false;
@@ -58,7 +138,7 @@ export class ThemeManager {
58
138
  }
59
139
 
60
140
  /**
61
- * simply toggle between bright and dark
141
+ * simply toggle between bright and dark, for now: the preference is unchanged
62
142
  */
63
143
  public toggleDarkBright() {
64
144
  this.goBrightBoolean = !this.goBrightBoolean;
@@ -67,6 +147,7 @@ export class ThemeManager {
67
147
 
68
148
  public dispose() {
69
149
  this.preferredColorSchemeMediaMatch.removeEventListener('change', this.preferredColorSchemeChangeHandler);
150
+ window.removeEventListener('storage', this.storageHandler);
70
151
  this.automaticGlobalThemeChangeSubscription?.unsubscribe();
71
152
  this.automaticGlobalThemeChangeSubscription = null;
72
153
  this.themeObservable.complete();
package/ts/index.ts CHANGED
@@ -6,6 +6,7 @@ import * as css from './domtools.css.js';
6
6
 
7
7
  export { css, breakpoints, elementBasic };
8
8
  export { DomTools, type IDomToolsConstructorOptions } from './domtools.classes.domtools.js';
9
+ export { ThemeManager, type TThemePreference } from './domtools.classes.thememanager.js';
9
10
  export { TypedRequest } from '@api.global/typedrequest';
10
11
  export { type IWebSetupConstructorOptions } from '@push.rocks/websetup';
11
12