Packages

phoenix_kit

2.22.21
2.62.2 2.62.1 2.62.0 2.61.0 2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 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 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.{Group, Presence, Registry, Tab}
  alias PhoenixKit.Utils.Routes
  alias PhoenixKitWeb.Components.Dashboard.TabItem

  import PhoenixKit.Dashboard.TabHelpers
  # 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

  ## Multi-Selector Attributes (optional)

  - `context_selector_configs` - List of all ContextSelector configs
  - `dashboard_contexts_map` - Map of key => list of contexts
  - `current_contexts_map` - Map of key => current context
  - `show_context_selectors_map` - Map of key => boolean
  """
  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
  # Multi-selector attributes
  attr :context_selector_configs, :list, default: []
  attr :dashboard_contexts_map, :map, default: %{}
  attr :current_contexts_map, :map, default: %{}
  attr :show_context_selectors_map, :map, default: %{}

  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, level: :user)

        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)

    # Check if multi-selector mode
    has_multi_selector = assigns.context_selector_configs != []

    assigns = assign(assigns, :has_multi_selector, has_multi_selector)

    ~H"""
    <nav class={["space-y-1", @class]} role="navigation" aria-label="Dashboard navigation">
      <%!-- Context Selector(s) at top (sub_position: :start) --%>
      <%= if @has_multi_selector do %>
        <%!-- Multi-selector mode --%>
        <PhoenixKitWeb.Components.Dashboard.MultiContextSelector.multi_context_selector_sidebar
          position={:sidebar}
          sub_position={:start}
          configs={@context_selector_configs}
          contexts_map={@dashboard_contexts_map}
          current_map={@current_contexts_map}
          show_map={@show_context_selectors_map}
        />
      <% else %>
        <%!-- Legacy single selector --%>
        <%= 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 %>
      <% 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.localized_label(@group) 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.localized_label(@group)}
          </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)
    subtab_active = any_subtab_active?(subtabs)

    show_subtabs =
      Tab.show_subtabs?(assigns.tab, assigns.tab.active) or subtab_active

    # If redirect_to_first_subtab is true, modify the parent tab's path
    display_tab = maybe_redirect_to_first_subtab(assigns.tab, subtabs)

    # Determine if parent should be highlighted
    # If highlight_with_subtabs is false (default) and a subtab is active, don't highlight parent
    highlight_with_subtabs = Map.get(assigns.tab, :highlight_with_subtabs, false)

    parent_active =
      if subtab_active and not highlight_with_subtabs do
        false
      else
        assigns.tab.active
      end

    assigns =
      assigns
      |> assign(:subtabs, subtabs)
      |> assign(:show_subtabs, show_subtabs)
      |> assign(:has_subtabs, subtabs != [])
      |> assign(:display_tab, display_tab)
      |> assign(:parent_active, parent_active)

    ~H"""
    <div class="tab-with-subtabs" data-tab-id={@tab.id} data-has-subtabs={@has_subtabs}>
      <%!-- Parent Tab --%>
      <TabItem.tab_item
        tab={@display_tab}
        active={@parent_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}
              parent_tab={@tab}
            />
          <% 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, level: :user)
      |> 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.localized_label(tab)}</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
  # Multi-selector attributes
  attr :context_selector_configs, :list, default: []
  attr :dashboard_contexts_map, :map, default: %{}
  attr :current_contexts_map, :map, default: %{}
  attr :show_context_selectors_map, :map, default: %{}

  def mobile_fab_menu(assigns) do
    all_tabs =
      Registry.get_tabs_with_active(assigns.current_path, scope: assigns.scope, level: :user)
      |> Enum.filter(&Tab.navigable?/1)

    # Get only top-level tabs for rendering (subtabs handled separately)
    top_level_tabs = filter_top_level(all_tabs)

    # Check if multi-selector mode
    has_multi_selector = assigns.context_selector_configs != []

    assigns =
      assigns
      |> assign(:all_tabs, all_tabs)
      |> assign(:top_level_tabs, top_level_tabs)
      |> assign(:has_multi_selector, has_multi_selector)

    ~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(s) --%>
          <%= if @has_multi_selector do %>
            <PhoenixKitWeb.Components.Dashboard.MultiContextSelector.multi_context_selector_mobile
              configs={@context_selector_configs}
              contexts_map={@dashboard_contexts_map}
              current_map={@current_contexts_map}
              show_map={@show_context_selectors_map}
            />
          <% else %>
            <%= 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 %>
          <% end %>
          <%!-- Navigation Tabs with Subtab Support --%>
          <ul class="menu p-2">
            <%= for tab <- @top_level_tabs do %>
              <.mobile_tab_with_subtabs
                tab={tab}
                all_tabs={@all_tabs}
                locale={@locale}
              />
            <% end %>
          </ul>
        </div>
      </div>
    </div>
    """
  end

  # Renders a mobile tab with its subtabs (if any).
  # Handles subtab visibility, redirect_to_first_subtab, and highlight_with_subtabs.
  attr :tab, :any, required: true
  attr :all_tabs, :list, required: true
  attr :locale, :string, default: nil

  defp mobile_tab_with_subtabs(assigns) do
    subtabs = get_subtabs_for(assigns.tab.id, assigns.all_tabs)
    subtab_active = any_subtab_active?(subtabs)

    show_subtabs =
      Tab.show_subtabs?(assigns.tab, assigns.tab.active) or subtab_active

    # Apply redirect_to_first_subtab logic
    display_tab = maybe_redirect_to_first_subtab(assigns.tab, subtabs)

    # Apply highlight_with_subtabs logic
    highlight_with_subtabs = Map.get(assigns.tab, :highlight_with_subtabs, false)

    parent_active =
      if subtab_active and not highlight_with_subtabs do
        false
      else
        assigns.tab.active
      end

    assigns =
      assigns
      |> assign(:subtabs, subtabs)
      |> assign(:show_subtabs, show_subtabs)
      |> assign(:has_subtabs, subtabs != [])
      |> assign(:display_tab, display_tab)
      |> assign(:parent_active, parent_active)

    ~H"""
    <%!-- Parent Tab --%>
    <li>
      <.link
        navigate={build_path(@display_tab.path, @locale)}
        class={[
          "flex items-center gap-3",
          @parent_active && "bg-primary text-primary-content"
        ]}
      >
        <%= if @tab.icon do %>
          <.icon name={@tab.icon} class="w-4 h-4" />
        <% end %>
        <span>{Tab.localized_label(@tab)}</span>
        <%= if @tab.badge do %>
          <PhoenixKitWeb.Components.Dashboard.Badge.dashboard_badge
            badge={@tab.badge}
            class="ml-auto badge-xs"
          />
        <% end %>
      </.link>
    </li>
    <%!-- Subtabs (indented, smaller) --%>
    <%= if @has_subtabs and @show_subtabs do %>
      <%= for subtab <- @subtabs do %>
        <li>
          <.link
            navigate={build_path(subtab.path, @locale)}
            class={[
              "flex items-center gap-3 pl-6 text-sm",
              subtab.active && "bg-primary/80 text-primary-content"
            ]}
          >
            <%= if subtab.icon do %>
              <.icon name={subtab.icon} class="w-3 h-3" />
            <% end %>
            <span>{Tab.localized_label(subtab)}</span>
            <%= if subtab.badge do %>
              <PhoenixKitWeb.Components.Dashboard.Badge.dashboard_badge
                badge={subtab.badge}
                class="ml-auto badge-xs"
              />
            <% end %>
          </.link>
        </li>
      <% end %>
    <% end %>
    """
  end

  # Helper functions

  defp get_overflow_tabs(scope, shown_count) do
    Registry.get_tabs(scope: scope, level: :user)
    |> Enum.filter(&Tab.navigable?/1)
    |> Enum.drop(shown_count)
  end

  # Always apply URL prefix via Routes.path
  # When locale is nil, use :none to skip locale prefix but still apply URL prefix
  defp build_path(path, nil) do
    if String.starts_with?(path, "/admin") do
      Routes.path(path)
    else
      Routes.path(path, locale: :none)
    end
  end

  defp build_path(path, locale) do
    if String.starts_with?(path, "/admin") do
      Routes.admin_path(path, locale)
    else
      Routes.path(path, locale: locale)
    end
  end

  # Check if any subtab is currently active
  defp any_subtab_active?(subtabs) do
    Enum.any?(subtabs, & &1.active)
  end

  # If redirect_to_first_subtab is enabled, replace the tab's path with the first subtab's path
  defp maybe_redirect_to_first_subtab(%{redirect_to_first_subtab: true} = tab, [first_subtab | _]) do
    %{tab | path: first_subtab.path}
  end

  defp maybe_redirect_to_first_subtab(tab, _subtabs), do: tab

  # 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