Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129import { 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<void>}
/** * 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 ( <header className="app-header"> {/* 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. */} <button className="header-toggle-btn" onClick={toggleSidebar} title={sidebarLabel} aria-label={sidebarLabel} aria-expanded={state.sidebarMode === 'full'} > <Menu /> </button>
<div className="header-title"> <span>Reams</span> </div>
{hasUpdate && ( <ActionButton variant="primary" className="header-update-btn" onClick={() => void handleUpdate()} disabled={updating}> {updating ? 'Updating…' : 'Update'} </ActionButton> )} {(updateError || coordinatedError) && ( <> <span role="alert" className="header-update-error">{updateError || coordinatedError}</span> <ActionButton className="header-update-btn" onClick={() => void handleRetry()} disabled={updating}> {updating ? 'Retrying…' : 'Retry'} </ActionButton> </> )}
<button className="header-toggle-btn" onClick={toggleRightPanel} title={state.rightPanelCollapsed ? 'Show right panel' : 'Hide right panel'} aria-label={state.rightPanelCollapsed ? 'Show right panel' : 'Hide right panel'} aria-expanded={!state.rightPanelCollapsed} > <PanelRight /> </button> </header> )}