Packages
petal_components
0.13.2
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
# slot trigger_element
@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..100_000_000)}" end)
|> assign_new(:js_lib, fn -> "alpine_js" end)
|> assign_new(:placement, fn -> "left" end)
|> assign_new(:label, fn -> nil end)
|> assign_new(:trigger_element, fn -> nil end)
|> assign_new(:extra_assigns, fn ->
assigns_to_attributes(assigns, ~w(
options_container_id
js_lib
placement
label
trigger_element
class
)a)
end)
~H"""
<div {@extra_assigns} {js_attributes("container", @js_lib, @options_container_id)} 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.Solid.chevron_down 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.Solid.dots_vertical 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
def dropdown_menu_item(assigns) do
assigns =
assigns
|> assign_new(:link_type, fn -> "button" end)
|> assign_new(:inner_block, fn -> nil end)
|> assign_new(:to, fn -> nil end)
|> assign_new(:classes, fn -> dropdown_menu_item_classes() end)
|> assign_new(:extra_assigns, fn ->
assigns_to_attributes(assigns, [
:link_type,
:classes
])
end)
~H"""
<.link link_type={@link_type} to={@to} class={@classes} {@extra_assigns}>
<%= if @inner_block do %>
<%= render_slot(@inner_block) %>
<% else %>
<%= @label %>
<% end %>
</.link>
"""
end
defp trigger_button_classes(nil, 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, nil),
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",
"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