From 177d3f1902dfb54daf1f2612eccfdfc1666636a2 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 07:39:14 +0200 Subject: [PATCH] fix(mui): stop dark/light oscillation (single data-theme owner) + animated ThemeToggle + white avatars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../dashboard/DashAttendanceToday.tsx | 4 +- src/admin/ui/AppShell.tsx | 34 +---------- src/admin/ui/MuiColorSchemeSync.tsx | 19 ------- src/admin/ui/MuiProvider.tsx | 2 - src/admin/ui/ThemeToggle.tsx | 57 +++++++++++++++++++ src/admin/ui/index.ts | 1 + 6 files changed, 62 insertions(+), 55 deletions(-) delete mode 100644 src/admin/ui/MuiColorSchemeSync.tsx create mode 100644 src/admin/ui/ThemeToggle.tsx diff --git a/src/admin/components/dashboard/DashAttendanceToday.tsx b/src/admin/components/dashboard/DashAttendanceToday.tsx index 6dc6c3b..d9766f8 100644 --- a/src/admin/components/dashboard/DashAttendanceToday.tsx +++ b/src/admin/components/dashboard/DashAttendanceToday.tsx @@ -89,8 +89,8 @@ export default function DashAttendanceToday({ height: 36, fontSize: "0.8rem", fontWeight: 700, - bgcolor: isDefault ? "action.hover" : `${color}.light`, - color: isDefault ? "text.secondary" : `${color}.main`, + bgcolor: isDefault ? "grey.600" : `${color}.main`, + color: "common.white", }} > {u.initials || "?"} diff --git a/src/admin/ui/AppShell.tsx b/src/admin/ui/AppShell.tsx index f73590f..a8420bb 100644 --- a/src/admin/ui/AppShell.tsx +++ b/src/admin/ui/AppShell.tsx @@ -6,7 +6,7 @@ import Drawer from "@mui/material/Drawer"; import IconButton from "@mui/material/IconButton"; import ScopedCssBaseline from "@mui/material/ScopedCssBaseline"; import { useAuth } from "../context/AuthContext"; -import { useTheme as useAppTheme } from "../../context/ThemeContext"; +import ThemeToggle from "./ThemeToggle"; import { setLogoutAlert } from "../utils/api"; import SidebarNav from "./SidebarNav"; import ShortcutsHelp from "../components/ShortcutsHelp"; @@ -15,7 +15,6 @@ const DRAWER_WIDTH = 248; export default function AppShell() { const { isAuthenticated, loading, user, logout } = useAuth(); - const { theme, toggleTheme } = useAppTheme(); const [mobileOpen, setMobileOpen] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const location = useLocation(); @@ -145,36 +144,7 @@ export default function AppShell() { - - {theme === "dark" ? ( - - - - ) : ( - - - - - )} - + diff --git a/src/admin/ui/MuiColorSchemeSync.tsx b/src/admin/ui/MuiColorSchemeSync.tsx deleted file mode 100644 index e64b009..0000000 --- a/src/admin/ui/MuiColorSchemeSync.tsx +++ /dev/null @@ -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; -} diff --git a/src/admin/ui/MuiProvider.tsx b/src/admin/ui/MuiProvider.tsx index 03a71b2..2ef339d 100644 --- a/src/admin/ui/MuiProvider.tsx +++ b/src/admin/ui/MuiProvider.tsx @@ -4,13 +4,11 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { cs } from "date-fns/locale"; import { theme } from "../theme"; -import MuiColorSchemeSync from "./MuiColorSchemeSync"; export default function MuiProvider({ children }: { children: ReactNode }) { return ( - {children} diff --git a/src/admin/ui/ThemeToggle.tsx b/src/admin/ui/ThemeToggle.tsx new file mode 100644 index 0000000..2ceb258 --- /dev/null +++ b/src/admin/ui/ThemeToggle.tsx @@ -0,0 +1,57 @@ +import IconButton from "@mui/material/IconButton"; +import { AnimatePresence, motion } from "framer-motion"; +import { useTheme } from "../../context/ThemeContext"; + +const sun = ( + + + + +); + +const moon = ( + + + +); + +/** + * 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 ( + + + + {isDark ? moon : sun} + + + + ); +} diff --git a/src/admin/ui/index.ts b/src/admin/ui/index.ts index 25ca0de..a220d0c 100644 --- a/src/admin/ui/index.ts +++ b/src/admin/ui/index.ts @@ -27,3 +27,4 @@ export type { StatCardColor } from "./StatCard"; export { default as FileUpload } from "./FileUpload"; export { default as ProgressBar } from "./ProgressBar"; export type { ProgressColor } from "./ProgressBar"; +export { default as ThemeToggle } from "./ThemeToggle";