Phase 5 cleanup
This commit is contained in:
parent
5536acd67d
commit
76a516a417
136 changed files with 6322 additions and 1985 deletions
90
packages/web/src/components/ThemeProvider.tsx
Normal file
90
packages/web/src/components/ThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
type Accent = 'sage' | 'cobalt' | 'terracotta' | 'graphite';
|
||||
|
||||
interface AccentTokens {
|
||||
brand: string;
|
||||
deep: string;
|
||||
soft: string;
|
||||
softInk: string;
|
||||
}
|
||||
|
||||
const ACCENTS: Record<Accent, AccentTokens> = {
|
||||
sage: { brand: '#2f6b4a', deep: '#1e4a32', soft: '#e6efe8', softInk: '#1e4a32' },
|
||||
cobalt: { brand: '#2e5aa8', deep: '#1d3d75', soft: '#e4eaf5', softInk: '#1d3d75' },
|
||||
terracotta: { brand: '#b55438', deep: '#7d3825', soft: '#f6e6de', softInk: '#7d3825' },
|
||||
graphite: { brand: '#2c2c28', deep: '#000000', soft: '#e8e6df', softInk: '#2c2c28' },
|
||||
};
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
accent: Accent;
|
||||
setTheme: (t: Theme) => void;
|
||||
setAccent: (a: Accent) => void;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function applyAccent(accent: Accent) {
|
||||
const ac = ACCENTS[accent];
|
||||
const r = document.documentElement.style;
|
||||
r.setProperty('--brand', ac.brand);
|
||||
r.setProperty('--brand-deep', ac.deep);
|
||||
r.setProperty('--brand-soft', ac.soft);
|
||||
r.setProperty('--brand-soft-ink', ac.softInk);
|
||||
r.setProperty('--viz-1', ac.brand);
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>('light');
|
||||
const [accent, setAccentState] = useState<Accent>('sage');
|
||||
|
||||
// Hydrate from localStorage on mount (server renders light/sage defaults).
|
||||
useEffect(() => {
|
||||
const savedTheme = (localStorage.getItem('mt-theme') as Theme | null) ?? 'light';
|
||||
const savedAccent = (localStorage.getItem('mt-accent') as Accent | null) ?? 'sage';
|
||||
setThemeState(savedTheme);
|
||||
setAccentState(savedAccent);
|
||||
}, []);
|
||||
|
||||
// Sync theme to DOM + localStorage.
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('mt-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
// Sync accent to DOM + localStorage.
|
||||
useEffect(() => {
|
||||
applyAccent(accent);
|
||||
localStorage.setItem('mt-accent', accent);
|
||||
}, [accent]);
|
||||
|
||||
function setTheme(t: Theme) {
|
||||
setThemeState(t);
|
||||
}
|
||||
|
||||
function setAccent(a: Accent) {
|
||||
setAccentState(a);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, accent, setTheme, setAccent, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue