Packages
petal_components
0.6.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/dropdown.ex
defmodule PetalComponents.Dropdown do
use Phoenix.Component
alias Phoenix.LiveView.JS
alias PetalComponents.Heroicons
import 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"
# prop js_lib, :string, default: "alpine_js", options: ["alpine_js", "live_view_js"]
# prop label, :string
# prop placement, :string, default: options: ["left", "right"]
# slot default
@doc """
<.dropdown label="Dropdown" js_lib="alpine_js|live_view_js">
<.dropdown_menu_item link_type="button">
<Heroicons.Outline.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_#{Enum.random(1..100000000)}" end)
|> assign_new(:js_lib, fn -> "alpine_js" end)
|> assign_new(:placement, fn -> "left" end)
~H"""
<div {js_attributes("container", @js_lib, @options_container_id)} class="relative z-10 inline-block text-left">
<div>
<%= if @label do %>
<button
link_type="button"
class="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 hover:bg-gray-50 focus:outline-none"
{js_attributes("button", @js_lib, @options_container_id)}
aria-haspopup="true"
>
<%= @label %>
<Heroicons.Solid.chevron_down class="w-5 h-5 ml-2 -mr-1" />
</button>
<% else %>
<button
link_type="button"
class="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-indigo-500"
{js_attributes("button", @js_lib, @options_container_id)}
aria-haspopup="true"
>
<span class="sr-only">Open options</span>
<Heroicons.Solid.dots_vertical class="w-5 h-5" />
</button>
<% end %>
</div>
<div
{js_attributes("options_container", @js_lib, @options_container_id)}
class={placement_class(@placement) <> " absolute w-56 mt-2 bg-white 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
def dropdown_menu_item(assigns) do
assigns = assigns
|> assign_new(:link_type, fn -> "button" end)
|> assign_new(:inner_block, fn -> nil end)
|> assign_new(:classes, fn -> dropdown_menu_item_classes() end)
|> assign_new(:extra_attributes, fn ->
Map.drop(assigns, [
:inner_block,
:link_type,
:classes,
:__changed__
])
end)
~H"""
<%= if @link_type == "button" do %>
<button class={@classes}>
<%= if @inner_block do %>
<%= render_slot(@inner_block) %>
<% else %>
<%= @label %>
<% end %>
</button>
<% else %>
<.link link_type={@link_type} to={@to} class={@classes} {@extra_attributes}>
<%= if @inner_block do %>
<%= render_slot(@inner_block) %>
<% else %>
<%= @label %>
<% end %>
</.link>
<% end %>
"""
end
def dropdown_menu_item_classes(),
do:
"block flex gap-2 items-center self-start justify-start px-4 py-2 text-sm text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 w-full text-left"
def js_attributes("container", "alpine_js", _options_container_id) do
%{
"x-data": "{open: false}",
"@keydown.escape.stop": "open = false",
"@click.outside": "open = false"
}
end
def js_attributes("button", "alpine_js", _options_container_id) do
%{
"@click": "open = !open",
"x-bind:aria-expanded": "open.toString()"
}
end
def 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
def 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
def 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
def js_attributes("options_container", "live_view_js", _options_container_id) do
%{
style: "display: none;",
}
end
def placement_class("left"), do: "right-0 origin-top-right"
def placement_class("right"), do: "left-0 origin-top-left"
end