♻️ refactor(nav): remove devOnly mode from nav layout and stabilize Footer (#13101)

* ♻️ refactor(nav): remove devOnly mode from nav layout and stabilize Footer during panel transitions

- Remove devOnly filtering from useNavLayout, treat all items as non-dev mode
- Move Pages to top nav position, remove video/image/settings/memory nav items
- Extract Footer from SideBarLayout into NavPanelDraggable outside animation layer
- Show settings ActionIcon in Footer when dev mode is enabled (hidden on settings page)

* 🔧 fix(footer): update settings icon in Footer component

- Replace Settings2 icon with Settings icon in the Footer when dev mode is enabled, ensuring consistency in the user interface.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-03-18 15:44:51 +08:00
committed by GitHub
parent 91ec7b412b
commit 8a90f79c11
5 changed files with 69 additions and 116 deletions

View File

@@ -3,15 +3,13 @@ import { type ReactNode } from 'react';
import { memo, Suspense } from 'react';
import SkeletonList, { SkeletonItem } from '@/features/NavPanel/components/SkeletonList';
import Footer from '@/routes/(main)/home/_layout/Footer';
interface SidebarLayoutProps {
body?: ReactNode;
footer?: ReactNode;
header?: ReactNode;
}
const SideBarLayout = memo<SidebarLayoutProps>(({ header, body, footer }) => {
const SideBarLayout = memo<SidebarLayoutProps>(({ header, body }) => {
return (
<Flexbox gap={4} style={{ height: '100%', overflow: 'hidden' }}>
<Suspense fallback={<SkeletonItem height={44} style={{ marginTop: 8 }} />}>{header}</Suspense>
@@ -20,7 +18,6 @@ const SideBarLayout = memo<SidebarLayoutProps>(({ header, body, footer }) => {
<Suspense fallback={<SkeletonList paddingBlock={8} />}>{body}</Suspense>
</TooltipGroup>
</ScrollShadow>
<Suspense>{footer || <Footer />}</Suspense>
</Flexbox>
);
});

View File

@@ -4,10 +4,11 @@ import { DraggablePanel, Freeze } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import { AnimatePresence, motion, useIsPresent } from 'motion/react';
import { type ReactNode } from 'react';
import { memo, useLayoutEffect, useMemo, useRef } from 'react';
import { memo, Suspense, useLayoutEffect, useMemo, useRef } from 'react';
import { isDesktop } from '@/const/version';
import { TOGGLE_BUTTON_ID } from '@/features/NavPanel/ToggleLeftPanelButton';
import Footer from '@/routes/(main)/home/_layout/Footer';
import { USER_DROPDOWN_ICON_ID } from '@/routes/(main)/home/_layout/Header/components/User';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
@@ -53,6 +54,7 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
min-height: 100%;
@@ -66,8 +68,7 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
min-width: 240px;
max-width: 100%;
min-height: 100%;
max-height: 100%;
min-height: 0;
`,
layer: css`
will-change: opacity, transform;
@@ -245,6 +246,9 @@ export const NavPanelDraggable = memo<NavPanelDraggableProps>(({ activeContent }
</div>
)}
</div>
<Suspense>
<Footer />
</Suspense>
</DraggablePanel>
);
});

View File

@@ -5,19 +5,9 @@ import { useTranslation } from 'react-i18next';
import { getRouteById } from '@/config/routes';
import { useGlobalStore } from '@/store/global';
import { SidebarTabKey } from '@/store/global/initialState';
import {
featureFlagsSelectors,
serverConfigSelectors,
useServerConfigStore,
} from '@/store/serverConfig';
import { useUserStore } from '@/store/user';
import { userGeneralSettingsSelectors } from '@/store/user/slices/settings/selectors';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
export interface NavItem {
/**
* true = dev mode only, false = simplified mode only, undefined = always
*/
devOnly?: boolean;
hidden?: boolean;
icon: any;
isNew?: boolean;
@@ -42,121 +32,72 @@ export interface NavLayout {
};
}
const filterByMode = (items: NavItem[], isDevMode: boolean): NavItem[] =>
items.filter((item) => item.devOnly === undefined || item.devOnly === isDevMode);
export const useNavLayout = (): NavLayout => {
const { t } = useTranslation('common');
const toggleCommandMenu = useGlobalStore((s) => s.toggleCommandMenu);
const { showMarket, showAiImage, hideGitHub } = useServerConfigStore(featureFlagsSelectors);
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode);
const { showMarket, hideGitHub } = useServerConfigStore(featureFlagsSelectors);
const topNavItems = useMemo(
() =>
filterByMode(
[
{
icon: SearchIcon,
key: 'search',
onClick: () => toggleCommandMenu(true),
title: t('tab.search'),
},
{
icon: HomeIcon,
key: SidebarTabKey.Home,
title: t('tab.home'),
url: '/',
},
{
devOnly: true,
icon: getRouteById('page')!.icon,
key: SidebarTabKey.Pages,
title: t('tab.pages'),
url: '/page',
},
{
devOnly: true,
hidden: !enableBusinessFeatures,
icon: getRouteById('video')!.icon,
key: SidebarTabKey.Video,
title: t('tab.video'),
url: '/video',
},
{
devOnly: true,
hidden: !showAiImage,
icon: getRouteById('image')!.icon,
key: SidebarTabKey.Image,
title: t('tab.aiImage'),
url: '/image',
},
{
hidden: !showMarket,
icon: getRouteById('community')!.icon,
key: SidebarTabKey.Community,
title: t('tab.marketplace'),
url: '/community',
},
],
isDevMode,
),
[t, toggleCommandMenu, showMarket, isDevMode, enableBusinessFeatures, showAiImage],
[
{
icon: SearchIcon,
key: 'search',
onClick: () => toggleCommandMenu(true),
title: t('tab.search'),
},
{
icon: HomeIcon,
key: SidebarTabKey.Home,
title: t('tab.home'),
url: '/',
},
{
icon: getRouteById('page')!.icon,
key: SidebarTabKey.Pages,
title: t('tab.pages'),
url: '/page',
},
{
hidden: !showMarket,
icon: getRouteById('community')!.icon,
key: SidebarTabKey.Community,
title: t('tab.marketplace'),
url: '/community',
},
] as NavItem[],
[t, toggleCommandMenu, showMarket],
);
const bottomMenuItems = useMemo(
() =>
filterByMode(
[
{
devOnly: true,
icon: getRouteById('settings')!.icon,
key: SidebarTabKey.Setting,
title: t('tab.setting'),
url: '/settings',
},
{
icon: getRouteById('resource')!.icon,
key: SidebarTabKey.Resource,
title: t('tab.resource'),
url: '/resource',
},
{
devOnly: true,
icon: getRouteById('memory')!.icon,
key: SidebarTabKey.Memory,
title: t('tab.memory'),
url: '/memory',
},
{
devOnly: false,
icon: getRouteById('page')!.icon,
key: SidebarTabKey.Pages,
title: t('tab.pages'),
url: '/page',
},
],
isDevMode,
),
[t, isDevMode],
[
{
icon: getRouteById('resource')!.icon,
key: SidebarTabKey.Resource,
title: t('tab.resource'),
url: '/resource',
},
] as NavItem[],
[t],
);
const footer = useMemo(
() => ({
hideGitHub: !!hideGitHub,
layout: (isDevMode ? 'expanded' : 'compact') as 'expanded' | 'compact',
showEvalEntry: isDevMode,
showSettingsEntry: !isDevMode,
layout: 'compact' as const,
showEvalEntry: false,
showSettingsEntry: true,
}),
[isDevMode, hideGitHub],
[hideGitHub],
);
const userPanel = useMemo(
() => ({
showDataImporter: isDevMode,
showMemory: !isDevMode,
showDataImporter: false,
showMemory: true,
}),
[isDevMode],
[],
);
return { bottomMenuItems, footer, topNavItems, userPanel };

View File

@@ -13,11 +13,12 @@ import {
FlaskConical,
Github,
Rocket,
Settings,
Settings2,
} from 'lucide-react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import ChangelogModal from '@/components/ChangelogModal';
import HighlightNotification from '@/components/HighlightNotification';
@@ -27,6 +28,8 @@ import { useFeedbackModal } from '@/hooks/useFeedbackModal';
import { useNavLayout } from '@/hooks/useNavLayout';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors/systemStatus';
import { useUserStore } from '@/store/user';
import { userGeneralSettingsSelectors } from '@/store/user/slices/settings/selectors';
const PRODUCT_HUNT_NOTIFICATION = {
actionHref: 'https://www.producthunt.com/products/lobehub?launch=lobehub',
@@ -40,6 +43,9 @@ const Footer = memo(() => {
const { t } = useTranslation('common');
const { analytics } = useAnalytics();
const { footer } = useNavLayout();
const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode);
const location = useLocation();
const isSettingsPage = location.pathname.startsWith('/settings');
const [shouldLoadChangelog, setShouldLoadChangelog] = useState(false);
const [isChangelogModalOpen, setIsChangelogModalOpen] = useState(false);
const [isProductHuntCardOpen, setIsProductHuntCardOpen] = useState(false);
@@ -119,7 +125,7 @@ const Footer = memo(() => {
const helpMenuItems: MenuProps['items'] = useMemo(
() => [
...(footer.showSettingsEntry
...(footer.showSettingsEntry && !isDevMode
? [
{
icon: <Icon icon={Settings2} />,
@@ -202,6 +208,7 @@ const Footer = memo(() => {
footer.layout,
footer.hideGitHub,
footer.showEvalEntry,
isDevMode,
t,
handleOpenFeedbackModal,
isWithinTimeWindow,
@@ -233,6 +240,11 @@ const Footer = memo(() => {
<DropdownMenu items={helpMenuItems} placement="topLeft">
<ActionIcon aria-label={t('userPanel.help')} icon={CircleHelp} size={16} />
</DropdownMenu>
{isDevMode && !isSettingsPage && (
<Link to="/settings">
<ActionIcon aria-label={t('userPanel.setting')} icon={Settings} size={16} />
</Link>
)}
</Flexbox>
)}
<ChangelogModal

View File

@@ -4,13 +4,12 @@ import SideBarLayout from '@/features/NavPanel/SideBarLayout';
import Body from './Body';
import { AgentModalProvider } from './Body/Agent/ModalProvider';
import Footer from './Footer';
import Header from './Header';
const Sidebar = memo(() => {
return (
<AgentModalProvider>
<SideBarLayout body={<Body />} footer={<Footer />} header={<Header />} />
<SideBarLayout body={<Body />} header={<Header />} />
</AgentModalProvider>
);
});