@uniweb/kit 0.9.30 → 0.9.32
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 +2 -2
- package/src/hooks/useThemeData.js +61 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"fuse.js": "^7.0.0",
|
|
40
40
|
"shiki": "^3.0.0",
|
|
41
41
|
"tailwind-merge": "^3.6.0",
|
|
42
|
-
"@uniweb/core": "0.7.
|
|
42
|
+
"@uniweb/core": "0.7.25",
|
|
43
43
|
"@uniweb/scene": "0.1.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -22,40 +22,31 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { useState, useEffect, useCallback } from 'react'
|
|
25
|
-
import { getUniweb, Theme } from '@uniweb/core'
|
|
25
|
+
import { getUniweb, Theme, hasDarkScheme } from '@uniweb/core'
|
|
26
26
|
|
|
27
27
|
// Storage key for appearance preference
|
|
28
28
|
const APPEARANCE_STORAGE_KEY = 'uniweb-appearance'
|
|
29
29
|
|
|
30
|
-
// CSS
|
|
30
|
+
// CSS classes for the two schemes
|
|
31
31
|
const DARK_SCHEME_CLASS = 'scheme-dark'
|
|
32
|
+
const LIGHT_SCHEME_CLASS = 'scheme-light'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* Read the scheme currently applied to the document.
|
|
36
|
+
*
|
|
37
|
+
* The runtime resolves and applies the boot scheme before React renders (see
|
|
38
|
+
* @uniweb/runtime's appearance.js), so this hook reads that result rather than
|
|
39
|
+
* re-deriving it from config + localStorage. One resolver, one boot-time writer.
|
|
40
|
+
*
|
|
41
|
+
* Deriving it independently here is what used to break: the runtime and this
|
|
42
|
+
* hook each applied their own answer, and the runtime's — which ignored the
|
|
43
|
+
* stored preference — won, because React runs child effects before parent ones.
|
|
35
44
|
*
|
|
36
|
-
* @param {Object} appearance - Theme appearance configuration
|
|
37
45
|
* @returns {string} 'light' or 'dark'
|
|
38
46
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const stored = localStorage.getItem(APPEARANCE_STORAGE_KEY)
|
|
43
|
-
if (stored === 'light' || stored === 'dark') {
|
|
44
|
-
return stored
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check system preference if respectSystemPreference is enabled
|
|
49
|
-
if (appearance?.respectSystemPreference && typeof window !== 'undefined') {
|
|
50
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
51
|
-
if (prefersDark && appearance.schemes?.includes('dark')) {
|
|
52
|
-
return 'dark'
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Fall back to default
|
|
57
|
-
const defaultScheme = appearance?.default || 'light'
|
|
58
|
-
return defaultScheme === 'system' ? 'light' : defaultScheme
|
|
47
|
+
function readAppliedScheme() {
|
|
48
|
+
if (typeof document === 'undefined') return 'light'
|
|
49
|
+
return document.documentElement.classList.contains(DARK_SCHEME_CLASS) ? 'dark' : 'light'
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
/**
|
|
@@ -150,17 +141,25 @@ export function useAppearance() {
|
|
|
150
141
|
const theme = useThemeData()
|
|
151
142
|
const appearance = theme?.getAppearance() || { default: 'light', allowToggle: false }
|
|
152
143
|
|
|
153
|
-
const [scheme, setSchemeState] = useState(
|
|
144
|
+
const [scheme, setSchemeState] = useState(readAppliedScheme)
|
|
154
145
|
|
|
155
|
-
// Apply scheme to document
|
|
146
|
+
// Apply scheme to document.
|
|
147
|
+
//
|
|
148
|
+
// Sets an explicit class both ways rather than relying on the absence of one:
|
|
149
|
+
// `default: 'system'` themes generate a `@media (prefers-color-scheme: dark)`
|
|
150
|
+
// block scoped to `:root:not(.scheme-light)`, so forcing light on a dark OS
|
|
151
|
+
// needs `scheme-light` present — removing `scheme-dark` alone would leave the
|
|
152
|
+
// media query still applying dark tokens.
|
|
156
153
|
const applyScheme = useCallback((newScheme) => {
|
|
157
|
-
if (typeof document
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
if (typeof document === 'undefined') return
|
|
155
|
+
|
|
156
|
+
const root = document.documentElement
|
|
157
|
+
if (newScheme === 'dark') {
|
|
158
|
+
root.classList.add(DARK_SCHEME_CLASS)
|
|
159
|
+
root.classList.remove(LIGHT_SCHEME_CLASS)
|
|
160
|
+
} else {
|
|
161
|
+
root.classList.add(LIGHT_SCHEME_CLASS)
|
|
162
|
+
root.classList.remove(DARK_SCHEME_CLASS)
|
|
164
163
|
}
|
|
165
164
|
}, [])
|
|
166
165
|
|
|
@@ -186,10 +185,9 @@ export function useAppearance() {
|
|
|
186
185
|
setScheme(newScheme)
|
|
187
186
|
}, [scheme, setScheme])
|
|
188
187
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
188
|
+
// No mount effect applying the initial scheme: the runtime already applied it
|
|
189
|
+
// before React rendered, and `scheme` was read back from that. Re-applying
|
|
190
|
+
// here is what made this hook a second writer.
|
|
193
191
|
|
|
194
192
|
// Listen for system preference changes
|
|
195
193
|
useEffect(() => {
|
|
@@ -200,19 +198,37 @@ export function useAppearance() {
|
|
|
200
198
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
201
199
|
|
|
202
200
|
const handleChange = (e) => {
|
|
203
|
-
// Only auto-switch if
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
201
|
+
// Only auto-switch if the visitor hasn't manually set a preference
|
|
202
|
+
let stored = null
|
|
203
|
+
try {
|
|
204
|
+
stored = localStorage.getItem(APPEARANCE_STORAGE_KEY)
|
|
205
|
+
} catch {
|
|
206
|
+
// Safari private mode throws on access — treat as "no preference"
|
|
210
207
|
}
|
|
208
|
+
if (stored) return
|
|
209
|
+
|
|
210
|
+
const newScheme = e.matches ? 'dark' : 'light'
|
|
211
|
+
// Light is always reachable; only going dark needs the site to support it.
|
|
212
|
+
// Uses the same @uniweb/core predicate as the runtime's boot resolver, so
|
|
213
|
+
// this mid-session path and first-visit agree on when dark is allowed.
|
|
214
|
+
if (newScheme === 'dark' && !hasDarkScheme(appearance)) return
|
|
215
|
+
|
|
216
|
+
// Track the OS without persisting. Going through setScheme() would write
|
|
217
|
+
// to localStorage, which reads back as a manual preference and stops this
|
|
218
|
+
// listener from ever following the OS again.
|
|
219
|
+
setSchemeState(newScheme)
|
|
220
|
+
applyScheme(newScheme)
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
mediaQuery.addEventListener('change', handleChange)
|
|
214
224
|
return () => mediaQuery.removeEventListener('change', handleChange)
|
|
215
|
-
}, [
|
|
225
|
+
}, [
|
|
226
|
+
appearance.respectSystemPreference,
|
|
227
|
+
appearance.allowToggle,
|
|
228
|
+
appearance.default,
|
|
229
|
+
appearance.schemes,
|
|
230
|
+
applyScheme,
|
|
231
|
+
])
|
|
216
232
|
|
|
217
233
|
return {
|
|
218
234
|
scheme,
|