From 860682403b710a3b12953f7c0b9817d414af4e66 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Sun, 15 Jun 2025 09:29:49 +0300 Subject: [PATCH] Replace actions_menu with dropdown component Authored with Claude, refined by hand --- .../components/add_record_component.ex | 24 ++-- .../components/core_components.ex | 51 -------- .../components/record_components.ex | 111 ++++++++---------- .../live/artist_live/show.html.heex | 31 ++--- .../live/collection_live/show.html.heex | 68 +++++------ .../live/wishlist_live/show.html.heex | 75 +++++------- priv/gettext/default.pot | 14 ++- priv/gettext/en/LC_MESSAGES/default.po | 14 ++- 8 files changed, 161 insertions(+), 227 deletions(-) diff --git a/lib/music_library_web/components/add_record_component.ex b/lib/music_library_web/components/add_record_component.ex index 9150fef8..91778d1a 100644 --- a/lib/music_library_web/components/add_record_component.ex +++ b/lib/music_library_web/components/add_record_component.ex @@ -88,22 +88,28 @@ defmodule MusicLibraryWeb.AddRecordComponent do

- <.actions_menu id={@release_group.id} background_container_target="#records > li"> - <:links> - <.link + <.dropdown id={"actions-#{@release_group.id}"} placement="bottom-end"> + <:toggle> + {gettext("Choose which format to import")} + <.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" + /> + + <.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)} - - - + + + """ diff --git a/lib/music_library_web/components/core_components.ex b/lib/music_library_web/components/core_components.ex index fe08396c..4969b152 100644 --- a/lib/music_library_web/components/core_components.ex +++ b/lib/music_library_web/components/core_components.ex @@ -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""" -
- - <.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)} - -
- """ - end - - defp default_actions_menu_button(assigns) do - ~H""" - {gettext("Open options")} - <.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 diff --git a/lib/music_library_web/components/record_components.ex b/lib/music_library_web/components/record_components.ex index 5d09ca77..05371aea 100644 --- a/lib/music_library_web/components/record_components.ex +++ b/lib/music_library_web/components/record_components.ex @@ -123,17 +123,25 @@ defmodule MusicLibraryWeb.RecordComponents do

<%!-- 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> +
li")} + phx-click-away={JS.remove_class("pointer-events-none", to: "#records > li")} > + {gettext("Actions")} + <.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" + /> +
+ + <.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 - 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 :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 - 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}) |> 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")} - - - + + + @@ -264,17 +263,20 @@ defmodule MusicLibraryWeb.RecordComponents do

{record.title}

- <.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> + {gettext("Actions")} + <.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" + /> + + <.focus_wrap id={"actions-#{record.id}-focus-wrap"}> + <.dropdown_link id={"actions-#{record.id}-show"} navigate={@record_show_path.(record)}> {gettext("Show")} - +
- <.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 :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 - 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")} - - - + + +

{format_label(record.format)} · {type_label(record.type)} diff --git a/lib/music_library_web/live/artist_live/show.html.heex b/lib/music_library_web/live/artist_live/show.html.heex index 0d0d4f46..c26923bb 100644 --- a/lib/music_library_web/live/artist_live/show.html.heex +++ b/lib/music_library_web/live/artist_live/show.html.heex @@ -6,12 +6,18 @@ <.country_flag country={@country} /> - <.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> + {gettext("Actions")} + <.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" + /> + + <.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 - 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")} - - - + + +

diff --git a/lib/music_library_web/live/collection_live/show.html.heex b/lib/music_library_web/live/collection_live/show.html.heex index d1512035..efc6b563 100644 --- a/lib/music_library_web/live/collection_live/show.html.heex +++ b/lib/music_library_web/live/collection_live/show.html.heex @@ -25,12 +25,18 @@

- <.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> + {gettext("Actions")} + <.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" + /> + + <.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 - 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 - 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 - 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 - 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 - 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 - 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")} - - - + + +
<%!-- TODO: replace with OSS version --%> diff --git a/lib/music_library_web/live/wishlist_live/show.html.heex b/lib/music_library_web/live/wishlist_live/show.html.heex index ef1a0d87..d573676e 100644 --- a/lib/music_library_web/live/wishlist_live/show.html.heex +++ b/lib/music_library_web/live/wishlist_live/show.html.heex @@ -25,12 +25,18 @@ · {format_label(@record.format)} · {type_label(@record.type)}

- <.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> + {gettext("Actions")} + <.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" + /> + + <.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 - 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 - 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 - 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 :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 - 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 - 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 - 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")} - - - + + +
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 7df63b12..937def88 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 58862c2e..f4da45b8 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -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 ""