@kolkrabbi/kol-framework 0.3.5 → 0.4.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.
- package/package.json +1 -1
- package/src/ThemeToggle.jsx +55 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "KOL app shell — sidenav, layout, theme toggle, footer, heroes + brand color layer. SideNav takes nav data as props so the tree stays app-local.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/ThemeToggle.jsx
CHANGED
|
@@ -3,9 +3,24 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
3
3
|
|
|
4
4
|
const STORAGE_KEY = 'kol-theme'
|
|
5
5
|
|
|
6
|
+
/* An explicit choice is an app-set data-theme attribute or a saved user
|
|
7
|
+
* toggle. Absent both, the DS is light-first and follows the system
|
|
8
|
+
* (prefers-color-scheme) — matching the theme CSS, which only darkens via
|
|
9
|
+
* explicit [data-theme="dark"] or the media query. */
|
|
10
|
+
function getExplicitTheme() {
|
|
11
|
+
const attr = document.documentElement.dataset.theme
|
|
12
|
+
if (attr === 'light' || attr === 'dark') return attr
|
|
13
|
+
try {
|
|
14
|
+
const saved = localStorage.getItem(STORAGE_KEY)
|
|
15
|
+
if (saved === 'light' || saved === 'dark') return saved
|
|
16
|
+
} catch { /* storage blocked */ }
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
function getInitialTheme() {
|
|
7
|
-
if (typeof document === 'undefined') return '
|
|
8
|
-
return
|
|
21
|
+
if (typeof document === 'undefined') return 'light'
|
|
22
|
+
return getExplicitTheme()
|
|
23
|
+
?? (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light')
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
/**
|
|
@@ -22,22 +37,50 @@ function getInitialTheme() {
|
|
|
22
37
|
* bg, no hover bg). For sidenav rows where the row should read
|
|
23
38
|
* as plain text + icon, not a button.
|
|
24
39
|
*
|
|
25
|
-
* Self-contained
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
40
|
+
* Self-contained but never imposing: initial state is the app-set
|
|
41
|
+
* `data-theme` attribute, else the user's saved toggle (`kol-theme`), else
|
|
42
|
+
* the system preference. `data-theme` + localStorage are written ONLY on a
|
|
43
|
+
* user toggle — mounting the component leaves the DOM untouched, so an
|
|
44
|
+
* undecided page keeps following prefers-color-scheme via the theme CSS.
|
|
45
|
+
* The icon-swap animation slides the `mode-toggle-01` / `mode-toggle-02`
|
|
46
|
+
* pair (v1 set) horizontally — two different halves of the split circle,
|
|
47
|
+
* so the slide is a visible flip.
|
|
29
48
|
*/
|
|
30
49
|
export default function ThemeToggle({ variant = 'icon', className = '' }) {
|
|
31
50
|
const [theme, setTheme] = useState(getInitialTheme)
|
|
32
51
|
|
|
33
52
|
useEffect(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
/* Re-stamp a previously saved user choice so persistence works in apps
|
|
54
|
+
* with no theme boot script — but only when the app itself hasn't set
|
|
55
|
+
* data-theme (an app-set attribute always wins over storage). */
|
|
56
|
+
const attr = document.documentElement.dataset.theme
|
|
57
|
+
let saved = null
|
|
58
|
+
try { saved = localStorage.getItem(STORAGE_KEY) } catch { /* storage blocked */ }
|
|
59
|
+
if (attr !== 'light' && attr !== 'dark') {
|
|
60
|
+
if (saved === 'light' || saved === 'dark') {
|
|
61
|
+
document.documentElement.dataset.theme = saved
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
/* No explicit choice anywhere — CSS follows the system on its own;
|
|
65
|
+
* this listener only keeps the toggle's icon in sync with it. */
|
|
66
|
+
const mq = window.matchMedia?.('(prefers-color-scheme: dark)')
|
|
67
|
+
if (!mq?.addEventListener) return
|
|
68
|
+
const onChange = (e) => {
|
|
69
|
+
try { if (localStorage.getItem(STORAGE_KEY)) return } catch { /* storage blocked */ }
|
|
70
|
+
setTheme(e.matches ? 'dark' : 'light')
|
|
71
|
+
}
|
|
72
|
+
mq.addEventListener('change', onChange)
|
|
73
|
+
return () => mq.removeEventListener('change', onChange)
|
|
74
|
+
}
|
|
75
|
+
}, [])
|
|
37
76
|
|
|
38
77
|
const isDark = theme === 'dark'
|
|
39
78
|
const next = isDark ? 'light' : 'dark'
|
|
40
|
-
const handleToggle = () =>
|
|
79
|
+
const handleToggle = () => {
|
|
80
|
+
setTheme(next)
|
|
81
|
+
document.documentElement.dataset.theme = next
|
|
82
|
+
try { localStorage.setItem(STORAGE_KEY, next) } catch { /* storage blocked */ }
|
|
83
|
+
}
|
|
41
84
|
|
|
42
85
|
const iconSwap = (size) => (
|
|
43
86
|
<span
|
|
@@ -49,8 +92,8 @@ export default function ThemeToggle({ variant = 'icon', className = '' }) {
|
|
|
49
92
|
className="flex transition-transform duration-500 ease-in-out"
|
|
50
93
|
style={{ width: size * 2, transform: isDark ? 'translateX(0)' : `translateX(-${size}px)` }}
|
|
51
94
|
>
|
|
52
|
-
<Icon name="
|
|
53
|
-
<Icon name="
|
|
95
|
+
<Icon name="mode-toggle-01" size={size} />
|
|
96
|
+
<Icon name="mode-toggle-02" size={size} />
|
|
54
97
|
</span>
|
|
55
98
|
</span>
|
|
56
99
|
)
|