Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124// src/layout/StatusBar.tsximport { useCallback, useRef, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react'import type { StatusBarActionManager } from '../plugin/ui/status-bar-action-manager'import type { StatusBarItem } from '../plugin/ui/status-bar-action-types'import { SaveStatusIndicator } from '../sync/SaveStatusIndicator'import type { SaveStatus } from '../sync/sync-contract'import './StatusBar.css'
function subscribeOnline(listener: () => void): () => void { window.addEventListener('online', listener) window.addEventListener('offline', listener) return () => { window.removeEventListener('online', listener) window.removeEventListener('offline', listener) }}
function isOnline(): boolean { return navigator.onLine }
interface StatusBarProps { /** The status bar action manager */ statusBarActions: StatusBarActionManager /** Current save status. When provided, renders a save indicator on the left side. */ saveStatus?: SaveStatus /** Whether the same document is open in another tab. */ isConcurrentTabOpen?: boolean /** Additional React content rendered on the right side before plugin items. */ extraRightItems?: ReactNode /** Additional React content rendered on the left side after the save indicator. */ extraLeftItems?: ReactNode}
/** * Status bar component that displays plugin-registered items. * * Subscribes to StatusBarActionManager via useSyncExternalStore and renders * each plugin's HTMLElement by mounting it into a wrapper div via useEffect + ref. * Right-aligned, positioned inside .app-shell-main, above the formatting toolbar. */export function StatusBar({ statusBarActions, saveStatus, isConcurrentTabOpen, extraRightItems, extraLeftItems }: StatusBarProps) { const online = useSyncExternalStore(subscribeOnline, isOnline, () => true) const subscribe = useCallback( (cb: () => void) => statusBarActions.subscribe(cb), [statusBarActions] ) const getSnapshot = useCallback( () => statusBarActions.getSnapshot(), [statusBarActions] ) const orderedItems = useSyncExternalStore(subscribe, getSnapshot)
// Subscribe to visibility setting const isVisible = useSyncExternalStore( subscribe, useCallback(() => statusBarActions.isVisible(), [statusBarActions]) )
const elements = useMemo(() => { return orderedItems.map((item: StatusBarItem) => ( <StatusBarItemMount key={item.id} item={item} /> )) }, [orderedItems])
const hasSaveContent = (saveStatus != null && saveStatus.state !== 'idle') || isConcurrentTabOpen === true const hasPluginItems = isVisible && orderedItems.length > 0 const hasExtraRightItems = extraRightItems != null const hasExtraLeftItems = extraLeftItems != null const willRender = hasPluginItems || hasSaveContent || hasExtraRightItems || hasExtraLeftItems || !online
if (!willRender) return null
return ( <div className="status-bar" role="status" aria-label="Status bar" > {saveStatus && ( <SaveStatusIndicator saveStatus={saveStatus} isConcurrentTabOpen={isConcurrentTabOpen ?? false} online={online} /> )} {!online && saveStatus?.state !== 'saved' && <span className="status-bar-offline">Offline</span>} {hasExtraLeftItems && ( <div className="status-bar-core-items status-bar-core-items--left"> {extraLeftItems} </div> )} <div className="status-bar-spacer" /> {hasExtraRightItems && ( <div className="status-bar-core-items"> {extraRightItems} </div> )} {hasPluginItems && elements} </div> )}
/** * Mounts a plugin's StatusBarItemEl into a wrapper div via useEffect. * This is necessary because plugins mutate their elements directly via the DOM API. */function StatusBarItemMount({ item }: { item: StatusBarItem }) { const wrapperRef = useRef<HTMLDivElement>(null)
useEffect(() => { const wrapper = wrapperRef.current if (!wrapper || !item.el) return wrapper.appendChild(item.el) return () => { // Remove the element from the wrapper on cleanup. // The element still lives in the manager; we're just detaching it from the DOM. if (wrapper.contains(item.el)) { wrapper.removeChild(item.el) } } }, [item.el])
return <div ref={wrapperRef} className="status-bar-item-wrapper" />}