/**
* 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