@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.
- package/CHANGELOG.md +25 -0
- package/apps/desktop/resources/locales/ar/menu.json +5 -1
- package/apps/desktop/resources/locales/bg-BG/menu.json +5 -1
- package/apps/desktop/resources/locales/de-DE/menu.json +5 -1
- package/apps/desktop/resources/locales/es-ES/menu.json +5 -1
- package/apps/desktop/resources/locales/fa-IR/menu.json +5 -1
- package/apps/desktop/resources/locales/fr-FR/menu.json +5 -1
- package/apps/desktop/resources/locales/it-IT/menu.json +5 -1
- package/apps/desktop/resources/locales/ja-JP/menu.json +5 -1
- package/apps/desktop/resources/locales/ko-KR/menu.json +5 -1
- package/apps/desktop/resources/locales/nl-NL/menu.json +5 -1
- package/apps/desktop/resources/locales/pl-PL/menu.json +5 -1
- package/apps/desktop/resources/locales/pt-BR/menu.json +5 -1
- package/apps/desktop/resources/locales/ru-RU/menu.json +5 -1
- package/apps/desktop/resources/locales/tr-TR/menu.json +5 -1
- package/apps/desktop/resources/locales/vi-VN/menu.json +5 -1
- package/apps/desktop/resources/locales/zh-CN/menu.json +5 -1
- package/apps/desktop/resources/locales/zh-TW/menu.json +5 -1
- package/apps/desktop/src/main/locales/default/menu.ts +5 -1
- package/apps/desktop/src/main/menus/impls/linux.ts +30 -0
- package/apps/desktop/src/main/menus/impls/macOS.test.ts +17 -0
- package/apps/desktop/src/main/menus/impls/macOS.ts +33 -0
- package/apps/desktop/src/main/menus/impls/windows.ts +30 -0
- package/changelog/v1.json +5 -0
- package/locales/ar/electron.json +24 -0
- package/locales/bg-BG/electron.json +24 -0
- package/locales/de-DE/electron.json +24 -0
- package/locales/en-US/electron.json +24 -0
- package/locales/es-ES/electron.json +24 -0
- package/locales/fa-IR/electron.json +24 -0
- package/locales/fr-FR/electron.json +24 -0
- package/locales/it-IT/electron.json +24 -0
- package/locales/ja-JP/electron.json +24 -0
- package/locales/ko-KR/electron.json +24 -0
- package/locales/nl-NL/electron.json +24 -0
- package/locales/pl-PL/electron.json +24 -0
- package/locales/pt-BR/electron.json +24 -0
- package/locales/ru-RU/electron.json +24 -0
- package/locales/tr-TR/electron.json +24 -0
- package/locales/vi-VN/electron.json +24 -0
- package/locales/zh-CN/electron.json +24 -0
- package/locales/zh-TW/electron.json +24 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/navigation.ts +12 -0
- package/src/components/PageTitle/index.tsx +11 -1
- package/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx +137 -0
- package/src/features/ElectronTitlebar/NavigationBar/index.tsx +86 -0
- package/src/features/ElectronTitlebar/helpers/routeMetadata.ts +214 -0
- package/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts +152 -0
- package/src/features/ElectronTitlebar/index.tsx +13 -5
- package/src/features/NavHeader/index.tsx +4 -2
- package/src/features/NavPanel/components/NavPanelDraggable.tsx +174 -0
- package/src/features/NavPanel/hooks/useNavPanel.ts +11 -35
- package/src/features/NavPanel/index.tsx +2 -126
- package/src/hooks/useTypeScriptHappyCallback.ts +7 -0
- package/src/locales/default/electron.ts +24 -0
- package/src/store/electron/actions/navigationHistory.ts +247 -0
- package/src/store/electron/initialState.ts +7 -1
- package/src/store/electron/store.ts +9 -2
- 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
|
-
|
|
36
|
-
style={{ minHeight: TITLE_BAR_HEIGHT }}
|
|
45
|
+
style={{ minHeight: TITLE_BAR_HEIGHT, padding }}
|
|
37
46
|
width={'100%'}
|
|
38
47
|
>
|
|
39
|
-
<
|
|
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 {
|
|
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
|
|
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
|
|
11
|
-
const [
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
+
onChange?.(width);
|
|
28
20
|
updatePreference({ leftPanelWidth: width });
|
|
29
21
|
},
|
|
30
|
-
[
|
|
22
|
+
[],
|
|
31
23
|
);
|
|
32
24
|
|
|
33
|
-
|
|
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
|
};
|