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.Core.CrumbSwitcher, only: [crumb_switcher: 1]
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 PhoenixKit.WebsiteAccess
alias PhoenixKitWeb.Components.Core.AdminLabel
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. 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."
attr :page_title_switcher, :map,
default: nil,
doc:
"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`."
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, 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."
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 :show_page_descriptions, :boolean,
default: nil,
doc:
"Overrides the `show_page_descriptions` setting for this render. `nil` (the default) reads the setting, which is off unless an operator turns it on under Settings → General; `page_subtitle` renders only when it is on."
attr :dev_environment, :boolean,
default: nil,
doc:
"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."
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: """
**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>
"""
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
# `