Layered daisyUI <dialog> modal stack driven by a modal_stack
assign. The function component renders the always-visible content
(default slot) plus one <dialog> per stack frame, delegating each
frame's body rendering to the :frame slot the host provides.
The host LV owns state — receiving :opened / :closed / :saved /
:deleted PubSub events, pushing/popping the stack, generating
frame_refs. See PhoenixKitProjects.Web.PopupHostLive for the
opinionated wrapper that does this automatically. Use the component
directly when you need full control (e.g. modal-stack alongside other
host state).
Reuses the daisyUI modal pattern from project_show_live.ex:1633-1662
— <dialog open class="modal modal-open"> + ESC handler +
modal-backdrop button.
Slots
:inner_block(default) — the always-visible content. Host typically embeds the root LV here vialive_render(@socket, ...).:frame(with:let={frame}) — per-stack-frame content. Receives the frame map (%{frame_ref, lv, session, id}) so the host can calllive_render(@socket, frame.lv, id: frame.id, session: frame.session).
Attrs
:modal_stack— list of frame maps (ordered bottom→top).:on_close— event name fired on ESC, backdrop-click, and explicit close buttons. Host'shandle_event/3must pop the top frame in response. Defaults to"close_top_modal".:class— outer wrapper class. Defaults to nil (no wrapping).
Z-index layering
Each frame's <dialog> gets z-[N] where N starts at 50 (matches the
start-project modal precedent) and increments by 10 per stack depth.
Stack cap at 5 frames matches PopupHostLive's @max_stack_depth.
Example
<.popup_host modal_stack={@modal_stack} on_close="close_top_modal">
{live_render(@socket, PhoenixKitProjects.Web.OverviewLive,
id: "embed-root",
session: %{
"mode" => "emit",
"pubsub_topic" => @host_topic,
"wrapper_class" => "flex flex-col w-full px-4 py-6 gap-6"
})}
<:frame :let={frame}>
{live_render(@socket, frame.lv, id: frame.id, session: frame.session)}
</:frame>
</.popup_host>