Packages
petal_components
4.2.0
4.8.0
4.7.0
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/accordion.ex
defmodule PetalComponents.Accordion do
use Phoenix.Component
alias Phoenix.LiveView.JS
import PetalComponents.Icon
import PetalComponents.Helpers, only: [compose_js: 2]
attr(:container_id, :string)
attr(:class, :any, default: nil, doc: "CSS class for parent container")
attr(:entries, :list, default: [%{}])
attr(:variant, :string, default: "default", values: ["default", "ghost"])
attr(:open_index, :integer, default: nil, doc: "Index of item to be open at render")
attr(:multiple, :boolean,
default: false,
doc: "When true, multiple sections can be expanded simultaneously"
)
attr :on_toggle, JS,
default: %JS{},
doc: "additional JS commands to run when an accordion section is toggled (LiveView.JS only)"
attr(:rest, :global)
slot :item, required: true, doc: "CSS class for parent container" do
attr(:heading, :string)
end
def accordion(assigns) do
assigns =
assigns
|> assign_new(:container_id, fn -> "accordion_#{Ecto.UUID.generate()}" end)
item =
for entry <- assigns.entries, item <- assigns.item do
item_heading = Map.get(item, :heading)
entry_heading = Map.get(entry, :heading)
if item_heading && entry_heading do
raise ArgumentError, "specify heading in either :item or :entries"
end
heading = item_heading || entry_heading
item
|> Map.put(:heading, heading)
|> Map.put(:entry, entry)
end
assigns = assign(assigns, :item, item)
~H"""
<div
id={@container_id}
class={[
@class,
if(@variant == "ghost", do: "pc-accordion--ghost")
]}
{@rest}
{js_attributes("container", @container_id, nil, nil, @open_index, @variant, @multiple)}
>
<%= for {current_item, i} <- Enum.with_index(@item) do %>
<% is_open = i == @open_index %>
<div
{js_attributes("item", @container_id, i, length(@item), is_open, @variant, @multiple)}
data-i={i}
data-open={if is_open, do: "true", else: "false"}
class={if(@variant == "ghost", do: "pc-accordion-item--ghost")}
>
<h2 id={content_panel_header_id(@container_id, i)}>
<button
type="button"
{js_attributes("button", @container_id, i, length(@item), is_open, @variant, @multiple, @on_toggle)}
class={
if @variant == "ghost" do
"pc-accordion-item__button--ghost"
else
[
"pc-accordion-item accordion-button",
if(i == 0, do: "pc-accordion-item--first"),
unless(i == length(@item) - 1, do: "pc-accordion-item--all-except-last"),
if(i == length(@item) - 1,
do:
"pc-accordion-item--last #{if !is_open, do: "pc-accordion-item--last--closed"}"
)
]
end
}
>
<span class={
if(@variant == "ghost",
do: "pc-accordion-item__heading--ghost",
else: "pc-accordion-item__heading"
)
}>
{current_item.heading}
</span>
<%= if @variant == "ghost" do %>
<span class="pc-accordion-item__icon-container--ghost">
<.icon
name="hero-plus-mini"
class={["pc-accordion-item__plus", if(is_open, do: "hidden")]}
/>
<.icon
name="hero-minus-mini"
class={["pc-accordion-item__minus", if(is_open, do: nil, else: "hidden")]}
/>
</span>
<% else %>
<.icon
name="hero-chevron-down-solid"
class={["pc-accordion-item__chevron", if(is_open, do: "rotate-180")]}
{js_attributes("icon", @container_id, i, length(@item), is_open, @variant, @multiple)}
/>
<% end %>
</button>
</h2>
<div
{js_attributes("content_container", @container_id, i, length(@item), is_open, @variant, @multiple)}
class="accordion-content-container"
>
<div class={
if(@variant == "ghost",
do: "pc-accordion-item__content--ghost",
else: [
"pc-accordion-item__content-container",
if(i == length(@item) - 1,
do: "pc-accordion-item__content-container--last",
else: "pc-accordion-item__content-container--not-last"
)
]
)
}>
{render_slot(current_item, current_item.entry)}
</div>
</div>
</div>
<% end %>
</div>
"""
end
defp js_attributes(type, container_id, i, l, open, variant, multiple, on_toggle \\ %JS{}) do
case type do
"container" ->
%{"phx-update": "ignore"}
"item" ->
%{}
"button" ->
detail =
%{container_id: container_id, index: i, length: l}
|> then(fn d -> if variant == "ghost", do: Map.put(d, :variant, "ghost"), else: d end)
|> then(fn d -> if multiple, do: Map.put(d, :multiple, true), else: d end)
click =
compose_js(
on_toggle,
JS.dispatch("click_accordion",
to: "##{container_id} [data-i='#{i}']",
detail: detail
)
)
%{
"phx-click": click,
"aria-controls": content_panel_id(container_id, i),
"aria-expanded": "#{open}"
}
"content_container" ->
%{
id: content_panel_id(container_id, i),
role: "region",
"aria-labelledby": content_panel_header_id(container_id, i),
style: if(open, do: "display: block;", else: "display: none;")
}
"icon" ->
if variant == "ghost" do
%{class: if(open, do: "hidden")}
else
%{class: if(open, do: "rotate-180")}
end
"icon_minus" ->
%{class: if(open, do: "", else: "hidden")}
_ ->
%{}
end
end
defp content_panel_header_id(container_id, idx) do
"acc-header-#{container_id}-#{idx}"
end
defp content_panel_id(container_id, idx) do
"acc-content-panel-#{container_id}-#{idx}"
end
end