/** * The locked-shell entry (01-sharing §5.1's all-boards rule) - what a bundle * whose every published board is locked to a stage mode boots into INSTEAD of * the canvas shell. The build swaps this module in for App.tsx, so the canvas * surface (sidebar, board grid, toolbar) is not hidden - its code never enters * the bundle. Publish-to-web, as an artifact rather than a flag. * * What remains is exactly the locked surface: boot, the landing rule, the play * or focus overlay (whose lock already refuses every exit), and deep links * within what the lock allows. */ import { Component, useEffect, useRef, type ReactNode } from 'react' import { useStore, landingBoard, landingMode, tabTitle } from './store.ts' import { enterFocus, enterPlay, enterSlides, PlayOverlay, playCtl } from './Play.tsx' import { UNPLACED, bootHash, parseHash, retargetBoot, writeHash } from './hash.ts' import { isLegacyLink, landLegacy } from './legacy-links.ts' import { useComments } from './comments-store.ts' import { isLatest, navigated, switchFor } from './goto.ts' /** App.tsx's headless-run probe (perf-run-probe.ts): a run's page is always a dev canvas, never a locked bundle. */ export function startPerfRunProbe(): void {} export class ShellBoundary extends Component<{ children: ReactNode }, { err: Error | null }> { state = { err: null as Error | null } static getDerivedStateFromError(err: Error) { return { err } } render() { if (!this.state.err) return this.props.children return (
shell crashed
{this.state.err.message}
) } } let booted = false /** A thread a link names: opened on the stage once the comments are on its board (a board's load * closes the open thread, so never before - the effect below opens it after). */ let pendingThread: string | null = null const openPending = () => { const c = useComments.getState() if (!pendingThread || c.board !== useStore.getState().board || !c.threads.some((t) => t.id === pendingThread)) return c.setActive(pendingThread) pendingThread = null } export function App() { const toasts = useStore((s) => s.toasts) const board = useStore((s) => s.board) /** The landing rule, enforced: the mode the lock pinned, entered on boot and * re-entered if anything ever drops the overlay - there is no other surface. */ const land = (at?: string) => { const s = useStore.getState() if (s.play) return const mode = landingMode(s.board) if (mode === 'focus') enterFocus(at) else if (mode === 'slides') enterSlides(at ? { at } : undefined) else enterPlay(at ? { at } : undefined) } useEffect(() => { if (booted) return booted = true const start = async () => { // an old All Scenes link: a real board, at the frame it names, with what to say about it let notice: string | null = null if (isLegacyLink(bootHash)) { const r = await landLegacy(bootHash); retargetBoot(r.hash); notice = r.notice } // the Not on a board view is dev's: a published canvas opens its default board instead if (bootHash.board === UNPLACED) retargetBoot({}) if (bootHash.board && bootHash.board !== useStore.getState().board) useStore.setState({ board: bootHash.board, boardAuto: false }) const ok = await boot() urlReady.current = true if (!ok) return if (bootHash.focus) enterFocus(bootHash.focus.at, { ...bootHash.focus, deep: true }) else if (bootHash.play?.slides) enterSlides(bootHash.play) else if (bootHash.play) enterPlay(bootHash.play) else land(bootHash.f?.[0]) if (notice) useStore.getState().toast(notice) if (bootHash.c) { pendingThread = bootHash.c; openPending() } } const boot = useStore.getState().boot void start() }, []) // the overlay IS the surface: if it ever closes, land again const play = useStore((s) => s.play) useEffect(() => { if (!play) setTimeout(land, 30) }, [play]) // the URL projection, minimal: stage state only (there is no selection here) const urlReady = useRef(false) useEffect(() => { if (!urlReady.current || !play) return if (play.focus && play.deep) writeHash({ focus: { at: play.at, device: play.device, theme: play.theme } }) else if (!play.focus) writeHash({ board: useStore.getState().board, play }) }) // back/forward + pasted links, within what the lock allows useEffect(() => { const onPop = async () => { // a lookup newer navigation overtook - a link, a goto, a step on the stage - is dropped const nav = navigated(), was = useStore.getState() const parsed = parseHash() const raw = parsed.board === UNPLACED ? {} : parsed // dev's view, and what it names: not here // an old All Scenes link lands on a real board; it, and the bare #/, with no board: the landing one const r = isLegacyLink(raw) ? await landLegacy(raw) : { hash: raw, notice: null } const h = r.hash const board = h.board ?? (h.focus ? undefined : await landingBoard()) const s = useStore.getState() if (!isLatest(nav) || s.board !== was.board || s.play?.at !== was.play?.at) return if (r.notice) s.toast(r.notice) if (h.c) pendingThread = h.c if (board && board !== s.board) { if (!await switchFor(board, nav)) return // failed, or a newer navigation's board and stage by now useStore.getState().setPlay(null) // the stage restarts on the board the link names, at its frame if (h.play?.slides) enterSlides(h.play) else if (h.play) enterPlay(h.play) else land(h.f?.[0]) return } openPending() // same board: its comments are here already if (h.f?.length && s.play) return playCtl.setAt(h.f[0]) // the stage on screen steps to the frame named if (h.focus) enterFocus(h.focus.at, { ...h.focus, deep: true }) // sync only holds WITHIN a mode: same-mode restores drive the MOUNTED // stage (sh:stage-set); crossing present↔slides re-enters and remounts else if (h.play && s.play && !!h.play.slides === !!s.play.slides) playCtl.sync(h.play) else if (h.play?.slides) enterSlides(h.play) else if (h.play) enterPlay(h.play) } window.addEventListener('popstate', onPop) window.addEventListener('hashchange', onPop) return () => { window.removeEventListener('popstate', onPop); window.removeEventListener('hashchange', onPop) } }, []) // comment liveness for the stage overlay - same wiring the canvas shell does useEffect(() => { if (!board) return const c = useComments.getState() void c.load(board).then(openPending) return c.live(board) }, [board]) const titles = useStore((s) => s.boardTitles) useEffect(() => { document.title = tabTitle(board) }, [board, titles]) return (
{toasts.map((t) =>
{t.text}
)}
) }