Packages

phoenix_kit

1.7.24
1.7.210 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
phoenix_kit lib phoenix_kit_web components dashboard tab_item.ex
Raw

lib/phoenix_kit_web/components/dashboard/tab_item.ex

defmodule PhoenixKitWeb.Components.Dashboard.TabItem do
@moduledoc """
Tab item component for dashboard navigation.
Renders individual tabs with support for:
- Icons and labels
- Badges and indicators
- Active state highlighting
- Attention animations
- External links
- Tooltips
- Presence indicators
"""
use Phoenix.Component
alias PhoenixKit.Dashboard.Tab
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Components.Dashboard.Badge, as: BadgeComponent
# Use the icon component from Core.Icon to avoid circular dependencies
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
@doc """
Renders a dashboard tab item.
## Attributes
- `tab` - The Tab struct
- `active` - Whether this tab is currently active
- `viewer_count` - Number of users viewing this tab (optional)
- `locale` - Current locale for path generation
- `compact` - Render in compact mode (icon only)
- `class` - Additional CSS classes
## Examples
<.tab_item tab={@tab} active={@tab.active} />
<.tab_item tab={@tab} active={true} viewer_count={3} />
"""
attr :tab, :any, required: true
attr :active, :boolean, default: false
attr :viewer_count, :integer, default: 0
attr :locale, :string, default: nil
attr :compact, :boolean, default: false
attr :class, :string, default: ""
def tab_item(assigns) do
cond do
Tab.divider?(assigns.tab) ->
render_divider(assigns)
Tab.group_header?(assigns.tab) ->
render_group_header(assigns)
true ->
render_tab(assigns)
end
end
defp render_tab(assigns) do
path = build_path(assigns.tab.path, assigns.locale)
is_subtab = Tab.subtab?(assigns.tab)
assigns =
assigns
|> assign(:path, path)
|> assign(:is_subtab, is_subtab)
~H"""
<%= if @tab.external do %>
<a
href={@path}
target={if @tab.new_tab, do: "_blank", else: nil}
rel={if @tab.new_tab, do: "noopener noreferrer", else: nil}
class={tab_classes(@active, @tab.attention, @is_subtab, @class)}
title={@tab.tooltip}
data-tab-id={@tab.id}
data-parent-id={@tab.parent}
>
<.tab_content
tab={@tab}
active={@active}
viewer_count={@viewer_count}
compact={@compact}
is_subtab={@is_subtab}
/>
<%= if @tab.external do %>
<.icon name="hero-arrow-top-right-on-square" class="w-3 h-3 ml-auto opacity-50" />
<% end %>
</a>
<% else %>
<.link
navigate={@path}
class={tab_classes(@active, @tab.attention, @is_subtab, @class)}
title={@tab.tooltip}
data-tab-id={@tab.id}
data-parent-id={@tab.parent}
>
<.tab_content
tab={@tab}
active={@active}
viewer_count={@viewer_count}
compact={@compact}
is_subtab={@is_subtab}
/>
</.link>
<% end %>
"""
end
defp render_divider(assigns) do
~H"""
<%= if @tab.label do %>
<div class={[
"px-3 py-2 text-xs font-semibold text-base-content/50 uppercase tracking-wider",
@class
]}>
{@tab.label}
</div>
<% else %>
<div class={["divider my-1", @class]}></div>
<% end %>
"""
end
defp render_group_header(assigns) do
collapsible = assigns.tab.metadata[:collapsible] || false
collapsed = assigns.tab.metadata[:collapsed] || false
assigns =
assigns
|> assign(:collapsible, collapsible)
|> assign(:collapsed, collapsed)
~H"""
<div
class={[
"px-3 py-2 text-xs font-semibold text-base-content/60 uppercase tracking-wider",
@collapsible && "cursor-pointer hover:text-base-content/80 flex items-center justify-between",
@class
]}
data-group-id={@tab.id}
data-collapsible={@collapsible}
phx-click={@collapsible && "toggle_group"}
phx-value-group={@tab.id}
>
<span class="flex items-center gap-2">
<%= if @tab.icon do %>
<.icon name={@tab.icon} class="w-3.5 h-3.5" />
<% end %>
{@tab.label}
</span>
<%= if @collapsible do %>
<.icon
name={if @collapsed, do: "hero-chevron-right", else: "hero-chevron-down"}
class="w-3.5 h-3.5"
/>
<% end %>
</div>
"""
end
@doc """
Renders the inner content of a tab (icon, label, badge, presence).
"""
attr :tab, :any, required: true
attr :active, :boolean, default: false
attr :viewer_count, :integer, default: 0
attr :compact, :boolean, default: false
attr :is_subtab, :boolean, default: false
def tab_content(assigns) do
~H"""
<div class="flex items-center gap-3 flex-1 min-w-0">
<%= if @tab.icon do %>
<.icon name={@tab.icon} class={icon_classes(@active, @tab.attention, @is_subtab)} />
<% end %>
<%= unless @compact do %>
<span class={["truncate", @is_subtab && "text-sm"]}>{@tab.label}</span>
<% end %>
</div>
<div class="flex items-center gap-2 ml-auto">
<%= if @tab.badge do %>
<BadgeComponent.dashboard_badge badge={@tab.badge} />
<% end %>
<%= if @viewer_count > 0 do %>
<BadgeComponent.presence_indicator count={@viewer_count} />
<% end %>
</div>
"""
end
@doc """
Renders a mobile-friendly tab item for bottom navigation.
"""
attr :tab, :any, required: true
attr :active, :boolean, default: false
attr :locale, :string, default: nil
attr :class, :string, default: ""
def mobile_tab_item(assigns) do
# Skip dividers and headers for mobile
if Tab.navigable?(assigns.tab) do
path = build_path(assigns.tab.path, assigns.locale)
assigns = assign(assigns, :path, path)
~H"""
<.link
navigate={@path}
class={mobile_tab_classes(@active, @tab.attention, @class)}
data-tab-id={@tab.id}
>
<div class="relative">
<%= if @tab.icon do %>
<.icon name={@tab.icon} class={mobile_icon_classes(@active)} />
<% end %>
<%= if @tab.badge && PhoenixKit.Dashboard.Badge.visible?(@tab.badge) do %>
<span class="absolute -top-1 -right-1">
<BadgeComponent.dashboard_badge badge={@tab.badge} class="badge-xs" />
</span>
<% end %>
</div>
<span class="text-xs mt-1 truncate">{@tab.label}</span>
</.link>
"""
else
~H""
end
end
# Helper functions
defp build_path(path, nil), do: path
defp build_path(path, locale) do
Routes.path(path, locale: locale)
end
defp tab_classes(active, attention, is_subtab, extra_class) do
base =
"flex items-center py-2 text-sm font-medium rounded-lg transition-all duration-200"
# Subtabs are indented with more left padding
padding_class = if is_subtab, do: "pl-9 pr-3", else: "px-3"
active_class =
if active do
if is_subtab do
"bg-primary/80 text-primary-content"
else
"bg-primary text-primary-content"
end
else
if is_subtab do
"text-base-content/70 hover:bg-base-200 hover:text-base-content"
else
"text-base-content hover:bg-base-200"
end
end
attention_class = attention_animation_class(attention)
[base, padding_class, active_class, attention_class, extra_class]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
defp icon_classes(_active, attention, is_subtab) do
# Subtabs have smaller icons
base = if is_subtab, do: "w-4 h-4 shrink-0", else: "w-5 h-5 shrink-0"
attention_class =
case attention do
:glow -> "drop-shadow-[0_0_8px_currentColor]"
_ -> nil
end
[base, attention_class]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
defp mobile_tab_classes(active, attention, extra_class) do
base = "flex flex-col items-center justify-center py-2 px-3 min-w-[4rem] transition-all"
active_class =
if active do
"text-primary"
else
"text-base-content/60 hover:text-base-content"
end
attention_class = attention_animation_class(attention)
[base, active_class, attention_class, extra_class]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
defp mobile_icon_classes(active) do
base = "w-6 h-6"
active_class = if active, do: "text-primary", else: ""
"#{base} #{active_class}"
end
defp attention_animation_class(nil), do: nil
defp attention_animation_class(:pulse), do: "animate-pulse"
defp attention_animation_class(:bounce), do: "animate-bounce"
defp attention_animation_class(:shake), do: "animate-shake"
defp attention_animation_class(:glow), do: "animate-glow"
defp attention_animation_class(_), do: nil
end