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 60 and increments
by 10 per stack depth. The dialogs open via the open attribute (not
showModal()), so they are NOT in the browser top layer and must outrank
the admin shell's fixed header (z-50) explicitly — same reasoning as
the project drawer's z-[60]/z-[70]. 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>
Summary
Functions
Attributes
modal_stack(:list) (required)on_close(:string) - Defaults to"close_top_modal".class(:string) - Defaults tonil.modal_box_class(:string) - daisyUImodal-boxsizing/class overrides. Default"w-11/12 max-w-6xl"takes 91% of the viewport width capped atmax-w-6xl(72rem ≈ 1152px) — wider than daisyUI's defaultmax-w-mdso embedded admin LVs (project show, assignment form, etc.) have room for tables + cards + timelines. Pass a different Tailwind size class ("max-w-4xl","max-w-7xl", etc.) if a host page wants a narrower or wider modal.Defaults to
"w-11/12 max-w-6xl".
Slots
inner_block(required)frame(required) - Accepts attributes:any(:any)