import { useStore } from '@nanostores/react' import type { ComponentProps, ReactNode } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' import { cn } from '@/lib/utils' import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics' import { toggleKeybindPanel } from '@/store/keybinds' import { $fileBrowserOpen, $panesFlipped, $sidebarOpen, toggleFileBrowserOpen, togglePanesFlipped, toggleSidebarOpen } from '@/store/layout' import { appViewForPath, isOverlayView } from '../routes' import { titlebarButtonClass } from './titlebar' export interface TitlebarTool { id: string label: string active?: boolean className?: string disabled?: boolean hidden?: boolean href?: string icon: ReactNode onSelect?: () => void title?: string to?: string } export type TitlebarToolSide = 'left' | 'right' export type SetTitlebarToolGroup = (id: string, tools: readonly TitlebarTool[], side?: TitlebarToolSide) => void interface TitlebarControlsProps extends ComponentProps<'div'> { leftTools?: readonly TitlebarTool[] tools?: readonly TitlebarTool[] onOpenSettings: () => void } export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: TitlebarControlsProps) { const { t } = useI18n() const navigate = useNavigate() const location = useLocation() const hapticsMuted = useStore($hapticsMuted) const fileBrowserOpen = useStore($fileBrowserOpen) const sidebarOpen = useStore($sidebarOpen) const panesFlipped = useStore($panesFlipped) const toggleHaptics = () => { if (!hapticsMuted) { triggerHaptic('tap') } toggleHapticsMuted() if (hapticsMuted) { window.requestAnimationFrame(() => triggerHaptic('success')) } } // Each titlebar button controls the pane physically on its side, so a flip // swaps which pane each one toggles. Default: sessions left, file browser // right. Flipped: file browser left, sessions right. Sidebar toggles never // carry an active highlight — they're plain show/hide affordances. const fileBrowserEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen } const sessionsEdge = { open: sidebarOpen, toggle: toggleSidebarOpen } const leftEdge = panesFlipped ? fileBrowserEdge : sessionsEdge const rightEdge = panesFlipped ? sessionsEdge : fileBrowserEdge const leftToolbarTools: TitlebarTool[] = [ { icon: , id: 'sidebar', label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar, onSelect: () => { triggerHaptic('tap') leftEdge.toggle() } }, { icon: , id: 'flip-panes', label: t.titlebar.swapSidebarSides, onSelect: () => { triggerHaptic('tap') togglePanesFlipped() }, title: t.titlebar.swapSidebarSidesTitle }, ...leftTools ] const rightSidebarTool: TitlebarTool = { icon: , id: 'right-sidebar', label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar, onSelect: () => { triggerHaptic('tap') rightEdge.toggle() } } // Static system tools — always pinned to the screen's right edge. const systemTools: TitlebarTool[] = [ { active: hapticsMuted, icon: , id: 'haptics', label: hapticsMuted ? t.titlebar.unmuteHaptics : t.titlebar.muteHaptics, onSelect: toggleHaptics }, { icon: , id: 'keybinds', label: t.titlebar.openKeybinds, onSelect: () => { triggerHaptic('open') toggleKeybindPanel() } }, { icon: , id: 'settings', label: t.titlebar.openSettings, onSelect: () => { triggerHaptic('open') onOpenSettings() } } ] // While a full-screen overlay (settings, command center, …) is open it should // visually own the window. These control clusters are `fixed` at a higher // z-index than the overlay card, so they'd otherwise bleed over it — hide them // and let the overlay's own chrome (close button, drag region) take over. if (isOverlayView(appViewForPath(location.pathname))) { return null } const visibleSystemTools = systemTools.filter(tool => !tool.hidden) const settingsTool = visibleSystemTools.find(tool => tool.id === 'settings') const visibleSystemToolsBeforeSettings = visibleSystemTools.filter(tool => tool.id !== 'settings') const visiblePaneTools = tools.filter(tool => !tool.hidden) return ( <>
{leftToolbarTools .filter(tool => !tool.hidden) .map(tool => ( ))}
{/* Pane-scoped tools (preview's monitor / devtools / refresh / X) render as their own fixed cluster. AppShell sets --shell-preview-toolbar-gap to either the static cluster's width (file-browser closed → cluster sits flush against system tools) or the file-browser pane's width (file-browser open → cluster sits flush against the file-browser pane, i.e. at the preview pane's right edge). No margin hacks needed. */} {visiblePaneTools.length > 0 && (
{visiblePaneTools.map(tool => ( ))}
)}
{visibleSystemToolsBeforeSettings.map(tool => ( ))} {settingsTool && }
) } function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType; tool: TitlebarTool }) { // Titlebar actions never show an active background — state reads from the // icon itself (e.g. the mute/unmute glyph). aria-pressed still carries it // for a11y. const className = cn(titlebarButtonClass, 'bg-transparent select-none', tool.className) if (tool.href) { return ( ) } return ( ) }