Packages
phoenix_kit
1.7.39
1.7.209
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/layouts/dashboard.html.heex
<%!--
PhoenixKit User Dashboard Layout
This layout is specifically designed for authenticated user dashboard pages.
It provides a clean interface for regular users to view their profile and activities.
Features:
- Dynamic tab navigation from PhoenixKit.Dashboard.Registry
- Support for custom tabs via config or runtime registration
- Real-time badge updates via PubSub
- Presence tracking for viewer counts
- Attention animations for important tabs
- Mobile-friendly navigation
Note: This layout does NOT use LayoutWrapper.app_layout as that's for admin pages only.
--%>
<% # Branding configuration
project_title = assigns[:project_title] || PhoenixKit.Settings.get_project_title()
project_title_suffix = assigns[:project_title_suffix]
project_title_suffix =
if is_nil(project_title_suffix),
do: PhoenixKit.Config.get(:project_title_suffix, "Dashboard"),
else: project_title_suffix
project_logo = assigns[:project_logo] || PhoenixKit.Config.get(:project_logo, nil)
project_icon = assigns[:project_icon] || PhoenixKit.Config.get(:project_icon, "hero-home")
project_logo_height =
assigns[:project_logo_height] || PhoenixKit.Config.get(:project_logo_height, "h-8")
project_logo_class =
assigns[:project_logo_class] || PhoenixKit.Config.get(:project_logo_class, nil)
project_home_url_raw = assigns[:project_home_url] || PhoenixKit.Config.get(:project_home_url, "/")
# Paths starting with ~/ get the URL prefix applied, others stay as-is
project_home_url =
case project_home_url_raw do
"~/" <> path -> PhoenixKit.Utils.Routes.path("/" <> path)
other -> other
end
show_title_with_logo = assigns[:show_title_with_logo]
show_title_with_logo =
if is_nil(show_title_with_logo),
do: PhoenixKit.Config.get(:show_title_with_logo, true),
else: show_title_with_logo
# Build full display title
display_title =
if project_title_suffix && project_title_suffix != "" do
"#{project_title} #{project_title_suffix}"
else
project_title
end %>
<% # Get tabs from registry (with active state based on current path)
dashboard_tabs =
assigns[:dashboard_tabs] ||
PhoenixKit.Dashboard.Registry.get_tabs_with_active(
assigns[:url_path] || "/dashboard",
scope: assigns[:phoenix_kit_current_scope]
)
# Get viewer counts if presence is enabled
viewer_counts = assigns[:tab_viewer_counts] || %{}
# Get collapsed groups
collapsed_groups = assigns[:collapsed_dashboard_groups] || MapSet.new()
# Get context selector data (may be nil if not configured)
show_context_selector = assigns[:show_context_selector] || false
dashboard_contexts = assigns[:dashboard_contexts] || []
current_context = assigns[:current_context]
context_selector_config =
assigns[:context_selector_config] || %PhoenixKit.Dashboard.ContextSelector{enabled: false}
# Multi-selector data (new)
context_selector_configs = assigns[:context_selector_configs] || []
dashboard_contexts_map = assigns[:dashboard_contexts_map] || %{}
current_contexts_map = assigns[:current_contexts_map] || %{}
show_context_selectors_map = assigns[:show_context_selectors_map] || %{}
has_multi_selector = context_selector_configs != [] %> <!DOCTYPE html>
<html lang="en">
<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()} />
<meta name="phoenix-kit-prefix" content={PhoenixKit.Utils.Routes.url_prefix()} />
<.live_title default={display_title}>
{assigns[:page_title] || "Dashboard"}
</.live_title>
<link phx-track-static rel="stylesheet" href="/assets/css/app.css" />
<%!-- PhoenixKit Custom Theme CSS (phoenix-light, phoenix-dark) --%>
<style data-phoenix-kit-themes>
<%= Phoenix.HTML.raw(PhoenixKit.ThemeConfig.custom_theme_css()) %>
</style>
<%!-- Dashboard animations CSS --%>
<style>
/* Shake animation for attention */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
20%, 40%, 60%, 80% { transform: translateX(2px); }
}
.animate-shake { animation: shake 0.5s ease-in-out; }
/* Glow animation for highlighting */
@keyframes glow {
0%, 100% { box-shadow: 0 0 5px currentColor; }
50% { box-shadow: 0 0 20px currentColor; }
}
.animate-glow { animation: glow 2s ease-in-out infinite; }
/* Badge pop animation */
@keyframes badge-pop {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.animate-badge-pop { animation: badge-pop 0.3s ease-out; }
/* Subtab animations */
@keyframes slide-in-left {
0% { opacity: 0; transform: translateX(-24px); }
100% { opacity: 1; transform: translateX(0); }
}
.animate-slide-in-left { animation: slide-in-left 0.35s ease-out; }
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
.animate-fade-in { animation: fade-in 0.3s ease-out; }
@keyframes collapse-open {
0% { opacity: 0; max-height: 0; }
100% { opacity: 1; max-height: 100px; }
}
.animate-collapse-open { animation: collapse-open 0.3s ease-out; }
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
</style>
</head>
<body class="bg-base-100 antialiased">
<div class="min-h-screen bg-base-100">
<%!-- User Dashboard Header --%>
<header class="bg-base-100 shadow-sm border-b border-base-300 fixed top-0 left-0 right-0 z-50">
<div class="flex items-center justify-between h-16 px-4">
<%!-- Left: Logo and Title --%>
<div class="flex items-center gap-3">
<.link
navigate={project_home_url}
class="flex items-center gap-3 hover:opacity-80 transition-opacity"
>
<%= if project_logo do %>
<%!-- Custom logo image --%>
<img
src={project_logo}
alt={project_title}
class={[project_logo_height, "w-auto", project_logo_class]}
/>
<% else %>
<%!-- Default icon in colored box --%>
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<.icon name={project_icon} class="w-5 h-5 text-primary-content" />
</div>
<% end %>
<%= if show_title_with_logo or is_nil(project_logo) do %>
<span class="font-bold text-base-content hidden sm:inline">
{display_title}
</span>
<% end %>
</.link>
<%!-- Context Selector(s) (header start - after logo) --%>
<%= if has_multi_selector do %>
<%!-- Multi-selector mode --%>
<PhoenixKitWeb.Components.Dashboard.MultiContextSelector.multi_context_selector
position={:header}
sub_position={:start}
configs={context_selector_configs}
contexts_map={dashboard_contexts_map}
current_map={current_contexts_map}
show_map={show_context_selectors_map}
class="hidden sm:flex"
/>
<% else %>
<%!-- Legacy single selector --%>
<%= if show_context_selector and context_selector_config.position == :header and context_selector_config.sub_position == :start do %>
<div class="hidden sm:flex items-center">
<%= if context_selector_config.separator do %>
<span class="text-base-content/30 mx-2">
{context_selector_config.separator}
</span>
<% end %>
<PhoenixKitWeb.Components.Dashboard.ContextSelector.context_selector
contexts={dashboard_contexts}
current={current_context}
config={context_selector_config}
/>
</div>
<% end %>
<% end %>
</div>
<%!-- Right: Context Selector, Theme, User Dropdown --%>
<div class="flex items-center gap-3">
<%!-- Context Selector(s) (header end - before theme/user) --%>
<%= if has_multi_selector do %>
<%!-- Multi-selector mode --%>
<PhoenixKitWeb.Components.Dashboard.MultiContextSelector.multi_context_selector
position={:header}
sub_position={:end}
configs={context_selector_configs}
contexts_map={dashboard_contexts_map}
current_map={current_contexts_map}
show_map={show_context_selectors_map}
class="hidden sm:flex"
/>
<% else %>
<%!-- Legacy single selector --%>
<%= if show_context_selector and context_selector_config.position == :header and context_selector_config.sub_position == :end do %>
<PhoenixKitWeb.Components.Dashboard.ContextSelector.context_selector
contexts={dashboard_contexts}
current={current_context}
config={context_selector_config}
class="hidden sm:flex"
/>
<% end %>
<% end %>
<%!-- Theme Switcher --%>
<PhoenixKitWeb.Components.Core.ThemeController.theme_controller
themes={PhoenixKit.Config.get(:dashboard_themes, :all)}
id="dashboard-theme-dropdown"
/>
<PhoenixKitWeb.Components.UserDashboardNav.user_dropdown
scope={assigns[:phoenix_kit_current_scope]}
current_path={assigns[:url_path] || "/dashboard"}
current_locale={assigns[:current_locale]}
admin_edit_url={assigns[:admin_edit_url]}
admin_edit_label={assigns[:admin_edit_label]}
/>
</div>
</div>
</header>
<%!-- Main Content Area --%>
<div class="pt-16">
<div class="flex">
<%!-- Left Sidebar Navigation (Desktop) --%>
<aside class="w-64 h-[calc(100vh-4rem)] bg-base-100 border-r border-base-300 hidden lg:flex flex-col sticky top-16">
<div class="p-4 flex-1 overflow-y-auto">
<PhoenixKitWeb.Components.Dashboard.Sidebar.dashboard_sidebar
current_path={assigns[:url_path] || "/dashboard"}
scope={assigns[:phoenix_kit_current_scope]}
locale={assigns[:current_locale_base]}
tabs={dashboard_tabs}
viewer_counts={viewer_counts}
collapsed_groups={collapsed_groups}
show_context_selector={show_context_selector}
dashboard_contexts={dashboard_contexts}
current_context={current_context}
context_selector_config={context_selector_config}
context_selector_configs={context_selector_configs}
dashboard_contexts_map={dashboard_contexts_map}
current_contexts_map={current_contexts_map}
show_context_selectors_map={show_context_selectors_map}
/>
<%!-- Optional extra sidebar content (e.g., shop filters + categories) --%>
<%= if assigns[:sidebar_after_shop] do %>
<div class="mt-3 pt-3 border-t border-base-300/50">
{assigns[:sidebar_after_shop]}
</div>
<% end %>
</div>
<%!-- Bottom Context Selector(s) (when sub_position: :end) --%>
<%= if has_multi_selector do %>
<%!-- Multi-selector mode --%>
<div class="p-4 border-t border-base-300 mt-auto">
<PhoenixKitWeb.Components.Dashboard.MultiContextSelector.multi_context_selector_sidebar
position={:sidebar}
sub_position={:end}
configs={context_selector_configs}
contexts_map={dashboard_contexts_map}
current_map={current_contexts_map}
show_map={show_context_selectors_map}
/>
</div>
<% else %>
<%!-- Legacy single selector --%>
<%= if show_context_selector and context_selector_config && context_selector_config.enabled and context_selector_config.position == :sidebar and context_selector_config.sub_position == :end do %>
<div class="p-4 border-t border-base-300 mt-auto">
<PhoenixKitWeb.Components.Dashboard.ContextSelector.sidebar_context_selector
contexts={dashboard_contexts}
current={current_context}
config={context_selector_config}
/>
</div>
<% end %>
<% end %>
</aside>
<%!-- Mobile FAB Menu --%>
<PhoenixKitWeb.Components.Dashboard.Sidebar.mobile_fab_menu
current_path={assigns[:url_path] || "/dashboard"}
scope={assigns[:phoenix_kit_current_scope]}
locale={assigns[:current_locale_base]}
show_context_selector={show_context_selector}
dashboard_contexts={dashboard_contexts}
current_context={current_context}
context_selector_config={context_selector_config}
context_selector_configs={context_selector_configs}
dashboard_contexts_map={dashboard_contexts_map}
current_contexts_map={current_contexts_map}
show_context_selectors_map={show_context_selectors_map}
/>
<%!-- Main Content --%>
<main class="flex-1 px-4 py-8">
<div class="max-w-6xl mx-auto">
<.flash_group flash={@flash} />
{render_slot(@inner_block)}
</div>
</main>
</div>
</div>
</div>
<%!-- Theme Controller JavaScript --%>
<script>
(function() {
const STORAGE_KEY = 'phx:theme';
const themeBaseMap = {
'phoenix-light': 'light', 'light': 'light', 'cupcake': 'light', 'bumblebee': 'light',
'emerald': 'light', 'corporate': 'light', 'retro': 'light', 'cyberpunk': 'light',
'valentine': 'light', 'garden': 'light', 'aqua': 'light', 'lofi': 'light',
'pastel': 'light', 'fantasy': 'light', 'wireframe': 'light', 'cmyk': 'light',
'autumn': 'light', 'acid': 'light', 'lemonade': 'light', 'winter': 'light',
'caramellatte': 'light', 'silk': 'light',
'phoenix-dark': 'dark', 'dark': 'dark', 'synthwave': 'dark', 'halloween': 'dark',
'forest': 'dark', 'black': 'dark', 'luxury': 'dark', 'dracula': 'dark',
'business': 'dark', 'night': 'dark', 'coffee': 'dark', 'dim': 'dark',
'nord': 'dark', 'sunset': 'dark', 'abyss': 'dark'
};
function getSystemTheme() {
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'phoenix-dark' : 'phoenix-light';
}
function applyTheme(theme) {
const html = document.documentElement;
const body = document.body;
let resolvedTheme = theme;
if (theme === 'system') {
resolvedTheme = getSystemTheme();
}
html.setAttribute('data-theme', resolvedTheme);
body.setAttribute('data-theme', resolvedTheme);
const base = themeBaseMap[resolvedTheme] || 'light';
html.style.colorScheme = base;
body.style.colorScheme = base;
}
function setTheme(theme) {
applyTheme(theme);
if (theme === 'system') {
localStorage.removeItem(STORAGE_KEY);
} else {
localStorage.setItem(STORAGE_KEY, theme);
}
// Update active indicators
document.querySelectorAll('[data-theme-target]').forEach(btn => {
const targets = (btn.dataset.themeTarget || '').split(',').map(s => s.trim()).filter(Boolean);
const isActive = targets.includes(theme);
if (btn.dataset.themeRole === 'dropdown-option') {
btn.classList.toggle('bg-base-200', isActive);
btn.classList.toggle('ring-2', isActive);
btn.classList.toggle('ring-primary/70', isActive);
btn.querySelectorAll('[data-theme-active-indicator]').forEach(icon => {
icon.classList.toggle('opacity-100', isActive);
icon.classList.toggle('scale-100', isActive);
icon.classList.toggle('scale-75', !isActive);
});
}
});
}
// Initialize
const saved = localStorage.getItem(STORAGE_KEY) || 'system';
applyTheme(saved);
// Listen for system theme changes
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener('change', () => {
if ((localStorage.getItem(STORAGE_KEY) || 'system') === 'system') {
applyTheme('system');
}
});
// Listen for theme change events
window.addEventListener('phx:set-theme', e => {
if (e?.detail?.theme) setTheme(e.detail.theme);
});
})();
</script>
</body>
</html>