43cb7e7469
- Restructure design tokens into :root/.dark raw vars mapped via @theme inline - Add light/dark/system theming: useTheme hook + FOUC guard + ThemeToggle - Keep iamcavalli palette (primary #1A463C, accent #DEF168) and Geist fonts - Derive brand-consistent dark palette (verde-nero bg, lightened primary) - Add scrollbar utilities (.no-scrollbar/.thin-scrollbar) - Install tailwindcss-animate, register via @plugin (Tailwind v4) - Mount ThemeToggle in admin sidebar footer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
/**
|
|
* App theme preference. 'system' follows the OS (prefers-color-scheme) and is
|
|
* the default; 'light' / 'dark' are explicit overrides. The resolved theme is
|
|
* applied by toggling the `dark` class on <html> (Tailwind darkMode via
|
|
* @custom-variant in globals.css).
|
|
*
|
|
* The same localStorage key is read by the inline FOUC guard in layout.tsx so
|
|
* the correct class is set before React mounts — keep THEME_STORAGE_KEY in sync there.
|
|
*/
|
|
export type ThemePref = 'system' | 'light' | 'dark'
|
|
|
|
export const THEME_STORAGE_KEY = 'iamcavalli:theme'
|
|
|
|
function systemPrefersDark(): boolean {
|
|
return (
|
|
typeof window !== 'undefined' &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
)
|
|
}
|
|
|
|
export function readThemePref(): ThemePref {
|
|
if (typeof window === 'undefined') return 'system'
|
|
const v = window.localStorage.getItem(THEME_STORAGE_KEY)
|
|
return v === 'light' || v === 'dark' ? v : 'system'
|
|
}
|
|
|
|
export function resolveTheme(pref: ThemePref): 'light' | 'dark' {
|
|
return pref === 'system' ? (systemPrefersDark() ? 'dark' : 'light') : pref
|
|
}
|
|
|
|
export function applyTheme(pref: ThemePref): void {
|
|
if (typeof document === 'undefined') return
|
|
document.documentElement.classList.toggle('dark', resolveTheme(pref) === 'dark')
|
|
}
|
|
|
|
/**
|
|
* Theme state hook. Initialises to 'system' on both server and first client
|
|
* render (avoids hydration mismatch — the FOUC guard already set the <html>
|
|
* class), then reconciles from localStorage after mount. Applies the resolved
|
|
* theme on change and, while in 'system' mode, re-applies live when the OS
|
|
* theme flips. Returns the raw preference, the resolved light/dark value, and a
|
|
* setter that persists (clearing the key for 'system' so the OS stays
|
|
* authoritative).
|
|
*/
|
|
export function useTheme() {
|
|
const [pref, setPref] = useState<ThemePref>('system')
|
|
|
|
useEffect(() => {
|
|
setPref(readThemePref())
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
applyTheme(pref)
|
|
if (pref !== 'system') return
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
const onChange = () => applyTheme('system')
|
|
mq.addEventListener('change', onChange)
|
|
return () => mq.removeEventListener('change', onChange)
|
|
}, [pref])
|
|
|
|
function setTheme(next: ThemePref) {
|
|
if (next === 'system') window.localStorage.removeItem(THEME_STORAGE_KEY)
|
|
else window.localStorage.setItem(THEME_STORAGE_KEY, next)
|
|
setPref(next)
|
|
}
|
|
|
|
return { pref, resolved: resolveTheme(pref), setTheme }
|
|
}
|