Migrates Tailwind v3 -> v4 (CSS-first, theme tokens for brand, workflow states and charts) and adds 23 shadcn/ui components on Radix. - lib/workflow.ts is now the single source of truth for state name, colour, icon and available actions; "Financial Review" is the canonical served name, with "Finance Review" kept as an alias so those rows stop falling through to the unstyled fallback and dropping out of every count - record view: TanStack Table with server-side sort/paginate, company column with initials avatar, page totals, density and a URL-synced query state - dashboard: one cached analytics fetch shared by tiles and charts, donut + aging + vendor spend, click-to-filter - detail view: rebuilt against the live detailview payload — restores the PO number and payment terms that the old key heuristics silently dropped, parses match_summary into a 3-way-match checklist, and separates the department's and reviewer's own words from AI-generated content - form modal on Radix Dialog (focus trap, scroll lock, escape handling)
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
import { createContext, useContext, useEffect, useState } from "react";
|
|
|
|
type Theme = "light" | "dark";
|
|
|
|
interface ThemeContextValue {
|
|
theme: Theme;
|
|
toggleTheme: () => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue>({
|
|
theme: "light",
|
|
toggleTheme: () => {},
|
|
});
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const [theme, setTheme] = useState<Theme>(() => {
|
|
const stored = localStorage.getItem("p2p_theme");
|
|
if (stored === "dark" || stored === "light") return stored;
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
});
|
|
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
root.classList.toggle("dark", theme === "dark");
|
|
localStorage.setItem("p2p_theme", theme);
|
|
}, [theme]);
|
|
|
|
// The colour-transition is opt-in per switch rather than a standing global
|
|
// rule, so ordinary hovers and mounts don't animate. Skipped entirely when
|
|
// the user prefers reduced motion.
|
|
const toggleTheme = () => {
|
|
const root = document.documentElement;
|
|
const animate = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (animate) {
|
|
root.classList.add("theming");
|
|
window.setTimeout(() => root.classList.remove("theming"), 220);
|
|
}
|
|
setTheme((t) => (t === "light" ? "dark" : "light"));
|
|
};
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useTheme = () => useContext(ThemeContext);
|