@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baseworks/ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The baseworks design system: tokens, primitives and the application frame. Plain CSS and React, no utility framework.",
5
5
  "type": "module",
6
6
  "publishConfig": {
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 per browser, so it has to be restored before the
10
- * first paint — `themeInitScript()` in <head>, or the page renders the system's
11
- * answer and then flips to the stored one.
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
- /** Force a form, or hand the decision back to the system. Remembered per browser. */
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 { localStorage.setItem(themeKey(prefix), mode) } catch { /* private mode: the choice just does not persist */ }
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 does not need this at all — with nothing stored,
50
- * `prefers-color-scheme` already decides.
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 t=localStorage.getItem("${themeKey(prefix)}");`
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
  }