Packages
petal_components
2.1.2
4.6.2
4.6.1
4.6.0
4.5.0
4.4.0
4.3.0
4.2.1
4.2.0
4.1.2
4.1.1
4.1.0
4.0.12
4.0.11
4.0.10
4.0.6
4.0.5
4.0.4
4.0.3
4.0.1
3.2.2
3.2.1
3.2.0
3.1.0
3.0.2
3.0.1
3.0.0
2.9.3
2.9.2
retired
2.9.1
retired
2.9.0
retired
2.8.4
2.8.3
2.8.2
2.8.1
2.8.0
2.7.4
2.7.3
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.2
2.5.1
2.5.0
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.1
2.2.0
2.1.2
2.1.1
2.1.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.9.3
1.9.2
1.9.1
1.9.0
1.8.0
1.7.1
1.7.0
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.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.0
1.2.14
1.2.13
1.2.12
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.19.10
0.19.9
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.1
0.14.0
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.0
Shadcn-style Phoenix LiveView components that AI assistants can actually use. Pair with the MCP server so AI coding tools can inspect the real component API.
Current section
Files
Jump to
Current section
Files
lib/petal_components/table.ex
defmodule PetalComponents.Table do
use Phoenix.Component
import PetalComponents.Avatar
@doc ~S"""
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
<:empty_state>No data here yet</:empty_state>
</.table>
"""
attr :id, :string
attr :class, :any, default: nil, doc: "CSS class"
attr :variant, :string, default: "surface", values: ["ghost", "surface"]
attr :rows, :list, default: [], doc: "the list of rows to render"
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col slot"
slot :col do
attr :label, :string
attr :class, :any
attr :row_class, :any
end
slot :empty_state,
doc: "A message to show when the table is empty, to be used together with :col" do
attr :row_class, :any
end
attr :rest, :global, include: ~w(colspan rowspan)
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
assigns = assign_new(assigns, :id, fn -> "table_#{Ecto.UUID.generate()}" end)
~H"""
<table class={["pc-table--#{@variant}", @class]} {@rest}>
<%= if length(@col) > 0 do %>
<thead>
<.tr>
<.th :for={col <- @col} class={col[:class]}><%= col[:label] %></.th>
</.tr>
</thead>
<tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
<%= if length(@empty_state) > 0 do %>
<.tr id={@id <> "-empty"} class="hidden only:table-row">
<.td
:for={empty_state <- @empty_state}
colspan={length(@col)}
class={empty_state[:row_class]}
>
<%= render_slot(empty_state) %>
</.td>
</.tr>
<% end %>
<.tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class={["group", @row_click && "pc-table__tr--row-click"]}
>
<.td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={[
@row_click && "pc-table__td--row-click",
i == 0 && "pc-table__td--first-col",
col[:row_class] && col[:row_class]
]}
>
<%= render_slot(col, @row_item.(row)) %>
</.td>
</.tr>
</tbody>
<% else %>
<%= render_slot(@inner_block) %>
<% end %>
</table>
"""
end
attr(:class, :any, default: nil, doc: "CSS class")
attr(:rest, :global, include: ~w(colspan rowspan))
slot(:inner_block, required: false)
def th(assigns) do
~H"""
<th class={["pc-table__th", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</th>
"""
end
attr(:class, :any, default: nil, doc: "CSS class")
attr(:rest, :global)
slot(:inner_block, required: false)
def tr(assigns) do
~H"""
<tr class={["pc-table__tr", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</tr>
"""
end
attr(:class, :any, default: nil, doc: "CSS class")
attr(:rest, :global, include: ~w(colspan headers rowspan))
slot(:inner_block, required: false)
def td(assigns) do
~H"""
<td class={["pc-table__td", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</td>
"""
end
attr(:class, :any, default: nil, doc: "CSS class")
attr(:label, :string, default: nil, doc: "Adds a label your user, e.g name")
attr(:sub_label, :string, default: nil, doc: "Adds a sub-label your to your user, e.g title")
attr(:rest, :global)
attr(:avatar_assigns, :map,
default: nil,
doc: "if using an avatar, this map will be passed to the avatar component as props"
)
def user_inner_td(assigns) do
~H"""
<div class={@class} {@rest}>
<div class="pc-table__user-inner-td">
<%= if @avatar_assigns do %>
<.avatar {@avatar_assigns} />
<% end %>
<div class="pc-table__user-inner-td__inner">
<div class="pc-table__user-inner-td__label">
<%= @label %>
</div>
<div class="pc-table__user-inner-td__sub-label">
<%= @sub_label %>
</div>
</div>
</div>
</div>
"""
end
end