diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7cdc3ae..80efaae 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -25,7 +25,7 @@ import { Settings as SettingsIcon, } from "lucide-react"; -import { createStyles } from "antd-style"; +import { createStyles, useTheme } from "antd-style"; import zhCN from "antd/locale/zh_CN"; import { useDomainStore } from "@renderer/stores"; import { useUIStore } from "@renderer/stores"; @@ -151,6 +151,7 @@ const useStyles = createStyles(({ token, css }) => ({ const App: React.FC = () => { const { t } = useTranslation("common"); const { styles } = useStyles(); + const token = useTheme(); const { currentDomain } = useDomainStore(); const { @@ -219,7 +220,7 @@ const App: React.FC = () => {
- + Kintone Manager
@@ -255,7 +256,7 @@ const App: React.FC = () => {
)} diff --git a/src/renderer/src/components/AppDetail/AppDetail.tsx b/src/renderer/src/components/AppDetail/AppDetail.tsx index 8433fed..8a7e1be 100644 --- a/src/renderer/src/components/AppDetail/AppDetail.tsx +++ b/src/renderer/src/components/AppDetail/AppDetail.tsx @@ -16,7 +16,7 @@ import { Smartphone, ArrowLeft, } from "lucide-react"; -import { createStyles } from "antd-style"; +import { createStyles, useTheme } from "antd-style"; import { useAppStore, useDomainStore, useSessionStore } from "@renderer/stores"; import { CodeViewer } from "../CodeViewer"; import { FileConfigResponse, isFileResource } from "@shared/types/kintone"; @@ -150,6 +150,7 @@ const useStyles = createStyles(({ token, css }) => ({ const AppDetail: React.FC = () => { const { t } = useTranslation("app"); const { styles } = useStyles(); + const token = useTheme(); const { currentDomain } = useDomainStore(); const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } = useAppStore(); @@ -455,7 +456,7 @@ const AppDetail: React.FC = () => {
- +

{currentApp.name}

ID: {currentApp.appId}
diff --git a/src/renderer/src/components/AppList/AppListItem.tsx b/src/renderer/src/components/AppList/AppListItem.tsx index 8825463..0c6ca9c 100644 --- a/src/renderer/src/components/AppList/AppListItem.tsx +++ b/src/renderer/src/components/AppList/AppListItem.tsx @@ -7,7 +7,7 @@ import React from "react"; import { motion } from "motion/react"; import { Tooltip, Tag } from "@lobehub/ui"; import { Pin, LayoutGrid } from "lucide-react"; -import { createStyles } from "antd-style"; +import { createStyles, useTheme } from "antd-style"; import type { AppDetail } from "@shared/types/kintone"; const useStyles = createStyles(({ token, css }) => ({ @@ -107,6 +107,7 @@ const AppListItem: React.FC = ({ t, }) => { const { styles } = useStyles(); + const token = useTheme(); const [isHovered, setIsHovered] = React.useState(false); // Pin overlay is visible when: @@ -144,7 +145,7 @@ const AppListItem: React.FC = ({
{/* App icon - hidden when pin overlay is visible */} {!showPinOverlay && ( - + )}
diff --git a/src/renderer/src/components/CodeViewer/CodeViewer.tsx b/src/renderer/src/components/CodeViewer/CodeViewer.tsx index 468b3c3..f85d2a3 100644 --- a/src/renderer/src/components/CodeViewer/CodeViewer.tsx +++ b/src/renderer/src/components/CodeViewer/CodeViewer.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Spin, Alert, Space, message } from "antd"; import { Button, Empty } from "@lobehub/ui"; import { Copy, Download } from "lucide-react"; -import { createStyles } from "antd-style"; +import { createStyles, useTheme } from "antd-style"; import CodeMirror from "@uiw/react-codemirror"; import { javascript } from "@codemirror/lang-javascript"; import { css } from "@codemirror/lang-css"; @@ -63,6 +63,8 @@ const CodeViewer: React.FC = ({ }) => { const { t } = useTranslation("file"); const { styles } = useStyles(); + const { appearance } = useTheme(); + const themeMode = appearance === "dark" ? "dark" : "light" as const; const { currentDomain } = useDomainStore(); const [loading, setLoading] = React.useState(true); @@ -249,7 +251,7 @@ const CodeViewer: React.FC = ({ height="100%" extensions={[language === "js" ? javascript() : css()]} editable={false} - theme="light" + theme={themeMode} basicSetup={{ lineNumbers: true, highlightActiveLineGutter: false, diff --git a/src/renderer/src/components/DomainManager/DomainForm.tsx b/src/renderer/src/components/DomainManager/DomainForm.tsx index 49a4c0c..bb2cc16 100644 --- a/src/renderer/src/components/DomainManager/DomainForm.tsx +++ b/src/renderer/src/components/DomainManager/DomainForm.tsx @@ -7,6 +7,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Button, Input, InputPassword, Modal } from "@lobehub/ui"; import { Form } from "antd"; +import { createStyles, useTheme } from "antd-style"; import { useDomainStore } from "@renderer/stores"; import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc"; import { CheckCircle2, XCircle } from "lucide-react"; @@ -36,6 +37,7 @@ type CreateErrorType = "connection" | "duplicate" | "unknown" | null; const DomainForm: React.FC = ({ open, onClose, domainId }) => { const { t } = useTranslation("domain"); + const token = useTheme(); const [form] = Form.useForm(); const { domains, createDomain, updateDomainById } = useDomainStore(); @@ -294,9 +296,9 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { const getIcon = () => { if (!testResult) return undefined; return testResult.success ? ( - + ) : ( - + ); }; @@ -379,7 +381,7 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { {/* Right side: Test button and Create/Update button */}
{createError && ( - + {createError.message} )} diff --git a/src/renderer/src/components/Settings/Settings.tsx b/src/renderer/src/components/Settings/Settings.tsx index a47cc1a..88bba5a 100644 --- a/src/renderer/src/components/Settings/Settings.tsx +++ b/src/renderer/src/components/Settings/Settings.tsx @@ -1,16 +1,17 @@ /** * Settings Component - * Application settings modal with language switcher + * Application settings modal with language switcher and theme selector */ import React from "react"; import { useTranslation } from "react-i18next"; 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 { createStyles } from "antd-style"; import { useLocaleStore } from "@renderer/stores/localeStore"; +import { useThemeStore, type ThemeMode } from "@renderer/stores/themeStore"; import { LOCALES, type LocaleCode } from "@shared/types/locale"; const { Title } = Typography; @@ -37,11 +38,18 @@ const useStyles = createStyles(({ token, css }) => ({ .ant-radio-button-wrapper { border-radius: ${token.borderRadius}px; border: 1px solid ${token.colorBorder}; + color: ${token.colorText}; } .ant-radio-button-wrapper::before { display: none; } + + .ant-radio-button-wrapper-checked { + background: ${token.colorPrimary} !important; + border-color: ${token.colorPrimary} !important; + color: ${token.colorBgContainer} !important; + } `, versionInfo: css` 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: }, + { value: "dark", labelKey: "darkTheme", icon: }, + { value: "auto", labelKey: "systemTheme", icon: }, +]; + const Settings: React.FC = () => { const { t } = useTranslation("settings"); const { styles } = useStyles(); const { locale, setLocale } = useLocaleStore(); + const { themeMode, setThemeMode } = useThemeStore(); const i18n = useTranslation().i18n; // Version and update state @@ -99,6 +118,10 @@ const Settings: React.FC = () => { await window.api.setLocale({ locale: newLocale }); }; + const handleThemeChange = (newTheme: ThemeMode) => { + setThemeMode(newTheme); + }; + const handleCheckUpdate = async () => { setCheckingUpdate(true); setUpdateInfo(null); @@ -153,6 +176,34 @@ const Settings: React.FC = () => { + {/* Theme Section */} +
+
+ + + {t("theme")} + +
+ handleThemeChange(e.target.value)} + optionType="button" + buttonStyle="solid" + className={styles.radioGroup} + > + {THEME_OPTIONS.map((option) => ( + + + {option.icon} + {t(option.labelKey)} + + + ))} + +
+ + + {/* About Section */}
@@ -161,7 +212,7 @@ const Settings: React.FC = () => { {t("about")}
- +
{t("version")}: {appVersion || "-"} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 98b8962..bcafced 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -5,8 +5,21 @@ import { ThemeProvider } from "@lobehub/ui"; import { I18nextProvider } from "react-i18next"; import i18n from "./i18n"; import App from "./App"; +import { useThemeStore } from "./stores/themeStore"; import "./index.css"; +const ThemeApp: React.FC = () => { + const { themeMode, setThemeMode } = useThemeStore(); + + return ( + + + + + + ); +}; + ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - + , diff --git a/src/renderer/src/stores/index.ts b/src/renderer/src/stores/index.ts index 636de9d..8cf391e 100644 --- a/src/renderer/src/stores/index.ts +++ b/src/renderer/src/stores/index.ts @@ -9,4 +9,6 @@ export { useDeployStore } from "./deployStore"; export { useVersionStore } from "./versionStore"; export { useUIStore } from "./uiStore"; export { useSessionStore } from "./sessionStore"; -export type { ViewMode, SelectedFile } from "./sessionStore"; \ No newline at end of file +export type { ViewMode, SelectedFile } from "./sessionStore"; +export { useThemeStore } from "./themeStore"; +export type { ThemeMode } from "./themeStore"; \ No newline at end of file diff --git a/src/renderer/src/stores/themeStore.ts b/src/renderer/src/stores/themeStore.ts new file mode 100644 index 0000000..4763236 --- /dev/null +++ b/src/renderer/src/stores/themeStore.ts @@ -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()( + persist( + (set) => ({ + // Initial state + themeMode: "auto", + + // Actions + setThemeMode: (mode) => set({ themeMode: mode }), + }), + { + name: "theme-storage", + }, + ), +);