Add dark/light mode toggle with adaptive badge colors
Implement theme switching via sun/moon toggle in nav bar. Dark
remains the default; light mode overrides surface, text, border,
accent, and status color tokens. Preference persists in localStorage
and falls back to prefers-color-scheme. An inline script in
index.html prevents flash of wrong theme on load.
Define a Tailwind v4 @custom-variant for light mode and update all
badge components (encounter method, rule, condition) to use
light:bg-{color}-100 / light:text-{color}-700 for readable contrast
on light surfaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
63
frontend/src/hooks/useTheme.ts
Normal file
63
frontend/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
const STORAGE_KEY = 'ant-theme'
|
||||
|
||||
function getSystemTheme(): Theme {
|
||||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
function getStoredTheme(): Theme | null {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored === 'dark' || stored === 'light') return stored
|
||||
return null
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
document.documentElement.style.colorScheme = theme
|
||||
}
|
||||
|
||||
const listeners = new Set<() => void>()
|
||||
let currentTheme: Theme = getStoredTheme() ?? getSystemTheme()
|
||||
|
||||
applyTheme(currentTheme)
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)')
|
||||
mediaQuery.addEventListener('change', () => {
|
||||
if (!getStoredTheme()) {
|
||||
currentTheme = getSystemTheme()
|
||||
applyTheme(currentTheme)
|
||||
for (const listener of listeners) listener()
|
||||
}
|
||||
})
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
function getSnapshot(): Theme {
|
||||
return currentTheme
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const theme = useSyncExternalStore(subscribe, getSnapshot)
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const next: Theme = currentTheme === 'dark' ? 'light' : 'dark'
|
||||
currentTheme = next
|
||||
localStorage.setItem(STORAGE_KEY, next)
|
||||
applyTheme(next)
|
||||
for (const listener of listeners) listener()
|
||||
}, [])
|
||||
|
||||
return { theme, toggle } as const
|
||||
}
|
||||
Reference in New Issue
Block a user