diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4b765e7..c901908 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ConfigProvider, App as AntApp, theme } from 'antd'; import enUS from 'antd/locale/en_US'; @@ -40,6 +40,16 @@ import SharePage from './pages/Share'; const ThemedApp: React.FC = () => { const isDark = useThemeStore((s) => s.isDark); const { locale } = useI18n(); + + // Propagate isDark to so plain CSS rules + // (e.g. `[data-theme='dark'] …` in responsive.css) and any other + // data-theme-aware styles can adapt alongside Ant Design's darkAlgorithm. + useEffect(() => { + if (typeof document === 'undefined') return; + document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); + document.documentElement.classList.toggle('dark', isDark); + }, [isDark]); + return ( { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 39e7204..03c58f4 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -51,6 +51,9 @@ export const SidebarMenu: React.FC<{ onNavigate?: () => void; style?: React.CSSP }) => { const { items, selectedKeys, onMenuClick, onLogout, t } = useSidebarMenuItems(onNavigate); + // Sidebar is rendered with theme="light" on the Sider below; the logout + // divider is therefore a light-mode border regardless of the global theme, + // so a static light color is correct here. return (
@@ -58,7 +61,7 @@ export const SidebarMenu: React.FC<{ onNavigate?: () => void; style?: React.CSSP {!collapsed && 3M-UI}
- , label: t('nav.logout'), onClick: onLogout }]} style={{ borderInlineEnd: 'none', borderTop: '1px solid rgba(5,5,5,0.06)' }} /> + , label: t('nav.logout'), onClick: onLogout }]} style={{ borderInlineEnd: 'none', borderTop: '1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06))' }} />
); }; diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts deleted file mode 100644 index ffe1ec6..0000000 --- a/frontend/src/hooks/useTheme.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useState, useEffect } from 'react'; - -type Theme = 'light' | 'dark'; - -export function useTheme() { - const [theme, setTheme] = useState(() => { - const saved = localStorage.getItem('3m-ui-theme'); - return (saved as Theme) || 'light'; - }); - - useEffect(() => { - localStorage.setItem('3m-ui-theme', theme); - document.documentElement.setAttribute('data-theme', theme); - }, [theme]); - - const toggle = () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); - - return { theme, toggle, isDark: theme === 'dark' }; -} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c305a22..5ec5e85 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Card, Row, Col, Statistic, Button, Space, Tag, Progress, Typography, Grid, message } from 'antd'; +import { Card, Row, Col, Statistic, Button, Space, Tag, Progress, Typography, Grid, message, theme } from 'antd'; import { PlayCircleOutlined, StopOutlined, RedoOutlined } from '@ant-design/icons'; import { fetchDashboard, startMihomo, stopMihomo, restartMihomo } from '../api/system'; import { isCanceledError } from '../api/client'; @@ -29,25 +29,6 @@ type ProcSample = { memory_percent?: number; }; -const muted: React.CSSProperties = { fontSize: 12, color: 'rgba(0,0,0,0.45)', lineHeight: 1.4 }; - -/** Usage level → color. ≥80% high (red), ≥50% medium (orange), else inherit. */ -const HIGH = '#cf1322'; -const MED = '#d46b08'; -const LOW = '#3f8600'; -function usageColor(pct: number): string | undefined { - if (pct >= 80) return HIGH; - if (pct >= 50) return MED; - return undefined; -} - -/** Bar variant: always resolves, so a healthy bar reads green rather than neutral. */ -function usageBarColor(pct: number): string { - if (pct >= 80) return HIGH; - if (pct >= 50) return MED; - return LOW; -} - /** * Process usage, grouped by **process**: Panel | Core. * @@ -71,9 +52,17 @@ const ProcessUsageWall: React.FC<{ memLabel: string; pidLabel: string; }> = ({ panel, core, coreRunning, panelLabel, coreLabel, cpuLabel, memLabel, pidLabel }) => { + const { token } = theme.useToken(); const screens = Grid.useBreakpoint(); const stacked = !screens.sm; // xs: groups stacked; sm+: side by side + /** Usage level → color. ≥80% high (red), ≥50% medium (orange), else inherit. */ + const usageColor = (pct: number): string | undefined => { + if (pct >= 80) return token.colorError; + if (pct >= 50) return token.colorWarning; + return undefined; + }; + const groups = [ { key: 'panel', @@ -117,7 +106,7 @@ const ProcessUsageWall: React.FC<{ }); const detailStyle: React.CSSProperties = { fontSize: 11, - color: 'rgba(0,0,0,0.45)', + color: token.colorTextSecondary, marginTop: 2, lineHeight: 1.3, whiteSpace: 'nowrap', @@ -126,7 +115,7 @@ const ProcessUsageWall: React.FC<{ }; const labelStyle: React.CSSProperties = { fontSize: 12, - color: 'rgba(0,0,0,0.45)', + color: token.colorTextSecondary, marginTop: 4, lineHeight: 1.4, whiteSpace: 'nowrap', @@ -135,10 +124,10 @@ const ProcessUsageWall: React.FC<{ }; const emptyStyle: React.CSSProperties = { ...numStyle(0), - color: 'rgba(0,0,0,0.35)', + color: token.colorTextDisabled, }; const cellStyle: React.CSSProperties = { textAlign: 'center', padding: '10px 4px', minWidth: 0 }; - const border = '1px solid rgba(0,0,0,0.06)'; + const border = `1px solid ${token.colorBorderSecondary}`; return (
{g.name} {g.live && g.pid ? ( - + {pidLabel} {g.pid} ) : null} @@ -214,11 +203,22 @@ const ProcessUsageWall: React.FC<{ const Dashboard: React.FC = () => { const { t } = useI18n(); const isMobile = useIsMobile(); + const { token } = theme.useToken(); const [data, setData] = useState(null); const [busy, setBusy] = useState(false); const cardSize = isMobile ? 'small' as const : 'default' as const; const gutter = isMobile ? ([8, 8] as [number, number]) : ([16, 16] as [number, number]); + // Theme-adaptive muted text style (was a hardcoded rgba(0,0,0,0.45)). + const muted: React.CSSProperties = { fontSize: 12, color: token.colorTextSecondary, lineHeight: 1.4 }; + + /** Bar variant: always resolves, so a healthy bar reads green rather than neutral. */ + const usageBarColor = (pct: number): string => { + if (pct >= 80) return token.colorError; + if (pct >= 50) return token.colorWarning; + return token.colorSuccess; + }; + const load = async (signal?: AbortSignal) => { try { const d = await fetchDashboard(signal); @@ -316,8 +316,8 @@ const Dashboard: React.FC = () => { - - + + @@ -356,7 +356,7 @@ const Dashboard: React.FC = () => { return (
- {m.label} + {m.label} {m.pct}% @@ -366,7 +366,7 @@ const Dashboard: React.FC = () => { showInfo={false} size="small" strokeColor={color} - trailColor="rgba(0,0,0,0.06)" + trailColor={token.colorBorderSecondary} strokeLinecap="butt" /> {m.detail ?
{m.detail}
: null} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 22f1d9c..cf47687 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Card, Form, Input, Button, Typography, message, Space, Dropdown, Steps, Alert } from 'antd'; +import { Card, Form, Input, Button, Typography, message, Space, Dropdown, Steps, Alert, theme } from 'antd'; import { UserOutlined, LockOutlined, @@ -25,7 +25,8 @@ const Login: React.FC = () => { const [form] = Form.useForm(); const totpInputRef = useRef(null); const { t, locale, setLocale } = useI18n(); - const { mode, setMode, isDark } = useThemeStore(); + const { mode, setMode } = useThemeStore(); + const { token } = theme.useToken(); const from = (location.state as { from?: { pathname?: string } } | null)?.from?.pathname || '/'; useEffect(() => { @@ -94,7 +95,7 @@ const Login: React.FC = () => { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - background: isDark ? '#141414' : '#f5f5f5', + background: token.colorBgLayout, padding: '24px 0', }} > diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx index f774f97..650f6cc 100644 --- a/frontend/src/pages/Logs.tsx +++ b/frontend/src/pages/Logs.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Card, List, Tag, Button, Space, Empty, Spin, message } from 'antd'; +import { Card, List, Tag, Button, Space, Empty, Spin, message, theme } from 'antd'; import { ReloadOutlined, ClearOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import client from '../api/client'; @@ -14,6 +14,7 @@ const levelColor: Record = { debug: 'default', info: 'blue', war const Logs: React.FC = () => { const { t } = useI18n(); const isMobile = useIsMobile(); + const { token } = theme.useToken(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); const [autoRefresh, setAutoRefresh] = useState(true); @@ -39,7 +40,7 @@ const Logs: React.FC = () => { {loading && logs.length === 0 ? : logs.length === 0 ? : ( ( - [{dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')}] + [{dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')}] {log.level?.toUpperCase()} {log.payload} diff --git a/frontend/src/styles/responsive.css b/frontend/src/styles/responsive.css index 6f41b25..b35aca9 100644 --- a/frontend/src/styles/responsive.css +++ b/frontend/src/styles/responsive.css @@ -385,6 +385,21 @@ html.dark .mobile-entity-card, border-color: rgba(255, 255, 255, 0.12); } +/* Dark mode: subtle lift for sticky surfaces whose hardcoded light shadow + (rgba(0,0,0,0.04)) is invisible on a dark background. */ +html.dark .app-bottom-nav, +[data-theme='dark'] .app-bottom-nav { + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35); +} + +/* Dark mode: lift the sticky header bar's blur scrim slightly so light text + stays readable over scrolling content. The var() fallback (rgba(0,0,0,0.06)) + is too faint on dark surfaces. */ +html.dark .app-header-bar, +[data-theme='dark'] .app-header-bar { + border-bottom-color: var(--ant-color-border-secondary, rgba(255, 255, 255, 0.08)) !important; +} + /* —— Mobile bottom navigation —— */ .app-bottom-nav {