defmodule PhoenixKitWeb.Components.LayoutWrapper do
@compile {:no_warn_undefined,
[PhoenixKit.Modules.Legal, PhoenixKit.Modules.Legal.CookieConsent]}
@moduledoc """
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+) --%>
<%!-- content --%>
## Configuration
Configure parent layout in config.exs:
config :phoenix_kit,
layout: {MyAppWeb.Layouts, :app}
"""
use Phoenix.Component
use PhoenixKitWeb, :verified_routes
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
import PhoenixKitWeb.Components.Core.Flash, only: [flash_group: 1]
import PhoenixKitWeb.Components.Core.PhoenixKitFavicon
import PhoenixKitWeb.Components.Core.PhoenixKitGlobals
import PhoenixKitWeb.Components.AdminNav
import PhoenixKitWeb.Components.Dashboard.AdminSidebar, only: [admin_sidebar: 1]
import PhoenixKitWeb.Components.InvitationBanner, only: [invitation_banners: 1]
alias Phoenix.HTML
alias Phoenix.LiveView.TagEngine
alias PhoenixKit.Config
alias PhoenixKit.Modules.Crawlers
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Modules.Storage.URLSigner
alias PhoenixKit.ThemeConfig
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.PhoenixVersion
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Users.Auth
@doc """
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
"""
attr :flash, :map, default: %{}
# Parent LiveView socket — required only to embed the sticky
# NotificationsBell (a nested LiveView) in the admin header. Callers
# pass `socket={@socket}`; when absent the bell is simply not rendered.
attr :socket, :any, default: nil
attr :phoenix_kit_current_scope, :any, default: nil
attr :phoenix_kit_current_user, :any, default: nil
attr :page_title, :string, default: nil
attr :page_subtitle, :string, default: nil
attr :page_section, :string,
default: nil,
doc:
"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."
attr :page_section_path, :string,
default: nil,
doc:
"Prefixed path (via `PhoenixKit.Utils.Routes.path/1`) the `page_section` crumb links to. Renders as plain text when omitted."
attr :page_crumbs, :list,
default: [],
doc:
"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. The last crumb stays visible below `sm` (the trail truncates from the left); earlier crumbs collapse with the section."
attr :page_action, :map,
default: nil,
doc:
"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 — for a `phx-click` action (or anything needing `phx-target`), use the `:action` slot instead. ⚠️ Plugin LiveViews rendered through the admin layout can only use this map: the layout threads it as an assign, and a slot cannot travel that way."
attr :current_path, :string, default: nil
attr :inner_content, :string, default: nil
attr :project_title, :string, default: nil
attr :show_admin_panel_label, :boolean,
default: nil,
doc:
"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."
attr :current_locale, :string, default: nil
attr :from_layout, :boolean, default: false
attr :pk_pending_invitations, :list, default: []
attr :module_assigns, :map,
default: %{},
doc:
"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."
slot :action,
doc: """
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>
"""
slot :toolbar,
doc: """
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
# `