defmodule SelectoComponents.Modal.ModalWrapper do
@moduledoc """
Provides a reusable modal wrapper component with animations and backdrop.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
@doc """
Modal wrapper component with backdrop and animations.
"""
def modal(assigns) do
assigns = assign_new(assigns, :icon, fn -> nil end)
assigns = assign_new(assigns, :icon_type, fn -> :info end)
assigns = assign_new(assigns, :show, fn -> true end)
assigns = assign_new(assigns, :on_prev, fn -> nil end)
assigns = assign_new(assigns, :on_next, fn -> nil end)
~H"""
<div id={@id} class="relative z-50">
<%!-- Backdrop --%>
<div
id={"#{@id}-bg"}
class="fixed inset-0 bg-gray-900/35 transition-opacity"
aria-hidden="true"
phx-click={@on_cancel}
/>
<%!-- Modal container --%>
<div
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div
id={"#{@id}-content"}
class={"relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 #{size_classes(@size)}"}
>
<%!-- Modal header --%>
<%= if @show_header do %>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<%= if @icon do %>
<div class={"mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10 #{icon_bg_class(@icon_type)}"}>
<%= render_slot(@icon) %>
</div>
<% end %>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
<h3 class="text-lg font-medium leading-6 text-gray-900" id={"#{@id}-title"}>
<%= @title %>
</h3>
<%= if @subtitle do %>
<div class="mt-2">
<p class="text-sm text-gray-500" id={"#{@id}-description"}>
<%= @subtitle %>
</p>
</div>
<% end %>
</div>
<%!-- Close button --%>
<button
type="button"
class="ml-auto bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
phx-click={@on_cancel}
>
<span class="sr-only">Close</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<% end %>
<%!-- Modal body --%>
<div class={"#{if @show_header, do: "px-4 pb-4 sm:px-6 sm:pb-4", else: "p-6"} max-h-[70vh] overflow-y-auto"}>
<%= render_slot(@inner_block) %>
</div>
<%!-- Modal footer --%>
<%= if @footer do %>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<%= render_slot(@footer) %>
</div>
<% end %>
</div>
</div>
</div>
<button type="button" class="hidden" phx-window-keydown={@on_cancel} phx-key="escape" />
<button :if={@on_prev} type="button" class="hidden" phx-window-keydown={@on_prev} phx-key="ArrowLeft" />
<button :if={@on_next} type="button" class="hidden" phx-window-keydown={@on_next} phx-key="ArrowRight" />
</div>
"""
end
@doc """
Show modal with animation.
"""
def show_modal(js \\ %JS{}) do
js
|> JS.show(
to: "#modal",
transition: {
"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"
}
)
|> JS.show(
to: "#modal-bg",
transition: {
"transition-all transform ease-out duration-300",
"opacity-0",
"opacity-100"
}
)
|> JS.show(
to: "#modal-content",
transition: {
"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"
},
time: 300
)
|> JS.focus_first(to: "#modal-content")
end
@doc """
Hide modal with animation.
"""
def hide_modal(js \\ %JS{}) do
js
|> JS.hide(
to: "#modal-content",
transition: {
"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
}
)
|> JS.hide(
to: "#modal-bg",
transition: {
"transition-all transform ease-in duration-200",
"opacity-100",
"opacity-0"
}
)
|> JS.hide(to: "#modal", time: 200)
|> JS.pop_focus()
end
# Helper functions
defp size_classes(:sm), do: "sm:w-full sm:max-w-sm"
defp size_classes(:md), do: "sm:w-full sm:max-w-lg"
defp size_classes(:lg), do: "sm:w-full sm:max-w-xl"
defp size_classes(:xl), do: "sm:w-full sm:max-w-2xl"
defp size_classes(:full), do: "sm:w-full sm:max-w-4xl"
defp size_classes(:third), do: "w-full sm:w-[33vw] sm:max-w-[33vw]"
defp size_classes(:fullscreen), do: "w-[96vw] max-w-[96vw]"
defp size_classes(_), do: "sm:w-full sm:max-w-lg"
defp icon_bg_class(:info), do: "bg-blue-100"
defp icon_bg_class(:success), do: "bg-green-100"
defp icon_bg_class(:warning), do: "bg-yellow-100"
defp icon_bg_class(:error), do: "bg-red-100"
defp icon_bg_class(_), do: "bg-gray-100"
end