From 310caa676b786adfb7a134cf2161be77b8f2ab2f Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 7 Mar 2026 17:01:16 +0800 Subject: [PATCH] fix(layout): restore auto theme option and prevent mobile theme picker overflow --- src/components/layout/MainLayout.tsx | 153 +++++++++++++++++++++++++-- src/styles/layout.scss | 105 ++++++++++++++++++ 2 files changed, 248 insertions(+), 10 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index be9ce9f..c62d283 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -35,6 +35,7 @@ import { versionApi } from '@/services/api'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; import { isSupportedLanguage } from '@/utils/language'; +import type { Theme } from '@/types'; const sidebarIcons: Record = { dashboard: , @@ -151,6 +152,39 @@ const headerIcons = { ), }; +const THEME_CARDS: Array<{ + key: Theme; + labelKey: string; + colors: { bg: string; card: string; border: string; text: string; textMuted: string }; +}> = [ + { + key: 'auto', + labelKey: 'theme.auto', + colors: { + bg: 'linear-gradient(135deg, #faf9f5 0 50%, #151412 50% 100%)', + card: 'linear-gradient(135deg, #f0eee8 0 50%, #1d1b18 50% 100%)', + border: '#bdb6ae', + text: '#2d2a26', + textMuted: 'linear-gradient(135deg, #a29c95 0 50%, #9c958d 50% 100%)', + }, + }, + { + key: 'white', + labelKey: 'theme.white', + colors: { bg: '#ffffff', card: '#ffffff', border: '#e5e5e5', text: '#2d2a26', textMuted: '#a29c95' }, + }, + { + key: 'light', + labelKey: 'theme.light', + colors: { bg: '#faf9f5', card: '#f0eee8', border: '#e3e1db', text: '#2d2a26', textMuted: '#a29c95' }, + }, + { + key: 'dark', + labelKey: 'theme.dark', + colors: { bg: '#151412', card: '#1d1b18', border: '#3a3530', text: '#f6f4f1', textMuted: '#9c958d' }, + }, +]; + const parseVersionSegments = (version?: string | null) => { if (!version) return null; const cleaned = version.trim().replace(/^v/i, ''); @@ -192,7 +226,7 @@ export function MainLayout() { const clearCache = useConfigStore((state) => state.clearCache); const theme = useThemeStore((state) => state.theme); - const cycleTheme = useThemeStore((state) => state.cycleTheme); + const setTheme = useThemeStore((state) => state.setTheme); const language = useLanguageStore((state) => state.language); const setLanguage = useLanguageStore((state) => state.setLanguage); @@ -200,9 +234,11 @@ export function MainLayout() { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [checkingVersion, setCheckingVersion] = useState(false); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); + const [themeMenuOpen, setThemeMenuOpen] = useState(false); const [brandExpanded, setBrandExpanded] = useState(true); const contentRef = useRef(null); const languageMenuRef = useRef(null); + const themeMenuRef = useRef(null); const brandCollapseTimer = useRef | null>(null); const headerRef = useRef(null); @@ -310,6 +346,32 @@ export function MainLayout() { }; }, [languageMenuOpen]); + useEffect(() => { + if (!themeMenuOpen) { + return; + } + + const handlePointerDown = (event: MouseEvent) => { + if (!themeMenuRef.current?.contains(event.target as Node)) { + setThemeMenuOpen(false); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setThemeMenuOpen(false); + } + }; + + document.addEventListener('mousedown', handlePointerDown); + document.addEventListener('keydown', handleEscape); + + return () => { + document.removeEventListener('mousedown', handlePointerDown); + document.removeEventListener('keydown', handleEscape); + }; + }, [themeMenuOpen]); + const handleBrandClick = useCallback(() => { if (!brandExpanded) { setBrandExpanded(true); @@ -325,8 +387,22 @@ export function MainLayout() { const toggleLanguageMenu = useCallback(() => { setLanguageMenuOpen((prev) => !prev); + setThemeMenuOpen(false); }, []); + const toggleThemeMenu = useCallback(() => { + setThemeMenuOpen((prev) => !prev); + setLanguageMenuOpen(false); + }, []); + + const handleThemeSelect = useCallback( + (nextTheme: Theme) => { + setTheme(nextTheme); + setThemeMenuOpen(false); + }, + [setTheme] + ); + const handleLanguageSelect = useCallback( (nextLanguage: string) => { if (!isSupportedLanguage(nextLanguage)) { @@ -572,15 +648,72 @@ export function MainLayout() { )} - +
+ + {themeMenuOpen && ( +
+ {THEME_CARDS.map((tc) => ( + + ))} +
+ )} +
diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 2b05a9e..4548f90 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -251,6 +251,111 @@ } } + .theme-menu { + position: relative; + display: inline-flex; + align-items: center; + + .theme-menu-popover { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: $z-dropdown; + padding: $spacing-sm $spacing-sm $spacing-xs; + display: flex; + gap: $spacing-xs; + width: max-content; + max-width: calc(100vw - 16px); + } + + .theme-card { + border: 2px solid transparent; + border-radius: $radius-md; + background: transparent; + cursor: pointer; + padding: 6px 6px 4px; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + transition: border-color $transition-fast, background-color $transition-fast; + + &:hover { + background: var(--bg-secondary); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba($primary-color, 0.22); + } + + &.active { + border-color: var(--primary-color); + } + } + + .theme-card-preview { + width: 72px; + height: 52px; + border-radius: $radius-sm; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .theme-card-header { + height: 10px; + flex-shrink: 0; + } + + .theme-card-body { + flex: 1; + display: flex; + min-height: 0; + } + + .theme-card-sidebar { + width: 16px; + flex-shrink: 0; + } + + .theme-card-content { + flex: 1; + padding: 5px 8px; + display: flex; + flex-direction: column; + gap: 4px; + justify-content: center; + } + + .theme-card-line { + height: 3px; + border-radius: 1px; + + &.short { + width: 60%; + } + } + + .theme-card-label { + font-size: 11px; + color: var(--text-primary); + font-weight: 500; + white-space: nowrap; + } + + @media (max-width: $breakpoint-mobile) { + .theme-menu-popover { + right: auto; + left: 50%; + transform: translateX(-50%); + display: grid; + grid-template-columns: repeat(2, max-content); + justify-content: center; + } + } + } + svg { display: block; }