🌓 feat(ui): add auto theme mode, refactor ThemeToggle, optimize header theme handling

- Feature: Introduce 'auto' theme mode
  - Detect system preference via matchMedia('(prefers-color-scheme: dark)')
  - Add useActualTheme context to expose the effective theme ('light'|'dark')
  - Persist selected mode in localStorage ('theme-mode') with 'auto' as default
  - Apply/remove `dark` class on <html> and sync `theme-mode` on <body>
  - Broadcast effective theme to iframes

- UI: Redesign ThemeToggle with Dropdown items and custom highlight
  - Replace non-existent IconMonitor with IconRefresh
  - Use Dropdown.Menu + Dropdown.Item with built-in icon prop
  - Selected state uses custom background highlight; hover state preserved
  - Remove checkmark; selection relies on background styling
  - Current button icon reflects selected mode

- Performance: reduce re-renders and unnecessary effects
  - Memoize theme options and current button icon (useMemo)
  - Simplify handleThemeToggle to accept only explicit modes ('light'|'dark'|'auto')
  - Minimize useEffect dependencies; remove unrelated deps

- Header: streamline useHeaderBar
  - Use useActualTheme for iframe theme messaging
  - Remove unused statusDispatch
  - Remove isNewYear from theme effect dependencies

- Home: send effective theme (useActualTheme) to external content iframes

- i18n: add/enhance theme-related copy in locales (en/zh)

- Chore: minor code cleanup and consistency
  - Improve readability and maintainability
  - Lint clean; no functional regressions
This commit is contained in:
t0ng7u
2025-08-23 03:02:35 +08:00
parent 08add538a0
commit 61ae19ac82
7 changed files with 173 additions and 47 deletions

View File

@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Button, Dropdown } from '@douyinfe/semi-ui';
import { IconLanguage } from '@douyinfe/semi-icons';
import { Languages } from 'lucide-react';
import { CN, GB } from 'country-flag-icons/react/3x2';
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
@@ -46,7 +46,7 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
}
>
<Button
icon={<IconLanguage className="text-lg" />}
icon={<Languages size={18} />}
aria-label={t('切换语言')}
theme="borderless"
type="tertiary"

View File

@@ -19,11 +19,11 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Button, Badge } from '@douyinfe/semi-ui';
import { IconBell } from '@douyinfe/semi-icons';
import { Bell } from 'lucide-react';
const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
const buttonProps = {
icon: <IconBell className="text-lg" />,
icon: <Bell size={18} />,
'aria-label': t('系统公告'),
onClick: onNoticeOpen,
theme: "borderless",

View File

@@ -17,20 +17,88 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { IconSun, IconMoon } from '@douyinfe/semi-icons';
import React, { useMemo } from 'react';
import { Button, Dropdown } from '@douyinfe/semi-ui';
import { Sun, Moon, Monitor } from 'lucide-react';
import { useActualTheme } from '../../../context/Theme';
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
const actualTheme = useActualTheme();
const themeOptions = useMemo(() => ([
{
key: 'light',
icon: <Sun size={18} />,
buttonIcon: <Sun size={18} />,
label: t('浅色模式'),
description: t('始终使用浅色主题')
},
{
key: 'dark',
icon: <Moon size={18} />,
buttonIcon: <Moon size={18} />,
label: t('深色模式'),
description: t('始终使用深色主题')
},
{
key: 'auto',
icon: <Monitor size={18} />,
buttonIcon: <Monitor size={18} />,
label: t('自动模式'),
description: t('跟随系统主题设置')
}
]), [t]);
const getItemClassName = (isSelected) =>
isSelected
? '!bg-semi-color-primary-light-default !font-semibold'
: 'hover:!bg-semi-color-fill-1';
const currentButtonIcon = useMemo(() => {
const currentOption = themeOptions.find(option => option.key === theme);
return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
}, [theme, themeOptions]);
return (
<Button
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
aria-label={t('切换主题')}
onClick={onThemeToggle}
theme="borderless"
type="tertiary"
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
/>
<Dropdown
position="bottomRight"
render={
<Dropdown.Menu>
{themeOptions.map((option) => (
<Dropdown.Item
key={option.key}
icon={option.icon}
onClick={() => onThemeToggle(option.key)}
className={getItemClassName(theme === option.key)}
>
<div className="flex flex-col">
<span>{option.label}</span>
<span className="text-xs text-semi-color-text-2">
{option.description}
</span>
</div>
</Dropdown.Item>
))}
{theme === 'auto' && (
<>
<Dropdown.Divider />
<div className="px-3 py-2 text-xs text-semi-color-text-2">
{t('当前跟随系统')}{actualTheme === 'dark' ? t('深色') : t('浅色')}
</div>
</>
)}
</Dropdown.Menu>
}
>
<Button
icon={currentButtonIcon}
aria-label={t('切换主题')}
theme="borderless"
type="tertiary"
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1"
/>
</Dropdown>
);
};