feat(theme): add dark mode support with Light/Dark/Auto modes

This commit is contained in:
2026-03-16 18:19:23 +08:00
parent b62ce11e23
commit a4fe857956
9 changed files with 122 additions and 21 deletions

View File

@@ -25,7 +25,7 @@ import {
Settings as SettingsIcon, Settings as SettingsIcon,
} from "lucide-react"; } from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import zhCN from "antd/locale/zh_CN"; import zhCN from "antd/locale/zh_CN";
import { useDomainStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
import { useUIStore } from "@renderer/stores"; import { useUIStore } from "@renderer/stores";
@@ -151,6 +151,7 @@ const useStyles = createStyles(({ token, css }) => ({
const App: React.FC = () => { const App: React.FC = () => {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const { styles } = useStyles(); const { styles } = useStyles();
const token = useTheme();
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { const {
@@ -219,7 +220,7 @@ const App: React.FC = () => {
<div <div
style={{ display: "flex", alignItems: "center", gap: 8 }} style={{ display: "flex", alignItems: "center", gap: 8 }}
> >
<Cloud size={24} style={{ color: "#1890ff" }} /> <Cloud size={24} style={{ color: token.colorPrimary }} />
<span className={styles.logoText}>Kintone Manager</span> <span className={styles.logoText}>Kintone Manager</span>
</div> </div>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}> <Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
@@ -255,7 +256,7 @@ const App: React.FC = () => {
<div <div
className={styles.resizeHandle} className={styles.resizeHandle}
onMouseDown={handleResizeStart} onMouseDown={handleResizeStart}
style={{ background: isResizing ? "#1890ff" : undefined }} style={{ background: isResizing ? token.colorPrimary : undefined }}
/> />
</> </>
)} )}

View File

@@ -16,7 +16,7 @@ import {
Smartphone, Smartphone,
ArrowLeft, ArrowLeft,
} from "lucide-react"; } from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import { useAppStore, useDomainStore, useSessionStore } from "@renderer/stores"; import { useAppStore, useDomainStore, useSessionStore } from "@renderer/stores";
import { CodeViewer } from "../CodeViewer"; import { CodeViewer } from "../CodeViewer";
import { FileConfigResponse, isFileResource } from "@shared/types/kintone"; import { FileConfigResponse, isFileResource } from "@shared/types/kintone";
@@ -150,6 +150,7 @@ const useStyles = createStyles(({ token, css }) => ({
const AppDetail: React.FC = () => { const AppDetail: React.FC = () => {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const { styles } = useStyles(); const { styles } = useStyles();
const token = useTheme();
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } = const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } =
useAppStore(); useAppStore();
@@ -455,7 +456,7 @@ const AppDetail: React.FC = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.title}> <div className={styles.title}>
<LayoutGrid size={24} style={{ color: "#1890ff" }} /> <LayoutGrid size={24} style={{ color: token.colorPrimary }} />
<h3 className={styles.appName}>{currentApp.name}</h3> <h3 className={styles.appName}>{currentApp.name}</h3>
<Tag>ID: {currentApp.appId}</Tag> <Tag>ID: {currentApp.appId}</Tag>
</div> </div>

View File

@@ -7,7 +7,7 @@ import React from "react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { Tooltip, Tag } from "@lobehub/ui"; import { Tooltip, Tag } from "@lobehub/ui";
import { Pin, LayoutGrid } from "lucide-react"; import { Pin, LayoutGrid } from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import type { AppDetail } from "@shared/types/kintone"; import type { AppDetail } from "@shared/types/kintone";
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
@@ -107,6 +107,7 @@ const AppListItem: React.FC<AppListItemProps> = ({
t, t,
}) => { }) => {
const { styles } = useStyles(); const { styles } = useStyles();
const token = useTheme();
const [isHovered, setIsHovered] = React.useState(false); const [isHovered, setIsHovered] = React.useState(false);
// Pin overlay is visible when: // Pin overlay is visible when:
@@ -144,7 +145,7 @@ const AppListItem: React.FC<AppListItemProps> = ({
</div> </div>
{/* App icon - hidden when pin overlay is visible */} {/* App icon - hidden when pin overlay is visible */}
{!showPinOverlay && ( {!showPinOverlay && (
<LayoutGrid size={16} style={{ color: "#1890ff" }} /> <LayoutGrid size={16} style={{ color: token.colorPrimary }} />
)} )}
</div> </div>
<Tooltip title={app.name}> <Tooltip title={app.name}>

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { Spin, Alert, Space, message } from "antd"; import { Spin, Alert, Space, message } from "antd";
import { Button, Empty } from "@lobehub/ui"; import { Button, Empty } from "@lobehub/ui";
import { Copy, Download } from "lucide-react"; import { Copy, Download } from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles, useTheme } from "antd-style";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { css } from "@codemirror/lang-css"; import { css } from "@codemirror/lang-css";
@@ -63,6 +63,8 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
}) => { }) => {
const { t } = useTranslation("file"); const { t } = useTranslation("file");
const { styles } = useStyles(); const { styles } = useStyles();
const { appearance } = useTheme();
const themeMode = appearance === "dark" ? "dark" : "light" as const;
const { currentDomain } = useDomainStore(); const { currentDomain } = useDomainStore();
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
@@ -249,7 +251,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
height="100%" height="100%"
extensions={[language === "js" ? javascript() : css()]} extensions={[language === "js" ? javascript() : css()]}
editable={false} editable={false}
theme="light" theme={themeMode}
basicSetup={{ basicSetup={{
lineNumbers: true, lineNumbers: true,
highlightActiveLineGutter: false, highlightActiveLineGutter: false,

View File

@@ -7,6 +7,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Input, InputPassword, Modal } from "@lobehub/ui"; import { Button, Input, InputPassword, Modal } from "@lobehub/ui";
import { Form } from "antd"; import { Form } from "antd";
import { createStyles, useTheme } from "antd-style";
import { useDomainStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc"; import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc";
import { CheckCircle2, XCircle } from "lucide-react"; import { CheckCircle2, XCircle } from "lucide-react";
@@ -36,6 +37,7 @@ type CreateErrorType = "connection" | "duplicate" | "unknown" | null;
const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => { const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
const { t } = useTranslation("domain"); const { t } = useTranslation("domain");
const token = useTheme();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { domains, createDomain, updateDomainById } = useDomainStore(); const { domains, createDomain, updateDomainById } = useDomainStore();
@@ -294,9 +296,9 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
const getIcon = () => { const getIcon = () => {
if (!testResult) return undefined; if (!testResult) return undefined;
return testResult.success ? ( return testResult.success ? (
<CheckCircle2 size={16} color="#52c41a" /> <CheckCircle2 size={16} color={token.colorSuccess} />
) : ( ) : (
<XCircle size={16} color="#ff4d4f" /> <XCircle size={16} color={token.colorError} />
); );
}; };
@@ -379,7 +381,7 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
{/* Right side: Test button and Create/Update button */} {/* Right side: Test button and Create/Update button */}
<div style={{ display: "flex", alignItems: "center", gap: 8 }}> <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{createError && ( {createError && (
<span style={{ color: "#ff4d4f", fontSize: 14 }}> <span style={{ color: token.colorError, fontSize: 14 }}>
{createError.message} {createError.message}
</span> </span>
)} )}

View File

@@ -1,16 +1,17 @@
/** /**
* Settings Component * Settings Component
* Application settings modal with language switcher * Application settings modal with language switcher and theme selector
*/ */
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Typography, Radio, Divider, Space, message } from "antd"; import { Typography, Radio, Divider, Space, message } from "antd";
import { Globe, Info, ExternalLink } from "lucide-react"; import { Globe, Info, ExternalLink, Sun, Moon, Monitor } from "lucide-react";
import { Button } from "@lobehub/ui"; import { Button } from "@lobehub/ui";
import { createStyles } from "antd-style"; import { createStyles } from "antd-style";
import { useLocaleStore } from "@renderer/stores/localeStore"; import { useLocaleStore } from "@renderer/stores/localeStore";
import { useThemeStore, type ThemeMode } from "@renderer/stores/themeStore";
import { LOCALES, type LocaleCode } from "@shared/types/locale"; import { LOCALES, type LocaleCode } from "@shared/types/locale";
const { Title } = Typography; const { Title } = Typography;
@@ -37,11 +38,18 @@ const useStyles = createStyles(({ token, css }) => ({
.ant-radio-button-wrapper { .ant-radio-button-wrapper {
border-radius: ${token.borderRadius}px; border-radius: ${token.borderRadius}px;
border: 1px solid ${token.colorBorder}; border: 1px solid ${token.colorBorder};
color: ${token.colorText};
} }
.ant-radio-button-wrapper::before { .ant-radio-button-wrapper::before {
display: none; display: none;
} }
.ant-radio-button-wrapper-checked {
background: ${token.colorPrimary} !important;
border-color: ${token.colorPrimary} !important;
color: ${token.colorBgContainer} !important;
}
`, `,
versionInfo: css` versionInfo: css`
display: flex; display: flex;
@@ -67,10 +75,21 @@ const useStyles = createStyles(({ token, css }) => ({
`, `,
})); }));
const THEME_OPTIONS: {
value: ThemeMode;
labelKey: string;
icon: React.ReactNode;
}[] = [
{ value: "light", labelKey: "lightTheme", icon: <Sun size={14} /> },
{ value: "dark", labelKey: "darkTheme", icon: <Moon size={14} /> },
{ value: "auto", labelKey: "systemTheme", icon: <Monitor size={14} /> },
];
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { styles } = useStyles(); const { styles } = useStyles();
const { locale, setLocale } = useLocaleStore(); const { locale, setLocale } = useLocaleStore();
const { themeMode, setThemeMode } = useThemeStore();
const i18n = useTranslation().i18n; const i18n = useTranslation().i18n;
// Version and update state // Version and update state
@@ -99,6 +118,10 @@ const Settings: React.FC = () => {
await window.api.setLocale({ locale: newLocale }); await window.api.setLocale({ locale: newLocale });
}; };
const handleThemeChange = (newTheme: ThemeMode) => {
setThemeMode(newTheme);
};
const handleCheckUpdate = async () => { const handleCheckUpdate = async () => {
setCheckingUpdate(true); setCheckingUpdate(true);
setUpdateInfo(null); setUpdateInfo(null);
@@ -153,6 +176,34 @@ const Settings: React.FC = () => {
<Divider /> <Divider />
{/* Theme Section */}
<div className={styles.section}>
<div className={styles.sectionTitle}>
<Monitor size={16} />
<Title level={5} style={{ margin: 0 }}>
{t("theme")}
</Title>
</div>
<Radio.Group
value={themeMode}
onChange={(e) => handleThemeChange(e.target.value)}
optionType="button"
buttonStyle="solid"
className={styles.radioGroup}
>
{THEME_OPTIONS.map((option) => (
<Radio.Button key={option.value} value={option.value}>
<Space size={4}>
{option.icon}
{t(option.labelKey)}
</Space>
</Radio.Button>
))}
</Radio.Group>
</div>
<Divider />
{/* About Section */} {/* About Section */}
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionTitle}> <div className={styles.sectionTitle}>

View File

@@ -5,8 +5,21 @@ import { ThemeProvider } from "@lobehub/ui";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import i18n from "./i18n"; import i18n from "./i18n";
import App from "./App"; import App from "./App";
import { useThemeStore } from "./stores/themeStore";
import "./index.css"; import "./index.css";
const ThemeApp: React.FC = () => {
const { themeMode, setThemeMode } = useThemeStore();
return (
<ThemeProvider themeMode={themeMode} onThemeModeChange={setThemeMode}>
<AntdApp>
<App />
</AntdApp>
</ThemeProvider>
);
};
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<ConfigProvider <ConfigProvider
@@ -18,11 +31,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
}} }}
> >
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<ThemeProvider> <ThemeApp />
<AntdApp>
<App />
</AntdApp>
</ThemeProvider>
</I18nextProvider> </I18nextProvider>
</ConfigProvider> </ConfigProvider>
</React.StrictMode>, </React.StrictMode>,

View File

@@ -10,3 +10,5 @@ export { useVersionStore } from "./versionStore";
export { useUIStore } from "./uiStore"; export { useUIStore } from "./uiStore";
export { useSessionStore } from "./sessionStore"; export { useSessionStore } from "./sessionStore";
export type { ViewMode, SelectedFile } from "./sessionStore"; export type { ViewMode, SelectedFile } from "./sessionStore";
export { useThemeStore } from "./themeStore";
export type { ThemeMode } from "./themeStore";

View File

@@ -0,0 +1,32 @@
/**
* Theme Store
* Manages theme mode preference with localStorage persistence
*/
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type ThemeMode = "light" | "dark" | "auto";
interface ThemeState {
// State
themeMode: ThemeMode;
// Actions
setThemeMode: (mode: ThemeMode) => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
// Initial state
themeMode: "auto",
// Actions
setThemeMode: (mode) => set({ themeMode: mode }),
}),
{
name: "theme-storage",
},
),
);