Imperal Docs
Recipes

Center-overlay panel

Build a modal-style center-overlay panel in Imperal Cloud: declare center_overlay on the panel and open it from the sidebar for a focused full-bleed editor.

Declare center_overlay=True — read before building

Center-overlay rendering is activated declaratively by center_overlay=True on the @ext.panel decorator (v4.1.8+). You do not need a special panel_id, and you do not need any frontend change — set center_overlay=True and the host renders the panel as a full-bleed overlay over the main content area when it is dispatched.

slot="center" describes which slot the panel belongs to, but the overlay behavior itself comes from the center_overlay=True flag. The panel handler is reached only when an explicit ui.Call or auto_action dispatches it (center-overlay panels are not called during batch discovery), so pick any panel_id you like and trigger it from a sidebar ui.Call/auto_action.

When you want a focused, full-bleed editor that appears over the main content area, declare center_overlay=True on your @ext.panel and use auto_action on the sidebar root to open it immediately after discovery. The panel_id can be any name you choose — in this example we use "editor" to mirror the Notes extension, but the overlay behavior is driven by the center_overlay=True declaration, not by a specific id.


panels.py — center-overlay complete example
from __future__ import annotations

from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="An extension with a full-bleed center-overlay editor.",
    actions_explicit=True,
)

# ── Inline stub data — replace with your real data fetch in production ─────
_NOTES = [
    {"id": "note-1", "title": "First note",  "body": "Body of note one."},
    {"id": "note-2", "title": "Second note", "body": "Body of note two."},
]


def _get_note(note_id: str) -> dict | None:
    return next((n for n in _NOTES if n["id"] == note_id), None)


# ── Left sidebar — discovery-safe, sets auto_action to open overlay ────────

@ext.panel(
    "sidebar",
    slot="left",
    title="Notes",
    icon="🗒️",
    default_width=280,
    min_width=200,
    max_width=500,
)
async def sidebar(
    ctx: object,
    active_note_id: str = "",
    view: str = "notes",
    **kwargs: object,
) -> object:
    notes = _NOTES  # replace with: notes = await fetch_notes(ctx)

    items = [
        ui.ListItem(
            id=note["id"],
            title=note["title"],
            selected=(note["id"] == active_note_id),
            # Dispatching the "editor" panel (declared with center_overlay=True)
            # opens it as a full-bleed center overlay over the main content area.
            on_click=ui.Call("__panel__editor", note_id=note["id"]),
        )
        for note in notes
    ]

    root = ui.Stack(
        children=[ui.List(items=items)],
        gap=2,
    )

    # auto_action: open the most-recent note on first load.
    # Conditional: only when no note is already active and we are in the
    # normal view (not trash). The host fires this once after discovery
    # (the discovery-once guard). Navigating away and returning to
    # the same extension page does NOT re-fire auto_action within the
    # same session.
    if notes and not active_note_id and view != "trash":
        root.props["auto_action"] = ui.Call(
            "__panel__editor", note_id=notes[0]["id"]
        )

    return root


# ── Center-overlay editor ──────────────────────────────────────────────────
# center_overlay=True is what makes this panel render as a full-bleed overlay
# (v4.1.8+). The panel_id can be any name you choose; here it is "editor".
# This handler is only reached via an explicit ui.Call or auto_action — center
# panels are not called during batch discovery.

@ext.panel(
    "editor",
    slot="center",
    center_overlay=True,
    title="Editor",
    icon="📝",
)
async def editor(ctx: object, note_id: str = "", **kwargs: object) -> object:
    # At batch discovery the center panel is NOT called (only left/right are
    # called at init). This handler is only reached via an explicit
    # ui.Call or auto_action. When called without note_id, return ui.Empty()
    # rather than None — ui.Empty() is the correct "no content yet" signal.
    if not note_id:
        return ui.Empty(message="Select a note to edit", icon="📄")

    note = _get_note(note_id)
    if note is None:
        return ui.Error(message="Note not found")

    return ui.Stack(
        children=[
            ui.Text(note["title"], variant="h2"),
            ui.RichEditor(
                content=note["body"],
                on_change=ui.Call("save_note", note_id=note_id),
            ),
        ],
        gap=4,
    )

Walk-through

What makes this panel a center overlay? The center_overlay=True flag on the @ext.panel decorator (v4.1.8+). When that flag is set, the host renders the panel as a full-bleed overlay over the main content area whenever it is dispatched via ui.Call or auto_action. The panel_id is just a name you choose — "editor" here mirrors the Notes extension, but any id works. You do not need a frontend change to register a new center-overlay panel; declaring center_overlay=True is enough.

Why does the center panel handler never see batch-discovery calls? Batch discovery only calls the panels registered in config.panels.left and config.panels.right. Center-overlay panels are not in that config. The editor handler is only reached when an explicit ui.Call or auto_action dispatches it. This means you do not need a return None guard for discovery — but you do need the if not note_id: return ui.Empty() guard for the case where auto_action or a ui.Call arrives without a valid note_id.

Why auto_action on the sidebar root and not on the editor panel itself? The host reads auto_action only from leftPanel.props.auto_action. Setting it anywhere else — on the editor's own root, on a right-panel root, on a child node — has no effect. The sidebar owns the auto_action prop; the editor is the target of the action.

The three-condition guard for auto_action. if notes and not active_note_id and view != "trash" mirrors the production pattern from notes/panels.py. The conditions are: (1) there is content to open — do not claim center for an empty list; (2) no note is already active — avoid re-claiming center when the sidebar refreshes while an editor is open; (3) not in the trash view — the trash view has different semantics and should not auto-open a note.


On this page