PhoenixKitWeb.Components.LayoutWrapper (phoenix_kit v2.32.1)

Copy Markdown View Source

Dynamic layout wrapper component for Phoenix v1.7- and v1.8+ compatibility.

This component automatically detects the Phoenix version and layout configuration to provide seamless integration with parent applications while maintaining backward compatibility.

Usage

Replace direct layout calls with the wrapper:

<%!-- OLD (Phoenix v1.7-) --%>
<%!-- Templates relied on router-level layout config --%>

<%!-- NEW (Phoenix v1.8+) --%>
<PhoenixKitWeb.Components.LayoutWrapper.app_layout flash={@flash}>
  <%!-- content --%>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>

Configuration

Configure parent layout in config.exs:

config :phoenix_kit,
  layout: {MyAppWeb.Layouts, :app}

Summary

Functions

Styles + pre-paint stamp for the admin sidebar's compact (icon-only) mode.

Renders content with the appropriate layout based on configuration and Phoenix version.

Rendering path for auth pages (login, register, reset, confirm, magic link, QR handoff, and the invite-only referral screen).

Renders a page's page_toolbar{Module, :fun} — with the LiveView's own assigns. Called by layouts/admin.html.heex, which is the only place that holds those assigns; the result goes into the :toolbar slot. Renders nothing when the page set no toolbar — and nothing, with a logged warning, when the pair names no fun/1: a page missing its toolbar beats the whole admin chrome raising on every render (the same policy nav_tabs applies to a dead tab).

Functions

admin_sidebar_compact_bootstrap(assigns)

Styles + pre-paint stamp for the admin sidebar's compact (icon-only) mode.

Render once, immediately BEFORE the sidebar markup. Everything about compact mode lives client-side, and deliberately so:

  • the sidebar is a function component, not a LiveView — there is no handle_event/3 owner for a phx-click, and giving one to every admin page (or bolting a global attach_hook onto the admin on_mount chain) would be a lot of machinery for a display preference;
  • it is a per-browser density choice, exactly like the theme, so it belongs in localStorage next to it rather than in a settings row;
  • a client-side toggle costs no round trip, and nothing for morphdom to fight over — the DOM is identical either way, only <html> changes.

Which makes the first paint the whole problem, and the reason this is a synchronous inline <script> rather than a hook in phoenix_kit.js:

  • it must run before the sidebar is parsed, or a viewer who chose compact gets a frame of the full-width menu on every load. An inline script placed above the markup does exactly that;
  • it must not depend on the host having re-run mix phoenix_kit.update to refresh its vendored phoenix_kit.js. Self-contained markup ships with the feature.

Same reasoning, and the same shape, as PhoenixKitWeb.Components.ThemeBootstrap — including the one-instance guard, since a host layout and the kit's own admin shell can both be on the page.

The CSS hides the label rather than removing it (clip-path, not display: none), so every link keeps its accessible name and the menu still reads correctly to a screen reader while collapsed.

app_layout(assigns)

Renders content with the appropriate layout based on configuration and Phoenix version.

Automatically handles:

  • Phoenix v1.8+ function component layouts
  • Phoenix v1.7- legacy layout configuration
  • Fallback to PhoenixKit layouts when no parent configured
  • Parent layout compatibility with PhoenixKit assigns

Attributes

  • flash - Flash messages (required)
  • phoenix_kit_current_scope - Current authentication scope (optional)
  • phoenix_kit_current_user - Current user (optional, for backwards compatibility)

Inner Block

  • inner_block - Content to render within the layout

Attributes

  • flash (:map) - Defaults to %{}.
  • socket (:any) - Defaults to nil.
  • phoenix_kit_current_scope (:any) - Defaults to nil.
  • phoenix_kit_current_user (:any) - Defaults to nil.
  • page_title (:string) - Defaults to nil.
  • page_subtitle (:string) - Defaults to nil.
  • page_section (:string) - Optional breadcrumb segment rendered between "Admin Panel" and page_title (e.g. "Users" on a user detail page). Desktop only — collapses along with the rest of the breadcrumb prefix on mobile. Defaults to nil.
  • page_section_path (:string) - Prefixed path (via PhoenixKit.Utils.Routes.path/1) the page_section crumb links to. Renders as plain text when omitted. Defaults to nil.
  • page_crumbs (:list) - Extra breadcrumb crumbs rendered between page_section and page_title, for pages nested deeper than one level (e.g. catalogue / category drill trails): [%{label: "Plumbing", path: "/…"}]. path is a push_navigate target; patch is a push_patch target for same-LiveView drill trails. Both are optional — omitted renders plain text. A crumb may also carry a :switcher (see page_title_switcher): a ▾ beside it lists the other things on that crumb's level. The last crumb stays visible below sm (the trail truncates from the left); earlier crumbs collapse with the section. Defaults to [].
  • page_title_switcher (:map) - A switcher on the page title itself: a ▾ beside it opening a searchable list of the other things on this level — GitHub's repository switcher. Same shape as a crumb's :switcher (%{title:, items: [%{label:, navigate: | patch:, current:}], search_placeholder:}); see PhoenixKitWeb.Components.Core.CrumbSwitcher. Defaults to nil.

  • page_action (:map) - Optional compact action button rendered right after the breadcrumb title: %{icon: "hero-plus", label: "New template", navigate: path}. Lets a page keep its primary create action without spending an in-content header row. label becomes the tooltip/aria-label; icon defaults to hero-plus. Navigation only, by design: it renders a real link, so middle-click, open-in-new-tab and copy-link keep working. For anything interactive — a phx-click, a modal, a JS command — use page_toolbar: {Module, :fun} on the socket (see the :toolbar slot), which reaches every page including plugin LiveViews rendered through the admin layout. Do not add click handling to this map. Defaults to nil.
  • current_path (:string) - Defaults to nil.
  • inner_content (:string) - Defaults to nil.
  • project_title (:string) - Defaults to nil.
  • show_admin_panel_label (:boolean) - Overrides the show_admin_panel_label setting for this render. nil (the default) reads the setting. Mirrors how project_title overrides Settings.get_project_title/0, and keeps the header renderable without a database. Defaults to nil.
  • dev_environment (:boolean) - Overrides PhoenixKit.WebsiteAccess.environment().looks_like_dev? for this render (nil, the default, reads it). Drives the small "[dev]" tag next to the project title — automatic, not a setting, so a dev/staging box never has to be told apart by an admin toggle. Defaults to nil.
  • current_locale (:string) - Defaults to nil.
  • from_layout (:boolean) - Defaults to false.
  • pk_pending_invitations (:list) - Defaults to [].
  • module_assigns (:map) - Module-supplied host-consumable assigns. Each key in this map is merged into the assigns set passed to the parent layout (Layouts.app), so a host's custom layout can read e.g. assigns[:phoenix_kit_publishing_translations] from publishing, or any other module-defined key. Plain conn.assigns don't reach a function-component layout — only declared attrs do — so this single map attribute is how modules thread arbitrary host-consumable data through the boundary without core having to declare each one explicitly. Defaults to %{}.

