Packages
petal_components
0.19.1
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/dropdown.ex
defmodule PetalComponents.Dropdown do
use Phoenix.Component
alias Phoenix.LiveView.JS
alias PetalComponents.Link
@transition_in_base "transition transform ease-out duration-100"
@transition_in_start "transform opacity-0 scale-95"
@transition_in_end "transform opacity-100 scale-100"
@transition_out_base "transition ease-in duration-75"
@transition_out_start "transform opacity-100 scale-100"
@transition_out_end "transform opacity-0 scale-95"
attr(:options_container_id, :string)
attr(:label, :string, default: nil, doc: "labels your dropdown option")
attr(:class, :string, default: "", doc: "CSS class for parent container")
attr(:js_lib, :string,
default: "alpine_js",
values: ["alpine_js", "live_view_js"],
doc: "javascript library used for toggling"
)
attr(:placement, :string, default: "left", values: ["left", "right"])
attr(:rest, :global)
slot(:trigger_element)
slot(:inner_block, required: false)
@doc """
<.dropdown label="Dropdown" js_lib="alpine_js|live_view_js">
<.dropdown_menu_item link_type="button">
<Heroicons.home class="w-5 h-5 text-gray-500" />
Button item with icon
</.dropdown_menu_item>
<.dropdown_menu_item link_type="a" to="/" label="a item" />
<.dropdown_menu_item link_type="live_patch" to="/" label="Live Patch item" />
<.dropdown_menu_item link_type="live_redirect" to="/" label="Live Redirect item" />
</.dropdown>
"""
def dropdown(assigns) do
assigns =
assigns
|> assign_new(:options_container_id, fn -> "dropdown_#{Ecto.UUID.generate()}" end)
~H"""
<div {@rest} {js_attributes("container", @js_lib, @options_container_id)} class={[@class, "relative inline-block text-left"]}>
<div>
<button
type="button"
class={trigger_button_classes(@label, @trigger_element)}
{js_attributes("button", @js_lib, @options_container_id)}
aria-haspopup="true"
>
<span class="sr-only">Open options</span>
<%= if @label do %>
<%= @label %>
<Heroicons.chevron_down solid class="w-5 h-5 ml-2 -mr-1 dark:text-gray-100" />
<% end %>
<%= if @trigger_element do %>
<%= render_slot(@trigger_element) %>
<% end %>
<%= if !@label && @trigger_element == [] do %>
<Heroicons.ellipsis_vertical solid class="w-5 h-5" />
<% end %>
</button>
</div>
<div
{js_attributes("options_container", @js_lib, @options_container_id)}
class={placement_class(@placement) <> " absolute z-30 w-56 mt-2 bg-white dark:bg-gray-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"}
role="menu"
id={@options_container_id}
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<div class="py-1" role="none">
<%= render_slot(@inner_block) %>
</div>
</div>
</div>
"""
end
attr(:to, :string, default: nil, doc: "link path")
attr(:label, :string, doc: "link label")
attr(:link_type, :string,
default: "button",
values: ["a", "live_patch", "live_redirect", "button"]
)
attr(:rest, :global, include: ~w(method download hreflang ping referrerpolicy rel target type))
slot(:inner_block, required: false)
def dropdown_menu_item(assigns) do
assigns =
assigns
|> assign(:classes, dropdown_menu_item_classes())
~H"""
<Link.a link_type={@link_type} to={@to} class={@classes} {@rest}>
<%= render_slot(@inner_block) || @label %>
</Link.a>
"""
end
defp trigger_button_classes(nil, []),
do:
"flex items-center text-gray-400 rounded-full hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-primary-500"
defp trigger_button_classes(_label, []),
do:
"inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm dark:text-gray-300 dark:bg-gray-900 dark:hover:bg-gray-800 dark:focus:bg-gray-800 hover:bg-gray-50 focus:outline-none"
defp trigger_button_classes(_label, _trigger_element),
do: "align-middle"
defp dropdown_menu_item_classes(),
do:
"block flex gap-2 items-center self-start dark:hover:bg-gray-700 dark:text-gray-300 justify-start px-4 py-2 text-sm text-gray-700 transition duration-150 dark:bg-gray-800 ease-in-out hover:bg-gray-100 w-full text-left"
defp js_attributes("container", "alpine_js", _options_container_id) do
%{
"x-data": "{open: false}",
"@keydown.escape.stop": "open = false",
"@click.outside": "open = false"
}
end
defp js_attributes("button", "alpine_js", _options_container_id) do
%{
"@click": "open = !open",
"@click.outside": "open = false",
"x-bind:aria-expanded": "open.toString()"
}
end
defp js_attributes("options_container", "alpine_js", _options_container_id) do
%{
"x-cloak": true,
"x-show": "open",
"x-transition:enter": @transition_in_base,
"x-transition:enter-start": @transition_in_start,
"x-transition:enter-end": @transition_in_end,
"x-transition:leave": @transition_out_base,
"x-transition:leave-start": @transition_out_start,
"x-transition:leave-end": @transition_out_end
}
end
defp js_attributes("container", "live_view_js", options_container_id) do
%{
"phx-click-away":
JS.hide(
to: "##{options_container_id}",
transition: {@transition_out_base, @transition_out_start, @transition_out_end}
)
}
end
defp js_attributes("button", "live_view_js", options_container_id) do
%{
"phx-click":
JS.toggle(
to: "##{options_container_id}",
display: "block",
in: {@transition_in_base, @transition_in_start, @transition_in_end},
out: {@transition_out_base, @transition_out_start, @transition_out_end}
)
}
end
defp js_attributes("options_container", "live_view_js", _options_container_id) do
%{
style: "display: none;"
}
end
defp placement_class("left"), do: "right-0 origin-top-right"
defp placement_class("right"), do: "left-0 origin-top-left"
end