@lobehub/lobehub 2.0.0-next.240 → 2.0.0-next.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/resources/locales/ar/menu.json +5 -1
  3. package/apps/desktop/resources/locales/bg-BG/menu.json +5 -1
  4. package/apps/desktop/resources/locales/de-DE/menu.json +5 -1
  5. package/apps/desktop/resources/locales/es-ES/menu.json +5 -1
  6. package/apps/desktop/resources/locales/fa-IR/menu.json +5 -1
  7. package/apps/desktop/resources/locales/fr-FR/menu.json +5 -1
  8. package/apps/desktop/resources/locales/it-IT/menu.json +5 -1
  9. package/apps/desktop/resources/locales/ja-JP/menu.json +5 -1
  10. package/apps/desktop/resources/locales/ko-KR/menu.json +5 -1
  11. package/apps/desktop/resources/locales/nl-NL/menu.json +5 -1
  12. package/apps/desktop/resources/locales/pl-PL/menu.json +5 -1
  13. package/apps/desktop/resources/locales/pt-BR/menu.json +5 -1
  14. package/apps/desktop/resources/locales/ru-RU/menu.json +5 -1
  15. package/apps/desktop/resources/locales/tr-TR/menu.json +5 -1
  16. package/apps/desktop/resources/locales/vi-VN/menu.json +5 -1
  17. package/apps/desktop/resources/locales/zh-CN/menu.json +5 -1
  18. package/apps/desktop/resources/locales/zh-TW/menu.json +5 -1
  19. package/apps/desktop/src/main/locales/default/menu.ts +5 -1
  20. package/apps/desktop/src/main/menus/impls/linux.ts +30 -0
  21. package/apps/desktop/src/main/menus/impls/macOS.test.ts +17 -0
  22. package/apps/desktop/src/main/menus/impls/macOS.ts +33 -0
  23. package/apps/desktop/src/main/menus/impls/windows.ts +30 -0
  24. package/changelog/v1.json +5 -0
  25. package/locales/ar/electron.json +24 -0
  26. package/locales/bg-BG/electron.json +24 -0
  27. package/locales/de-DE/electron.json +24 -0
  28. package/locales/en-US/electron.json +24 -0
  29. package/locales/es-ES/electron.json +24 -0
  30. package/locales/fa-IR/electron.json +24 -0
  31. package/locales/fr-FR/electron.json +24 -0
  32. package/locales/it-IT/electron.json +24 -0
  33. package/locales/ja-JP/electron.json +24 -0
  34. package/locales/ko-KR/electron.json +24 -0
  35. package/locales/nl-NL/electron.json +24 -0
  36. package/locales/pl-PL/electron.json +24 -0
  37. package/locales/pt-BR/electron.json +24 -0
  38. package/locales/ru-RU/electron.json +24 -0
  39. package/locales/tr-TR/electron.json +24 -0
  40. package/locales/vi-VN/electron.json +24 -0
  41. package/locales/zh-CN/electron.json +24 -0
  42. package/locales/zh-TW/electron.json +24 -0
  43. package/package.json +1 -1
  44. package/packages/electron-client-ipc/src/events/navigation.ts +12 -0
  45. package/src/components/PageTitle/index.tsx +11 -1
  46. package/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx +137 -0
  47. package/src/features/ElectronTitlebar/NavigationBar/index.tsx +86 -0
  48. package/src/features/ElectronTitlebar/helpers/routeMetadata.ts +214 -0
  49. package/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts +152 -0
  50. package/src/features/ElectronTitlebar/index.tsx +13 -5
  51. package/src/features/NavHeader/index.tsx +4 -2
  52. package/src/features/NavPanel/components/NavPanelDraggable.tsx +174 -0
  53. package/src/features/NavPanel/hooks/useNavPanel.ts +11 -35
  54. package/src/features/NavPanel/index.tsx +2 -126
  55. package/src/hooks/useTypeScriptHappyCallback.ts +7 -0
  56. package/src/locales/default/electron.ts +24 -0
  57. package/src/store/electron/actions/navigationHistory.ts +247 -0
  58. package/src/store/electron/initialState.ts +7 -1
  59. package/src/store/electron/store.ts +9 -2
  60. package/src/store/global/selectors/systemStatus.ts +4 -1
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Route metadata mapping for navigation history
3
+ * Provides title and icon information based on route path
4
+ */
5
+ import {
6
+ Brain,
7
+ Circle,
8
+ Compass,
9
+ Database,
10
+ FileText,
11
+ Home,
12
+ Image,
13
+ type LucideIcon,
14
+ MessageSquare,
15
+ Rocket,
16
+ Settings,
17
+ Users,
18
+ } from 'lucide-react';
19
+
20
+ export interface RouteMetadata {
21
+ icon?: LucideIcon;
22
+ /** i18n key for the title (namespace: electron) */
23
+ titleKey: string;
24
+ /** Whether this route should use document.title for more specific title */
25
+ useDynamicTitle?: boolean;
26
+ }
27
+
28
+ interface RoutePattern {
29
+ icon?: LucideIcon;
30
+ test: (pathname: string) => boolean;
31
+ /** i18n key for the title (namespace: electron) */
32
+ titleKey: string;
33
+ /** Whether this route should use document.title for more specific title */
34
+ useDynamicTitle?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Route patterns ordered by specificity (most specific first)
39
+ */
40
+ const routePatterns: RoutePattern[] = [
41
+ // Settings routes
42
+ {
43
+ icon: Settings,
44
+ test: (p) => p.startsWith('/settings/provider'),
45
+ titleKey: 'navigation.provider',
46
+ },
47
+ {
48
+ icon: Settings,
49
+ test: (p) => p.startsWith('/settings'),
50
+ titleKey: 'navigation.settings',
51
+ },
52
+
53
+ // Agent/Chat routes - use dynamic title for specific chat names
54
+ {
55
+ icon: MessageSquare,
56
+ test: (p) => p.startsWith('/agent/'),
57
+ titleKey: 'navigation.chat',
58
+ useDynamicTitle: true,
59
+ },
60
+ {
61
+ icon: MessageSquare,
62
+ test: (p) => p === '/agent',
63
+ titleKey: 'navigation.chat',
64
+ },
65
+
66
+ // Group routes - use dynamic title for specific group names
67
+ {
68
+ icon: Users,
69
+ test: (p) => p.startsWith('/group/'),
70
+ titleKey: 'navigation.groupChat',
71
+ useDynamicTitle: true,
72
+ },
73
+ {
74
+ icon: Users,
75
+ test: (p) => p === '/group',
76
+ titleKey: 'navigation.group',
77
+ },
78
+
79
+ // Community/Discover routes
80
+ {
81
+ icon: Compass,
82
+ test: (p) => p.startsWith('/community/assistant'),
83
+ titleKey: 'navigation.discoverAssistants',
84
+ },
85
+ {
86
+ icon: Compass,
87
+ test: (p) => p.startsWith('/community/model'),
88
+ titleKey: 'navigation.discoverModels',
89
+ },
90
+ {
91
+ icon: Compass,
92
+ test: (p) => p.startsWith('/community/provider'),
93
+ titleKey: 'navigation.discoverProviders',
94
+ },
95
+ {
96
+ icon: Compass,
97
+ test: (p) => p.startsWith('/community/mcp'),
98
+ titleKey: 'navigation.discoverMcp',
99
+ },
100
+ {
101
+ icon: Compass,
102
+ test: (p) => p.startsWith('/community'),
103
+ titleKey: 'navigation.discover',
104
+ },
105
+
106
+ // Resource/Knowledge routes
107
+ {
108
+ icon: Database,
109
+ test: (p) => p.startsWith('/resource/library'),
110
+ titleKey: 'navigation.knowledgeBase',
111
+ },
112
+ {
113
+ icon: Database,
114
+ test: (p) => p.startsWith('/resource'),
115
+ titleKey: 'navigation.resources',
116
+ },
117
+
118
+ // Memory routes
119
+ {
120
+ icon: Brain,
121
+ test: (p) => p.startsWith('/memory/identities'),
122
+ titleKey: 'navigation.memoryIdentities',
123
+ },
124
+ {
125
+ icon: Brain,
126
+ test: (p) => p.startsWith('/memory/contexts'),
127
+ titleKey: 'navigation.memoryContexts',
128
+ },
129
+ {
130
+ icon: Brain,
131
+ test: (p) => p.startsWith('/memory/preferences'),
132
+ titleKey: 'navigation.memoryPreferences',
133
+ },
134
+ {
135
+ icon: Brain,
136
+ test: (p) => p.startsWith('/memory/experiences'),
137
+ titleKey: 'navigation.memoryExperiences',
138
+ },
139
+ {
140
+ icon: Brain,
141
+ test: (p) => p.startsWith('/memory'),
142
+ titleKey: 'navigation.memory',
143
+ },
144
+
145
+ // Image routes
146
+ {
147
+ icon: Image,
148
+ test: (p) => p.startsWith('/image'),
149
+ titleKey: 'navigation.image',
150
+ },
151
+
152
+ // Page routes - use dynamic title for specific page names
153
+ {
154
+ icon: FileText,
155
+ test: (p) => p.startsWith('/page/'),
156
+ titleKey: 'navigation.page',
157
+ useDynamicTitle: true,
158
+ },
159
+ {
160
+ icon: FileText,
161
+ test: (p) => p === '/page',
162
+ titleKey: 'navigation.pages',
163
+ },
164
+
165
+ // Onboarding
166
+ {
167
+ icon: Rocket,
168
+ test: (p) => p.startsWith('/desktop-onboarding') || p.startsWith('/onboarding'),
169
+ titleKey: 'navigation.onboarding',
170
+ },
171
+
172
+ // Home (default)
173
+ {
174
+ icon: Home,
175
+ test: (p) => p === '/' || p === '',
176
+ titleKey: 'navigation.home',
177
+ },
178
+ ];
179
+
180
+ /**
181
+ * Get route metadata based on pathname
182
+ * @param pathname - The current route pathname
183
+ * @returns Route metadata with titleKey, icon, and useDynamicTitle flag
184
+ */
185
+ export const getRouteMetadata = (pathname: string): RouteMetadata => {
186
+ // Find the first matching pattern
187
+ for (const pattern of routePatterns) {
188
+ if (pattern.test(pathname)) {
189
+ return {
190
+ icon: pattern.icon,
191
+ titleKey: pattern.titleKey,
192
+ useDynamicTitle: pattern.useDynamicTitle,
193
+ };
194
+ }
195
+ }
196
+
197
+ // Default fallback
198
+ return {
199
+ icon: Circle,
200
+ titleKey: 'navigation.lobehub',
201
+ };
202
+ };
203
+
204
+ /**
205
+ * Get route icon based on pathname or URL
206
+ * @param url - The route URL (may include query string)
207
+ * @returns LucideIcon component or undefined
208
+ */
209
+ export const getRouteIcon = (url: string): LucideIcon | undefined => {
210
+ // Extract pathname from URL
211
+ const pathname = url.split('?')[0];
212
+ const metadata = getRouteMetadata(pathname);
213
+ return metadata.icon;
214
+ };
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
4
+ import { useCallback, useEffect, useRef } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useLocation, useNavigate } from 'react-router-dom';
7
+
8
+ import { useElectronStore } from '@/store/electron';
9
+
10
+ import { getRouteMetadata } from '../helpers/routeMetadata';
11
+
12
+ /**
13
+ * Hook to manage navigation history in Electron desktop app
14
+ * Provides browser-like back/forward functionality
15
+ */
16
+ export const useNavigationHistory = () => {
17
+ const { t } = useTranslation('electron');
18
+ const navigate = useNavigate();
19
+ const location = useLocation();
20
+
21
+ // Get store state and actions
22
+ const isNavigatingHistory = useElectronStore((s) => s.isNavigatingHistory);
23
+ const historyCurrentIndex = useElectronStore((s) => s.historyCurrentIndex);
24
+ const historyEntries = useElectronStore((s) => s.historyEntries);
25
+ const currentPageTitle = useElectronStore((s) => s.currentPageTitle);
26
+ const pushHistory = useElectronStore((s) => s.pushHistory);
27
+ const replaceHistory = useElectronStore((s) => s.replaceHistory);
28
+ const setIsNavigatingHistory = useElectronStore((s) => s.setIsNavigatingHistory);
29
+ const storeGoBack = useElectronStore((s) => s.goBack);
30
+ const storeGoForward = useElectronStore((s) => s.goForward);
31
+ const canGoBackFn = useElectronStore((s) => s.canGoBack);
32
+ const canGoForwardFn = useElectronStore((s) => s.canGoForward);
33
+ const getCurrentEntry = useElectronStore((s) => s.getCurrentEntry);
34
+
35
+ // Track previous location to avoid duplicate entries
36
+ const prevLocationRef = useRef<string | null>(null);
37
+
38
+ // Calculate can go back/forward
39
+ const canGoBack = historyCurrentIndex > 0;
40
+ const canGoForward = historyCurrentIndex < historyEntries.length - 1;
41
+
42
+ /**
43
+ * Go back in history
44
+ */
45
+ const goBack = useCallback(() => {
46
+ if (!canGoBackFn()) return;
47
+
48
+ const targetEntry = storeGoBack();
49
+ if (targetEntry) {
50
+ navigate(targetEntry.url);
51
+ }
52
+ }, [canGoBackFn, storeGoBack, navigate]);
53
+
54
+ /**
55
+ * Go forward in history
56
+ */
57
+ const goForward = useCallback(() => {
58
+ if (!canGoForwardFn()) return;
59
+
60
+ const targetEntry = storeGoForward();
61
+ if (targetEntry) {
62
+ navigate(targetEntry.url);
63
+ }
64
+ }, [canGoForwardFn, storeGoForward, navigate]);
65
+
66
+ // Listen to route changes and push history
67
+ useEffect(() => {
68
+ const currentUrl = location.pathname + location.search;
69
+
70
+ // Skip if this is a back/forward navigation
71
+ if (isNavigatingHistory) {
72
+ setIsNavigatingHistory(false);
73
+ prevLocationRef.current = currentUrl;
74
+ return;
75
+ }
76
+
77
+ // Skip if same as previous location
78
+ if (prevLocationRef.current === currentUrl) {
79
+ return;
80
+ }
81
+
82
+ // Skip if same as current entry
83
+ const currentEntry = getCurrentEntry();
84
+ if (currentEntry?.url === currentUrl) {
85
+ prevLocationRef.current = currentUrl;
86
+ return;
87
+ }
88
+
89
+ // Get metadata for this route
90
+ const metadata = getRouteMetadata(location.pathname);
91
+ const presetTitle = t(metadata.titleKey as any) as string;
92
+
93
+ // Push history with preset title (will be updated by PageTitle if useDynamicTitle)
94
+ pushHistory({
95
+ metadata: {
96
+ timestamp: Date.now(),
97
+ },
98
+ title: presetTitle,
99
+ url: currentUrl,
100
+ });
101
+
102
+ prevLocationRef.current = currentUrl;
103
+ }, [
104
+ location.pathname,
105
+ location.search,
106
+ isNavigatingHistory,
107
+ setIsNavigatingHistory,
108
+ getCurrentEntry,
109
+ pushHistory,
110
+ t,
111
+ ]);
112
+
113
+ // Update current history entry title when PageTitle component updates
114
+ useEffect(() => {
115
+ if (!currentPageTitle) return;
116
+
117
+ const currentEntry = getCurrentEntry();
118
+ if (!currentEntry) return;
119
+
120
+ // Check if current route supports dynamic title
121
+ const metadata = getRouteMetadata(location.pathname);
122
+ if (!metadata.useDynamicTitle) return;
123
+
124
+ // Skip if title is already the same
125
+ if (currentEntry.title === currentPageTitle) return;
126
+
127
+ // Update the current history entry with the dynamic title
128
+ replaceHistory({
129
+ ...currentEntry,
130
+ title: currentPageTitle,
131
+ });
132
+ }, [currentPageTitle, getCurrentEntry, replaceHistory, location.pathname]);
133
+
134
+ // Listen to broadcast events from main process (Electron menu)
135
+ useWatchBroadcast('historyGoBack', () => {
136
+ goBack();
137
+ });
138
+
139
+ useWatchBroadcast('historyGoForward', () => {
140
+ goForward();
141
+ });
142
+
143
+ return {
144
+ canGoBack,
145
+ canGoForward,
146
+ currentEntry: getCurrentEntry(),
147
+ goBack,
148
+ goForward,
149
+ historyEntries,
150
+ historyIndex: historyCurrentIndex,
151
+ };
152
+ };
@@ -1,12 +1,13 @@
1
1
  import { Flexbox } from '@lobehub/ui';
