@baseworks/ui 0.2.0 → 0.2.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.
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/theme.ts +53 -10
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -56,6 +56,6 @@ export { useResource } from './use-resource'
|
|
|
56
56
|
export type { Resource, ResourceOptions } from './use-resource'
|
|
57
57
|
export { age, until, untilEpoch, money, amount } from './format'
|
|
58
58
|
|
|
59
|
-
export { applyTheme, currentTheme, resolvedTheme, themeInitScript } from './theme'
|
|
59
|
+
export { applyTheme, currentTheme, resolvedTheme, storedTheme, themeInitScript, THEME_COOKIE } from './theme'
|
|
60
60
|
export type { ThemeMode } from './theme'
|
|
61
61
|
export { ThemeToggle } from './theme-toggle'
|
package/src/theme.ts
CHANGED
|
@@ -6,15 +6,45 @@
|
|
|
6
6
|
* <html> forces a form in both directions, and `ThemeToggle` is the control
|
|
7
7
|
* that sets it.
|
|
8
8
|
*
|
|
9
|
-
* The choice is remembered
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The choice is remembered in the realm's theme cookie, `bw_theme` — the one
|
|
10
|
+
* the pages a service draws read and write too (the sign-in page, the
|
|
11
|
+
* organization pages, the account pages; `@baseworks/core/pages`). So a theme
|
|
12
|
+
* chosen on `/account` is the theme the product opens in, and the other way
|
|
13
|
+
* round. It has to be restored before the first paint: `themeInitScript()` in
|
|
14
|
+
* <head>, or the page renders the system's answer and then flips.
|
|
15
|
+
*
|
|
16
|
+
* Until 0.2.1 the choice lived in localStorage under `<prefix>.theme`, where a
|
|
17
|
+
* server-drawn page could never see it. That key is still read when there is
|
|
18
|
+
* no cookie, so nobody's choice is lost, and still written, so an older copy
|
|
19
|
+
* of this package on the same origin keeps agreeing.
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
22
|
export type ThemeMode = 'system' | 'light' | 'dark'
|
|
15
23
|
|
|
16
24
|
const themeKey = (prefix: string) => `${prefix}.theme`
|
|
17
25
|
|
|
26
|
+
/** The realm's theme cookie. Its values are `light`, `dark` and `auto`. */
|
|
27
|
+
export const THEME_COOKIE = 'bw_theme'
|
|
28
|
+
|
|
29
|
+
/** What the cookie says, or undefined when it is absent or says something else. */
|
|
30
|
+
function cookieTheme(): ThemeMode | undefined {
|
|
31
|
+
if (typeof document === 'undefined') return undefined
|
|
32
|
+
const m = new RegExp(`(?:^|;\\s*)${THEME_COOKIE}=([^;]*)`).exec(document.cookie)
|
|
33
|
+
const v = m?.[1]
|
|
34
|
+
return v === 'light' || v === 'dark' ? v : v === 'auto' ? 'system' : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The remembered choice: the cookie, else what an earlier version stored, else the system. */
|
|
38
|
+
export function storedTheme(prefix = 'bw'): ThemeMode {
|
|
39
|
+
const fromCookie = cookieTheme()
|
|
40
|
+
if (fromCookie) return fromCookie
|
|
41
|
+
try {
|
|
42
|
+
const legacy = localStorage.getItem(themeKey(prefix))
|
|
43
|
+
if (legacy === 'light' || legacy === 'dark') return legacy
|
|
44
|
+
} catch { /* no storage: nothing was stored */ }
|
|
45
|
+
return 'system'
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
/** What the document is showing now — not what is stored. */
|
|
19
49
|
export function currentTheme(): ThemeMode {
|
|
20
50
|
if (typeof document === 'undefined') return 'system'
|
|
@@ -34,22 +64,35 @@ export function resolvedTheme(): 'light' | 'dark' {
|
|
|
34
64
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
35
65
|
}
|
|
36
66
|
|
|
37
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Force a form, or hand the decision back to the system. Remembered in the
|
|
69
|
+
* realm's cookie, so the pages a service draws agree; `prefix` only names the
|
|
70
|
+
* older localStorage key.
|
|
71
|
+
*/
|
|
38
72
|
export function applyTheme(mode: ThemeMode, prefix = 'bw'): void {
|
|
39
73
|
if (mode === 'system') delete document.documentElement.dataset['theme']
|
|
40
74
|
else document.documentElement.dataset['theme'] = mode
|
|
41
|
-
try {
|
|
75
|
+
try {
|
|
76
|
+
const secure = location.protocol === 'https:' ? '; Secure' : ''
|
|
77
|
+
document.cookie = `${THEME_COOKIE}=${mode === 'system' ? 'auto' : mode}; Path=/; Max-Age=31536000; SameSite=Lax${secure}`
|
|
78
|
+
} catch { /* cookies refused: the choice just does not persist */ }
|
|
79
|
+
try { localStorage.setItem(themeKey(prefix), mode) } catch { /* private mode: the same */ }
|
|
42
80
|
}
|
|
43
81
|
|
|
44
82
|
/**
|
|
45
83
|
* The snippet for <head>, for a product that uses `applyTheme`. It is
|
|
46
|
-
* deliberately tiny and wrapped in try/catch: a browser that refuses
|
|
47
|
-
* localStorage must still render the page.
|
|
84
|
+
* deliberately tiny and wrapped in try/catch: a browser that refuses cookies
|
|
85
|
+
* or localStorage must still render the page.
|
|
86
|
+
*
|
|
87
|
+
* It reads the realm's cookie first and the older localStorage key only when
|
|
88
|
+
* there is no cookie. `auto` in the cookie means "the system decides", which
|
|
89
|
+
* is what the page does with nothing set.
|
|
48
90
|
*
|
|
49
|
-
* A product with no toggle
|
|
50
|
-
*
|
|
91
|
+
* A product with no toggle should still include this: its pages then open in
|
|
92
|
+
* the theme the person chose on the account page.
|
|
51
93
|
*/
|
|
52
94
|
export function themeInitScript(prefix = 'bw'): string {
|
|
53
|
-
return `try{var
|
|
95
|
+
return `try{var m=/(?:^|;\\s*)${THEME_COOKIE}=([^;]*)/.exec(document.cookie),t=m?m[1]:null;`
|
|
96
|
+
+ `if(t===null)t=localStorage.getItem("${themeKey(prefix)}");`
|
|
54
97
|
+ `if(t==="dark"||t==="light")document.documentElement.dataset.theme=t}catch(e){}`
|
|
55
98
|
}
|