defmodule PUI.Layout do
@moduledoc """
Reusable application shell primitives for documentation sites, dashboards, and
other two-pane LiveView layouts.
The layout API is split into small components so applications can compose
their own shell without inheriting opinionated navigation data structures.
## Basic Usage
<.app_layout id="docs-shell">
<:sidebar>
<.sidebar>
<:header>
PUI
<:header>
<.content_header shell_id="docs-shell" breadcrumb_current="Getting Started" />
Content goes here.
## Collapsible Navigation
Use `collapsible={true}` on `sidebar_menu_item/1` when an item owns nested
links. The submenu state is handled by the bundled `PUI.Sidebar` hook.
<.sidebar_menu_item
title="Components"
icon="hero-squares-2x2"
collapsible
expanded
>
<:subitem>
<.link href="/docs/button" class="block rounded-md px-2 py-1.5 text-sm">Button
## Components
| Component | Description |
|-----------|-------------|
| `app_layout/1` | Root shell with collapsible sidebar state |
| `sidebar/1` | Sidebar surface with header and footer slots |
| `sidebar_menu_item/1` | Sidebar link row with optional collapsible submenu |
| `content_header/1` | Sticky content header with sidebar toggle and breadcrumbs |
"""
use Phoenix.Component
import PUI.Popover, only: [tooltip: 1]
import PUI.Dropdown, only: [menu_button: 1]
@doc """
Renders a two-pane application shell.
## Attributes
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `id` | `string` | `"app-layout"` | Shell DOM id, used by `content_header/1` toggle |
| `class` | `string` | `""` | Additional classes for the root shell |
| `content_class` | `string` | `"w-full px-4 py-4"` | Classes for the scrollable main content element |
## Slots
| Name | Required | Description |
|------|----------|-------------|
| `sidebar` | Yes | Sidebar content, usually `sidebar/1` |
| `header` | No | Sticky header content, usually `content_header/1` |
| `inner_block` | Yes | Main page content |
"""
attr :id, :string, default: "app-layout"
attr :class, :string, default: ""
attr :content_class, :string, default: "w-full px-4 py-4"
slot :sidebar, required: true
slot :header
slot :inner_block, required: true
def app_layout(assigns) do
~H"""
{render_slot(@sidebar)}
<%= if @header != [] do %>
{render_slot(@header)}
<% end %>
{render_slot(@inner_block)}
"""
end
@doc """
Renders a collapsible sidebar surface.
## Attributes
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `id` | `string` | `"app-sidebar"` | Sidebar DOM id |
| `class` | `string` | `""` | Additional classes for the sidebar |
| `expanded_width_class` | `string` | `"w-72"` | Width class when the shell is expanded |
| `collapsed_width_class` | `string` | `"group-data-[collapsed=true]/pui-layout:w-12"` | Width class when collapsed |
## Slots
| Name | Required | Description |
|------|----------|-------------|
| `header` | No | Logo, workspace switcher, or sidebar title |
| `inner_block` | Yes | Sidebar navigation/content |
| `footer` | No | Account controls or secondary actions |
"""
attr :id, :string, default: "app-sidebar"
attr :class, :string, default: ""
attr :expanded_width_class, :string, default: "w-72"
attr :collapsed_width_class, :string, default: "group-data-[collapsed=true]/pui-layout:w-12"
slot :header
slot :inner_block, required: true
slot :footer
def sidebar(assigns) do
~H"""
"""
end
@doc """
Renders a sidebar item as a link or as a collapsible submenu trigger.
## Attributes
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `id` | `string` | derived from `title` | Stable DOM id |
| `title` | `string` | required | Visible item label and link title |
| `icon` | `string` | required | Heroicon class, such as `"hero-home"` |
| `navigate` | `string` | `nil` | LiveView navigation target |
| `href` | `string` | `nil` | Link href |
| `patch` | `string` | `nil` | LiveView patch target |
| `collapsible` | `boolean` | `false` | Render a submenu trigger instead of a link |
| `expanded` | `boolean` | `false` | Initial submenu state |
| `current` | `boolean` | `false` | Applies active item styling |
| `class` | `string` | `""` | Additional classes for the row |
## Slots
| Name | Required | Description |
|------|----------|-------------|
| `trailing` | No | Badge, count, or secondary row content |
| `subitem` | No | Repeated submenu rows for collapsible items |
"""
attr :id, :string, default: nil
attr :title, :string, required: true
attr :icon, :string, required: true
attr :navigate, :string, default: nil
attr :href, :string, default: nil
attr :patch, :string, default: nil
attr :collapsible, :boolean, default: false
attr :expanded, :boolean, default: false
attr :class, :string, default: ""
attr :current, :boolean, default: false
slot :trailing
slot :subitem
def sidebar_menu_item(assigns) do
assigns =
assign(
assigns,
:id,
assigns.id ||
assigns.title
|> String.downcase()
|> String.replace(~r/[^a-z0-9]+/, "-")
|> String.trim("-")
|> then(&"sidebar-item-#{&1}")
)
~H"""