2
2
  import { Divider } from 'antd';
3
- import { memo } from 'react';
3
+ import { memo, useMemo } from 'react';
4
4
 
5
5
  import { useElectronStore } from '@/store/electron';
6
6
  import { electronStylish } from '@/styles/electron';
7
7
  import { isMacOS } from '@/utils/platform';
8
8
 
9
9
  import Connection from './Connection';
10
+ import NavigationBar from './NavigationBar';
10
11
  import { UpdateModal } from './UpdateModal';
11
12
  import { UpdateNotification } from './UpdateNotification';
12
13
  import WinControl from './WinControl';
@@ -25,6 +26,15 @@ const TitleBar = memo(() => {
25
26
  useWatchThemeUpdate();
26
27
 
27
28
  const showWinControl = isAppStateInit && !isMac;
29
+
30
+ const padding = useMemo(() => {
31
+ if (showWinControl) {
32
+ return '0 12px 0 0';
33
+ }
34
+
35
+ return '0 12px';
36
+ }, [showWinControl, isMac]);
37
+
28
38
  return (
29
39
  <Flexbox
30
40
  align={'center'}
@@ -32,12 +42,10 @@ const TitleBar = memo(() => {
32
42
  height={TITLE_BAR_HEIGHT}
33
43
  horizontal
34
44
  justify={'space-between'}
35
- paddingInline={showWinControl ? '12px 0' : 12}
36
- style={{ minHeight: TITLE_BAR_HEIGHT }}
45
+ style={{ minHeight: TITLE_BAR_HEIGHT, padding }}
37
46
  width={'100%'}
38
47
  >
39
- <div />
40
- <div>{/* TODO */}</div>
48
+ <NavigationBar />
41
49
 
42
50
  <Flexbox align={'center'} gap={4} horizontal>
43
51
  <Flexbox className={electronStylish.nodrag} gap={8} horizontal>
@@ -2,7 +2,8 @@ import { Flexbox, type FlexboxProps, TooltipGroup } from '@lobehub/ui';
2
2
  import { type CSSProperties, type ReactNode, memo } from 'react';
3
3
 
4
4
  import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
5
- import { useNavPanel } from '@/features/NavPanel/hooks/useNavPanel';
5
+ import { useGlobalStore } from '@/store/global';
6
+ import { systemStatusSelectors } from '@/store/global/selectors';
6
7
 
7
8
  export interface NavHeaderProps extends Omit<FlexboxProps, 'children'> {
8
9
  children?: ReactNode;
@@ -18,7 +19,8 @@ export interface NavHeaderProps extends Omit<FlexboxProps, 'children'> {
18
19
 
19
20
  const NavHeader = memo<NavHeaderProps>(
20
21
  ({ showTogglePanelButton = true, style, children, left, right, styles, ...rest }) => {
21
- const { expand } = useNavPanel();
22
+ const expand = useGlobalStore(systemStatusSelectors.showLeftPanel);
23
+
22
24
  const noContent = !left && !right;
23
25
 
24
26
  if (noContent && expand) return;
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+
3
+ import { DraggablePanel } from '@lobehub/ui';
4
+ import { createStaticStyles, cssVar } from 'antd-style';
5
+ import { AnimatePresence, motion } from 'motion/react';
6
+ import { type ReactNode, memo, useMemo, useRef } from 'react';
7
+
8
+ import { USER_DROPDOWN_ICON_ID } from '@/app/[variants]/(main)/home/_layout/Header/components/User';
9
+ import { isDesktop } from '@/const/version';
10
+ import { TOGGLE_BUTTON_ID } from '@/features/NavPanel/ToggleLeftPanelButton';
11
+ import { useGlobalStore } from '@/store/global';
12
+ import { systemStatusSelectors } from '@/store/global/selectors';
13
+ import { isMacOS } from '@/utils/platform';
14
+
15
+ import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
16
+ import { BACK_BUTTON_ID } from './BackButton';
17
+
18
+ const motionVariants = {
19
+ animate: { opacity: 1, x: 0 },
20
+ exit: {
21
+ opacity: 0,
22
+ x: '-20%',
23
+ },
24
+ initial: {
25
+ opacity: 0,
26
+ x: 0,
27
+ },
28
+ transition: {
29
+ duration: 0.4,
30
+ ease: [0.4, 0, 0.2, 1],
31
+ },
32
+ } as const;
33
+
34
+ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
35
+ content: css`
36
+ position: relative;
37
+
38
+ overflow: hidden;
39
+ display: flex;
40
+
41
+ height: 100%;
42
+ min-height: 100%;
43
+ max-height: 100%;
44
+ `,
45
+ inner: css`
46
+ position: relative;
47
+ inset: 0;
48
+
49
+ overflow: hidden;
50
+ flex: 1;
51
+ flex-direction: column;
52
+
53
+ min-width: 240px;
54
+ `,
55
+ panel: css`
56
+ user-select: none;
57
+ height: 100%;
58
+ color: ${cssVar.colorTextSecondary};
59
+ background: ${isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout};
60
+
61
+ * {
62
+ user-select: none;
63
+ }
64
+
65
+ #${TOGGLE_BUTTON_ID} {
66
+ width: 0 !important;
67
+ opacity: 0;
68
+ transition:
69
+ opacity,
70
+ width 0.2s ${cssVar.motionEaseOut};
71
+ }
72
+
73
+ #${USER_DROPDOWN_ICON_ID} {
74
+ width: 0 !important;
75
+ opacity: 0;
76
+ transition:
77
+ opacity,
78
+ width 0.2s ${cssVar.motionEaseOut};
79
+ }
80
+
81
+ #${BACK_BUTTON_ID} {
82
+ width: 0 !important;
83
+ opacity: 0;
84
+ transition: all 0.2s ${cssVar.motionEaseOut};
85
+ }
86
+
87
+ &:hover {
88
+ #${TOGGLE_BUTTON_ID} {
89
+ width: 32px !important;
90
+ opacity: 1;
91
+ }
92
+
93
+ #${USER_DROPDOWN_ICON_ID} {
94
+ width: 14px !important;
95
+ opacity: 1;
96
+ }
97
+
98
+ &:hover {
99
+ #${BACK_BUTTON_ID} {
100
+ width: 24px !important;
101
+ opacity: 1;
102
+ }
103
+ }
104
+ }
105
+ `,
106
+ }));
107
+
108
+ interface NavPanelDraggableProps {
109
+ activeContent: {
110
+ key: string;
111
+ node: ReactNode;
112
+ };
113
+ }
114
+
115
+ const classNames = {
116
+ content: draggableStyles.content,
117
+ };
118
+
119
+ export const NavPanelDraggable = memo<NavPanelDraggableProps>(({ activeContent }) => {
120
+ const [expand, togglePanel] = useGlobalStore((s) => [
121
+ systemStatusSelectors.showLeftPanel(s),
122
+ s.toggleLeftPanel,
123
+ ]);
124
+ const handleSizeChange = useNavPanelSizeChangeHandler();
125
+
126
+ const defaultWidthRef = useRef(0);
127
+ if (defaultWidthRef.current === 0) {
128
+ defaultWidthRef.current = systemStatusSelectors.leftPanelWidth(useGlobalStore.getState());
129
+ }
130
+
131
+ const defaultSize = useMemo(
132
+ () => ({
133
+ height: '100%',
134
+ width: defaultWidthRef.current,
135
+ }),
136
+ [defaultWidthRef.current],
137
+ );
138
+ const styles = useMemo(
139
+ () => ({
140
+ background: isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout,
141
+ zIndex: 11,
142
+ }),
143
+ [isDesktop, isMacOS()],
144
+ );
145
+ return (
146
+ <DraggablePanel
147
+ className={draggableStyles.panel}
148
+ classNames={classNames}
149
+ defaultSize={defaultSize}
150
+ expand={expand}
151
+ expandable={false}
152
+ maxWidth={400}
153
+ minWidth={240}
154
+ onExpandChange={togglePanel}
155
+ onSizeDragging={handleSizeChange}
156
+ placement="left"
157
+ showBorder={false}
158
+ style={styles}
159
+ >
160
+ <AnimatePresence initial={false} mode="popLayout">
161
+ <motion.div
162
+ animate={motionVariants.animate}
163
+ className={draggableStyles.inner}
164
+ exit={motionVariants.exit}
165
+ initial={motionVariants.initial}
166
+ key={activeContent.key}
167
+ transition={motionVariants.transition}
168
+ >
169
+ {activeContent.node}
170
+ </motion.div>
171
+ </AnimatePresence>
172
+ </DraggablePanel>
173
+ );
174
+ });
@@ -2,49 +2,25 @@
2
2
 
3
3
  import { type DraggablePanelProps } from '@lobehub/ui';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { useCallback, useState } from 'react';
6
5
 
6
+ import { useTypeScriptHappyCallback } from '@/hooks/useTypeScriptHappyCallback';
7
7
  import { useGlobalStore } from '@/store/global';
8
8
  import { systemStatusSelectors } from '@/store/global/selectors';
9
9
 
10
- export const useNavPanel = () => {
11
- const [leftPanelWidth, sessionExpandable, togglePanel, updatePreference] = useGlobalStore((s) => [
12
- systemStatusSelectors.leftPanelWidth(s),
13
- systemStatusSelectors.showLeftPanel(s),
14
- s.toggleLeftPanel,
15
- s.updateSystemStatus,
16
- ]);
17
-
18
- const [tmpWidth, setWidth] = useState(leftPanelWidth);
19
-
20
- if (tmpWidth !== leftPanelWidth) setWidth(leftPanelWidth);
21
-
22
- const handleSizeChange: DraggablePanelProps['onSizeChange'] = useCallback(
23
- (_: any, size: any) => {
24
- const width = size?.width;
10
+ export const useNavPanelSizeChangeHandler = (onChange?: (width: number) => void) => {
11
+ const handleSizeChange: DraggablePanelProps['onSizeChange'] = useTypeScriptHappyCallback(
12
+ (_, size) => {
13
+ const width = typeof size?.width === 'string' ? Number.parseInt(size.width) : size?.width;
25
14
  if (!width || width < 64) return;
15
+ const s = useGlobalStore.getState();
16
+ const leftPanelWidth = systemStatusSelectors.leftPanelWidth(s);
17
+ const updatePreference = s.updateSystemStatus;
26
18
  if (isEqual(width, leftPanelWidth)) return;
27
- setWidth(width);
19
+ onChange?.(width);
28
20
  updatePreference({ leftPanelWidth: width });
29
21
  },
30
- [sessionExpandable, leftPanelWidth, updatePreference],
22
+ [],
31
23
  );
32
24
 
33
- const openPanel = useCallback(() => {
34
- togglePanel(true);
35
- }, [togglePanel]);
36
-
37
- const closePanel = useCallback(() => {
38
- togglePanel(false);
39
- }, [togglePanel]);
40
-
41
- return {
42
- closePanel,
43
- defaultWidth: tmpWidth,
44
- expand: sessionExpandable,
45
- handleSizeChange,
46
- openPanel,
47
- togglePanel,
48
- width: leftPanelWidth,
49
- };
25
+ return handleSizeChange;
50
26
  };