Packages
petal_components
1.5.5
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
import PetalComponents.Helpers
alias Phoenix.LiveView.JS
attr(:container_id, :string)
attr(:class, :string, default: "", doc: "CSS class for parent container")
attr(:entries, :list, default: [%{}])
attr(:js_lib, :string,
default: "alpine_js",
values: ["alpine_js", "live_view_js"],
doc: "javascript library used for toggling"
)
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}
{@rest}
{js_attributes("container", @js_lib, @container_id, nil, nil)}
>
<%= for {current_item, i} <- Enum.with_index(@item) do %>
<div {js_attributes("item", @js_lib, @container_id, i, length(@item))} data-i={i}>
<h2>
<button
type="button"
{js_attributes("button", @js_lib, @container_id, i, length(@item))}
class={
build_class([
"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 @js_lib == "live_view_js", do: "pc-accordion-item--last--closed"}"
)
])
}
>
<span class="pc-accordion-item__heading">
<%= current_item.heading %>
</span>
<Heroicons.chevron_down
solid
class="pc-accordion-item__chevron"
{js_attributes("icon", @js_lib, @container_id, i, length(@item))}
/>
</button>
</h2>
<div
{js_attributes("content_container", @js_lib, @container_id, i, length(@item))}
class="accordion-content-container"
>
<div class={
build_class([
"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>
<script>
window.addEventListener("click_accordion", e => {
let i = e.detail.index;
let l = e.detail.length
let clickedAccordionItem = e.target;
let currentlyOpenAccordionItem = document.querySelector("[data-open='true']")
let isClosingClickedAccordionItem = !!currentlyOpenAccordionItem && currentlyOpenAccordionItem == clickedAccordionItem;
let isLastAccordionItem = i == l - 1;
// Close open accordion item
if(currentlyOpenAccordionItem) {
currentlyOpenAccordionItem.dataset.open = false
currentlyOpenAccordionItem.querySelector("svg").classList.remove("rotate-180");
currentlyOpenAccordionItem.querySelector(`.accordion-content-container`).style.display = "none";
currentlyOpenAccordionItem.querySelector(`.accordion-button`).classList.remove("pc-accordion-item__content-container--highlight-accordion-button-on-expanded-js-attributes");
if(isLastAccordionItem){
clickedAccordionItem.querySelector(`.accordion-button`).classList.add("pc-accordion-item--last--closed");
}
}
// Open clicked accordion item (if not already open)
if (!isClosingClickedAccordionItem) {
clickedAccordionItem.dataset.open = true
clickedAccordionItem.querySelector("svg").classList.add("rotate-180");
clickedAccordionItem.querySelector(`.accordion-content-container`).style.display = "block";
clickedAccordionItem.querySelector(`.accordion-button`).classList.add("pc-accordion-item__content-container--highlight-accordion-button-on-expanded-js-attributes");
if(isLastAccordionItem){
clickedAccordionItem.querySelector(`.accordion-button`).classList.remove("pc-accordion-item--last--closed");
}
}
})
</script>
"""
end
defp js_attributes("container", "alpine_js", _container_id, _i, _) do
%{
"x-data": "{ active: null }"
}
end
defp js_attributes("item", "alpine_js", _container_id, i, _) do
%{
"x-data": "{
id: #{i},
get expanded() {
return this.active === this.id
},
set expanded(value) {
this.active = value ? this.id : null
},
}"
}
end
defp js_attributes("button", "alpine_js", _container_id, i, l) when i == l - 1 do
%{
"x-on:click": "expanded = !expanded",
":class":
"expanded ? 'pc-accordion-item__content-container--highlight-accordion-button-on-expanded-js-attributes' : 'pc-accordion-item--last--closed'",
":aria-expanded": "expanded"
}
end
defp js_attributes("button", "alpine_js", _container_id, _i, _l) do
%{
"x-on:click": "expanded = !expanded",
":class":
"expanded ? 'pc-accordion-item__content-container--highlight-accordion-button-on-expanded-js-attributes' : ''",
":aria-expanded": "expanded"
}
end
defp js_attributes("content_container", "alpine_js", _container_id, _, _) do
%{
"x-show": "expanded",
"x-cloak": true,
"x-collapse": true
}
end
defp js_attributes("icon", "alpine_js", _container_id, _, _) do
%{
":class": "{ 'rotate-180': expanded }"
}
end
defp js_attributes("container", "live_view_js", _container_id, _i, _) do
%{}
end
defp js_attributes("item", "live_view_js", _container_id, _i, _) do
%{}
end
defp js_attributes("button", "live_view_js", container_id, i, l) do
%{
"phx-click":
JS.dispatch("click_accordion",
to: "##{container_id} [data-i='#{i}']",
detail: %{container_id: container_id, index: i, length: l}
)
}
end
defp js_attributes("content_container", "live_view_js", _container_id, _i, _) do
%{
style: "display: none;"
}
end
defp js_attributes("icon", "live_view_js", _container_id, _, _) do
%{}
end
end