fix(mui): stop dark/light oscillation (single data-theme owner) + animated ThemeToggle + white avatars
Removed MuiColorSchemeSync: it called MUI setMode which ALSO wrote data-theme, fighting ThemeContext (the other writer) → theme flip-flop loop. ThemeContext is now the sole owner; MUI's cssVariables CSS (scoped by [data-theme]) applies from the attribute, no JS bridge needed. New animated ThemeToggle kit button (sun/moon crossfade+rotate) replaces the static AppShell toggle. DashAttendanceToday avatars now solid-fill + white initials (matches StatCard/activity badges). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,8 +89,8 @@ export default function DashAttendanceToday({
|
|||||||
height: 36,
|
height: 36,
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
bgcolor: isDefault ? "action.hover" : `${color}.light`,
|
bgcolor: isDefault ? "grey.600" : `${color}.main`,
|
||||||
color: isDefault ? "text.secondary" : `${color}.main`,
|
color: "common.white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{u.initials || "?"}
|
{u.initials || "?"}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Drawer from "@mui/material/Drawer";
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
|
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useTheme as useAppTheme } from "../../context/ThemeContext";
|
import ThemeToggle from "./ThemeToggle";
|
||||||
import { setLogoutAlert } from "../utils/api";
|
import { setLogoutAlert } from "../utils/api";
|
||||||
import SidebarNav from "./SidebarNav";
|
import SidebarNav from "./SidebarNav";
|
||||||
import ShortcutsHelp from "../components/ShortcutsHelp";
|
import ShortcutsHelp from "../components/ShortcutsHelp";
|
||||||
@@ -15,7 +15,6 @@ const DRAWER_WIDTH = 248;
|
|||||||
|
|
||||||
export default function AppShell() {
|
export default function AppShell() {
|
||||||
const { isAuthenticated, loading, user, logout } = useAuth();
|
const { isAuthenticated, loading, user, logout } = useAuth();
|
||||||
const { theme, toggleTheme } = useAppTheme();
|
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [loggingOut, setLoggingOut] = useState(false);
|
const [loggingOut, setLoggingOut] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -145,36 +144,7 @@ export default function AppShell() {
|
|||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box sx={{ flex: 1 }} />
|
<Box sx={{ flex: 1 }} />
|
||||||
<IconButton
|
<ThemeToggle />
|
||||||
onClick={toggleTheme}
|
|
||||||
aria-label={theme === "dark" ? "Světlý režim" : "Tmavý režim"}
|
|
||||||
title={theme === "dark" ? "Světlý režim" : "Tmavý režim"}
|
|
||||||
>
|
|
||||||
{theme === "dark" ? (
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="5" />
|
|
||||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="main" sx={{ flex: 1, px: { xs: 2, md: 3 }, pb: 4 }}>
|
<Box component="main" sx={{ flex: 1, px: { xs: 2, md: 3 }, pb: 4 }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useColorScheme } from "@mui/material/styles";
|
|
||||||
import { useTheme as useAppTheme } from "../../context/ThemeContext";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ThemeContext is the single owner of the `data-theme` attribute. This mirrors
|
|
||||||
* its value into MUI's JS color-scheme state so theme.applyStyles('dark') and
|
|
||||||
* useColorScheme().mode stay in sync with the attribute the CSS selector reads.
|
|
||||||
*/
|
|
||||||
export default function MuiColorSchemeSync() {
|
|
||||||
const { theme } = useAppTheme();
|
|
||||||
const { mode, setMode } = useColorScheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mode !== theme) setMode(theme as "light" | "dark");
|
|
||||||
}, [theme, mode, setMode]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,11 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|||||||
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
||||||
import { cs } from "date-fns/locale";
|
import { cs } from "date-fns/locale";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../theme";
|
||||||
import MuiColorSchemeSync from "./MuiColorSchemeSync";
|
|
||||||
|
|
||||||
export default function MuiProvider({ children }: { children: ReactNode }) {
|
export default function MuiProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme} defaultMode="dark">
|
<ThemeProvider theme={theme} defaultMode="dark">
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={cs}>
|
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={cs}>
|
||||||
<MuiColorSchemeSync />
|
|
||||||
{children}
|
{children}
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
57
src/admin/ui/ThemeToggle.tsx
Normal file
57
src/admin/ui/ThemeToggle.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
|
|
||||||
|
const sun = (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const moon = (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated dark/light theme toggle: the sun/moon icon crossfades + rotates on
|
||||||
|
* switch. Reads ThemeContext (the single owner of the `data-theme` attribute).
|
||||||
|
*/
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
const label = isDark ? "Světlý režim" : "Tmavý režim";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton onClick={toggleTheme} aria-label={label} title={label}>
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
|
<motion.span
|
||||||
|
key={theme}
|
||||||
|
initial={{ rotate: -90, opacity: 0, scale: 0.5 }}
|
||||||
|
animate={{ rotate: 0, opacity: 1, scale: 1 }}
|
||||||
|
exit={{ rotate: 90, opacity: 0, scale: 0.5 }}
|
||||||
|
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||||
|
style={{ display: "inline-flex" }}
|
||||||
|
>
|
||||||
|
{isDark ? moon : sun}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,3 +27,4 @@ export type { StatCardColor } from "./StatCard";
|
|||||||
export { default as FileUpload } from "./FileUpload";
|
export { default as FileUpload } from "./FileUpload";
|
||||||
export { default as ProgressBar } from "./ProgressBar";
|
export { default as ProgressBar } from "./ProgressBar";
|
||||||
export type { ProgressColor } from "./ProgressBar";
|
export type { ProgressColor } from "./ProgressBar";
|
||||||
|
export { default as ThemeToggle } from "./ThemeToggle";
|
||||||
|
|||||||
Reference in New Issue
Block a user