/** * 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 = (
Makelore logo
); 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 ? (
) : 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 (
); }