/**
* 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 { observeNativeFullscreen } from '@/lib/window-fullscreen';
import { cn } from '@/lib/utils';
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useSettingsStore } from '@/stores/settings';
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
import logoWordmarkSource from '@/assets/makelore-wordmark-source.png';
import type { SidebarPeekSource } from './sidebar-peek';
import { ModuleSwitcher } from './ModuleSwitcher';
import { getClassroomTitlebarWidth, TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from './titlebar-metrics';
type SidebarPeekChange = (open: boolean, source: SidebarPeekSource) => void;
type TitleBarProps = {
integrated?: boolean;
workspaceLayout?: boolean;
overlay?: boolean;
showSidebarControls?: boolean;
pageTitle?: string;
sidebarPeekOpen?: boolean;
onSidebarPeekChange?: SidebarPeekChange;
};
export function TitleBar({ integrated = false, workspaceLayout = false, overlay = false, showSidebarControls = true, 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,
showSidebarControls = true,
pageTitle,
children,
windowControlsWidth = 0,
nativeTrafficLights = false,
sidebarPeekOpen = false,
onSidebarPeekChange,
}: {
integrated: boolean;
workspaceLayout?: boolean;
overlay?: boolean;
showSidebarControls?: boolean;
pageTitle?: string;
children?: React.ReactNode;
windowControlsWidth?: number;
nativeTrafficLights?: boolean;
sidebarPeekOpen?: boolean;
onSidebarPeekChange?: SidebarPeekChange;
}) {
const [nativeFullscreen, setNativeFullscreen] = useState(false);
useEffect(() => {
if (nativeTrafficLights) return observeNativeFullscreen(setNativeFullscreen);
}, [nativeTrafficLights]);
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
const classroomTitlebarWidth = getClassroomTitlebarWidth(nativeTrafficLights ? 'darwin' : 'win32', sidebarCollapsed, sidebarWidth);
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
// keeps project conversations in the sidebar and must preserve its title rail.
const sidebarPreviewTitlebarVisible = sidebarCollapsed && sidebarPeekOpen && !workspaceLayout && !overlay;
const sidebarTitlebarHidden = sidebarCollapsed && !sidebarPreviewTitlebarVisible;
const sidebarToggle = (
);
const sidebarTogglePortal = (
handleSidebarPeekEnter('titlebar') : undefined}
onPointerLeave={workspaceLayout ? () => handleSidebarPeekLeave('titlebar') : undefined}
onFocusCapture={workspaceLayout ? () => handleSidebarPeekEnter('focus') : undefined}
onBlurCapture={workspaceLayout ? (event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) handleSidebarPeekLeave('focus');
} : undefined}
>
{workspaceLayout ?
{sidebarToggle}
: sidebarToggle}
);
const titlebarLogo = (
);
return (
{integrated && showSidebarControls ? (
<>
handleSidebarPeekEnter('titlebar') : undefined}
onPointerLeave={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekLeave('titlebar') : undefined}
/>
{typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal}
>
) : null}
{integrated ? (
{pageTitle ? (
{pageTitle}
) : !workspaceLayout && !overlay ? (
{activeProject?.name ?? 'Makelore 工作台'}
) : null}
{!overlay && !workspaceLayout ? titlebarLogo : null}
{children}
) : (
{children}
)}
{integrated && overlay && (nativeTrafficLights || showSidebarControls) ? titlebarLogo : null}
);
}
function WindowsTitleBar({ integrated, workspaceLayout, overlay, showSidebarControls, 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 (
);
}