Replace actions_menu with dropdown component

Authored with Claude, refined by hand
This commit is contained in:
Claudio Ortolina
2025-06-15 09:29:49 +03:00
parent fa77b62473
commit 860682403b
8 changed files with 161 additions and 227 deletions
@@ -88,22 +88,28 @@ defmodule MusicLibraryWeb.AddRecordComponent do
</p>
</div>
<.actions_menu id={@release_group.id} background_container_target="#records > li">
<:links>
<.link
<.dropdown id={"actions-#{@release_group.id}"} placement="bottom-end">
<:toggle>
<span class="sr-only">{gettext("Choose which format to import")}</span>
<.icon
name="hero-plus"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</:toggle>
<.focus_wrap id={"actions-#{@release_group.id}-focus-wrap"}>
<.dropdown_link
:for={format <- Records.Record.formats()}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@release_group.id}-#{format}-import"}
phx-click={
JS.push("import", value: %{id: @release_group.id, format: format}, page_loading: true)
}
>
{format_label(format)}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
</li>
"""
@@ -110,57 +110,6 @@ defmodule MusicLibraryWeb.CoreComponents do
"""
end
attr :id, :string, required: true
attr :class, :string, default: nil
attr :background_container_target, :string, required: true
slot :links, required: true
slot :button, required: false
def actions_menu(assigns) do
~H"""
<div class={["relative flex-none", @class]}>
<button
type="button"
class="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
aria-expanded="false"
aria-haspopup="true"
phx-click={toggle_actions_menu(@id, @background_container_target)}
>
{render_slot(@button) || default_actions_menu_button(%{})}
</button>
<.focus_wrap
id={"actions-#{@id}"}
class={[
"hidden pointer-events-auto absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-zinc-800 py-2 shadow-lg ring-1 ring-zinc-900/5 focus:outline-hidden"
]}
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
phx-click-away={close_actions_menu(@id, @background_container_target)}
>
{render_slot(@links)}
</.focus_wrap>
</div>
"""
end
defp default_actions_menu_button(assigns) do
~H"""
<span class="sr-only">{gettext("Open options")}</span>
<.icon name="hero-ellipsis-vertical" class="-mt-1 h-5 w-5" aria-hidden="true" data-slot="icon" />
"""
end
defp toggle_actions_menu(id, background_container_target) do
JS.toggle(to: "#actions-#{id}")
|> JS.toggle_class("pointer-events-none", to: background_container_target)
end
defp close_actions_menu(id, background_container_target) do
JS.hide(to: "#actions-#{id}")
|> JS.remove_class("pointer-events-none", to: background_container_target)
end
## JS Commands
def show(js \\ %JS{}, selector) do
@@ -123,17 +123,25 @@ defmodule MusicLibraryWeb.RecordComponents do
</p>
</div>
<%!-- TODO: replace with OSS version --%>
<.actions_menu id={record.id} background_container_target="#records > li">
<:links>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-show"}
navigate={@record_show_path.(record)}
<.dropdown id={"actions-#{record.id}"} placement="bottom-end">
<:toggle>
<div
phx-click={JS.toggle_class("pointer-events-none", to: "#records > li")}
phx-click-away={JS.remove_class("pointer-events-none", to: "#records > li")}
>
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</div>
</:toggle>
<.focus_wrap id={"actions-#{record.id}-focus-wrap"} class="pointer-events-auto">
<.dropdown_link id={"actions-#{record.id}-show"} navigate={@record_show_path.(record)}>
{gettext("Show")}
</.link>
</.dropdown_link>
<a
href={MusicBrainz.ReleaseGroup.url(record.musicbrainz_id)}
target=".blank"
@@ -145,21 +153,12 @@ defmodule MusicLibraryWeb.RecordComponents do
{gettext("View on MusicBrainz")}
</a>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-edit"}
patch={@record_edit_path.(record)}
>
<.dropdown_link id={"actions-#{record.id}-edit"} patch={@record_edit_path.(record)}>
{gettext("Edit")}
</.link>
</.dropdown_link>
<.link
<.dropdown_link
:if={!record.purchased_at}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-purchase"}
phx-click={
JS.dispatch("music_library:confetti")
@@ -167,20 +166,20 @@ defmodule MusicLibraryWeb.RecordComponents do
}
>
{gettext("Purchased")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
</.dropdown_link>
<.dropdown_separator />
<.dropdown_link
id={"actions-#{record.id}-delete"}
phx-click={JS.push("delete", value: %{id: record.id}) |> hide("##{id}")}
data-confirm={gettext("Are you sure?")}
class={[
"text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
]}
>
{gettext("Delete")}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
</li>
</ul>
@@ -264,17 +263,20 @@ defmodule MusicLibraryWeb.RecordComponents do
<p class="pointer-events-none mt-2 block truncate text-sm font-medium text-zinc-900 dark:text-zinc-300">
{record.title}
</p>
<.actions_menu id={record.id} background_container_target="#records > li" class="mt-2">
<:links>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-show"}
navigate={@record_show_path.(record)}
>
<.dropdown id={"actions-#{record.id}"} placement="bottom-end" class="mt-2">
<:toggle>
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</:toggle>
<.focus_wrap id={"actions-#{record.id}-focus-wrap"}>
<.dropdown_link id={"actions-#{record.id}-show"} navigate={@record_show_path.(record)}>
{gettext("Show")}
</.link>
</.dropdown_link>
<a
href={MusicBrainz.ReleaseGroup.url(record.musicbrainz_id)}
target=".blank"
@@ -286,21 +288,12 @@ defmodule MusicLibraryWeb.RecordComponents do
{gettext("View on MusicBrainz")}
</a>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-edit"}
patch={@record_edit_path.(record)}
>
<.dropdown_link id={"actions-#{record.id}-edit"} patch={@record_edit_path.(record)}>
{gettext("Edit")}
</.link>
</.dropdown_link>
<.link
<.dropdown_link
:if={!record.purchased_at}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{record.id}-purchase"}
phx-click={
JS.dispatch("music_library:confetti")
@@ -308,20 +301,18 @@ defmodule MusicLibraryWeb.RecordComponents do
}
>
{gettext("Purchased")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{record.id}-delete"}
phx-click={JS.push("delete", value: %{id: record.id}) |> hide("##{id}")}
data-confirm={gettext("Are you sure?")}
class="text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
>
{gettext("Delete")}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
<p class="pointer-events-none block text-sm font-medium text-zinc-500">
{format_label(record.format)} · {type_label(record.type)}
@@ -6,12 +6,18 @@
<.country_flag country={@country} />
</h1>
<.actions_menu id={@artist.musicbrainz_id} background_container_target="#records > li">
<:links>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown id={"actions-#{@artist.musicbrainz_id}"} placement="bottom-end">
<:toggle>
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</:toggle>
<.focus_wrap id={"actions-#{@artist.musicbrainz_id}-focus-wrap"}>
<.dropdown_link
id={"actions-#{@artist.musicbrainz_id}-refresh-image"}
phx-click={JS.push("refresh_artist_image", value: %{id: @artist.musicbrainz_id})}
>
@@ -22,12 +28,9 @@
data-slot="icon"
/>
{gettext("Refresh image")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@artist.musicbrainz_id}-refresh-artist-info"}
phx-click={JS.push("refresh_artist_info", value: %{id: @artist.musicbrainz_id})}
>
@@ -38,9 +41,9 @@
data-slot="icon"
/>
{gettext("Refresh info")}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
<div class="mt-4 flex items-center justify-between">
@@ -25,12 +25,18 @@
</span>
</p>
</div>
<.actions_menu id={@record.id} background_container_target="#records > li">
<:links>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
<:toggle>
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</:toggle>
<.focus_wrap id={"actions-#{@record.id}-focus-wrap"}>
<.dropdown_link
id={"actions-#{@record.id}-edit"}
patch={~p"/collection/#{@record}/show/edit"}
>
@@ -41,12 +47,9 @@
data-slot="icon"
/>
{gettext("Edit")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
@@ -57,12 +60,9 @@
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
@@ -73,12 +73,9 @@
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
@@ -89,12 +86,9 @@
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-extract-colors-fast"}
phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :fast})}
>
@@ -105,12 +99,9 @@
data-slot="icon"
/>
{gettext("Extract colors (fast)")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-extract-colors-slow"}
phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :slow})}
>
@@ -121,15 +112,14 @@
data-slot="icon"
/>
{gettext("Extract colors (slow)")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
<.dropdown_separator />
<.dropdown_link
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
class="text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
>
<.icon
name="hero-trash"
@@ -138,9 +128,9 @@
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
<div class="mt-4 md:mt-8">
<%!-- TODO: replace with OSS version --%>
@@ -25,12 +25,18 @@
· {format_label(@record.format)} · {type_label(@record.type)}
</p>
</div>
<.actions_menu id={@record.id} background_container_target="#records > li">
<:links>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
<:toggle>
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
aria-hidden="true"
data-slot="icon"
/>
</:toggle>
<.focus_wrap id={"actions-#{@record.id}-focus-wrap"}>
<.dropdown_link
id={"actions-#{@record.id}-edit"}
patch={~p"/wishlist/#{@record}/show/edit"}
>
@@ -41,12 +47,9 @@
data-slot="icon"
/>
{gettext("Edit")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
@@ -57,12 +60,9 @@
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
@@ -73,12 +73,9 @@
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
@@ -89,13 +86,10 @@
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
</.dropdown_link>
<.link
<.dropdown_link
:if={!@record.purchased_at}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-purchase"}
phx-click={
JS.dispatch("music_library:confetti")
@@ -109,12 +103,9 @@
data-slot="icon"
/>
{gettext("Purchased")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-extract-colors-fast"}
phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :fast})}
>
@@ -125,12 +116,9 @@
data-slot="icon"
/>
{gettext("Extract colors (fast)")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
<.dropdown_link
id={"actions-#{@record.id}-extract-colors-slow"}
phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :slow})}
>
@@ -141,15 +129,14 @@
data-slot="icon"
/>
{gettext("Extract colors (slow)")}
</.link>
</.dropdown_link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
<.dropdown_separator />
<.dropdown_link
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
class="text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
>
<.icon
name="hero-trash"
@@ -158,9 +145,9 @@
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</:links>
</.actions_menu>
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</div>
<div class="mt-4 md:mt-8">
+9 -5
View File
@@ -114,11 +114,6 @@ msgstr ""
msgid "No results"
msgstr ""
#: lib/music_library_web/components/core_components.ex
#, elixir-autogen, elixir-format
msgid "Open options"
msgstr ""
#: lib/music_library_web/controllers/session_html/new.html.heex
#, elixir-autogen, elixir-format
msgid "Password"
@@ -256,6 +251,7 @@ msgstr ""
msgid "Scrobble activity"
msgstr ""
#: lib/music_library_web/components/add_record_component.ex
#: lib/music_library_web/live/stats_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Choose which format to import"
@@ -910,3 +906,11 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Wishlisted on"
msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/artist_live/show.html.heex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
+9 -5
View File
@@ -114,11 +114,6 @@ msgstr ""
msgid "No results"
msgstr ""
#: lib/music_library_web/components/core_components.ex
#, elixir-autogen, elixir-format
msgid "Open options"
msgstr ""
#: lib/music_library_web/controllers/session_html/new.html.heex
#, elixir-autogen, elixir-format
msgid "Password"
@@ -256,6 +251,7 @@ msgstr ""
msgid "Scrobble activity"
msgstr ""
#: lib/music_library_web/components/add_record_component.ex
#: lib/music_library_web/live/stats_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Choose which format to import"
@@ -910,3 +906,11 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy
msgid "Wishlisted on"
msgstr ""
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/artist_live/show.html.heex
#: lib/music_library_web/live/collection_live/show.html.heex
#: lib/music_library_web/live/wishlist_live/show.html.heex
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""