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 %> <.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 %> <% 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} /> <% end %> """ end defp render_divider(assigns) do ~H""" <%= if @tab.label do %>
{@tab.label}
<% else %>
<% 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"""
<%= if @tab.icon do %> <.icon name={@tab.icon} class="w-3.5 h-3.5" /> <% end %> {@tab.label} <%= if @collapsible do %> <.icon name={if @collapsed, do: "hero-chevron-right", else: "hero-chevron-down"} class="w-3.5 h-3.5" /> <% end %>
""" 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"""
<%= if @tab.icon do %> <.icon name={@tab.icon} class={icon_classes(@active, @tab.attention, @is_subtab)} /> <% end %> <%= unless @compact do %> {@tab.label} <% end %>
<%= if @tab.badge do %> <% end %> <%= if @viewer_count > 0 do %> <% end %>
""" 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} >
<%= if @tab.icon do %> <.icon name={@tab.icon} class={mobile_icon_classes(@active)} /> <% end %> <%= if @tab.badge && PhoenixKit.Dashboard.Badge.visible?(@tab.badge) do %> <% end %>
{@tab.label} """ 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