mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
refactor: migrate theme management to next-themes (#11112)
* refactor: migrate theme management to `next-themes` and remove theme from route variants and global store. Signed-off-by: Innei <tukon479@gmail.com> * refactor: Unify theme mode to 'system' instead of 'auto' and streamline Electron theme synchronization. Signed-off-by: Innei <tukon479@gmail.com> * refactor: Remove LOBE_THEME_APPEARANCE constant and simplify desktop theme source assignment. Signed-off-by: Innei <tukon479@gmail.com> * chore: Update antd-style dependency from npm alias to specific alpha version. Signed-off-by: Innei <tukon479@gmail.com> * chore: update pnpm lockfile Signed-off-by: Innei <tukon479@gmail.com> * feat: Default theme to system and update Next.js RSC payload path example. Signed-off-by: Innei <tukon479@gmail.com> * feat: add `dev:static` script for static renderer development Signed-off-by: Innei <tukon479@gmail.com> * refactor: replace useThemeMode with custom useIsDark hook for theme detection and add ClientOnly component Signed-off-by: Innei <tukon479@gmail.com> * refactor: Remove `extractStaticStyle` import and cache prop from `StyleRegistry`. Signed-off-by: Innei <tukon479@gmail.com> * chore: Remove debug console log for current appearance. Signed-off-by: Innei <tukon479@gmail.com> * fix: Migrate legacy 'auto' theme mode to 'system' and refine theme background CSS selectors. Signed-off-by: Innei <tukon479@gmail.com> * feat: Add window dragging to desktop onboarding layout and update antd-style dependency. * refactor: Refine global background styling to target body elements, remove token-based background, and clean up debugging script. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
4196d9783e
commit
3a30d9aed1
78 changed files with 402 additions and 475 deletions
|
|
@ -18,6 +18,7 @@
|
|||
"build:mac:local": "npm run build && UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.js --publish never",
|
||||
"build:win": "npm run build && electron-builder --win --config electron-builder.js --publish never",
|
||||
"dev": "electron-vite dev",
|
||||
"dev:static": "cross-env DESKTOP_RENDERER_STATIC=1 npm run electron:dev",
|
||||
"electron:dev": "electron-vite dev",
|
||||
"electron:run-unpack": "electron .",
|
||||
"format": "prettier --write ",
|
||||
|
|
@ -57,11 +58,11 @@
|
|||
"@lobechat/file-loaders": "workspace:*",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@types/async-retry": "^1.4.9",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/set-cookie-parser": "^2.4.10",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251210.1",
|
||||
"async-retry": "^1.3.3",
|
||||
"consola": "^3.4.2",
|
||||
|
|
@ -104,4 +105,4 @@
|
|||
"electron-builder"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
|
|||
networkProxy: defaultProxySettings,
|
||||
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
||||
storagePath: appStorageDir,
|
||||
themeMode: 'auto',
|
||||
themeMode: 'system',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -258,9 +258,8 @@ export default class SystemController extends ControllerModule {
|
|||
return nativeTheme.themeSource;
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async setSystemThemeMode(themeMode: ThemeMode) {
|
||||
nativeTheme.themeSource = themeMode === 'auto' ? 'system' : themeMode;
|
||||
private async setSystemThemeMode(themeMode: ThemeMode) {
|
||||
nativeTheme.themeSource = themeMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -142,10 +142,17 @@ export class App {
|
|||
* This allows nativeTheme.shouldUseDarkColors to be used consistently everywhere
|
||||
*/
|
||||
private initializeThemeMode() {
|
||||
const themeMode = this.storeManager.get('themeMode');
|
||||
let themeMode = this.storeManager.get('themeMode');
|
||||
|
||||
// Migrate legacy 'auto' value to 'system' (nativeTheme.themeSource doesn't accept 'auto')
|
||||
if (Object.is(themeMode, 'auto')) {
|
||||
themeMode = 'system';
|
||||
this.storeManager.set('themeMode', themeMode);
|
||||
logger.info(`Migrated legacy theme mode 'auto' to 'system'`);
|
||||
}
|
||||
|
||||
if (themeMode) {
|
||||
nativeTheme.themeSource = themeMode === 'auto' ? 'system' : themeMode;
|
||||
nativeTheme.themeSource = themeMode;
|
||||
logger.debug(
|
||||
`Theme mode initialized to: ${themeMode} (themeSource: ${nativeTheme.themeSource})`,
|
||||
);
|
||||
|
|
@ -401,4 +408,4 @@ export class App {
|
|||
// 执行清理操作
|
||||
this.staticFileServerManager.destroy();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ export interface ElectronMainStore {
|
|||
networkProxy: NetworkProxySettings;
|
||||
shortcuts: Record<string, string>;
|
||||
storagePath: string;
|
||||
themeMode: 'dark' | 'light' | 'auto';
|
||||
themeMode: 'dark' | 'light' | 'system';
|
||||
}
|
||||
|
||||
export type StoreKey = keyof ElectronMainStore;
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@
|
|||
"@lobehub/icons": "^4.0.2",
|
||||
"@lobehub/market-sdk": "^0.25.1",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.8.0",
|
||||
"@lobehub/ui": "^4.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
"@next/third-parties": "^16.1.1",
|
||||
|
|
@ -234,7 +234,7 @@
|
|||
"@zumer/snapdom": "^1.9.14",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^6.1.1",
|
||||
"antd-style": "^4.1.0",
|
||||
"antd-style": "4.1.0",
|
||||
"async-retry": "^1.3.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "1.4.6",
|
||||
|
|
@ -281,6 +281,7 @@
|
|||
"next": "^16.1.1",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"nodemailer": "^7.0.11",
|
||||
|
|
@ -453,4 +454,4 @@
|
|||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import { FileSearchResult } from '@lobechat/types';
|
||||
import { Center, Flexbox, MaterialFileTypeIcon, Text, Tooltip } from '@lobehub/ui';
|
||||
import { cx, useThemeMode } from 'antd-style';
|
||||
import { cx } from 'antd-style';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { styles } from './style';
|
||||
|
|
@ -12,7 +13,8 @@ export interface FileItemProps extends FileSearchResult {
|
|||
}
|
||||
|
||||
const FileItem = memo<FileItemProps>(({ fileId, fileName, relevanceScore }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === 'dark';
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinInterventionProps } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Skeleton, Text } from '@lobehub/ui';
|
||||
import { useThemeMode } from 'antd-style';
|
||||
import { createPatch } from 'diff';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import path from 'path-browserify-esm';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Diff, Hunk, parseDiff } from 'react-diff-view';
|
||||
|
|
@ -29,7 +29,8 @@ const EditLocalFile = memo<BuiltinInterventionProps<EditLocalFileParams>>(({ arg
|
|||
},
|
||||
);
|
||||
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === 'dark';
|
||||
// Generate diff from full file content
|
||||
const files = useMemo(() => {
|
||||
if (!fileData?.content) return [];
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { type EditLocalFileState } from '@lobechat/builtin-tool-local-system';
|
|||
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { type BuiltinRenderProps } from '@lobechat/types';
|
||||
import { Alert, Flexbox, Icon, Skeleton } from '@lobehub/ui';
|
||||
import { useThemeMode } from 'antd-style';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import path from 'path-browserify-esm';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Diff, Hunk, parseDiff } from 'react-diff-view';
|
||||
|
|
@ -28,7 +28,8 @@ const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFile
|
|||
return [];
|
||||
}
|
||||
}, [pluginState?.diffText]);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === 'dark';
|
||||
|
||||
if (!args) return <Skeleton active />;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
export const LOBE_THEME_APPEARANCE = 'LOBE_THEME_APPEARANCE';
|
||||
|
||||
export const LOBE_THEME_PRIMARY_COLOR = 'LOBE_THEME_PRIMARY_COLOR';
|
||||
|
||||
export const LOBE_THEME_NEUTRAL_COLOR = 'LOBE_THEME_NEUTRAL_COLOR';
|
||||
|
|
|
|||
|
|
@ -31,31 +31,26 @@ export interface IRouteVariants {
|
|||
locale: Locales;
|
||||
neutralColor?: string;
|
||||
primaryColor?: string;
|
||||
theme: 'dark' | 'light';
|
||||
}
|
||||
|
||||
const SUPPORTED_THEMES = ['dark', 'light'] as const;
|
||||
|
||||
export const DEFAULT_VARIANTS: IRouteVariants = {
|
||||
isMobile: false,
|
||||
locale: DEFAULT_LANG,
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
const SPLITTER = '__';
|
||||
|
||||
export class RouteVariants {
|
||||
static serializeVariants = (variants: IRouteVariants): string =>
|
||||
[variants.locale, Number(variants.isMobile), variants.theme].join(SPLITTER);
|
||||
[variants.locale, Number(variants.isMobile)].join(SPLITTER);
|
||||
|
||||
static deserializeVariants = (serialized: string): IRouteVariants => {
|
||||
try {
|
||||
const [locale, isMobile, theme] = serialized.split(SPLITTER);
|
||||
const [locale, isMobile] = serialized.split(SPLITTER);
|
||||
|
||||
return {
|
||||
isMobile: isMobile === '1',
|
||||
locale: RouteVariants.isValidLocale(locale) ? (locale as Locales) : DEFAULT_VARIANTS.locale,
|
||||
theme: RouteVariants.isValidTheme(theme) ? (theme as any) : DEFAULT_VARIANTS.theme,
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULT_VARIANTS };
|
||||
|
|
@ -68,6 +63,4 @@ export class RouteVariants {
|
|||
});
|
||||
|
||||
private static isValidLocale = (locale: string): boolean => locales.includes(locale as any);
|
||||
|
||||
private static isValidTheme = (theme: string): boolean => SUPPORTED_THEMES.includes(theme as any);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ export interface UserPathData {
|
|||
videos?: string; // User's home directory
|
||||
}
|
||||
|
||||
export type ThemeMode = 'auto' | 'dark' | 'light';
|
||||
export type ThemeMode = 'system' | 'dark' | 'light';
|
||||
export type ThemeAppearance = 'dark' | 'light' | string;
|
||||
|
|
|
|||
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
import { Center, Flexbox, Text } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { cx, useThemeMode } from 'antd-style';
|
||||
import { cx } from 'antd-style';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { ProductLogo } from '@/components/Branding';
|
||||
import LangButton from '@/features/User/UserPanel/LangButton';
|
||||
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import { styles } from './style';
|
||||
|
||||
const AuthContainer: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
return (
|
||||
<Flexbox className={styles.outerContainer} height={'100%'} padding={8} width={'100%'}>
|
||||
<Flexbox
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
export const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
||||
|
||||
// Divider 样式
|
||||
divider: css`
|
||||
// Divider 样式
|
||||
divider: css`
|
||||
height: 24px;
|
||||
`,
|
||||
|
||||
|
||||
|
||||
|
||||
// 内层容器 - 深色模式
|
||||
innerContainerDark: css`
|
||||
// 内层容器 - 深色模式
|
||||
innerContainerDark: css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
|
@ -23,11 +18,8 @@ innerContainerDark: css`
|
|||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
|
||||
|
||||
|
||||
|
||||
// 内层容器 - 浅色模式
|
||||
innerContainerLight: css`
|
||||
// 内层容器 - 浅色模式
|
||||
innerContainerLight: css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
|
@ -38,10 +30,8 @@ innerContainerLight: css`
|
|||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
|
||||
|
||||
|
||||
// 外层容器
|
||||
outerContainer: css`
|
||||
// 外层容器
|
||||
outerContainer: css`
|
||||
position: relative;
|
||||
`,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { NuqsAdapter } from 'nuqs/adapters/next/app';
|
||||
import { type FC, type PropsWithChildren } from 'react';
|
||||
|
||||
import ClientOnly from '@/components/client/ClientOnly';
|
||||
|
||||
import AuthContainer from './_layout';
|
||||
|
||||
const AuthLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<NuqsAdapter>
|
||||
<AuthContainer>{children}</AuthContainer>
|
||||
</NuqsAdapter>
|
||||
<ClientOnly>
|
||||
<NuqsAdapter>
|
||||
<AuthContainer>{children}</AuthContainer>
|
||||
</NuqsAdapter>
|
||||
</ClientOnly>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
|
||||
import { Center, Flexbox, Text } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { cx, useThemeMode } from 'antd-style';
|
||||
import { cx } from 'antd-style';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import LangButton from '@/features/User/UserPanel/LangButton';
|
||||
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import { styles } from './style';
|
||||
|
||||
const OnboardingContainer: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
return (
|
||||
<Flexbox className={styles.outerContainer} height={'100%'} padding={8} width={'100%'}>
|
||||
<Flexbox
|
||||
|
|
@ -21,6 +22,7 @@ const OnboardingContainer: FC<PropsWithChildren> = ({ children }) => {
|
|||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.drag)}
|
||||
gap={8}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
height: 24px;
|
||||
`,
|
||||
|
||||
drag: css`
|
||||
-webkit-app-region: drag;
|
||||
`,
|
||||
// 内层容器 - 深色模式
|
||||
innerContainerDark: css`
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { cssVar, useThemeMode } from 'antd-style';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { type FC, type PropsWithChildren, useMemo } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
import { styles } from './DesktopLayoutContainer/style';
|
||||
|
||||
const DesktopLayoutContainer: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const [expand] = useGlobalStore((s) => [systemStatusSelectors.showLeftPanel(s)]);
|
||||
|
||||
// CSS 变量用于动态样式
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { AlertCircle, X } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
|
@ -10,6 +10,7 @@ import React, { memo, useMemo } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
|
@ -53,7 +54,7 @@ interface PluginTagProps {
|
|||
}
|
||||
|
||||
const PluginTag = memo<PluginTagProps>(({ pluginId, onRemove }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
// Extract identifier
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Button, Flexbox, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DefaultFooter from '@/features/Setting/Footer';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
|
|
@ -27,7 +28,7 @@ const styles = createStaticStyles(({ css }) => ({
|
|||
|
||||
const Footer = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { isAuthenticated, signIn } = useMarketAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleSignIn = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Flexbox, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { type CSSProperties, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { threadSelectors } from '@/store/chat/selectors';
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ interface ThreadProps {
|
|||
|
||||
const Thread = memo<ThreadProps>(({ id, placement, style }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const threads = useChatStore(threadSelectors.getThreadsBySourceMsgId(id), isEqual);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ const AgentBuilder = memo(() => {
|
|||
const setChatPanelExpanded = useProfileStore((s) => s.setChatPanelExpanded);
|
||||
const groupAgentBuilderId = useAgentStore(builtinAgentSelectors.groupAgentBuilderId);
|
||||
|
||||
console.log('groupAgentBuilderId', groupAgentBuilderId);
|
||||
const [width, setWidth] = useState<string | number>(360);
|
||||
|
||||
const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { AlertCircle, X } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
|
@ -10,6 +10,7 @@ import React, { memo, useMemo } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
|
@ -53,7 +54,7 @@ interface PluginTagProps {
|
|||
}
|
||||
|
||||
const PluginTag = memo<PluginTagProps>(({ pluginId, onRemove }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
// Extract identifier
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Avatar, Block, Flexbox, Input } from '@lobehub/ui';
|
||||
import { Popover } from 'antd';
|
||||
import { useThemeMode } from 'antd-style';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import EmojiPicker from '@/components/EmojiPicker';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { globalGeneralSelectors } from '@/store/global/selectors';
|
||||
|
|
@ -18,7 +18,7 @@ interface EditingProps {
|
|||
|
||||
const Editing = memo<EditingProps>(({ id, title, avatar, toggleEditing }) => {
|
||||
const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const editing = useHomeStore((s) => s.agentRenamingId === id);
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const Footer = memo(() => {
|
|||
</a>
|
||||
)}
|
||||
</Flexbox>
|
||||
<ThemeButton placement={'top'} size={16} />
|
||||
<ThemeButton placement={'topCenter'} size={16} />
|
||||
</Flexbox>
|
||||
<LabsModal onClose={handleCloseLabsModal} open={isLabsModalOpen} />
|
||||
<ChangelogModal
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { useTheme, useThemeMode } from 'antd-style';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { Activity, type FC, type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useHomeStore } from '@/store/home';
|
||||
|
||||
import RecentHydration from './RecentHydration';
|
||||
|
|
@ -14,7 +15,7 @@ interface LayoutProps {
|
|||
}
|
||||
|
||||
const Layout: FC<LayoutProps> = ({ children }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const theme = useTheme(); // Keep for colorBgContainerSecondary (not in cssVar)
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { Avatar, Block, Flexbox, Text } from '@lobehub/ui';
|
||||
import { cssVar, useThemeMode } from 'antd-style';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { RECENT_BLOCK_SIZE } from '@/app/[variants]/(main)/home/features/const';
|
||||
import { DEFAULT_AVATAR } from '@/const/meta';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { type DiscoverAssistantItem } from '@/types/discover';
|
||||
|
||||
const CommunityAgentItem = memo<DiscoverAssistantItem>(
|
||||
({ title, avatar, backgroundColor, author, description }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
return (
|
||||
<Block
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { Block, Center, Grid, type GridProps, Text } from '@lobehub/ui';
|
||||
import { cssVar, useThemeMode } from 'antd-style';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import useMergeState from 'use-merge-value';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
export interface AspectRatioSelectProps extends Omit<GridProps, 'children' | 'onChange'> {
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
|
|
@ -14,7 +16,7 @@ export interface AspectRatioSelectProps extends Omit<GridProps, 'children' | 'on
|
|||
|
||||
const AspectRatioSelect = memo<AspectRatioSelectProps>(
|
||||
({ options, onChange, value, defaultValue, ...rest }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const [active, setActive] = useMergeState('1:1', {
|
||||
defaultValue: defaultValue || '1:1',
|
||||
onChange,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { ModelIcon } from '@lobehub/icons';
|
||||
import { Flexbox, Text } from '@lobehub/ui';
|
||||
import { Popover } from 'antd';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { type AiModelForSelect } from 'model-bank';
|
||||
import numeral from 'numeral';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import NewModelBadge from '@/components/ModelSelect/NewModelBadge';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const POPOVER_MAX_WIDTH = 320;
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ const ImageModelItem = memo<ImageModelItemProps>(
|
|||
showBadge = true,
|
||||
...model
|
||||
}) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const priceLabel = useMemo(() => {
|
||||
// Priority 1: Use exact price
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { Block, Center, Grid, type GridProps, Select, Text } from '@lobehub/ui';
|
||||
import { cssVar, useThemeMode } from 'antd-style';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
import useMergeState from 'use-merge-value';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
export interface SizeSelectProps extends Omit<GridProps, 'children' | 'onChange'> {
|
||||
defaultValue?: 'auto' | string;
|
||||
onChange?: (value: string) => void;
|
||||
|
|
@ -26,7 +28,7 @@ const canParseAsRatio = (value: string): boolean => {
|
|||
};
|
||||
|
||||
const SizeSelect = memo<SizeSelectProps>(({ options, onChange, value, defaultValue, ...rest }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const [active, setActive] = useMergeState('auto', {
|
||||
defaultValue,
|
||||
onChange,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
import { ChatInput } from '@lobehub/editor/react';
|
||||
import { Button, Flexbox, TextArea } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loginRequired } from '@/components/Error/loginRequiredNotification';
|
||||
import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useImageStore } from '@/store/image';
|
||||
import { createImageSelectors } from '@/store/image/selectors';
|
||||
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
||||
|
|
@ -39,7 +40,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
}));
|
||||
|
||||
const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('image');
|
||||
const { value, setValue } = useGenerationConfigParam('prompt');
|
||||
const isCreating = useImageStore(createImageSelectors.isCreating);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
import { type ReactNode, memo, useMemo } from 'react';
|
||||
import { GroupedVirtuoso } from 'react-virtuoso';
|
||||
|
||||
import Loading from '@/app/[variants]/(main)/memory/features/Loading';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import { useScrollParent } from './useScrollParent';
|
||||
|
||||
|
|
@ -31,7 +32,9 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
|
||||
export type GroupBy = 'day' | 'month';
|
||||
|
||||
interface TimelineViewProps<T extends { capturedAt?: Date | string; createdAt?: Date | string; id: string }> {
|
||||
interface TimelineViewProps<
|
||||
T extends { capturedAt?: Date | string; createdAt?: Date | string; id: string },
|
||||
> {
|
||||
data: T[];
|
||||
/**
|
||||
* Custom date field extractor for grouping
|
||||
|
|
@ -68,7 +71,9 @@ const getDateValue = <T extends { capturedAt?: Date | string; createdAt?: Date |
|
|||
return item.capturedAt ?? item.createdAt ?? new Date();
|
||||
};
|
||||
|
||||
function TimelineViewInner<T extends { capturedAt?: Date | string; createdAt?: Date | string; id: string }>({
|
||||
function TimelineViewInner<
|
||||
T extends { capturedAt?: Date | string; createdAt?: Date | string; id: string },
|
||||
>({
|
||||
data,
|
||||
groupBy = 'day',
|
||||
getDateForGrouping,
|
||||
|
|
@ -78,7 +83,7 @@ function TimelineViewInner<T extends { capturedAt?: Date | string; createdAt?: D
|
|||
renderHeader,
|
||||
renderItem,
|
||||
}: TimelineViewProps<T>) {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const scrollParent = useScrollParent();
|
||||
|
||||
const { groupCounts, sortedPeriods, groupedItems } = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Block, Flexbox, Input } from '@lobehub/ui';
|
||||
import { Popover } from 'antd';
|
||||
import { useThemeMode } from 'antd-style';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EmojiPicker from '@/components/EmojiPicker';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useFileStore } from '@/store/file';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { globalGeneralSelectors } from '@/store/global/selectors';
|
||||
|
|
@ -18,7 +18,7 @@ interface EditingProps {
|
|||
|
||||
const Editing = memo<EditingProps>(({ documentId, title, currentEmoji, toggleEditing }) => {
|
||||
const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('file');
|
||||
|
||||
const editing = useFileStore((s) => s.renamingPageId === documentId);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Select, Skeleton } from '@lobehub/ui';
|
|||
import { Segmented, Switch } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Ban, Gauge, Loader2Icon, Monitor, Moon, Mouse, Sun, Waves } from 'lucide-react';
|
||||
import { useTheme as useNextThemesTheme } from 'next-themes';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -22,16 +23,15 @@ const Common = memo(() => {
|
|||
const { t } = useTranslation('setting');
|
||||
|
||||
const general = useUserStore((s) => settingsSelectors.currentSettings(s).general, isEqual);
|
||||
const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
|
||||
const { theme, setTheme } = useNextThemesTheme();
|
||||
const language = useGlobalStore(systemStatusSelectors.language);
|
||||
const [setSettings, isUserStateInit] = useUserStore((s) => [s.setSettings, s.isUserStateInit]);
|
||||
const [setThemeMode, switchLocale, isStatusInit] = useGlobalStore((s) => [
|
||||
s.switchThemeMode,
|
||||
s.switchLocale,
|
||||
s.isStatusInit,
|
||||
]);
|
||||
const [switchLocale, isStatusInit] = useGlobalStore((s) => [s.switchLocale, s.isStatusInit]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Use the theme value from next-themes, default to 'system'
|
||||
const currentTheme = theme || 'system';
|
||||
|
||||
const handleLangChange = (value: LocaleMode) => {
|
||||
switchLocale(value);
|
||||
};
|
||||
|
|
@ -39,13 +39,13 @@ const Common = memo(() => {
|
|||
if (!(isStatusInit && isUserStateInit))
|
||||
return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
|
||||
|
||||
const theme: FormGroupItemType = {
|
||||
const themeFormGroup: FormGroupItemType = {
|
||||
children: [
|
||||
{
|
||||
children: (
|
||||
<ImageSelect
|
||||
height={60}
|
||||
onChange={setThemeMode}
|
||||
onChange={(value) => setTheme(value === 'auto' ? 'system' : value)}
|
||||
options={[
|
||||
{
|
||||
icon: Sun,
|
||||
|
|
@ -63,11 +63,11 @@ const Common = memo(() => {
|
|||
icon: Monitor,
|
||||
img: imageUrl('theme_auto.webp'),
|
||||
label: t('settingCommon.themeMode.auto'),
|
||||
value: 'auto',
|
||||
value: 'system',
|
||||
},
|
||||
]}
|
||||
unoptimized={isDesktop}
|
||||
value={themeMode}
|
||||
value={currentTheme}
|
||||
width={100}
|
||||
/>
|
||||
),
|
||||
|
|
@ -173,7 +173,7 @@ const Common = memo(() => {
|
|||
<Form
|
||||
collapsible={false}
|
||||
initialValues={general}
|
||||
items={[theme]}
|
||||
items={[themeFormGroup]}
|
||||
itemsType={'group'}
|
||||
onValuesChange={async (v) => {
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import { BRANDING_PROVIDER } from '@lobechat/business-const';
|
|||
import { ProviderCombine, ProviderIcon } from '@lobehub/icons';
|
||||
import { Avatar, Flexbox, Skeleton, Text } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { cssVar, cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { BrandingProviderCard } from '@/business/client/features/BrandingProviderCard';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { type AiProviderListItem } from '@/types/aiProvider';
|
||||
|
||||
import EnableSwitch from './EnableSwitch';
|
||||
|
|
@ -19,7 +20,7 @@ interface ProviderCardProps extends AiProviderListItem {
|
|||
const ProviderCard = memo<ProviderCardProps>(
|
||||
({ id, description, name, enabled, source, logo, loading, onProviderSelect }) => {
|
||||
const { t } = useTranslation('providers');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { cssVar, useThemeMode } from 'antd-style';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
interface TotalCardProps {
|
||||
count: string | number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TotalCard = memo<TotalCardProps>(({ title, count }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
return (
|
||||
<Flexbox
|
||||
padding={12}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,26 @@
|
|||
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||
import { useThemeMode } from 'antd-style';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme as useNextThemesTheme } from 'next-themes';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const Header = memo(() => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
|
||||
const { setTheme } = useNextThemesTheme();
|
||||
const isDark = useIsDark();
|
||||
|
||||
return (
|
||||
<ChatHeader
|
||||
right={
|
||||
<ActionIcon
|
||||
icon={isDarkMode ? Moon : Sun}
|
||||
onClick={() => switchThemeMode(isDarkMode ? 'light' : 'dark')}
|
||||
icon={isDark ? Moon : Sun}
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
size={MOBILE_HEADER_ICON_SIZE}
|
||||
/>
|
||||
}
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next';
|
||||
import { type ThemeAppearance } from 'antd-style';
|
||||
import { type ResolvingViewport } from 'next';
|
||||
import Script from 'next/script';
|
||||
import { type ReactNode, Suspense } from 'react';
|
||||
|
|
@ -26,7 +25,7 @@ export interface RootLayoutProps extends DynamicLayoutProps {
|
|||
const RootLayout = async ({ children, params }: RootLayoutProps) => {
|
||||
const { variants } = await params;
|
||||
|
||||
const { locale, isMobile, theme, primaryColor, neutralColor } =
|
||||
const { locale, isMobile, primaryColor, neutralColor } =
|
||||
RouteVariants.deserializeVariants(variants);
|
||||
|
||||
const direction = isRtlLang(locale) ? 'rtl' : 'ltr';
|
||||
|
|
@ -34,7 +33,6 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
|
|||
const renderContent = () => {
|
||||
return (
|
||||
<GlobalProvider
|
||||
appearance={theme}
|
||||
isMobile={isMobile}
|
||||
locale={locale}
|
||||
neutralColor={neutralColor}
|
||||
|
|
@ -50,8 +48,9 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<html dir={direction} lang={locale}>
|
||||
<html dir={direction} lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* <script dangerouslySetInnerHTML={{ __html: 'setTimeout(() => {debugger}, 16)' }} /> */}
|
||||
{process.env.DEBUG_REACT_SCAN === '1' && (
|
||||
<Script
|
||||
crossOrigin={'anonymous'}
|
||||
|
|
@ -99,7 +98,6 @@ export const generateViewport = async (props: DynamicLayoutProps): ResolvingView
|
|||
};
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
const themes: ThemeAppearance[] = ['dark', 'light'];
|
||||
const mobileOptions = isDesktop ? [false] : [true, false];
|
||||
// only static for serveral page, other go to dynamtic
|
||||
const staticLocales: Locales[] = [DEFAULT_LANG, 'zh-CN'];
|
||||
|
|
@ -107,16 +105,13 @@ export const generateStaticParams = () => {
|
|||
const variants: { variants: string }[] = [];
|
||||
|
||||
for (const locale of staticLocales) {
|
||||
for (const theme of themes) {
|
||||
for (const isMobile of mobileOptions) {
|
||||
variants.push({
|
||||
variants: RouteVariants.serializeVariants({
|
||||
isMobile,
|
||||
locale,
|
||||
theme: theme as 'dark' | 'light',
|
||||
}),
|
||||
});
|
||||
}
|
||||
for (const isMobile of mobileOptions) {
|
||||
variants.push({
|
||||
variants: RouteVariants.serializeVariants({
|
||||
isMobile,
|
||||
locale,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
|
||||
import { Center, Flexbox, Text } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { cx, useThemeMode } from 'antd-style';
|
||||
import { cx } from 'antd-style';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import LangButton from '@/features/User/UserPanel/LangButton';
|
||||
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import { styles } from './style';
|
||||
|
||||
const OnBoardingContainer: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
return (
|
||||
<Flexbox className={styles.outerContainer} height={'100%'} padding={8} width={'100%'}>
|
||||
<Flexbox
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { Block, Button, Flexbox, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Undo2Icon } from 'lucide-react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import LobeMessage from '@/app/[variants]/onboarding/components/LobeMessage';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
|
@ -76,7 +77,7 @@ interface ModeSelectionStepProps {
|
|||
const ModeSelectionStep = memo<ModeSelectionStepProps>(({ onBack, onNext }) => {
|
||||
const { t } = useTranslation('onboarding');
|
||||
const navigate = useNavigate();
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const imageStyles = useMemo<React.CSSProperties>(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
const DesktopRouterClient = dynamic(() => import('./DesktopClientRouter'), {
|
||||
loading: () => <Loading debugId="DesktopRouter" />,
|
||||
ssr: false,
|
||||
});
|
||||
import DesktopClientRouter from './DesktopClientRouter';
|
||||
|
||||
const useIsClient = () => {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
return isClient;
|
||||
};
|
||||
const DesktopRouter = () => {
|
||||
return <DesktopRouterClient />;
|
||||
const isClient = useIsClient();
|
||||
if (!isClient) return null;
|
||||
return <DesktopClientRouter />;
|
||||
};
|
||||
|
||||
export default DesktopRouter;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { createStaticStyles, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
dividerDark: css`
|
||||
flex: none;
|
||||
|
|
@ -19,7 +21,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
}));
|
||||
|
||||
const Divider = memo(() => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
return <div className={isDarkMode ? styles.dividerDark : styles.dividerLight} />;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Flexbox, Icon, Modal } from '@lobehub/ui';
|
||||
import { createStaticStyles, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const prefixCls = 'ant';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
|
@ -52,7 +54,7 @@ interface DataStyleModalProps {
|
|||
|
||||
const DataStyleModal = memo<DataStyleModalProps>(
|
||||
({ icon, onOpenChange, title, open, children, width = 550, height }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Center, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { createStaticStyles, useThemeMode , responsive } from 'antd-style';
|
||||
import { createStaticStyles, responsive } from 'antd-style';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
desc: css`
|
||||
width: 280px;
|
||||
|
|
@ -48,7 +50,7 @@ interface FeatureListProps {
|
|||
}
|
||||
|
||||
const FeatureList = memo<FeatureListProps>(({ data }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
return (
|
||||
<Flexbox gap={32}>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Flexbox, Icon, Tag, Tooltip } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { BoltIcon, RotateCwIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { AsyncTaskStatus, type FileParsingTask } from '@/types/asyncTask';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
|
@ -36,7 +37,7 @@ interface EmbeddingStatusProps extends FileParsingTask {
|
|||
const EmbeddingStatus = memo<EmbeddingStatusProps>(
|
||||
({ chunkCount, embeddingStatus, embeddingError, onClick, onErrorClick, className }) => {
|
||||
const { t } = useTranslation(['components', 'common']);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
switch (embeddingStatus) {
|
||||
case AsyncTaskStatus.Processing: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Button, Flexbox, Icon, Tag, Tooltip } from '@lobehub/ui';
|
||||
import { Badge } from 'antd';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { BoltIcon, Loader2Icon, RotateCwIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { AsyncTaskStatus, type FileParsingTask } from '@/types/asyncTask';
|
||||
|
||||
import EmbeddingStatus from './EmbeddingStatus';
|
||||
|
|
@ -52,7 +53,7 @@ const FileParsingStatus = memo<FileParsingStatusProps>(
|
|||
hideEmbeddingButton,
|
||||
}) => {
|
||||
const { t } = useTranslation(['components', 'common']);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
switch (chunkingStatus) {
|
||||
case AsyncTaskStatus.Processing: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { ActionIcon, Flexbox, type FlexboxProps } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
cancelIcon: css`
|
||||
position: absolute;
|
||||
|
|
@ -70,7 +72,7 @@ const Notification = memo<NotificationProps>(
|
|||
className,
|
||||
...rest
|
||||
}) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { className: wrapperClassName, ...restWrapper } = wrapper;
|
||||
return (
|
||||
show && (
|
||||
|
|
|
|||
17
src/components/client/ClientOnly.tsx
Normal file
17
src/components/client/ClientOnly.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { type FC, type PropsWithChildren, useEffect, useState } from 'react';
|
||||
|
||||
const ClientOnly: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ClientOnly;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { LOBE_CHAT_CLOUD, UTM_SOURCE } from '@lobechat/business-const';
|
||||
import { Button, Center, Flexbox, Icon , lobeStaticStylish } from '@lobehub/ui';
|
||||
import { Button, Center, Flexbox, Icon, lobeStaticStylish } from '@lobehub/ui';
|
||||
import { useSize } from 'ahooks';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -11,6 +11,7 @@ import Marquee from 'react-fast-marquee';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OFFICIAL_URL } from '@/const/url';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { isOnServerSide } from '@/utils/env';
|
||||
|
||||
export const BANNER_HEIGHT = 40;
|
||||
|
|
@ -50,7 +51,7 @@ const CloudBanner = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|||
const contentRef = useRef(null);
|
||||
const size = useSize(ref);
|
||||
const contentSize = useSize(contentRef);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('common');
|
||||
const [isTruncated, setIsTruncated] = useState(mobile);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const ThemeMenu = memo(() => {
|
|||
<div className={styles.itemLabel}>{t('cmdk.themeDark')}</div>
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => handleThemeChange('auto')} value="theme-auto">
|
||||
<Command.Item onSelect={() => handleThemeChange('system')} value="theme-system">
|
||||
<Monitor className={styles.icon} />
|
||||
<div className={styles.itemContent}>
|
||||
<div className={styles.itemLabel}>{t('cmdk.themeAuto')}</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export interface ChatMessage {
|
|||
role: 'user' | 'assistant';
|
||||
}
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'auto';
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
export type PageType = 'theme' | 'ask-ai' | string;
|
||||
|
||||
|
|
@ -25,4 +25,7 @@ export type MenuContext =
|
|||
| 'page'
|
||||
| 'painting';
|
||||
|
||||
export type ContextType = Extract<MenuContext, 'agent' | 'group' | 'resource' | 'settings' | 'page' | 'painting'>;
|
||||
export type ContextType = Extract<
|
||||
MenuContext,
|
||||
'agent' | 'group' | 'resource' | 'settings' | 'page' | 'painting'
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useDebounce } from 'ahooks';
|
||||
import { useTheme as useNextThemesTheme } from 'next-themes';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
|
@ -37,7 +38,7 @@ export const useCommandMenu = () => {
|
|||
} = useCommandMenuContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
|
||||
const { setTheme } = useNextThemesTheme();
|
||||
const createAgent = useAgentStore((s) => s.createAgent);
|
||||
const refreshAgentList = useHomeStore((s) => s.refreshAgentList);
|
||||
const inboxAgentId = useAgentStore(builtinAgentSelectors.inboxAgentId);
|
||||
|
|
@ -106,7 +107,7 @@ export const useCommandMenu = () => {
|
|||
};
|
||||
|
||||
const handleThemeChange = (theme: ThemeMode) => {
|
||||
switchThemeMode(theme);
|
||||
setTheme(theme);
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Center, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors, messageStateSelectors } from '@/store/chat/selectors';
|
||||
import { dotLoading } from '@/styles/loading';
|
||||
|
|
@ -57,7 +58,7 @@ interface ArtifactProps extends MarkdownElementProps {
|
|||
|
||||
const Render = memo<ArtifactProps>(({ identifier, title, type, language, children, id }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const hasChildren = !!children;
|
||||
const str = ((children as string) || '').toString?.();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { type ChatFileChunk } from '@lobechat/types';
|
||||
import { Center, Flexbox, Text, Tooltip } from '@lobehub/ui';
|
||||
import { cx, useThemeMode } from 'antd-style';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import FileIcon from '@/components/FileIcon';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
import { styles } from './style';
|
||||
|
|
@ -13,7 +14,7 @@ export interface ChunkItemProps extends ChatFileChunk {
|
|||
}
|
||||
|
||||
const ChunkItem = memo<ChunkItemProps>(({ id, fileId, similarity, text, filename, fileType }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
// Note: openFilePreview is a portal action, kept in ChatStore as it's a global UI state
|
||||
const openFilePreview = useChatStore((s) => s.openFilePreview);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { type ChatFileChunk } from '@lobechat/types';
|
||||
import { Flexbox, Icon } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { BookOpenTextIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import ChunkItem from './ChunkItem';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
|
|
@ -47,7 +49,7 @@ interface FileChunksProps {
|
|||
|
||||
const FileChunks = memo<FileChunksProps>(({ data }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Flexbox, Icon, SearchResultCards, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { ChevronDown, ChevronRight, Globe } from 'lucide-react';
|
||||
import { AnimatePresence, m as motion } from 'motion/react';
|
||||
import Image from 'next/image';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { type GroundingSearch } from '@/types/search';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
|
@ -46,7 +47,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
|
||||
const SearchGrounding = memo<GroundingSearch>(({ searchQueries, citations }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,15 @@
|
|||
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
||||
import { LOBE_THEME_APP_ID } from '@lobehub/ui';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const sidebarColors = {
|
||||
dark: '#000',
|
||||
light: '#f8f8f8',
|
||||
};
|
||||
export const useWatchThemeUpdate = () => {
|
||||
const [isAppStateInit, systemAppearance, updateElectronAppState, isMac] = useElectronStore(
|
||||
(s) => [
|
||||
s.isAppStateInit,
|
||||
s.appState.systemAppearance,
|
||||
s.updateElectronAppState,
|
||||
s.appState.isMac,
|
||||
],
|
||||
);
|
||||
const [switchThemeMode, switchLocale] = useGlobalStore((s) => [
|
||||
s.switchThemeMode,
|
||||
s.switchLocale,
|
||||
]);
|
||||
|
||||
useWatchBroadcast('themeChanged', ({ themeMode }) => {
|
||||
switchThemeMode(themeMode, { skipBroadcast: true });
|
||||
});
|
||||
const [updateElectronAppState] = useElectronStore((s) => [s.updateElectronAppState]);
|
||||
const [switchLocale] = useGlobalStore((s) => [s.switchLocale]);
|
||||
|
||||
useWatchBroadcast('localeChanged', ({ locale }) => {
|
||||
switchLocale(locale as any, { skipBroadcast: true });
|
||||
|
|
@ -36,20 +18,21 @@ export const useWatchThemeUpdate = () => {
|
|||
useWatchBroadcast('systemThemeChanged', ({ themeMode }) => {
|
||||
updateElectronAppState({ systemAppearance: themeMode });
|
||||
});
|
||||
const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
|
||||
useLayoutEffect(() => {
|
||||
ensureElectronIpc().system.setSystemThemeMode(themeMode);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isAppStateInit || !isMac) return;
|
||||
document.documentElement.style.background = 'none';
|
||||
const { theme } = useTheme();
|
||||
|
||||
const lobeApp = document.querySelector('#' + LOBE_THEME_APP_ID);
|
||||
if (!lobeApp) return;
|
||||
useEffect(() => {
|
||||
if (!isDesktop) return;
|
||||
if (!theme) return;
|
||||
|
||||
if (systemAppearance) {
|
||||
document.body.style.background = `color-mix(in srgb, ${sidebarColors[systemAppearance as 'dark' | 'light']} 86%, transparent)`;
|
||||
}
|
||||
}, [systemAppearance, isAppStateInit, isMac]);
|
||||
(async () => {
|
||||
try {
|
||||
await ensureElectronIpc().system.updateThemeModeHandler(
|
||||
theme as 'dark' | 'light' | 'system',
|
||||
);
|
||||
} catch {
|
||||
// Ignore errors in non-electron environment
|
||||
}
|
||||
})();
|
||||
}, [theme]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
import { Avatar, Flexbox, Icon, Skeleton, Text, Tooltip } from '@lobehub/ui';
|
||||
import { Switch } from 'antd';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Bot, Loader2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@/const/meta';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { type LobeAgentSession } from '@/types/session';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
|
|
@ -59,7 +60,7 @@ interface AgentCardProps {
|
|||
const AgentCard = memo<AgentCardProps>(
|
||||
({ agent, inGroup, isHost, loading, onAction, operationLoading }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
import { Avatar, Flexbox, Icon, Text, Tooltip } from '@lobehub/ui';
|
||||
import { Switch } from 'antd';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Bot, Loader2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DEFAULT_SUPERVISOR_AVATAR } from '@/const/meta';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
|
|
@ -53,7 +54,7 @@ interface HostMemberCardProps {
|
|||
}
|
||||
|
||||
const HostMemberCard = memo<HostMemberCardProps>(({ checked, loading, onToggle }) => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const title = t('settingGroupMembers.host.title');
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
import { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND } from '@lobehub/editor';
|
||||
import { Block, Icon } from '@lobehub/ui';
|
||||
import { Button, Space } from 'antd';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
import { usePageEditorStore } from './store';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
|
|
@ -36,7 +38,7 @@ const styles = createStaticStyles(({ css }) => ({
|
|||
|
||||
const DiffAllToolbar = memo(() => {
|
||||
const { t } = useTranslation('editor');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
const [editor, performSave] = usePageEditorStore((s) => [s.editor, s.performSave]);
|
||||
const [hasPendingDiffs, setHasPendingDiffs] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,65 +1,54 @@
|
|||
import { ActionIcon, Dropdown, type DropdownProps, Icon } from '@lobehub/ui';
|
||||
import { ActionIcon, DropdownMenu, type DropdownMenuProps, Icon } from '@lobehub/ui';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme as useNextThemesTheme } from 'next-themes';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type MenuProps } from '@/components/Menu';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
const themeIcons = {
|
||||
auto: Monitor,
|
||||
dark: Moon,
|
||||
light: Sun,
|
||||
system: Monitor,
|
||||
};
|
||||
|
||||
const ThemeButton: FC<{ placement?: DropdownProps['placement']; size?: number }> = ({
|
||||
const ThemeButton: FC<{ placement?: DropdownMenuProps['placement']; size?: number }> = ({
|
||||
placement,
|
||||
size,
|
||||
}) => {
|
||||
const [themeMode, switchThemeMode] = useGlobalStore((s) => [
|
||||
systemStatusSelectors.themeMode(s),
|
||||
s.switchThemeMode,
|
||||
]);
|
||||
const { setTheme, theme } = useNextThemesTheme();
|
||||
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
const items = useMemo<DropdownMenuProps['items']>(
|
||||
() => [
|
||||
{
|
||||
icon: <Icon icon={themeIcons.auto} />,
|
||||
key: 'auto',
|
||||
icon: <Icon icon={themeIcons.system} />,
|
||||
key: 'system',
|
||||
label: t('settingCommon.themeMode.auto'),
|
||||
onClick: () => switchThemeMode('auto'),
|
||||
onClick: () => setTheme('system'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={themeIcons.light} />,
|
||||
key: 'light',
|
||||
label: t('settingCommon.themeMode.light'),
|
||||
onClick: () => switchThemeMode('light'),
|
||||
onClick: () => setTheme('light'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={themeIcons.dark} />,
|
||||
key: 'dark',
|
||||
label: t('settingCommon.themeMode.dark'),
|
||||
onClick: () => switchThemeMode('dark'),
|
||||
onClick: () => setTheme('dark'),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
[setTheme, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items,
|
||||
selectable: true,
|
||||
selectedKeys: [themeMode],
|
||||
}}
|
||||
placement={placement}
|
||||
>
|
||||
<ActionIcon icon={themeIcons[themeMode]} size={size || { blockSize: 32, size: 16 }} />
|
||||
</Dropdown>
|
||||
<DropdownMenu items={items} placement={placement}>
|
||||
<ActionIcon
|
||||
icon={themeIcons[(theme as 'dark' | 'light' | 'system') || 'system']}
|
||||
size={size || { blockSize: 32, size: 16 }}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
11
src/hooks/useIsDark.ts
Normal file
11
src/hooks/useIsDark.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useTheme as useNextThemesTheme } from 'next-themes';
|
||||
|
||||
/**
|
||||
* Hook to check if the current theme is dark
|
||||
* @returns boolean - true if current theme is dark, false otherwise
|
||||
*/
|
||||
export const useIsDark = (): boolean => {
|
||||
const { resolvedTheme } = useNextThemesTheme();
|
||||
|
||||
return resolvedTheme === 'dark';
|
||||
};
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
import { dark } from '@clerk/themes';
|
||||
import { type ElementsConfig, type Theme } from '@clerk/types';
|
||||
import { BRANDING_URL } from '@lobechat/business-const';
|
||||
import { createStaticStyles, cssVar, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const prefixCls = 'cl';
|
||||
|
||||
|
|
@ -95,7 +97,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|||
})) as Partial<Record<keyof ElementsConfig, any>>;
|
||||
|
||||
export const useAppearance = () => {
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const navbarStyle = cx(styles.navbar, isDarkMode && (styles as any).navbar_dark);
|
||||
const scrollBoxStyle = cx(styles.scrollBox, isDarkMode && (styles as any).scrollBox_dark);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
import { BRANDING_NAME } from '@lobechat/business-const';
|
||||
import { Block, Modal, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx, useThemeMode } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { PRIVACY_URL, TERMS_URL } from '@/const/url';
|
||||
import AuthCard from '@/features/AuthCard';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
|
|
@ -34,7 +35,7 @@ interface MarketAuthConfirmModalProps {
|
|||
const MarketAuthConfirmModal = memo<MarketAuthConfirmModalProps>(
|
||||
({ open, onConfirm, onCancel }) => {
|
||||
const { t } = useTranslation('marketAuth');
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDark();
|
||||
|
||||
const footer = (
|
||||
<Text align={'center'} as={'div'} fontSize={13} type={'secondary'}>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
ThemeProvider,
|
||||
} from '@lobehub/ui';
|
||||
import { message as antdMessage } from 'antd';
|
||||
import { type ThemeAppearance, createStaticStyles, cx, useTheme } from 'antd-style';
|
||||
import { createStaticStyles, cx, useTheme } from 'antd-style';
|
||||
import 'antd/dist/reset.css';
|
||||
import { AppConfigContext } from 'antd/es/app/context';
|
||||
import * as motion from 'motion/react-m';
|
||||
|
|
@ -17,13 +17,10 @@ import Link from 'next/link';
|
|||
import { type ReactNode, memo, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import AntdStaticMethods from '@/components/AntdStaticMethods';
|
||||
import {
|
||||
LOBE_THEME_APPEARANCE,
|
||||
LOBE_THEME_NEUTRAL_COLOR,
|
||||
LOBE_THEME_PRIMARY_COLOR,
|
||||
} from '@/const/theme';
|
||||
import { LOBE_THEME_NEUTRAL_COLOR, LOBE_THEME_PRIMARY_COLOR } from '@/const/theme';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { getUILocaleAndResources } from '@/libs/getUILocaleAndResources';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
|
@ -92,7 +89,6 @@ export interface AppThemeProps {
|
|||
children?: ReactNode;
|
||||
customFontFamily?: string;
|
||||
customFontURL?: string;
|
||||
defaultAppearance?: ThemeAppearance;
|
||||
defaultNeutralColor?: NeutralColors;
|
||||
defaultPrimaryColor?: PrimaryColors;
|
||||
globalCDN?: boolean;
|
||||
|
|
@ -101,16 +97,16 @@ export interface AppThemeProps {
|
|||
const AppTheme = memo<AppThemeProps>(
|
||||
({
|
||||
children,
|
||||
defaultAppearance,
|
||||
defaultPrimaryColor,
|
||||
defaultNeutralColor,
|
||||
globalCDN,
|
||||
customFontURL,
|
||||
customFontFamily,
|
||||
}) => {
|
||||
const themeMode = useGlobalStore(systemStatusSelectors.themeMode);
|
||||
const language = useGlobalStore(systemStatusSelectors.language);
|
||||
const theme = useTheme();
|
||||
const antdTheme = useTheme();
|
||||
const isDark = useIsDark();
|
||||
|
||||
const [primaryColor, neutralColor, animationMode] = useUserStore((s) => [
|
||||
userGeneralSettingsSelectors.primaryColor(s),
|
||||
userGeneralSettingsSelectors.neutralColor(s),
|
||||
|
|
@ -154,29 +150,29 @@ const AppTheme = memo<AppThemeProps>(
|
|||
antdMessage.config({ top: messageTop });
|
||||
}, [messageTop]);
|
||||
|
||||
const currentAppearence = isDark ? 'dark' : 'light';
|
||||
|
||||
return (
|
||||
<AppConfigContext.Provider value={appConfig}>
|
||||
<ThemeProvider
|
||||
appearance={themeMode !== 'auto' ? themeMode : undefined}
|
||||
appearance={currentAppearence}
|
||||
className={cx(styles.app, styles.scrollbar, styles.scrollbarPolyfill)}
|
||||
customTheme={{
|
||||
neutralColor: neutralColor ?? defaultNeutralColor,
|
||||
primaryColor: primaryColor ?? defaultPrimaryColor,
|
||||
}}
|
||||
defaultAppearance={defaultAppearance}
|
||||
onAppearanceChange={(appearance) => {
|
||||
if (themeMode !== 'auto') return;
|
||||
|
||||
setCookie(LOBE_THEME_APPEARANCE, appearance);
|
||||
}}
|
||||
defaultAppearance={currentAppearence}
|
||||
defaultThemeMode={currentAppearence}
|
||||
theme={{
|
||||
cssVar: { key: 'lobe-vars' },
|
||||
token: {
|
||||
fontFamily: customFontFamily ? `${customFontFamily},${theme.fontFamily}` : undefined,
|
||||
fontFamily: customFontFamily
|
||||
? `${customFontFamily},${antdTheme.fontFamily}`
|
||||
: undefined,
|
||||
motion: animationMode !== 'disabled',
|
||||
motionUnit: animationMode === 'agile' ? 0.05 : 0.1,
|
||||
},
|
||||
}}
|
||||
themeMode={themeMode}
|
||||
>
|
||||
{!!customFontURL && <FontLoader url={customFontURL} />}
|
||||
<GlobalStyle />
|
||||
|
|
|
|||
22
src/layout/GlobalProvider/NextThemeProvider.tsx
Normal file
22
src/layout/GlobalProvider/NextThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface NextThemeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function NextThemeProvider({ children }: NextThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="data-theme"
|
||||
defaultTheme="system"
|
||||
disableTransitionOnChange
|
||||
enableSystem
|
||||
forcedTheme={undefined}
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,24 +1,29 @@
|
|||
'use client';
|
||||
|
||||
import { StyleProvider, extractStaticStyle } from 'antd-style';
|
||||
import { StyleProvider } from 'antd-style';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { type PropsWithChildren, useRef } from 'react';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
||||
const StyleRegistry = ({ children }: PropsWithChildren) => {
|
||||
const isInsert = useRef(false);
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
// avoid duplicate css insert
|
||||
// refs: https://github.com/vercel/next.js/discussions/49354#discussioncomment-6279917
|
||||
if (isInsert.current) return;
|
||||
|
||||
isInsert.current = true;
|
||||
|
||||
// @ts-ignore
|
||||
return extractStaticStyle().map((item) => item.style);
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
html body {background: #f8f8f8;}
|
||||
html[data-theme="dark"] body { background-color: #000; }
|
||||
${isDesktop ? 'html body, html { background: none; }' : ''}
|
||||
${isDesktop ? 'html[data-theme="dark"] body { background: color-mix(in srgb, #000 86%, transparent); }' : ''}
|
||||
${isDesktop ? 'html[data-theme="light"] body { background: color-mix(in srgb, #f8f8f8 86%, transparent); }' : ''}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return <StyleProvider cache={extractStaticStyle.cache}>{children}</StyleProvider>;
|
||||
return <StyleProvider>{children}</StyleProvider>;
|
||||
};
|
||||
|
||||
export default StyleRegistry;
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ import AppTheme from './AppTheme';
|
|||
import { GroupWizardProvider } from './GroupWizardProvider';
|
||||
import ImportSettings from './ImportSettings';
|
||||
import Locale from './Locale';
|
||||
import NextThemeProvider from './NextThemeProvider';
|
||||
import QueryProvider from './Query';
|
||||
import StoreInitialization from './StoreInitialization';
|
||||
import StyleRegistry from './StyleRegistry';
|
||||
|
||||
interface GlobalLayoutProps {
|
||||
appearance: string;
|
||||
children: ReactNode;
|
||||
isMobile: boolean;
|
||||
locale: string;
|
||||
|
|
@ -36,7 +36,7 @@ const GlobalLayout = async ({
|
|||
neutralColor,
|
||||
primaryColor,
|
||||
locale: userLocale,
|
||||
appearance,
|
||||
|
||||
isMobile,
|
||||
variants,
|
||||
}: GlobalLayoutProps) => {
|
||||
|
|
@ -45,44 +45,46 @@ const GlobalLayout = async ({
|
|||
// get default feature flags to use with ssr
|
||||
const serverFeatureFlags = getServerFeatureFlagsValue();
|
||||
const serverConfig = await getServerGlobalConfig();
|
||||
|
||||
return (
|
||||
<StyleRegistry>
|
||||
<Locale antdLocale={antdLocale} defaultLang={userLocale}>
|
||||
<AppTheme
|
||||
customFontFamily={appEnv.CUSTOM_FONT_FAMILY}
|
||||
customFontURL={appEnv.CUSTOM_FONT_URL}
|
||||
defaultAppearance={appearance}
|
||||
defaultNeutralColor={neutralColor as any}
|
||||
defaultPrimaryColor={primaryColor as any}
|
||||
globalCDN={appEnv.CDN_USE_GLOBAL}
|
||||
>
|
||||
<ServerConfigStoreProvider
|
||||
featureFlags={serverFeatureFlags}
|
||||
isMobile={isMobile}
|
||||
segmentVariants={variants}
|
||||
serverConfig={serverConfig}
|
||||
<NextThemeProvider>
|
||||
<AppTheme
|
||||
customFontFamily={appEnv.CUSTOM_FONT_FAMILY}
|
||||
customFontURL={appEnv.CUSTOM_FONT_URL}
|
||||
defaultNeutralColor={neutralColor as any}
|
||||
defaultPrimaryColor={primaryColor as any}
|
||||
globalCDN={appEnv.CDN_USE_GLOBAL}
|
||||
>
|
||||
<QueryProvider>
|
||||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<TooltipGroup layoutAnimation={false}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</TooltipGroup>
|
||||
<ModalHost />
|
||||
<ContextMenuHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
</QueryProvider>
|
||||
<StoreInitialization />
|
||||
<Suspense>
|
||||
{ENABLE_BUSINESS_FEATURES ? <ReferralProvider /> : null}
|
||||
<ImportSettings />
|
||||
{process.env.NODE_ENV === 'development' && <DevPanel />}
|
||||
</Suspense>
|
||||
</ServerConfigStoreProvider>
|
||||
</AppTheme>
|
||||
<ServerConfigStoreProvider
|
||||
featureFlags={serverFeatureFlags}
|
||||
isMobile={isMobile}
|
||||
segmentVariants={variants}
|
||||
serverConfig={serverConfig}
|
||||
>
|
||||
<QueryProvider>
|
||||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<TooltipGroup layoutAnimation={false}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</TooltipGroup>
|
||||
<ModalHost />
|
||||
<ContextMenuHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
</QueryProvider>
|
||||
<StoreInitialization />
|
||||
<Suspense>
|
||||
{ENABLE_BUSINESS_FEATURES ? <ReferralProvider /> : null}
|
||||
<ImportSettings />
|
||||
{process.env.NODE_ENV === 'development' && <DevPanel />}
|
||||
</Suspense>
|
||||
</ServerConfigStoreProvider>
|
||||
</AppTheme>
|
||||
</NextThemeProvider>
|
||||
</Locale>
|
||||
</StyleRegistry>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
||||
import { parseDefaultThemeFromCountry } from '@lobechat/utils/server';
|
||||
import debug from 'debug';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
|
@ -8,7 +7,6 @@ import urlJoin from 'url-join';
|
|||
import { auth } from '@/auth';
|
||||
import { OAUTH_AUTHORIZED } from '@/const/auth';
|
||||
import { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
||||
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { appEnv } from '@/envs/app';
|
||||
import { authEnv } from '@/envs/auth';
|
||||
|
|
@ -39,10 +37,6 @@ export function defineConfig() {
|
|||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 1. Read user preferences from cookies
|
||||
const theme = (request.cookies.get(LOBE_THEME_APPEARANCE)?.value ||
|
||||
parseDefaultThemeFromCountry(request)) as 'dark' | 'light';
|
||||
|
||||
// locale has three levels
|
||||
// 1. search params
|
||||
// 2. cookie
|
||||
|
|
@ -67,17 +61,14 @@ export function defineConfig() {
|
|||
deviceType: device.type,
|
||||
hasCookies: {
|
||||
locale: !!request.cookies.get(LOBE_LOCALE_COOKIE)?.value,
|
||||
theme: !!request.cookies.get(LOBE_THEME_APPEARANCE)?.value,
|
||||
},
|
||||
locale,
|
||||
theme,
|
||||
});
|
||||
|
||||
// 2. Create normalized preference values
|
||||
const route = RouteVariants.serializeVariants({
|
||||
isMobile: device.type === 'mobile',
|
||||
locale,
|
||||
theme,
|
||||
});
|
||||
|
||||
logDefault('Serialized route variant: %s', route);
|
||||
|
|
@ -99,8 +90,8 @@ export function defineConfig() {
|
|||
|
||||
// refs: https://github.com/lobehub/lobe-chat/pull/5866
|
||||
// new handle segment rewrite: /${route}${originalPathname}
|
||||
// / -> /zh-CN__0__dark
|
||||
// /discover -> /zh-CN__0__dark/discover
|
||||
// / -> /zh-CN__0
|
||||
// /discover -> /zh-CN__0/discover
|
||||
// All SPA routes that use react-router-dom should be rewritten to just /${route}
|
||||
const spaRoutes = [
|
||||
'/chat',
|
||||
|
|
|
|||
|
|
@ -408,19 +408,4 @@ describe('createPreferenceSlice', () => {
|
|||
expect(result.current.status.noWideScreen).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('switchThemeMode', () => {
|
||||
it('should switch theme mode', async () => {
|
||||
const { result } = renderHook(() => useGlobalStore());
|
||||
|
||||
// Perform the action
|
||||
act(() => {
|
||||
useGlobalStore.setState({ isStatusInit: true });
|
||||
result.current.switchThemeMode('light');
|
||||
});
|
||||
|
||||
// Assert that updateUserSettings was called with the correct theme mode
|
||||
expect(result.current.status.themeMode).toEqual('light');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ThemeMode } from 'antd-style';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { withSWR } from '~test-utils';
|
||||
|
||||
|
|
@ -117,42 +116,6 @@ describe('generalActionSlice', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('switchThemeMode', () => {
|
||||
it('should update theme mode in system status', () => {
|
||||
const { result } = renderHook(() => useGlobalStore());
|
||||
const themeMode: ThemeMode = 'dark';
|
||||
|
||||
act(() => {
|
||||
useGlobalStore.setState({ isStatusInit: true });
|
||||
result.current.switchThemeMode(themeMode);
|
||||
});
|
||||
|
||||
expect(result.current.status.themeMode).toBe(themeMode);
|
||||
});
|
||||
|
||||
it('should not update theme mode if status is not initialized', () => {
|
||||
const { result } = renderHook(() => useGlobalStore());
|
||||
const themeMode: ThemeMode = 'dark';
|
||||
|
||||
act(() => {
|
||||
result.current.switchThemeMode(themeMode);
|
||||
});
|
||||
|
||||
expect(result.current.status.themeMode).toBe(initialState.status.themeMode);
|
||||
});
|
||||
|
||||
it('should handle light theme mode', () => {
|
||||
const { result } = renderHook(() => useGlobalStore());
|
||||
|
||||
act(() => {
|
||||
useGlobalStore.setState({ isStatusInit: true });
|
||||
result.current.switchThemeMode('light');
|
||||
});
|
||||
|
||||
expect(result.current.status.themeMode).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useInitSystemStatus', () => {
|
||||
it('should reset transient UI states when loading from localStorage', async () => {
|
||||
const mockStatus = {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { type ThemeMode } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { gt, parse, valid } from 'semver';
|
||||
import { type SWRResponse } from 'swr';
|
||||
import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
||||
import { CURRENT_VERSION, isDesktop } from '@/const/version';
|
||||
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
||||
import { globalService } from '@/services/global';
|
||||
import type { SystemStatus } from '@/store/global/initialState';
|
||||
import { type LocaleMode } from '@/types/locale';
|
||||
import { setCookie } from '@/utils/client/cookie';
|
||||
import { switchLang } from '@/utils/client/switchLang';
|
||||
import { merge } from '@/utils/merge';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
|
@ -23,7 +20,6 @@ export interface GlobalGeneralAction {
|
|||
openAgentInNewWindow: (agentId: string) => Promise<void>;
|
||||
openTopicInNewWindow: (agentId: string, topicId: string) => Promise<void>;
|
||||
switchLocale: (locale: LocaleMode, params?: { skipBroadcast?: boolean }) => void;
|
||||
switchThemeMode: (themeMode: ThemeMode, params?: { skipBroadcast?: boolean }) => void;
|
||||
updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
|
||||
useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
|
||||
useInitSystemStatus: () => SWRResponse;
|
||||
|
|
@ -114,23 +110,6 @@ export const generalActionSlice: StateCreator<
|
|||
})();
|
||||
}
|
||||
},
|
||||
switchThemeMode: (themeMode, { skipBroadcast } = {}) => {
|
||||
get().updateSystemStatus({ themeMode });
|
||||
|
||||
setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
|
||||
|
||||
if (isDesktop && !skipBroadcast) {
|
||||
(async () => {
|
||||
try {
|
||||
const { ensureElectronIpc } = await import('@/utils/electron/ipc');
|
||||
|
||||
await ensureElectronIpc().system.updateThemeModeHandler(themeMode);
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme in main process:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
updateSystemStatus: (status, action) => {
|
||||
if (!get().isStatusInit) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ThemeMode } from 'antd-style';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
import { DatabaseLoadingState, type MigrationSQL, type MigrationTableItem } from '@/types/clientDB';
|
||||
|
|
@ -129,10 +128,6 @@ export interface SystemStatus {
|
|||
showRightPanel?: boolean;
|
||||
showSystemRole?: boolean;
|
||||
systemRoleExpandedMap: Record<string, boolean>;
|
||||
/**
|
||||
* theme mode
|
||||
*/
|
||||
themeMode?: ThemeMode;
|
||||
/**
|
||||
* 是否使用短格式显示 token
|
||||
*/
|
||||
|
|
@ -196,7 +191,6 @@ export const INITIAL_STATUS = {
|
|||
showRightPanel: true,
|
||||
showSystemRole: false,
|
||||
systemRoleExpandedMap: {},
|
||||
themeMode: 'auto',
|
||||
tokenDisplayFormatShort: true,
|
||||
topicPageSize: 20,
|
||||
zenMode: false,
|
||||
|
|
|
|||
|
|
@ -87,24 +87,4 @@ describe('systemStatusSelectors', () => {
|
|||
expect(systemStatusSelectors.portalWidth(noPortalWidth)).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('theme mode', () => {
|
||||
it('should return the correct theme', () => {
|
||||
const s: GlobalState = merge(initialState, {
|
||||
status: {
|
||||
themeMode: 'light',
|
||||
},
|
||||
});
|
||||
expect(systemStatusSelectors.themeMode(s)).toBe('light');
|
||||
});
|
||||
|
||||
it('should return auto if not set', () => {
|
||||
const s: GlobalState = merge(initialState, {
|
||||
status: {
|
||||
themeMode: undefined,
|
||||
},
|
||||
});
|
||||
expect(systemStatusSelectors.themeMode(s)).toBe('auto');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ const showImagePanel = (s: GlobalState) => s.status.showImagePanel;
|
|||
const showImageTopicPanel = (s: GlobalState) => s.status.showImageTopicPanel;
|
||||
const hidePWAInstaller = (s: GlobalState) => s.status.hidePWAInstaller;
|
||||
const isShowCredit = (s: GlobalState) => s.status.isShowCredit;
|
||||
const themeMode = (s: GlobalState) => s.status.themeMode || 'auto';
|
||||
const language = (s: GlobalState) => s.status.language || 'auto';
|
||||
|
||||
const showChatHeader = (s: GlobalState) => !s.status.zenMode;
|
||||
|
|
@ -80,7 +79,6 @@ export const systemStatusSelectors = {
|
|||
showRightPanel,
|
||||
showSystemRole,
|
||||
systemStatus,
|
||||
themeMode,
|
||||
tokenDisplayFormatShort,
|
||||
topicGroupKeys,
|
||||
topicPageSize,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css`
|
|||
min-height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
|
||||
@media (min-device-width: 576px) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ describe('RouteVariants', () => {
|
|||
expect(DEFAULT_VARIANTS).toEqual({
|
||||
isMobile: false,
|
||||
locale: DEFAULT_LANG,
|
||||
theme: 'light',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -21,30 +20,27 @@ describe('RouteVariants', () => {
|
|||
const variants: IRouteVariants = {
|
||||
isMobile: false,
|
||||
locale: 'en-US',
|
||||
theme: 'light',
|
||||
};
|
||||
const result = RouteVariants.serializeVariants(variants);
|
||||
expect(result).toBe('en-US__0__light');
|
||||
expect(result).toBe('en-US__0');
|
||||
});
|
||||
|
||||
it('should serialize variants with mobile enabled', () => {
|
||||
const variants: IRouteVariants = {
|
||||
isMobile: true,
|
||||
locale: 'zh-CN',
|
||||
theme: 'dark',
|
||||
};
|
||||
const result = RouteVariants.serializeVariants(variants);
|
||||
expect(result).toBe('zh-CN__1__dark');
|
||||
expect(result).toBe('zh-CN__1');
|
||||
});
|
||||
|
||||
it('should serialize variants with different locales', () => {
|
||||
const variants: IRouteVariants = {
|
||||
isMobile: false,
|
||||
locale: 'ja-JP',
|
||||
theme: 'light',
|
||||
};
|
||||
const result = RouteVariants.serializeVariants(variants);
|
||||
expect(result).toBe('ja-JP__0__light');
|
||||
expect(result).toBe('ja-JP__0');
|
||||
});
|
||||
|
||||
it('should serialize variants with custom colors', () => {
|
||||
|
|
@ -53,31 +49,28 @@ describe('RouteVariants', () => {
|
|||
locale: 'en-US',
|
||||
neutralColor: '#cccccc',
|
||||
primaryColor: '#ff0000',
|
||||
theme: 'dark',
|
||||
};
|
||||
const result = RouteVariants.serializeVariants(variants);
|
||||
expect(result).toBe('en-US__1__dark');
|
||||
expect(result).toBe('en-US__1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deserializeVariants', () => {
|
||||
it('should deserialize valid serialized string', () => {
|
||||
const serialized = 'en-US__0__light';
|
||||
const serialized = 'en-US__0';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result).toEqual({
|
||||
isMobile: false,
|
||||
locale: 'en-US',
|
||||
theme: 'light',
|
||||
});
|
||||
});
|
||||
|
||||
it('should deserialize mobile variants', () => {
|
||||
const serialized = 'zh-CN__1__dark';
|
||||
const serialized = 'zh-CN__1';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result).toEqual({
|
||||
isMobile: true,
|
||||
locale: 'zh-CN',
|
||||
theme: 'dark',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -94,22 +87,11 @@ describe('RouteVariants', () => {
|
|||
});
|
||||
|
||||
it('should handle invalid locale by falling back to default', () => {
|
||||
const serialized = 'invalid-locale__0__light';
|
||||
const serialized = 'invalid-locale__0';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result).toEqual({
|
||||
isMobile: false,
|
||||
locale: DEFAULT_LANG,
|
||||
theme: 'light',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid theme by falling back to default', () => {
|
||||
const serialized = 'en-US__0__invalid-theme';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result).toEqual({
|
||||
isMobile: false,
|
||||
locale: 'en-US',
|
||||
theme: 'light',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -120,19 +102,19 @@ describe('RouteVariants', () => {
|
|||
});
|
||||
|
||||
it('should handle isMobile value correctly for "0"', () => {
|
||||
const serialized = 'en-US__0__dark';
|
||||
const serialized = 'en-US__0';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result.isMobile).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle isMobile value correctly for "1"', () => {
|
||||
const serialized = 'en-US__1__dark';
|
||||
const serialized = 'en-US__1';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result.isMobile).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle isMobile value correctly for other values', () => {
|
||||
const serialized = 'en-US__2__dark';
|
||||
const serialized = 'en-US__2';
|
||||
const result = RouteVariants.deserializeVariants(serialized);
|
||||
expect(result.isMobile).toBe(false);
|
||||
});
|
||||
|
|
@ -141,25 +123,23 @@ describe('RouteVariants', () => {
|
|||
describe('getVariantsFromProps', () => {
|
||||
it('should extract and deserialize variants from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'en-US__0__light' }),
|
||||
params: Promise.resolve({ variants: 'en-US__0' }),
|
||||
};
|
||||
const result = await RouteVariants.getVariantsFromProps(props);
|
||||
expect(result).toEqual({
|
||||
isMobile: false,
|
||||
locale: 'en-US',
|
||||
theme: 'light',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mobile variants from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'zh-CN__1__dark' }),
|
||||
params: Promise.resolve({ variants: 'zh-CN__1' }),
|
||||
};
|
||||
const result = await RouteVariants.getVariantsFromProps(props);
|
||||
expect(result).toEqual({
|
||||
isMobile: true,
|
||||
locale: 'zh-CN',
|
||||
theme: 'dark',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -175,7 +155,7 @@ describe('RouteVariants', () => {
|
|||
describe('getIsMobile', () => {
|
||||
it('should extract isMobile as false from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'en-US__0__light' }),
|
||||
params: Promise.resolve({ variants: 'en-US__0' }),
|
||||
};
|
||||
const result = await RouteVariants.getIsMobile(props);
|
||||
expect(result).toBe(false);
|
||||
|
|
@ -183,7 +163,7 @@ describe('RouteVariants', () => {
|
|||
|
||||
it('should extract isMobile as true from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'en-US__1__dark' }),
|
||||
params: Promise.resolve({ variants: 'en-US__1' }),
|
||||
};
|
||||
const result = await RouteVariants.getIsMobile(props);
|
||||
expect(result).toBe(true);
|
||||
|
|
@ -201,7 +181,7 @@ describe('RouteVariants', () => {
|
|||
describe('getLocale', () => {
|
||||
it('should extract locale from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'zh-CN__0__light' }),
|
||||
params: Promise.resolve({ variants: 'zh-CN__0' }),
|
||||
};
|
||||
const result = await RouteVariants.getLocale(props);
|
||||
expect(result).toBe('zh-CN');
|
||||
|
|
@ -209,7 +189,7 @@ describe('RouteVariants', () => {
|
|||
|
||||
it('should extract different locale from props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'ja-JP__1__dark' }),
|
||||
params: Promise.resolve({ variants: 'ja-JP__1' }),
|
||||
};
|
||||
const result = await RouteVariants.getLocale(props);
|
||||
expect(result).toBe('ja-JP');
|
||||
|
|
@ -225,7 +205,7 @@ describe('RouteVariants', () => {
|
|||
|
||||
it('should return default locale for invalid locale in props', async () => {
|
||||
const props: DynamicLayoutProps = {
|
||||
params: Promise.resolve({ variants: 'invalid-locale__0__light' }),
|
||||
params: Promise.resolve({ variants: 'invalid-locale__0' }),
|
||||
};
|
||||
const result = await RouteVariants.getLocale(props);
|
||||
expect(result).toBe(DEFAULT_LANG);
|
||||
|
|
@ -254,24 +234,14 @@ describe('RouteVariants', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should create variants with custom theme', () => {
|
||||
const result = RouteVariants.createVariants({ theme: 'dark' });
|
||||
expect(result).toEqual({
|
||||
...DEFAULT_VARIANTS,
|
||||
theme: 'dark',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create variants with multiple custom options', () => {
|
||||
const result = RouteVariants.createVariants({
|
||||
isMobile: true,
|
||||
locale: 'ja-JP',
|
||||
theme: 'dark',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
isMobile: true,
|
||||
locale: 'ja-JP',
|
||||
theme: 'dark',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -293,14 +263,12 @@ describe('RouteVariants', () => {
|
|||
locale: 'zh-CN',
|
||||
neutralColor: '#aaaaaa',
|
||||
primaryColor: '#00ff00',
|
||||
theme: 'dark',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
isMobile: true,
|
||||
locale: 'zh-CN',
|
||||
neutralColor: '#aaaaaa',
|
||||
primaryColor: '#00ff00',
|
||||
theme: 'dark',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -310,7 +278,6 @@ describe('RouteVariants', () => {
|
|||
const original: IRouteVariants = {
|
||||
isMobile: true,
|
||||
locale: 'zh-CN',
|
||||
theme: 'dark',
|
||||
};
|
||||
const serialized = RouteVariants.serializeVariants(original);
|
||||
const deserialized = RouteVariants.deserializeVariants(serialized);
|
||||
|
|
@ -329,7 +296,6 @@ describe('RouteVariants', () => {
|
|||
const original: IRouteVariants = {
|
||||
isMobile: false,
|
||||
locale: locale as any,
|
||||
theme: 'light',
|
||||
};
|
||||
const serialized = RouteVariants.serializeVariants(original);
|
||||
const deserialized = RouteVariants.deserializeVariants(serialized);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { RouteVariants } from '@lobechat/desktop-bridge';
|
|||
import { type DynamicLayoutProps } from '@/types/next';
|
||||
|
||||
export { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
||||
export { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
||||
export {
|
||||
DEFAULT_LANG,
|
||||
DEFAULT_VARIANTS,
|
||||
|
|
|
|||
Loading…
Reference in a new issue