feat: dark/light theming system + design tokens (OMC design system port)
- 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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
'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 }
|
||||
}
|
||||
Reference in New Issue
Block a user