/**
* 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 (
);
}
/** 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;
}