/** * Which of the panel's full-surface overlays is showing. * * ★★ THEY ARE MUTUALLY EXCLUSIVE, and that is not a style choice. The history * drawer (`AssistantConversationList.tsx:87`) and the preview surface * (`preview/AssistantPreviewOverlay.tsx:145`) are BOTH `absolute inset-0 z-10` with an * opaque `bg-bg-surface`, inside the same relative panel. Equal z-index means * the later one in DOM order simply paints over the earlier one, so allowing * both produces three separate defects at once: * * - the covered surface is invisible while its own toggle still reports * `aria-pressed="true"`, so a screen reader announces a drawer the sighted * user cannot see and the sighted user sees a pressed button doing nothing; * - closing the top one makes a surface the user had forgotten reappear * unbidden, over their conversation; * - which one wins depends on JSX order, so a future reorder silently swaps * the behaviour with nothing to catch it. * * Modelled as one slot with two toggles rather than two independent booleans, * because "at most one is true" is then true by construction instead of by * everyone remembering to close the other one. */ export interface PanelOverlayState { readonly historyOpen: boolean; readonly previewOpen: boolean; } /** Nothing showing — the state the panel opens in. */ export declare const NO_PANEL_OVERLAY: PanelOverlayState; /** * The state after the user presses one of the two toggles. * * Pressing the toggle of the overlay already showing closes it; pressing the * other one swaps to it. There is deliberately no way to express "both". */ export declare function nextPanelOverlays(current: PanelOverlayState, toggled: "history" | "preview"): PanelOverlayState; /** * What Escape should dismiss. * * ★★ THE TOPMOST LAYER, NOT THE WHOLE ASSISTANT. `useAssistantKeyboardShortcuts` * calls `onClose()` on Escape (`:39-43`), and both host apps mount the panel as * `{isOpen && }` — so with an overlay open, Escape did not * close the overlay, it UNMOUNTED the assistant and took the in-flight turn, * the composer draft and the attachments with it. The user's mental model of * Escape is "back out of the thing I just opened"; here it destroyed a * conversation instead. */ export declare function escapeDismisses(current: PanelOverlayState): "overlay" | "panel"; /** * Attribute marking the assistant panel's root element. * * ★★ A CROSS-PACKAGE CONTRACT, not a styling hook. Each host app's own Escape * handler reads it through `assistantPanelOwnsEscape` to decide whether the * panel is about to handle the key itself. Renaming it silently reintroduces * the double-close below, in a different repo, with every test still green. * * Deliberately NOT the `data-testid` — that is a test handle, free to change. * This one is load-bearing at runtime. */ export declare const ASSISTANT_PANEL_ATTR = "data-assistant-panel"; /** * Attribute naming the full-surface overlay the panel is currently showing. * * ★★ Present only while one is open, and read by `assistantPanelOwnsEscape` — * see the "focus fell to ``" case there. Absent means no overlay. */ export declare const ASSISTANT_OVERLAY_ATTR = "data-assistant-overlay"; /** Which overlay is showing, as the attribute value — `null` when none is. */ export declare function activePanelOverlay(current: PanelOverlayState): "history" | "preview" | null; /** * Is `active` the panel itself, or inside it? * * Extracted so `useAssistantKeyboardShortcuts` (`:32`) and * `assistantPanelOwnsEscape` cannot drift apart. The host has to decline * exactly the key presses the panel takes — no more, or Escape stops working * altogether; no fewer, or it fires twice — and "exactly" is only checkable if * both sides ask one question. */ export declare function focusIsInsidePanel(panel: { contains(node: Node | null): boolean; } | null, active: Node | null): boolean; /** * Will the assistant panel handle this Escape itself? * * ★★★ HOSTS MUST CALL THIS BEFORE CLOSING THE PANEL ON ESCAPE. Both host apps * register a `document` keydown listener so Escape closes the panel even when * focus is elsewhere on the page (healthybowl-app and vibecontrols-app, * `src/components/chrome/AIAssistantOverlay.tsx`). The panel registers one too * (`useAssistantKeyboardShortcuts.ts:80`), routed through `escapeDismisses` so * an open overlay is backed out of rather than the whole assistant. * * Both listeners are on `document`, so ONE key press ran BOTH: the overlay * closed AND the panel unmounted, taking the in-flight turn with it — the exact * defect `escapeDismisses` exists to prevent, reintroduced one layer up where * this package could not see it. Measured on alpha 2026-09-11: preview open, * focus on the overlay, one Escape, and the panel was gone. * * ★★ `stopPropagation()` CANNOT FIX IT. The two listeners sit on the SAME node, * and propagation stops travel BETWEEN nodes; only `stopImmediatePropagation()` * reaches a sibling listener, and only one registered later. The panel's is not * reliably earlier: `AIAssistantPanel.tsx:207` passes a fresh inline arrow as * `onClose`, which is in the hook's dependency array (`:82`), so the panel's * effect re-subscribes on EVERY render and keeps moving to the back of the * queue. Neither side can depend on order. * * So ownership is decided by STATE, not by event order — whoever holds focus * owns the key: * - focus inside the panel -> the panel decides (overlay first, then close) * - focus anywhere else -> the host closes the panel, exactly as before * * That partition is total and disjoint BETWEEN THESE TWO LISTENERS, whichever * runs first. It says nothing about any other Escape handler on the page, and * the qualifier matters — the first draft of this comment claimed Escape was * "neither dropped nor double-handled" full stop, which is not true: * * - SUB-LAYERS INSIDE THE PANEL still need their own answer. The More menu * and the attachment menu each own Escape while open; they take it in the * CAPTURE phase and stop it, so it never reaches either listener here. * Before that, the panel's hook saw focus inside the panel, found no * full-surface overlay open, and closed the whole assistant — one press * dismissed a dropdown and destroyed the in-flight turn. * - ESCAPE CAN STILL BE DROPPED when focus is OUTSIDE the panel and a * `role="dialog"` descendant is mounted inside it (the read-only voice * transcript drawer): the host defers to the dialog, the dialog and this * package both require focus inside the panel, and nothing acts. Pre-dates * this contract and is not fixed by it. * - A FULL-SURFACE OVERLAY OPEN WITH FOCUS ON A REAL ELEMENT OUTSIDE the * panel still closes the whole assistant rather than backing out of the * overlay. Pre-existing. Note this no longer covers focus of "nothing" * (`` or null), which is the common way to get here and is handled * above — a control unmounting itself on click drops focus to `` * without the user going anywhere. * * This list is kept current deliberately. Earlier drafts described gaps that * had been closed and missed one that was reachable in three clicks, which is * worse than no list at all: the next reader trusts it. */ export declare function assistantPanelOwnsEscape(doc?: Pick): boolean; /** * Does THIS panel claim Escape, given what is focused? * * ★★★ ONE definition, used by both sides. `assistantPanelOwnsEscape` maps it * over every mounted panel for the hosts; `useAssistantKeyboardShortcuts` asks * it about its own panel. They MUST agree: the host declines exactly what the * panel accepts. When they drifted — the predicate widened to cover focus of * "nothing" and the hook's guard was left asking only "is focus inside?" — the * host deferred and the panel declined the same key, and Escape did nothing at * all with the history drawer still open. A zero-handler case is quieter than * the double-handler it replaced and no better. */ export declare function panelOwnsEscape(panel: { contains(node: Node | null): boolean; hasAttribute(name: string): boolean; } | null, active: Node | null, body: Node | null): boolean; //# sourceMappingURL=panelOverlays.d.ts.map