Slots

  • action - Superseded by :toolbar / page_toolbar — prefer those for new code: they reach every page, including plugin LiveViews routed through the admin layout, which this slot never can. Kept for existing direct callers.

    The same compact action button, for pages whose primary action is not a navigation — a phx-click, a JS command, anything needing phx-target. The map attribute cannot express those and cannot address a LiveComponent.

    Takes render priority over the page_action attribute. Content is wrapped in the same chip shell, and the contract is one compact control: a multi-action toolbar belongs in the page body, not the breadcrumb bar.

    ⚠️ Only reaches views calling app_layout/1 directly. Plugin LiveViews render through layouts/admin.html.heex, which threads page_action as an assign — slots do not travel through assigns — so those keep the map.

    <:action>
      <button phx-click="new_device" phx-target={@myself} title="Add device">
        <.icon name="hero-plus" class="w-4 h-4" />
      </button>
    </:action>
  • toolbar - Controls that belong to the page's identity, rendered in the breadcrumb bar right after the title — a status picker, the page's ⋮ menu. Rendered as given (no chip shell); distinct from page_action / :action, which stay the one compact create chip.

    Two ways in. A view calling app_layout/1 directly passes this slot. A plugin LiveView rendered through layouts/admin.html.heex cannot pass a slot, so it assigns page_toolbar: {Module, :fun} on its socket: the layout calls render_page_toolbar/1 with the LiveView's own assigns (change-tracked) and puts the result in this slot — phx-change / phx-click inside it reach the LiveView as usual, because the layout renders inside it. Embedded mounts have no breadcrumb bar, so a page that is also embeddable renders the same component in its body there.

    # in a plugin LiveView's mount:
    assign(socket, page_toolbar: {__MODULE__, :header_toolbar})
    # `def header_toolbar(assigns)` renders, with ~H, e.g. a
    # `<form id="status" phx-change="change_status">` select and the
    # page's `<.table_row_menu>`; both events land in handle_event/3.
  • inner_block

auth_layout(assigns)

Rendering path for auth pages (login, register, reset, confirm, magic link, QR handoff, and the invite-only referral screen).

Auth pages do not render inside the host's Layouts.app. That layout is where a mix phx.new app keeps its logo, framework version and off-site links, so wrapping sign-in in it put Phoenix Framework branding on the login page of every kit install — reported by more than one host. Admin already works this way (render_admin_with_parent/1 never calls the host's :app); auth was the outlier.

The host's root layout still applies, because PhoenixKitWeb.Integration never calls put_root_layout. That is where the document shell, assets and CSRF come from, and it is unaffected.

A host that genuinely wants its own chrome on sign-in opts back in:

config :phoenix_kit, auth_uses_host_layout: true

⚠️ Two things a host may notice when it does not: anything wired into the app layout rather than root — a cookie-consent banner, analytics, a theme toggle — stops appearing on auth pages only. Root-level wiring is unaffected. Stock phx.new puts assets and CSRF in root, so conventional hosts see only the branding disappear, which is the point.

Attributes

  • flash (:map) (required)
  • phoenix_kit_current_scope (:any) - Defaults to nil.
  • page_title (:string) - Defaults to nil.
  • current_path (:string) - Defaults to nil.
  • pk_pending_invitations (:list) - Defaults to [].

Slots

  • inner_block (required)

get_language_flag(code)

render_page_toolbar(assigns)

@spec render_page_toolbar(map()) :: Phoenix.LiveView.Rendered.t() | nil

Renders a page's page_toolbar{Module, :fun} — with the LiveView's own assigns. Called by layouts/admin.html.heex, which is the only place that holds those assigns; the result goes into the :toolbar slot. Renders nothing when the page set no toolbar — and nothing, with a logged warning, when the pair names no fun/1: a page missing its toolbar beats the whole admin chrome raising on every render (the same policy nav_tabs applies to a dead tab).