feat(theme): add dark mode support with Light/Dark/Auto modes
This commit is contained in:
@@ -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 = () => {
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
|
||||
@@ -255,7 +256,7 @@ const App: React.FC = () => {
|
||||
<div
|
||||
className={styles.resizeHandle}
|
||||
onMouseDown={handleResizeStart}
|
||||
style={{ background: isResizing ? "#1890ff" : undefined }}
|
||||
style={{ background: isResizing ? token.colorPrimary : undefined }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<LayoutGrid size={24} style={{ color: "#1890ff" }} />
|
||||
<LayoutGrid size={24} style={{ color: token.colorPrimary }} />
|
||||
<h3 className={styles.appName}>{currentApp.name}</h3>
|
||||
<Tag>ID: {currentApp.appId}</Tag>
|
||||
</div>
|
||||
|
||||
@@ -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<AppListItemProps> = ({
|
||||
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<AppListItemProps> = ({
|
||||
</div>
|
||||
{/* App icon - hidden when pin overlay is visible */}
|
||||
{!showPinOverlay && (
|
||||
<LayoutGrid size={16} style={{ color: "#1890ff" }} />
|
||||
<LayoutGrid size={16} style={{ color: token.colorPrimary }} />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title={app.name}>
|
||||
|
||||
@@ -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<CodeViewerProps> = ({
|
||||
}) => {
|
||||
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<CodeViewerProps> = ({
|
||||
height="100%"
|
||||
extensions={[language === "js" ? javascript() : css()]}
|
||||
editable={false}
|
||||
theme="light"
|
||||
theme={themeMode}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
highlightActiveLineGutter: false,
|
||||
|
||||
@@ -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<DomainFormProps> = ({ 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<DomainFormProps> = ({ open, onClose, domainId }) => {
|
||||
const getIcon = () => {
|
||||
if (!testResult) return undefined;
|
||||
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 */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
{createError && (
|
||||
<span style={{ color: "#ff4d4f", fontSize: 14 }}>
|
||||
<span style={{ color: token.colorError, fontSize: 14 }}>
|
||||
{createError.message}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -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: <Sun size={14} /> },
|
||||
{ value: "dark", labelKey: "darkTheme", icon: <Moon size={14} /> },
|
||||
{ value: "auto", labelKey: "systemTheme", icon: <Monitor size={14} /> },
|
||||
];
|
||||
|
||||
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 = () => {
|
||||
|
||||
<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 */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>
|
||||
|
||||
@@ -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 (
|
||||
<ThemeProvider themeMode={themeMode} onThemeModeChange={setThemeMode}>
|
||||
<AntdApp>
|
||||
<App />
|
||||
</AntdApp>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider
|
||||
@@ -18,11 +31,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
}}
|
||||
>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider>
|
||||
<AntdApp>
|
||||
<App />
|
||||
</AntdApp>
|
||||
</ThemeProvider>
|
||||
<ThemeApp />
|
||||
</I18nextProvider>
|
||||
</ConfigProvider>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -10,3 +10,5 @@ export { useVersionStore } from "./versionStore";
|
||||
export { useUIStore } from "./uiStore";
|
||||
export { useSessionStore } 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