import React, { useEffect } from 'react'; import { Icon } from '@/components/icon/Icon'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { getDesktopWindowControlsOrder, invokeDesktop } from '@/lib/desktop'; import type { DesktopWindowControlAction, DesktopWindowControlsSide } from '@/lib/desktop'; import { useUIStore } from '@/stores/useUIStore'; // macOS chrome colors; intentionally theme-independent — these replicate a // foreign platform's chrome, not OpenChamber's own status tokens, so the // theme-system hex rule does not apply. const TRAFFIC_LIGHT_FILL: Record = { close: '#FF5F57', minimize: '#FEBC2E', maximize: '#28C940', }; // Glyphs are a uniform translucent black, matching macOS — opacity lets the // fill tint the symbol so it reads on all three fills without per-action colors. const TRAFFIC_LIGHT_GLYPH = 'rgba(0, 0, 0, 0.7)'; const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction }> = ({ action }) => { if (action === 'close') { return ; } if (action === 'minimize') { return ; } // Green maximize/restore: same `add` (+) glyph for both states. The label // (restore vs maximize) still flips via isMaximized in TrafficLightButton. return ; }; type TrafficLightButtonProps = { action: DesktopWindowControlAction; isMaximized: boolean; onActivate: (action: DesktopWindowControlAction) => void; }; const TrafficLightButton: React.FC = ({ action, isMaximized, onActivate }) => { const { t } = useI18n(); const fill = TRAFFIC_LIGHT_FILL[action]; const label = action === 'close' ? t('header.windowControls.close') : action === 'minimize' ? t('header.windowControls.minimize') : isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize'); return ( ); }; type WindowsWindowControlsProps = { visible: boolean; position?: DesktopWindowControlsSide; }; export const WindowsWindowControls = React.memo(function WindowsWindowControls({ visible, position = 'right', }: WindowsWindowControlsProps) { const { t } = useI18n(); const [isMaximized, setIsMaximized] = React.useState(false); const desktopWindowControlsStyle = useUIStore((state) => state.desktopWindowControlsStyle); useEffect(() => { if (!visible) { return; } let disposed = false; void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state') .then((state) => { if (!disposed) { setIsMaximized(Boolean(state?.maximized)); } }) .catch(() => {}); const handleMaximizedChange = (event: Event) => { const detail = (event as CustomEvent<{ maximized?: boolean }>).detail; setIsMaximized(Boolean(detail?.maximized)); }; window.addEventListener('openchamber:window-maximized-changed', handleMaximizedChange); return () => { disposed = true; window.removeEventListener('openchamber:window-maximized-changed', handleMaximizedChange); }; }, [visible]); if (!visible) { return null; } const isLeft = position === 'left'; // Order is side-driven for both styles: close, minimize, maximize on the // left; minimize, maximize, close on the right. const order = getDesktopWindowControlsOrder(position); const activate = (action: DesktopWindowControlAction) => { if (action === 'close') { void invokeDesktop('desktop_close_current_window'); return; } if (action === 'minimize') { void invokeDesktop('desktop_minimize_current_window'); return; } void invokeDesktop<{ maximized?: boolean }>('desktop_toggle_current_window_maximized') .then((state) => setIsMaximized(Boolean(state?.maximized))) .catch(() => {}); }; // Traffic-light chrome: macOS-style 14px circles in an h-8 band. Glyphs // reveal on any-cluster hover (group-hover/wctl). TitlebarLeftControls // measures and republishes the cluster width via ResizeObserver, so the // narrower footprint takes effect without touching reserved-width constants. if (desktopWindowControlsStyle === 'traffic-lights') { return (
{order.map((action) => ( ))}
); } // Classic Windows-style square buttons. Left side matches the h-8 titlebar // icon cluster (app menu / sidebar / project actions) and avoids negative // margins so TitlebarLeftControls publishes an accurate reserved width — // otherwise the project-actions chevron overlaps the session title. Right // side keeps a taller h-12 Windows-style hit target. const buttonClassName = cn( 'app-region-no-drag inline-flex items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', isLeft ? 'h-8 w-8 rounded-md' : 'h-12 w-11', ); const containerClassName = isLeft ? 'app-region-no-drag mr-1 flex h-8 shrink-0 items-center' : 'app-region-no-drag ml-1 flex h-12 shrink-0 items-center'; const renderControl = (action: DesktopWindowControlAction) => { if (action === 'minimize') { return ( ); } if (action === 'maximize') { return ( ); } return ( ); }; return (
{order.map(renderControl)}
); });