feat(theme): add dark mode support with Light/Dark/Auto modes
This commit is contained in:
@@ -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 }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}>
|
||||||
@@ -161,7 +212,7 @@ const Settings: React.FC = () => {
|
|||||||
{t("about")}
|
{t("about")}
|
||||||
</Title>
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.versionInfo}>
|
<div className={styles.versionInfo}>
|
||||||
<span>{t("version")}:</span>
|
<span>{t("version")}:</span>
|
||||||
<span className={styles.versionNumber}>{appVersion || "-"}</span>
|
<span className={styles.versionNumber}>{appVersion || "-"}</span>
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ export { useDeployStore } from "./deployStore";
|
|||||||
export { useVersionStore } from "./versionStore";
|
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";
|
||||||
32
src/renderer/src/stores/themeStore.ts
Normal file
32
src/renderer/src/stores/themeStore.ts
Normal 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user