101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
'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 {
|
|
light: { brand: string; deep: string; soft: string; softInk: string; brandInk: string };
|
|
dark: { brand: string; deep: string; soft: string; softInk: string; brandInk: string };
|
|
}
|
|
|
|
const ACCENTS: Record<Accent, AccentTokens> = {
|
|
sage: {
|
|
light: { brand: '#10b981', deep: '#059669', soft: 'rgba(16,185,129,0.1)', softInk: '#047857', brandInk: '#ffffff' },
|
|
dark: { brand: '#34d399', deep: '#10b981', soft: 'rgba(52,211,153,0.15)', softInk: '#6ee7b7', brandInk: '#022c22' },
|
|
},
|
|
cobalt: {
|
|
light: { brand: '#3b82f6', deep: '#2563eb', soft: 'rgba(59,130,246,0.1)', softInk: '#1d4ed8', brandInk: '#ffffff' },
|
|
dark: { brand: '#60a5fa', deep: '#3b82f6', soft: 'rgba(96,165,250,0.15)', softInk: '#93c5fd', brandInk: '#172554' },
|
|
},
|
|
terracotta: {
|
|
light: { brand: '#f43f5e', deep: '#e11d48', soft: 'rgba(244,63,94,0.1)', softInk: '#be123c', brandInk: '#ffffff' },
|
|
dark: { brand: '#fb7185', deep: '#f43f5e', soft: 'rgba(251,113,133,0.15)', softInk: '#fda4af', brandInk: '#4c0519' },
|
|
},
|
|
graphite: {
|
|
light: { brand: '#52525b', deep: '#3f3f46', soft: 'rgba(82,82,91,0.1)', softInk: '#27272a', brandInk: '#ffffff' },
|
|
dark: { brand: '#a1a1aa', deep: '#71717a', soft: 'rgba(161,161,170,0.15)', softInk: '#d4d4d8', brandInk: '#18181b' },
|
|
},
|
|
};
|
|
|
|
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, theme: Theme) {
|
|
const ac = ACCENTS[accent][theme];
|
|
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('--brand-ink', ac.brandInk);
|
|
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, theme);
|
|
localStorage.setItem('mt-accent', accent);
|
|
}, [accent, theme]);
|
|
|
|
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>
|
|
);
|
|
}
|