Packages

phoenix_kit

1.7.25
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
phoenix_kit lib phoenix_kit_web components dashboard sidebar.ex
Raw

lib/phoenix_kit_web/components/dashboard/sidebar.ex

defmodule PhoenixKitWeb.Components.Dashboard.Sidebar do
@moduledoc """
Sidebar component for the user dashboard.
Renders the complete dashboard navigation with:
- Grouped tabs with headers
- Active state highlighting
- Badge indicators
- Presence counts
- Attention animations
- Mobile bottom navigation
- Collapsible groups
- Context selector (when `position: :sidebar` is configured)
## Usage
<.dashboard_sidebar
current_path={@url_path}
scope={@phoenix_kit_current_scope}
locale={@current_locale}
/>
## Live Updates
The sidebar automatically updates when tabs change if you subscribe to updates:
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(PhoenixKit.PubSub, PhoenixKit.Dashboard.pubsub_topic())
end
{:ok, socket}
end
def handle_info({:tab_updated, _tab}, socket) do
{:noreply, assign(socket, :tabs, PhoenixKit.Dashboard.get_tabs())}
end
"""
use Phoenix.Component
alias PhoenixKit.Dashboard.{Presence, Registry, Tab}
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Components.Dashboard.TabItem
# Use the icon component from Core.Icon to avoid circular dependencies
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
@doc """
Renders the complete dashboard sidebar with all tabs.
## Attributes
- `current_path` - The current URL path for active state detection
- `scope` - The current authentication scope for visibility filtering
- `locale` - The current locale for path generation
- `tabs` - Optional pre-loaded tabs (defaults to loading from registry)
- `viewer_counts` - Optional map of tab_id => viewer_count
- `collapsed_groups` - Set of collapsed group IDs
- `show_presence` - Show presence indicators (default: true)
- `compact` - Render in compact mode (default: false)
- `class` - Additional CSS classes
- `show_context_selector` - Show context selector at top of sidebar (default: false)
- `dashboard_contexts` - List of available contexts
- `current_context` - Currently selected context
- `context_selector_config` - ContextSelector config struct
"""
attr :current_path, :string, default: "/dashboard"
attr :scope, :any, default: nil
attr :locale, :string, default: nil
attr :tabs, :list, default: nil
attr :viewer_counts, :map, default: %{}
attr :collapsed_groups, :any, default: MapSet.new()
attr :show_presence, :boolean, default: true
attr :compact, :boolean, default: false
attr :class, :string, default: ""
attr :show_context_selector, :boolean, default: false
attr :dashboard_contexts, :list, default: []
attr :current_context, :any, default: nil
attr :context_selector_config, :any, default: nil
def dashboard_sidebar(assigns) do
# Load tabs if not provided
tabs =
case assigns.tabs do
nil -> Registry.get_tabs_with_active(assigns.current_path, scope: assigns.scope)
tabs -> add_active_state(tabs, assigns.current_path)
end
# Group tabs
grouped_tabs = group_tabs(tabs)
groups = Registry.get_groups()
# Get viewer counts if not provided and presence is enabled
viewer_counts =
if assigns.show_presence and map_size(assigns.viewer_counts) == 0 do
Presence.get_all_tab_counts()
else
assigns.viewer_counts
end
assigns =
assigns
|> assign(:tabs, tabs)
|> assign(:grouped_tabs, grouped_tabs)
|> assign(:groups, groups)
|> assign(:viewer_counts, viewer_counts)
~H"""
<nav class={["space-y-1", @class]} role="navigation" aria-label="Dashboard navigation">
<%!-- Context Selector at top (sub_position: :start) --%>
<%= if show_context_selector_at?(@show_context_selector, @context_selector_config, :start) do %>
<PhoenixKitWeb.Components.Dashboard.ContextSelector.sidebar_context_selector
contexts={@dashboard_contexts}
current={@current_context}
config={@context_selector_config}
/>
<% end %>
<%= for group <- sorted_groups(@groups, @grouped_tabs) do %>
<.tab_group
group={group}
tabs={Map.get(@grouped_tabs, group.id, [])}
viewer_counts={@viewer_counts}
locale={@locale}
collapsed={MapSet.member?(@collapsed_groups, group.id)}
compact={@compact}
/>
<% end %>
<%!-- Render ungrouped tabs with possible context selector by priority --%>
<.tabs_with_context_selector
tabs={filter_top_level(Map.get(@grouped_tabs, nil, []))}
all_tabs={Map.get(@grouped_tabs, nil, [])}
viewer_counts={@viewer_counts}
locale={@locale}
compact={@compact}
show_context_selector={
show_context_selector_with_priority?(@show_context_selector, @context_selector_config)
}
dashboard_contexts={@dashboard_contexts}
current_context={@current_context}
context_selector_config={@context_selector_config}
/>
<%!-- Note: Bottom context selector (sub_position: :end) is rendered by the layout, not here --%>
</nav>
"""
end
@doc """
Renders a group of tabs with optional header.
"""
attr :group, :map, required: true
attr :tabs, :list, required: true
attr :viewer_counts, :map, default: %{}
attr :locale, :string, default: nil
attr :collapsed, :boolean, default: false
attr :compact, :boolean, default: false
def tab_group(assigns) do
~H"""
<div
class="space-y-1"
data-group-id={@group.id}
data-collapsed={@collapsed}
>
<%!-- Group Header (if labeled) --%>
<%= if @group[:label] do %>
<div
class={[
"px-3 py-2 text-xs font-semibold text-base-content/50 uppercase tracking-wider",
@group[:collapsible] &&
"cursor-pointer hover:text-base-content/70 flex items-center justify-between"
]}
phx-click={@group[:collapsible] && "toggle_dashboard_group"}
phx-value-group={@group.id}
>
<span class="flex items-center gap-2">
<%= if @group[:icon] do %>
<.icon name={@group[:icon]} class="w-3.5 h-3.5" />
<% end %>
{@group[:label]}
</span>
<%= if @group[:collapsible] do %>
<.icon
name={if @collapsed, do: "hero-chevron-right-mini", else: "hero-chevron-down-mini"}
class="w-4 h-4"
/>
<% end %>
</div>
<% end %>
<%!-- Group Tabs --%>
<div class={[@collapsed && "hidden"]}>
<%= for tab <- filter_top_level(@tabs) do %>
<.tab_with_subtabs
tab={tab}
all_tabs={@tabs}
viewer_counts={@viewer_counts}
locale={@locale}
compact={@compact}
/>
<% end %>
</div>
</div>
"""
end
@doc """
Renders a tab along with its subtabs (if any).
Subtabs are shown based on the parent tab's `subtab_display` setting:
- `:when_active` - Subtabs only shown when parent is active
- `:always` - Subtabs always visible
"""
attr :tab, :any, required: true
attr :all_tabs, :list, required: true
attr :viewer_counts, :map, default: %{}
attr :locale, :string, default: nil
attr :compact, :boolean, default: false
def tab_with_subtabs(assigns) do
subtabs = get_subtabs_for(assigns.tab.id, assigns.all_tabs)
show_subtabs =
Tab.show_subtabs?(assigns.tab, assigns.tab.active) or any_subtab_active?(subtabs)
assigns =
assigns
|> assign(:subtabs, subtabs)
|> assign(:show_subtabs, show_subtabs)
|> assign(:has_subtabs, subtabs != [])
~H"""
<div class="tab-with-subtabs" data-tab-id={@tab.id} data-has-subtabs={@has_subtabs}>
<%!-- Parent Tab --%>
<TabItem.tab_item
tab={@tab}
active={@tab.active}
viewer_count={Map.get(@viewer_counts, @tab.id, 0)}
locale={@locale}
compact={@compact}
/>
<%!-- Subtabs --%>
<%= if @has_subtabs and @show_subtabs do %>
<div class="subtabs pl-2 border-l-2 border-base-300 ml-4 mt-1 space-y-0.5">
<%= for subtab <- @subtabs do %>
<TabItem.tab_item
tab={subtab}
active={subtab.active}
viewer_count={Map.get(@viewer_counts, subtab.id, 0)}
locale={@locale}
compact={@compact}
/>
<% end %>
</div>
<% end %>
</div>
"""
end
@doc """
Renders tabs with a context selector inserted at the appropriate priority position.
"""
attr :tabs, :list, required: true
attr :all_tabs, :list, required: true
attr :viewer_counts, :map, default: %{}
attr :locale, :string, default: nil
attr :compact, :boolean, default: false
attr :show_context_selector, :boolean, default: false
attr :dashboard_contexts, :list, default: []
attr :current_context, :any, default: nil
attr :context_selector_config, :any, default: nil
def tabs_with_context_selector(assigns) do
context_priority = get_context_selector_priority(assigns.context_selector_config)
# Create list of items with their priorities, including context selector if needed
items =
assigns.tabs
|> Enum.map(fn tab -> {:tab, tab, tab.priority} end)
|> maybe_add_context_selector(assigns.show_context_selector, context_priority)
|> Enum.sort_by(fn {_type, _item, priority} -> priority end)
assigns = assign(assigns, :items, items)
~H"""
<%= for item <- @items do %>
<%= case item do %>
<% {:context_selector, _, _} -> %>
<PhoenixKitWeb.Components.Dashboard.ContextSelector.sidebar_context_selector
contexts={@dashboard_contexts}
current={@current_context}
config={@context_selector_config}
/>
<% {:tab, tab, _} -> %>
<.tab_with_subtabs
tab={tab}
all_tabs={@all_tabs}
viewer_counts={@viewer_counts}
locale={@locale}
compact={@compact}
/>
<% end %>
<% end %>
"""
end
defp maybe_add_context_selector(items, false, _priority), do: items
defp maybe_add_context_selector(items, true, nil), do: items
defp maybe_add_context_selector(items, true, priority) do
[{:context_selector, nil, priority} | items]
end
@doc """
Renders a mobile-friendly bottom navigation bar.
## Attributes
- `current_path` - The current URL path for active state detection
- `scope` - The current authentication scope
- `locale` - The current locale
- `max_tabs` - Maximum tabs to show (default: 5)
- `class` - Additional CSS classes
"""
attr :current_path, :string, default: "/dashboard"
attr :scope, :any, default: nil
attr :locale, :string, default: nil
attr :max_tabs, :integer, default: 5
attr :class, :string, default: ""
def mobile_navigation(assigns) do
tabs =
Registry.get_tabs_with_active(assigns.current_path, scope: assigns.scope)
|> Enum.filter(&Tab.navigable?/1)
|> Enum.take(assigns.max_tabs)
assigns = assign(assigns, :tabs, tabs)
~H"""
<nav
class={[
"fixed bottom-0 left-0 right-0 bg-base-100 border-t border-base-300 z-50 lg:hidden",
@class
]}
role="navigation"
aria-label="Mobile navigation"
>
<div class="flex items-center justify-around">
<%= for tab <- @tabs do %>
<TabItem.mobile_tab_item
tab={tab}
active={tab.active}
locale={@locale}
/>
<% end %>
<.more_menu tabs={get_overflow_tabs(@scope, @max_tabs)} locale={@locale} />
</div>
</nav>
"""
end
@doc """
Renders a "more" dropdown menu for overflow tabs on mobile.
"""
attr :tabs, :list, required: true
attr :locale, :string, default: nil
def more_menu(assigns) do
~H"""
<%= if length(@tabs) > 0 do %>
<div class="dropdown dropdown-top dropdown-end">
<label
tabindex="0"
class="flex flex-col items-center justify-center py-2 px-3 cursor-pointer text-base-content/60 hover:text-base-content"
>
<.icon name="hero-ellipsis-horizontal" class="w-6 h-6" />
<span class="text-xs mt-1">More</span>
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52 mb-2">
<%= for tab <- @tabs do %>
<li>
<.link navigate={build_path(tab.path, @locale)} class="flex items-center gap-2">
<%= if tab.icon do %>
<.icon name={tab.icon} class="w-4 h-4" />
<% end %>
<span>{tab.label}</span>
<%= if tab.badge do %>
<PhoenixKitWeb.Components.Dashboard.Badge.dashboard_badge
badge={tab.badge}
class="badge-xs"
/>
<% end %>
</.link>
</li>
<% end %>
</ul>
</div>
<% end %>
"""
end
@doc """
Renders a floating action button for mobile that opens a tab menu.
Includes context selector at the top if configured and user has multiple contexts.
"""
attr :current_path, :string, default: "/dashboard"
attr :scope, :any, default: nil
attr :locale, :string, default: nil
attr :class, :string, default: ""
attr :show_context_selector, :boolean, default: false
attr :dashboard_contexts, :list, default: []
attr :current_context, :any, default: nil
attr :context_selector_config, :any, default: nil
def mobile_fab_menu(assigns) do
tabs =
Registry.get_tabs_with_active(assigns.current_path, scope: assigns.scope)
|> Enum.filter(&Tab.navigable?/1)
assigns = assign(assigns, :tabs, tabs)
~H"""
<div class={["fixed bottom-4 right-4 z-50 lg:hidden", @class]}>
<div class="dropdown dropdown-top dropdown-end">
<label tabindex="0" class="btn btn-primary btn-circle shadow-lg">
<.icon name="hero-bars-3" class="w-5 h-5" />
</label>
<div
tabindex="0"
class="dropdown-content shadow bg-base-100 rounded-box w-56 mb-2 border border-base-300 max-h-96 overflow-y-auto"
>
<%!-- Mobile Context Selector --%>
<%= if @show_context_selector and @context_selector_config && @context_selector_config.enabled do %>
<PhoenixKitWeb.Components.Dashboard.ContextSelector.mobile_context_selector
contexts={@dashboard_contexts}
current={@current_context}
config={@context_selector_config}
/>
<% end %>
<%!-- Navigation Tabs --%>
<ul class="menu p-2">
<%= for tab <- @tabs do %>
<li>
<.link
navigate={build_path(tab.path, @locale)}
class={[
"flex items-center gap-3",
tab.active && "bg-primary text-primary-content"
]}
>
<%= if tab.icon do %>
<.icon name={tab.icon} class="w-4 h-4" />
<% end %>
<span>{tab.label}</span>
<%= if tab.badge do %>
<PhoenixKitWeb.Components.Dashboard.Badge.dashboard_badge
badge={tab.badge}
class="ml-auto badge-xs"
/>
<% end %>
</.link>
</li>
<% end %>
</ul>
</div>
</div>
</div>
"""
end
# Helper functions
defp add_active_state(tabs, current_path) do
Enum.map(tabs, fn tab ->
Map.put(tab, :active, Tab.matches_path?(tab, current_path))
end)
end
defp group_tabs(tabs) do
Enum.group_by(tabs, & &1.group)
end
defp sorted_groups(groups, grouped_tabs) do
# Get groups that have tabs
group_ids_with_tabs = Map.keys(grouped_tabs) |> Enum.reject(&is_nil/1)
# Filter to groups that have tabs and sort by priority
groups
|> Enum.filter(&(&1.id in group_ids_with_tabs))
|> Enum.sort_by(& &1.priority)
end
defp get_overflow_tabs(scope, shown_count) do
Registry.get_tabs(scope: scope)
|> Enum.filter(&Tab.navigable?/1)
|> Enum.drop(shown_count)
end
defp build_path(path, nil), do: path
defp build_path(path, locale) do
Routes.path(path, locale: locale)
end
# Filter to only top-level tabs (no parent)
defp filter_top_level(tabs) do
Enum.filter(tabs, &Tab.top_level?/1)
end
# Get subtabs for a given parent tab ID
defp get_subtabs_for(parent_id, all_tabs) do
Enum.filter(all_tabs, fn tab ->
tab.parent == parent_id
end)
|> Enum.sort_by(& &1.priority)
end
# Check if any subtab is currently active
defp any_subtab_active?(subtabs) do
Enum.any?(subtabs, & &1.active)
end
# Check if context selector should show at a specific position
defp show_context_selector_at?(false, _config, _position), do: false
defp show_context_selector_at?(_show, nil, _position), do: false
defp show_context_selector_at?(_show, %{enabled: false}, _position), do: false
defp show_context_selector_at?(true, %{position: :sidebar, sub_position: :start}, :start),
do: true
defp show_context_selector_at?(_, _, _), do: false
# Check if context selector should show with priority (among tabs)
defp show_context_selector_with_priority?(false, _config), do: false
defp show_context_selector_with_priority?(_show, nil), do: false
defp show_context_selector_with_priority?(_show, %{enabled: false}), do: false
defp show_context_selector_with_priority?(true, %{
position: :sidebar,
sub_position: {:priority, _}
}),
do: true
defp show_context_selector_with_priority?(_, _), do: false
# Get the priority value for the context selector
defp get_context_selector_priority(%{sub_position: {:priority, n}}), do: n
defp get_context_selector_priority(_), do: nil
end