import { useStore } from '@nanostores/react' import type { CSSProperties, ReactNode } from 'react' import { useSyncExternalStore } from 'react' import { Backdrop } from '@/components/Backdrop' import { PaneShell } from '@/components/pane-shell' import { SidebarProvider } from '@/components/ui/sidebar' import { $fileBrowserOpen, $sidebarOpen, FILE_BROWSER_DEFAULT_WIDTH, FILE_BROWSER_PANE_ID, setSidebarOpen } from '@/store/layout' import { $paneWidthOverride } from '@/store/panes' import { $connection } from '@/store/session' import { StatusbarControls, type StatusbarItem } from './statusbar-controls' import { TITLEBAR_HEIGHT, titlebarControlsPosition } from './titlebar' import { TitlebarControls, type TitlebarTool } from './titlebar-controls' interface AppShellProps { children: ReactNode leftStatusbarItems?: readonly StatusbarItem[] leftTitlebarTools?: readonly TitlebarTool[] onOpenSettings: () => void overlays?: ReactNode statusbarItems?: readonly StatusbarItem[] titlebarTools?: readonly TitlebarTool[] } // Renderer-side fallback so layout snaps even when the main-process fullscreen event // hasn't landed yet (e.g. dev reloads, before the IPC bridge is wired). function subscribeWindowSize(cb: () => void) { window.addEventListener('resize', cb) window.addEventListener('fullscreenchange', cb) return () => { window.removeEventListener('resize', cb) window.removeEventListener('fullscreenchange', cb) } } const viewportIsFullscreen = () => window.innerWidth >= window.screen.width && window.innerHeight >= window.screen.height export function AppShell({ children, leftStatusbarItems, leftTitlebarTools, onOpenSettings, overlays, statusbarItems, titlebarTools }: AppShellProps) { const sidebarOpen = useStore($sidebarOpen) const fileBrowserOpen = useStore($fileBrowserOpen) const fileBrowserWidthOverride = useStore($paneWidthOverride(FILE_BROWSER_PANE_ID)) const connection = useStore($connection) const viewportFullscreen = useSyncExternalStore(subscribeWindowSize, viewportIsFullscreen, () => false) const isFullscreen = Boolean(connection?.isFullscreen) || viewportFullscreen const titlebarControls = titlebarControlsPosition(connection?.windowButtonPosition, isFullscreen) // Width Windows/Linux reserve for the OS-painted min/max/close overlay (zero // on macOS, where window controls sit on the left and are reported via // windowButtonPosition instead). The right tool cluster has to clear them. const nativeOverlayWidth = connection?.nativeOverlayWidth ?? 0 const titlebarToolsRight = nativeOverlayWidth > 0 ? `${nativeOverlayWidth}px` : '0.75rem' const titlebarContentInset = sidebarOpen ? 0 : titlebarControls.left + TITLEBAR_HEIGHT + Math.round(TITLEBAR_HEIGHT / 2) // The static system cluster (file-browser, haptics, settings) is hardcoded // in TitlebarControls. Pane-supplied tools (preview's group) render in a // separate cluster anchored further left. // // Width math has to include the `gap-x-1` (0.25rem) between buttons: // N buttons + (N - 1) inner gaps, plus one extra 0.25rem of breathing room // between the pane-tool cluster and the system cluster so they don't sit // flush against each other. Modeled as N gaps (N - 1 inner + 1 trailing) // to keep the formula generic for any pane-tool count. const SYSTEM_TOOL_COUNT = 4 const paneToolCount = titlebarTools?.filter(tool => !tool.hidden).length ?? 0 const systemToolsWidth = `calc(${SYSTEM_TOOL_COUNT} * (var(--titlebar-control-size) + 0.25rem))` const fileBrowserWidth = fileBrowserWidthOverride !== undefined ? `${fileBrowserWidthOverride}px` : FILE_BROWSER_DEFAULT_WIDTH // Where the pane-tool cluster's right edge sits, measured from the inner // titlebar padding (--titlebar-tools-right). Two anchors: // - file-browser closed → flush against static cluster's left edge // - file-browser open → flush against the file-browser pane's left edge // (= preview pane's right edge) const previewToolbarGap = fileBrowserOpen ? fileBrowserWidth : systemToolsWidth // Used by the drag region to know where the rightmost interactive element // ends. When pane tools are present, that's `gap + paneCount * controlSize // + paneCount * 0.25rem` (the leftmost button is at `tools-right + gap + // paneCount * (size + gap-x-1)`). Otherwise the static cluster's footprint // is enough. const titlebarToolsWidth = paneToolCount > 0 ? `calc(${previewToolbarGap} + ${paneToolCount} * (var(--titlebar-control-size) + 0.25rem))` : systemToolsWidth return ( descendants. Resolves to the chat-sidebar // pane track via PaneShell's emitted --pane-chat-sidebar-width. '--sidebar-width': 'var(--pane-chat-sidebar-width)', '--titlebar-height': `${TITLEBAR_HEIGHT}px`, '--titlebar-content-inset': `${titlebarContentInset}px`, '--titlebar-controls-left': `${titlebarControls.left}px`, '--titlebar-controls-top': `${titlebarControls.top}px`, '--titlebar-tools-right': titlebarToolsRight, '--titlebar-tools-width': titlebarToolsWidth, // Anchor for the pane-tool cluster's right edge in TitlebarControls. // Sourced from the layout store rather than the PaneShell-emitted // --pane-*-width vars because the titlebar is a sibling of PaneShell // and CSS variables resolve at the consumer's scope. '--shell-preview-toolbar-gap': previewToolbarGap } as CSSProperties } >
{overlays}
) }