mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
💄 style(desktop): move panel toggle into titlebar top-left (#15515)
* ✨ feat(desktop): move panel toggle into titlebar top-left Place a persistent collapse/expand toggle at the titlebar's top-left corner on desktop, to the right of the macOS traffic lights. The NavigationBar now splits into a left group (toggle) and a right group (back / forward / clock) with space-between: expanded, the right group hugs the sidebar's right edge; collapsed, the controls cluster at the left edge like codex. ToggleLeftPanelButton gains an optional `id` prop so the titlebar instance can opt out of the shared TOGGLE_BUTTON_ID, avoiding a duplicate DOM id and NavPanelDraggable's hover-reveal CSS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(desktop): expand untracked directories in git status `git status --porcelain` defaults to `--untracked-files=normal`, which collapses whole untracked directories into a single `?? path/` entry. That trailing-slash path then flowed into `readUntrackedAsPatch` as if it were a file — `stat()` reported `isFile()=false`, an empty patch was returned, and the Review panel rendered "无法加载该文件的 diff" against a directory row. Pass `-u` so git expands those directories into their individual files; each file then produces a real synthetic patch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 💄 style(desktop): scope titlebar toggle to macOS, hide in-page toggles there The persistent titlebar toggle now renders only on macOS; Windows/Linux keep the original right-aligned navigation controls and their in-page toggles. On macOS desktop, ToggleLeftPanelButton instances hide themselves (the titlebar owns the control) unless `forceVisible` is set, removing the now-redundant sidebar-header and content-header toggles. NavHeader also skips rendering its empty toggle-only bar in this case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -637,7 +637,7 @@ export default class GitController extends ControllerModule {
|
||||
async getGitWorkingTreeStatus(dirPath: string): Promise<GitWorkingTreeStatus> {
|
||||
const execFileAsync = promisify(execFile);
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
|
||||
cwd: dirPath,
|
||||
timeout: 5000,
|
||||
});
|
||||
@@ -689,7 +689,7 @@ export default class GitController extends ControllerModule {
|
||||
const modified: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
|
||||
cwd: dirPath,
|
||||
timeout: 5000,
|
||||
});
|
||||
@@ -830,7 +830,7 @@ export default class GitController extends ControllerModule {
|
||||
const entries: Entry[] = [];
|
||||
const submoduleDirtyEntries: Entry[] = [];
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
|
||||
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-u', '-z'], {
|
||||
cwd: dirPath,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight, Clock } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import type { GlobalState } from '@/store/global/initialState';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
@@ -13,10 +14,21 @@ import { electronStylish } from '@/styles/electron';
|
||||
import { isMacOS } from '@/utils/platform';
|
||||
|
||||
import { useNavigationHistory } from '../navigation/useNavigationHistory';
|
||||
import { TITLE_BAR_HORIZONTAL_PADDING } from './layout';
|
||||
import RecentlyViewed from './RecentlyViewed';
|
||||
|
||||
const isMac = isMacOS();
|
||||
|
||||
// Reserve space for macOS traffic lights so the toggle sits to their right.
|
||||
// Matches the popup TitleBar's MAC_TRAFFIC_LIGHT_WIDTH (80) minus the titlebar's
|
||||
// own horizontal padding, which already offsets the left edge.
|
||||
const MAC_TRAFFIC_LIGHT_WIDTH = 80;
|
||||
const macTrafficLightPadding = MAC_TRAFFIC_LIGHT_WIDTH - TITLE_BAR_HORIZONTAL_PADDING;
|
||||
|
||||
// A persistent titlebar toggle must not share the sidebar toggle's id, or it
|
||||
// would create a duplicate DOM id and get caught by NavPanelDraggable's hover CSS.
|
||||
const NAV_TOGGLE_ID = 'titlebar_toggle_left_panel_button';
|
||||
|
||||
const navPanelSelector = (s: GlobalState) => {
|
||||
const showLeftPanel = systemStatusSelectors.showLeftPanel(s);
|
||||
if (!showLeftPanel) return 0;
|
||||
@@ -74,13 +86,24 @@ const NavigationBar = memo(() => {
|
||||
horizontal
|
||||
align="center"
|
||||
data-width={leftPanelWidth}
|
||||
justify="end"
|
||||
gap={8}
|
||||
justify={isMac ? 'space-between' : 'end'}
|
||||
style={{
|
||||
paddingLeft: isMac ? macTrafficLightPadding : 0,
|
||||
paddingRight: 8,
|
||||
width: isLeftPanelVisible ? `${leftPanelWidth - 12}px` : '150px',
|
||||
// Expanded: span the sidebar width so the right group hugs its right edge.
|
||||
// Collapsed (macOS): shrink to content so the controls cluster at the left edge.
|
||||
width: isLeftPanelVisible ? `${leftPanelWidth - 12}px` : isMac ? 'auto' : '150px',
|
||||
transition: !isLeftPanelVisible ? 'width 0.2s' : 'none',
|
||||
}}
|
||||
>
|
||||
{/* The persistent panel toggle is macOS-only; other platforms keep the
|
||||
in-page toggles, so the titlebar shows just the navigation controls. */}
|
||||
{isMac && (
|
||||
<Flexbox horizontal align="center" className={electronStylish.nodrag}>
|
||||
<ToggleLeftPanelButton forceVisible id={NAV_TOGGLE_ID} size="small" />
|
||||
</Flexbox>
|
||||
)}
|
||||
<Flexbox horizontal align="center" className={electronStylish.nodrag} gap={2}>
|
||||
<ActionIcon disabled={!canGoBack} icon={ArrowLeft} size="small" onClick={goBack} />
|
||||
<ActionIcon disabled={!canGoForward} icon={ArrowRight} size="small" onClick={goForward} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Flexbox, TooltipGroup } from '@lobehub/ui';
|
||||
import { type CSSProperties, type ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
|
||||
import ToggleLeftPanelButton, { isMacDesktop } from '@/features/NavPanel/ToggleLeftPanelButton';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
@@ -39,7 +39,9 @@ const NavHeader = memo<NavHeaderProps>(
|
||||
|
||||
const noContent = !left && !right;
|
||||
|
||||
if (noContent && expand) return;
|
||||
// When empty, this header only rendered to host the collapse toggle. Hide it
|
||||
// when expanded, and also on macOS desktop where the toggle moved to the titlebar.
|
||||
if (noContent && (expand || isMacDesktop)) return;
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
|
||||
@@ -9,22 +9,40 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DESKTOP_HEADER_ICON_SMALL_SIZE } from '@/const/layoutTokens';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { isMacOS } from '@/utils/platform';
|
||||
|
||||
export const TOGGLE_BUTTON_ID = 'toggle_left_panel_button';
|
||||
|
||||
// On macOS desktop a persistent toggle lives in the titlebar (NavigationBar),
|
||||
// so in-page instances hide themselves to avoid duplicate collapse buttons.
|
||||
export const isMacDesktop = isDesktop && isMacOS();
|
||||
|
||||
interface ToggleLeftPanelButtonProps {
|
||||
/**
|
||||
* Render even on macOS desktop, where in-page instances hide by default.
|
||||
* The persistent titlebar toggle sets this.
|
||||
*/
|
||||
forceVisible?: boolean;
|
||||
icon?: ActionIconProps['icon'];
|
||||
/**
|
||||
* DOM id for the button. Defaults to the shared {@link TOGGLE_BUTTON_ID} which
|
||||
* NavPanelDraggable targets for its hover-reveal CSS. Pass a custom id (or `null`)
|
||||
* to opt out — e.g. for a persistent instance rendered outside the sidebar panel,
|
||||
* to avoid duplicate ids and the hover-hide behavior.
|
||||
*/
|
||||
id?: string | null;
|
||||
showActive?: boolean;
|
||||
size?: ActionIconProps['size'];
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
const ToggleLeftPanelButton = memo<ToggleLeftPanelButtonProps>(
|
||||
({ title, showActive, icon, size }) => {
|
||||
({ title, showActive, icon, size, id = TOGGLE_BUTTON_ID, forceVisible }) => {
|
||||
const [expand, togglePanel] = useGlobalStore((s) => [
|
||||
systemStatusSelectors.showLeftPanel(s),
|
||||
s.toggleLeftPanel,
|
||||
@@ -33,11 +51,13 @@ const ToggleLeftPanelButton = memo<ToggleLeftPanelButtonProps>(
|
||||
|
||||
const { t } = useTranslation(['chat', 'hotkey']);
|
||||
|
||||
if (isMacDesktop && !forceVisible) return null;
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
active={showActive ? expand : undefined}
|
||||
icon={icon || (expand ? PanelLeftClose : PanelLeftOpen)}
|
||||
id={TOGGLE_BUTTON_ID}
|
||||
id={id ?? undefined}
|
||||
size={size || DESKTOP_HEADER_ICON_SMALL_SIZE}
|
||||
title={title || t('toggleLeftPanel.title', { ns: 'hotkey' })}
|
||||
tooltipProps={{
|
||||
|
||||
Reference in New Issue
Block a user