Add DaisyUI-inspired Tabs component

Create tabs component based on DaisyUI design system:

- Styles: bordered (underline), lifted (raised active), boxed (pill)
- Support for slot-based tab items with active state
- Individual tab_item component for more control
- Accessible with role="tablist" and aria-selected
- Support for phx-click handlers on tabs

Import component globally in claper_web.ex.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Lion
2026-01-17 23:10:52 +00:00
parent 8caf8872a2
commit 97292775e4
2 changed files with 158 additions and 0 deletions

View File

@@ -112,6 +112,7 @@ defmodule ClaperWeb do
# Import design system components
import ClaperWeb.Component.Button
import ClaperWeb.Component.Badge
import ClaperWeb.Component.Tabs
unquote(verified_routes())
end

View File

@@ -0,0 +1,157 @@
defmodule ClaperWeb.Component.Tabs do
@moduledoc """
DaisyUI-inspired Tabs component for Claper.
## Styles
- `:bordered` - Simple tabs with bottom border on active (default)
- `:lifted` - Lifted appearance with border on sides and top for active
- `:boxed` - Pill-shaped tabs with background, active has primary fill
## Examples
<.tabs>
<:tab>Tab 1</:tab>
<:tab active>Tab 2</:tab>
<:tab>Tab 3</:tab>
</.tabs>
<.tabs style={:boxed}>
<:tab>Home</:tab>
<:tab active>Profile</:tab>
<:tab>Settings</:tab>
</.tabs>
<.tabs style={:lifted}>
<:tab>Overview</:tab>
<:tab active>Details</:tab>
<:tab>History</:tab>
</.tabs>
## With click handlers
<.tabs style={:boxed}>
<:tab active={@active_tab == "home"} phx-click="set_tab" phx-value-tab="home">Home</:tab>
<:tab active={@active_tab == "profile"} phx-click="set_tab" phx-value-tab="profile">Profile</:tab>
</.tabs>
"""
use ClaperWeb, :view_component
attr :style, :atom, default: :bordered, values: [:bordered, :lifted, :boxed]
attr :class, :string, default: nil
attr :rest, :global
slot :tab, required: true do
attr :active, :boolean
attr :class, :string
end
def tabs(assigns) do
~H"""
<div
role="tablist"
class={[
container_classes(@style),
@class
]}
{@rest}
>
<%= for {tab, _index} <- Enum.with_index(@tab) do %>
<button
role="tab"
class={[
tab_base_classes(@style),
tab_state_classes(@style, tab[:active] || false)
]}
aria-selected={tab[:active] || false}
{assigns_to_attributes(tab, [:active, :class, :inner_block])}
>
{render_slot(tab)}
</button>
<% end %>
</div>
"""
end
@doc """
Renders a single tab item. Use this for more control over individual tabs.
## Examples
<.tab_item active>Active Tab</.tab_item>
<.tab_item phx-click="change_tab">Inactive Tab</.tab_item>
"""
attr :style, :atom, default: :bordered, values: [:bordered, :lifted, :boxed]
attr :active, :boolean, default: false
attr :class, :string, default: nil
attr :rest, :global, include: ~w(phx-click phx-target phx-value-tab)
slot :inner_block, required: true
def tab_item(assigns) do
~H"""
<button
role="tab"
class={[
tab_base_classes(@style),
tab_state_classes(@style, @active),
@class
]}
aria-selected={@active}
{@rest}
>
{render_slot(@inner_block)}
</button>
"""
end
# Container classes based on style
defp container_classes(:bordered) do
"flex items-center border-b border-gray-200"
end
defp container_classes(:lifted) do
"flex items-end"
end
defp container_classes(:boxed) do
"inline-flex items-center bg-gray-100 p-0.5 rounded-full"
end
# Base tab classes based on style
defp tab_base_classes(:bordered) do
"px-4 py-1.5 text-sm font-normal font-display transition-all duration-200 rounded-t-lg -mb-px"
end
defp tab_base_classes(:lifted) do
"px-4 py-1.5 text-sm font-normal font-display transition-all duration-200 rounded-t-lg border-b border-gray-200"
end
defp tab_base_classes(:boxed) do
"px-4 py-1.5 text-sm font-normal font-display transition-all duration-200 rounded-full"
end
# Tab state classes (active/inactive) based on style
defp tab_state_classes(:bordered, true) do
"text-gray-800 border-b-2 border-gray-800 font-medium"
end
defp tab_state_classes(:bordered, false) do
"text-gray-500 hover:text-gray-700 border-b-2 border-transparent"
end
defp tab_state_classes(:lifted, true) do
"text-gray-800 bg-white border-l border-r border-t border-gray-200 border-b-0 -mb-px"
end
defp tab_state_classes(:lifted, false) do
"text-gray-500 hover:text-gray-700 bg-transparent"
end
defp tab_state_classes(:boxed, true) do
"text-white bg-primary-500 font-bold"
end
defp tab_state_classes(:boxed, false) do
"text-gray-500 hover:text-gray-700 bg-transparent"
end
end