import { useEffect, useState, useSyncExternalStore } from 'react' import { Menu, PanelRight } from 'lucide-react' import { applyPwaUpdate, registerPwaUpdateFlush, retryPwaUpdate, subscribeToUpdates, updateAvailable, updateError as getUpdateError } from '../app/pwa-updates' import { useLayout } from './LayoutContext' import type { SidebarMode, EditorMode } from './layout-state' import { ActionButton } from './primitives' import './Header.css' /** * Get the aria-label for the sidebar toggle button based on current mode. * Describes what clicking the button will do next. */ function getSidebarToggleLabel(mode: SidebarMode): string { switch (mode) { case 'hidden': return 'Show ribbon' case 'ribbon': return 'Show sidebar' case 'full': return 'Hide sidebar' } } interface HeaderProps { /** @deprecated Mode controls are provided by the editor, not the app header. */ mode?: EditorMode /** @deprecated Mode controls are provided by the editor, not the app header. */ onModeChange?: () => void onBeforeUpdate?: () => Promise } /** * App header with sidebar toggle, app branding, and right panel toggle. * * The "Reams" branding is informational and never clears local user data. */ export function Header({ onBeforeUpdate }: HeaderProps) { const { state, toggleSidebar, toggleRightPanel } = useLayout() const sidebarLabel = getSidebarToggleLabel(state.sidebarMode) const hasUpdate = useSyncExternalStore(subscribeToUpdates, updateAvailable, () => false) const coordinatedError = useSyncExternalStore(subscribeToUpdates, getUpdateError, () => '') const [updating, setUpdating] = useState(false) const [updateError, setUpdateError] = useState('') // Only advertise a flush when a real callback exists; an absent callback must // not register a no-op flush that would let an update reload without saving. useEffect(() => { if (!onBeforeUpdate) return return registerPwaUpdateFlush(onBeforeUpdate) }, [onBeforeUpdate]) const handleUpdate = async () => { if (updating) return setUpdating(true) setUpdateError('') try { await applyPwaUpdate() } catch (error) { setUpdateError(error instanceof Error ? error.message : 'Could not apply update') } finally { setUpdating(false) } } // Explicit retry after a failed flush or a startup mismatch. Unlike the // Update button it does not require a staged waiting worker: it flushes // toward the retained approved target (or the controller's approved release) // and reloads, or recovers a mismatch with a canonical reload. const handleRetry = async () => { if (updating) return setUpdating(true) setUpdateError('') try { await retryPwaUpdate() } catch (error) { setUpdateError(error instanceof Error ? error.message : 'Could not apply update') } finally { setUpdating(false) } } return (
{/* aria-expanded maps 3-state sidebar to binary: true only when 'full'. Both 'hidden' and 'ribbon' modes are considered "collapsed" for a11y purposes since the main content (file tree) is not visible in either state. */}
Reams
{hasUpdate && ( void handleUpdate()} disabled={updating}> {updating ? 'Updating…' : 'Update'} )} {(updateError || coordinatedError) && ( <> {updateError || coordinatedError} void handleRetry()} disabled={updating}> {updating ? 'Retrying…' : 'Retry'} )}
) }