/** * The dialog layer โ€” transient UI contexts hosted by the layer-3 shell. * * A dialog is an alternate view of a stack (`docs/bgm/interactions.md` ยง4): * the deck is "lifted" off the table into the dialog (it stays on its path), * shown in order with an insertion cursor. The cursor *is* the index: * `move(id, path, index)`'s index is discovered by scrolling the visible deck. * * Opening/closing a dialog never issues a command and never touches the game * state โ€” it is pure UI. Only its action buttons issue commands. */ import { useMemo, useState } from 'react'; import type { Package } from '@tts/bgm'; import { useInteractionStore } from './interactions.js'; import { useTabletopStore, childrenByPath } from './state.js'; /** * Render the topmost dialog on the interaction store's stack as an HTML * overlay. Renders nothing when the stack is empty. The dialog shows the * stack at its path in order, with an insertion cursor the player scrolls; * the insert button issues a `move` of the held part (or the top of the * stack) to that path at the cursor. */ export function DialogLayer({ pkg }: { pkg: Package }) { const dialogs = useInteractionStore((s) => s.dialogs); const top = dialogs[dialogs.length - 1]; if (!top) return null; const dialog = pkg.dialogs.get(top.id); if (!dialog) return null; return ( ); } /** The stack-inspector dialog: a lifted, ordered view of a path's stack. */ function StackDialog({ pkg, title, body, path, }: { pkg: Package; title?: string; body?: string; path: string; }) { const parts = useTabletopStore((s) => s.parts); const movePart = useTabletopStore((s) => s.movePart); const held = useInteractionStore((s) => s.held); const drop = useInteractionStore((s) => s.drop); const popDialog = useInteractionStore((s) => s.popDialog); // The stack at `path`, in order. const stack = useMemo(() => childrenByPath(parts)[path] ?? [], [parts, path]); const [cursor, setCursor] = useState(stack.length); // The part to insert: the held part, or the top of the stack when none is // held (a "look at the top" / reorder use). const source = held?.id; const insert = () => { if (!source) return; movePart(source, path, cursor); drop(); popDialog(); }; return (
{title &&

{title}

} {body &&

{body}

}
{stack.length === 0 && ( Empty stack )} {stack.map((id, i) => (
{i === cursor && }
))} {stack.length > 0 && cursor === stack.length && }
); } /** A small marker between cards showing the insertion point. */ function Cursor() { return ( ); } /** A short label for a part id (`package:type#id` โ†’ `type#id`). */ function label(pkg: Package, id: string): string { const key = id.split(':').slice(1).join(':'); const part = pkg.parts.get(key); return part?.id ?? key; }