/**
* TitleBar Component
* macOS: light application toolbar with native traffic lights overlaid by the
* full-size hidden title bar.
* Windows: drag region with custom minimize/maximize/close controls.
* Linux: use native window chrome (no custom title bar).
*/
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Copy, FolderOpen, Minus, PanelLeft, PanelLeftClose, Square, X } from 'lucide-react';
import { invokeIpc } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useSettingsStore } from '@/stores/settings';
import logoWordmarkSource from '@/assets/makelore-wordmark-source.png';
import type { SidebarPeekSource } from './sidebar-peek';
type SidebarPeekChange = (open: boolean, source: SidebarPeekSource) => void;
// Three 44px buttons plus the control container's 8px padding on each side.
const WINDOWS_TITLEBAR_CONTROLS_WIDTH = 148;
type TitleBarProps = {
integrated?: boolean;
workspaceLayout?: boolean;
overlay?: boolean;
pageTitle?: string;
sidebarPeekOpen?: boolean;
onSidebarPeekChange?: SidebarPeekChange;
};
export function TitleBar({ integrated = false, workspaceLayout = false, overlay = false, pageTitle, sidebarPeekOpen = false, onSidebarPeekChange }: TitleBarProps) {
const platform = window.electron?.platform;
if (platform === 'darwin') {
return ;
}
// Linux keeps the native frame/title bar for better IME compatibility.
if (platform !== 'win32') {
return null;
}
return ;
}
function ProductTitleBar({
integrated,
workspaceLayout = false,
overlay = false,
pageTitle,
children,
windowControlsWidth = 0,
nativeTrafficLights = false,
sidebarPeekOpen = false,
onSidebarPeekChange,
}: {
integrated: boolean;
workspaceLayout?: boolean;
overlay?: boolean;
pageTitle?: string;
children?: React.ReactNode;
windowControlsWidth?: number;
nativeTrafficLights?: boolean;
sidebarPeekOpen?: boolean;
onSidebarPeekChange?: SidebarPeekChange;
}) {
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
const toolbarButtonClass = 'no-drag motion-press flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-surface-subtle hover:text-foreground';
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
const toggleSidebar = () => setSidebarCollapsed(!sidebarCollapsed);
const handleSidebarPeekEnter = (source: SidebarPeekSource) => {
if (sidebarCollapsed) onSidebarPeekChange?.(true, source);
};
const handleSidebarPeekLeave = (source: SidebarPeekSource) => {
if (sidebarCollapsed) onSidebarPeekChange?.(false, source);
};
// The project sidebar preview is rendered below the title bar, so mirror
// its full width in the title bar for ordinary pages. The chat workspace
// has an independent conversation rail and must keep its pinned columns.
const sidebarPreviewTitlebarVisible = sidebarCollapsed && sidebarPeekOpen && !workspaceLayout && !overlay;
const sidebarTitlebarHidden = sidebarCollapsed && !sidebarPreviewTitlebarVisible;
const conversationSurfaceOffset = sidebarTitlebarHidden
? (nativeTrafficLights ? '-132px' : '-52px')
: '0px';
const sidebarToggle = (
);
const sidebarTogglePortal = (
{sidebarToggle}
);
const titlebarLogo = (
);
return (
{integrated ? (
<>
handleSidebarPeekEnter('titlebar') : undefined}
onPointerLeave={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekLeave('titlebar') : undefined}
/>
{typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal}
>
) : null}
{integrated ? (
{workspaceLayout ? (
) : null}
{pageTitle ? (
{pageTitle}
) : !workspaceLayout && !overlay ? (
{activeProject?.name ?? 'Makelore 工作台'}
) : null}
{!overlay ? titlebarLogo : null}
{children}
) : (
{children}
)}
{integrated && overlay ? titlebarLogo : null}
);
}
function WindowsTitleBar({ integrated, workspaceLayout, overlay, pageTitle, sidebarPeekOpen, onSidebarPeekChange }: Omit
& { integrated: boolean }) {
const [maximized, setMaximized] = useState(false);
useEffect(() => {
// Check initial state
invokeIpc('window:isMaximized').then((val) => {
setMaximized(val as boolean);
});
}, []);
const handleMinimize = () => {
invokeIpc('window:minimize');
};
const handleMaximize = () => {
invokeIpc('window:maximize').then(() => {
invokeIpc('window:isMaximized').then((val) => {
setMaximized(val as boolean);
});
});
};
const handleClose = () => {
invokeIpc('window:close');
};
return (
);
}