Current section
Files
Jump to
Current section
Files
lib/ptah_ui/components/swap.ex
defmodule PtahUi.Components.Swap do
@moduledoc """
Swap — toggles visibility between two (or three) child elements using a hidden checkbox.
Clicking anywhere on the component toggles the active state client-side.
Pass `phx-change` / `name` via `:rest` to sync state with the server.
## Examples
<%!-- Text swap --%>
<.swap>
<:on>ON</:on>
<:off>OFF</:off>
</.swap>
<%!-- Icon swap with rotate effect --%>
<.swap effect="rotate">
<:on><.icon name="hero-sun" class="w-6 h-6" /></:on>
<:off><.icon name="hero-moon" class="w-6 h-6" /></:off>
</.swap>
<%!-- Controlled by server --%>
<.swap checked={@dark_mode} phx-change="toggle_theme" name="dark_mode">
<:on><.icon name="hero-sun" class="w-6 h-6" /></:on>
<:off><.icon name="hero-moon" class="w-6 h-6" /></:off>
</.swap>
"""
use Phoenix.Component
attr :id, :string, default: nil
attr :checked, :boolean, default: false, doc: "Initial active state."
attr :effect, :string, default: "none",
values: ~w(none rotate flip),
doc: "Visual transition effect when toggling."
attr :class, :string, default: nil
attr :rest, :global, include: ~w(phx-change name value form disabled)
slot :on, required: true, doc: "Content shown when the swap is active (checkbox checked)."
slot :off, required: true, doc: "Content shown when the swap is inactive (checkbox unchecked)."
slot :indeterminate,
doc: "Content shown when indeterminate (requires setting input.indeterminate=true via JS)."
def swap(assigns) do
~H"""
<label
class={[
"inline-grid cursor-pointer select-none",
@effect == "flip" && "[perspective:800px]",
@class
]}
>
<input
type="checkbox"
id={@id}
checked={@checked}
class="peer sr-only"
{@rest}
/>
<span class={["[grid-area:1/1] transition-all duration-300", off_class(@effect)]}>
<%= render_slot(@off) %>
</span>
<span class={["[grid-area:1/1] transition-all duration-300", on_class(@effect)]}>
<%= render_slot(@on) %>
</span>
<span
:if={@indeterminate != []}
class="[grid-area:1/1] transition-all duration-300 opacity-0 peer-indeterminate:opacity-100"
>
<%= render_slot(@indeterminate) %>
</span>
</label>
"""
end
# swap-off: visible by default; hidden (+ transformed) when checkbox is checked
defp off_class("none"), do: "peer-checked:opacity-0"
defp off_class("rotate"), do: "peer-checked:opacity-0 peer-checked:rotate-45"
defp off_class("flip"), do: "peer-checked:opacity-0 peer-checked:[transform:rotateY(-180deg)]"
# swap-on: hidden by default; visible (+ transforms reset) when checked
defp on_class("none"), do: "opacity-0 peer-checked:opacity-100"
defp on_class("rotate"), do: "opacity-0 -rotate-45 peer-checked:opacity-100 peer-checked:rotate-0"
defp on_class("flip"),
do: "opacity-0 [transform:rotateY(180deg)] peer-checked:opacity-100 peer-checked:[transform:rotateY(0deg)]"
end