From 4d016cdab58a43c5e9e91f302afd25004af9660f Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 07:45:20 +0200 Subject: [PATCH] feat(mui): premium coordinated dark/light cross-fade (theme-transition class, toggle-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThemeContext adds a .theme-transition class to for ~300ms during a real toggle (skipped on initial mount); a scoped base.css rule applies a uniform 0.3s color/bg/border/fill/stroke/shadow transition to the whole tree only while that class is present — so the entire UI fades together on switch, while normal interactions keep their own transitions. Respects prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/base.css | 27 +++++++++++++++++++++++++++ src/context/ThemeContext.tsx | 22 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/admin/base.css b/src/admin/base.css index 87cfe29..db4bb5b 100644 --- a/src/admin/base.css +++ b/src/admin/base.css @@ -13,6 +13,33 @@ html { overflow-x: hidden; } +/* Coordinated dark/light cross-fade. ThemeContext adds `.theme-transition` to + for ~300ms during a toggle (only on real switches, not initial load), + so the whole UI fades its theme-driven colors together. Normal interactions + keep their own transitions, because this rule applies only while the class + is present. */ +html.theme-transition, +html.theme-transition *, +html.theme-transition *::before, +html.theme-transition *::after { + transition: + background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + fill 0.3s cubic-bezier(0.4, 0, 0.2, 1), + stroke 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +@media (prefers-reduced-motion: reduce) { + html.theme-transition, + html.theme-transition *, + html.theme-transition *::before, + html.theme-transition *::after { + transition: none !important; + } +} + html, body, #root { diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 290c9c7..4a6d4a9 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -3,6 +3,7 @@ import { useContext, useState, useEffect, + useRef, type ReactNode, } from "react"; @@ -20,14 +21,33 @@ export function ThemeProvider({ children }: { children: ReactNode }) { } return "dark"; }); + const firstRun = useRef(true); useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); + const root = document.documentElement; + // Coordinated cross-fade on real toggles only (not the initial mount): + // arm a uniform color transition across the whole tree for the switch + // window, then remove it so normal interactions keep their own transitions. + let timer: number | undefined; + if (!firstRun.current) { + root.classList.add("theme-transition"); + timer = window.setTimeout( + () => root.classList.remove("theme-transition"), + 320, + ); + } + firstRun.current = false; + + root.setAttribute("data-theme", theme); localStorage.setItem("boha-theme", theme); const themeColor = theme === "dark" ? "#12121a" : "#ffffff"; document .querySelector('meta[name="theme-color"]') ?.setAttribute("content", themeColor); + + return () => { + if (timer) clearTimeout(timer); + }; }, [theme]); const toggleTheme = () => {