Packages
phoenix_kit
1.0.0
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/components/layout_wrapper.ex
defmodule PhoenixKitWeb.Components.LayoutWrapper do
@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+) -->
<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}
"""
use Phoenix.Component
use PhoenixKitWeb, :verified_routes
import PhoenixKitWeb.CoreComponents, only: [flash_group: 1]
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.PhoenixVersion
@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: %{}
attr :phoenix_kit_current_scope, :any, default: nil
attr :phoenix_kit_current_user, :any, default: nil
attr :page_title, :string, default: nil
attr :current_path, :string, default: nil
slot :inner_block, required: true
def app_layout(assigns) do
# For admin pages, render simplified layout without parent headers
if admin_page?(assigns) do
render_admin_only_layout(assigns)
else
case get_layout_config() do
{module, function} when is_atom(module) and is_atom(function) ->
render_with_parent_layout(assigns, module, function)
nil ->
render_with_phoenix_kit_layout(assigns)
end
end
end
## Private Implementation
# Check if current page is an admin page that needs navigation
defp admin_page?(assigns) do
case assigns[:current_path] do
nil -> false
path when is_binary(path) -> String.contains?(path, "/admin/")
_ -> false
end
end
# Wrap inner_block with admin navigation if needed
defp wrap_inner_block_with_admin_nav_if_needed(assigns) do
if admin_page?(assigns) do
# Import AdminNav functions for use in template
import PhoenixKitWeb.AdminNav
# Import Scope for user info
alias PhoenixKit.Users.Auth.Scope
# Create new inner_block slot that wraps original content with admin navigation
original_inner_block = assigns[:inner_block]
new_inner_block = [
%{
inner_block: fn _slot_assigns, _index ->
# Create template assigns with needed values
template_assigns = %{
original_inner_block: original_inner_block,
current_path: assigns[:current_path],
phoenix_kit_current_scope: assigns[:phoenix_kit_current_scope]
}
assigns = template_assigns
~H"""
<!-- PhoenixKit Admin Layout following EZNews pattern -->
<!-- Mobile Header (показывается только на мобильных в админке) -->
<header class="bg-base-100 shadow-sm border-b border-base-300 lg:hidden">
<div class="flex items-center justify-between h-16 px-4">
<!-- Mobile Menu Button -->
<label for="admin-mobile-menu" class="btn btn-square btn-primary drawer-button p-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</label>
<!-- Logo -->
<div class="flex items-center">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center mr-2">
<svg
class="w-5 h-5 text-primary-content"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.031 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<span class="font-bold text-base-content">PhoenixKit Admin</span>
</div>
<!-- Theme Switcher Mobile -->
<.admin_theme_controller mobile={true} />
</div>
</header>
<div class="drawer lg:drawer-open">
<input id="admin-mobile-menu" type="checkbox" class="drawer-toggle" />
<!-- Main content -->
<div class="drawer-content flex flex-col">
<!-- Page content from parent layout -->
{render_slot(@original_inner_block)}
</div>
<!-- Desktop/Mobile Sidebar (БЕЗ overlay на десктопе) -->
<div class="drawer-side">
<label for="admin-mobile-menu" class="drawer-overlay lg:hidden"></label>
<aside class="min-h-full w-64 bg-base-100 shadow-lg border-r border-base-300 flex flex-col">
<!-- Sidebar header (только на десктопе) -->
<div class="px-4 py-6 border-b border-base-300 hidden lg:block">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<svg
class="w-5 h-5 text-primary-content"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.031 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<div>
<h2 class="font-bold text-base-content">PhoenixKit Admin</h2>
</div>
</div>
</div>
<!-- Navigation (заполняет доступное пространство) -->
<nav class="px-4 py-6 space-y-2 flex-1">
<!-- System Section -->
<div class="text-xs font-semibold text-base-content/60 uppercase tracking-wide mb-2">
System
</div>
<.admin_nav_item
href="/phoenix_kit/admin/dashboard"
icon="dashboard"
label="Dashboard"
current_path={@current_path || ""}
/>
<div class="divider my-3"></div>
<!-- User Management Section -->
<div class="text-xs font-semibold text-base-content/60 uppercase tracking-wide mb-2">
User Management
</div>
<.admin_nav_item
href="/phoenix_kit/admin/users"
icon="users"
label="Users"
current_path={@current_path || ""}
/>
<.admin_nav_item
href="/phoenix_kit/admin/roles"
icon="roles"
label="Roles"
current_path={@current_path || ""}
/>
</nav>
<!-- Bottom Section: Theme & User Info -->
<div class="p-4 border-t border-base-300 space-y-3">
<!-- Theme Controller (только на десктопе) -->
<div class="hidden lg:block">
<.admin_theme_controller mobile={false} />
</div>
<!-- User Info -->
<.admin_user_info scope={@phoenix_kit_current_scope} />
</div>
</aside>
</div>
</div>
<!-- Auto-close mobile drawer on navigation -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const drawerToggle = document.getElementById('admin-mobile-menu');
const navLinks = document.querySelectorAll('.drawer-side a');
navLinks.forEach(link => {
link.addEventListener('click', () => {
if (drawerToggle && window.innerWidth < 1024) {
drawerToggle.checked = false;
}
});
});
});
// Admin theme controller for PhoenixKit with animated slider
const adminThemeController = {
init() {
const savedTheme = localStorage.getItem('phoenix_kit_theme') || 'system';
this.setTheme(savedTheme);
this.setupListeners();
},
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('phoenix_kit_theme', theme);
// Update slider position via CSS data attribute
document.documentElement.setAttribute('data-theme', theme);
// Update active state for all theme buttons
document.querySelectorAll('[data-theme-target]').forEach(btn => {
if (btn.dataset.themeTarget === theme) {
btn.classList.add('text-primary');
} else {
btn.classList.remove('text-primary');
}
});
},
setupListeners() {
// Listen to Phoenix LiveView theme events
document.addEventListener('phx:set-admin-theme', (e) => {
this.setTheme(e.detail.theme);
});
}
};
// Initialize admin theme controller
adminThemeController.init();
</script>
"""
end
}
]
# Return assigns with new inner_block
assign(assigns, :inner_block, new_inner_block)
else
# Not an admin page, return assigns unchanged
assigns
end
end
# Render with parent application layout (Phoenix v1.8+ function component approach)
defp render_with_parent_layout(assigns, module, function) do
# Prepare assigns for parent layout compatibility
assigns = prepare_parent_layout_assigns(assigns)
# Dynamically call the parent layout function based on Phoenix version
case PhoenixVersion.get_strategy() do
:modern ->
render_modern_parent_layout(assigns, module, function)
:legacy ->
render_legacy_parent_layout(assigns, module, function)
end
end
# Phoenix v1.8+ approach - function components
defp render_modern_parent_layout(assigns, module, function) do
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
# Use apply/3 to dynamically call the parent layout function
apply(module, function, [assigns])
rescue
UndefinedFunctionError ->
# Fallback to PhoenixKit layout if parent function doesn't exist
render_with_phoenix_kit_layout(assigns)
end
# Phoenix v1.7- approach - templates (legacy support)
defp render_legacy_parent_layout(assigns, _module, _function) do
# For legacy Phoenix, layouts are handled at router level
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
# Just render content without wrapper - layout comes from router
~H"""
{render_slot(@inner_block)}
"""
end
# Render admin pages with simplified layout (no parent headers)
defp render_admin_only_layout(assigns) do
# Wrap inner content with admin navigation
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
~H"""
<!DOCTYPE html>
<html lang="en" data-theme="light" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
<.live_title default="PhoenixKit Admin">
{assigns[:page_title] || "Admin"}
</.live_title>
<link phx-track-static rel="stylesheet" href="/assets/app.css" />
<script defer phx-track-static type="text/javascript" src="/assets/app.js" />
</head>
<body class="bg-base-200 antialiased">
<!-- Admin pages without parent headers -->
<main class="min-h-screen">
<.flash_group flash={@flash} />
{render_slot(@inner_block)}
</main>
</body>
</html>
"""
end
# Fallback to PhoenixKit's own layout
defp render_with_phoenix_kit_layout(assigns) do
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
~H"""
<PhoenixKitWeb.Layouts.app {prepare_phoenix_kit_assigns(assigns)}>
{render_slot(@inner_block)}
</PhoenixKitWeb.Layouts.app>
"""
end
# Prepare assigns for parent layout compatibility
defp prepare_parent_layout_assigns(assigns) do
assigns
|> Map.put_new(:current_user, get_current_user_for_parent(assigns))
|> Map.put_new(:phoenix_kit_integrated, true)
|> Map.put_new(:phoenix_kit_version, get_phoenix_kit_version())
|> Map.put_new(:phoenix_version_info, PhoenixVersion.get_version_info())
end
# Prepare assigns specifically for PhoenixKit layout
defp prepare_phoenix_kit_assigns(assigns) do
assigns
|> Map.put_new(:phoenix_kit_standalone, true)
end
# Extract current user from scope for parent layout compatibility
defp get_current_user_for_parent(assigns) do
case assigns[:phoenix_kit_current_scope] do
nil -> assigns[:phoenix_kit_current_user]
scope -> Scope.user(scope)
end
end
# Get layout configuration from application environment with Phoenix version compatibility
defp get_layout_config do
case Application.get_env(:phoenix_kit, :phoenix_version_strategy) do
:modern ->
# Phoenix v1.8+ - get layouts_module and assume :app function
case Application.get_env(:phoenix_kit, :layouts_module) do
nil -> nil
module -> {module, :app}
end
:legacy ->
# Phoenix v1.7- - use legacy layout config
Application.get_env(:phoenix_kit, :layout)
nil ->
# Fallback - check for legacy layout config first
Application.get_env(:phoenix_kit, :layout)
end
end
# Get PhoenixKit version
defp get_phoenix_kit_version do
case Application.spec(:phoenix_kit) do
nil ->
"unknown"
spec ->
spec
|> Keyword.get(:vsn, "unknown")
|> to_string()
end
end
end