lib/phoenix/ui/components/alert.ex

defmodule Phoenix.UI.Components.Alert do
  @moduledoc """
  Provides Alert component
  """
  import Phoenix.UI.Components.Heroicon

  use Phoenix.UI, :component

  @default_action_icon_name "x-mark"
  @default_icon_color "inherity"
  @default_icon_variant "outline"
  @default_severity "info"
  @default_variant "standard"

  @doc """
  Renders alert component.

  ## Examples

      ```
      <.alert>
        ruh roh!
      </.alert>
      ```

  """
  @spec alert(Socket.assigns()) :: Rendered.t()
  def alert(raw_assigns) do
    assigns =
      raw_assigns
      |> assign_new(:icon, fn -> [] end)
      |> assign_new(:severity, fn -> @default_severity end)
      |> assign_new(:variant, fn -> @default_variant end)
      |> build_action_attrs()
      |> build_action_wrapper_attrs()
      |> build_container_attrs()
      |> build_icon_attrs()
      |> build_icon_wrapper_attrs()

    ~H"""
    <div {@container_attrs}>
      <%= if assigns[:icon] do %>
        <div {@icon_wrapper_attrs}>
          <%= if is_slot?(@icon) do %>
            <%= render_slot(@icon) %>
          <% else %>
            <.heroicon {@icon_attrs} />
          <% end %>
        </div>
      <% end %>
      <%= if assigns[:title] do %>
        <div class="font-bold mb-1">
          <%= if is_bitstring(@title) do %>
            <%= @title %>
          <% else %>
            <%= render_slot(@title) %>
          <% end %>
        </div>
      <% end %>
      <div>
        <%= if assigns[:content] do %>
          <%= render_slot(@content) %>
        <% else %>
          <%= render_slot(@inner_block) %>
        <% end %>
      </div>
      <%= if assigns[:action] do %>
        <div {@action_wrapper_attrs}>
          <%= if is_slot?(@action) do %>
            <%= render_slot(@action) %>
          <% else %>
            <.heroicon {@action_attrs} />
          <% end %>
        </div>
      <% end %>
    </div>
    """
  end

  ### Container Attrs ##########################

  defp build_container_attrs(assigns) do
    class = build_class(~w(
      alert relative py-4 rounded-lg
      #{classes(:container, :color, assigns)}
      #{classes(:container, :spacing, assigns)}
      #{Map.get(assigns, :extend_class)}
    ))

    attrs =
      assigns
      |> assigns_to_attributes([
        :action_attrs,
        :action_wrapper_attrs,
        :action,
        :color,
        :content,
        :extend_class,
        :icon_attrs,
        :icon_wrapper_attrs,
        :icon,
        :title,
        :variant
      ])
      |> Keyword.put_new(:class, class)
      |> Keyword.put_new(:role, "alert")

    assign(assigns, :container_attrs, attrs)
  end

  ### Icon Attrs ##########################

  defp build_icon_attrs(%{icon: false} = assigns), do: assigns

  defp build_icon_attrs(%{icon: icon, severity: severity} = assigns) do
    extend_class = build_class(~w(
      alert-icon
      #{Keyword.get(icon, :extend_class)}
    ))

    attrs =
      icon
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put(:extend_class, extend_class)
      |> Keyword.put_new(:color, @default_icon_color)
      |> Keyword.put_new(:name, icon_mapping(severity))
      |> Keyword.put_new(:variant, @default_icon_variant)

    assign(assigns, :icon_attrs, attrs)
  end

  defp build_icon_attrs(assigns), do: assigns

  ### Icon Wrapper Attrs ##########################

  defp build_icon_wrapper_attrs(%{icon: false} = assigns), do: assigns

  defp build_icon_wrapper_attrs(%{icon: _icon} = assigns) do
    class = build_class(~w(
      alert-icon-wrapper absolute left-4
      #{classes(:icon_wrapper, :color, assigns)}
      #{classes(:icon_wrapper, :position, assigns)}
    ))

    assign(assigns, :icon_wrapper_attrs, %{class: class})
  end

  defp build_icon_wrapper_attrs(assigns), do: assigns

  ### Action Attrs ##########################

  defp build_action_attrs(%{action: action} = assigns) do
    extend_class = build_class(~w(
      alert-action cursor-pointer
      #{Keyword.get(action, :extend_class)}
    ))

    attrs =
      action
      |> assigns_to_attributes([:extend_class])
      |> Keyword.put(:extend_class, extend_class)
      |> Keyword.put_new(:name, @default_action_icon_name)
      |> Keyword.put_new(:variant, @default_icon_variant)

    assign(assigns, :action_attrs, attrs)
  end

  defp build_action_attrs(assigns), do: assigns

  ### Action Wrapper Attrs ##########################

  defp build_action_wrapper_attrs(%{action: _action} = assigns) do
    class = build_class(~w(
      alert-action-wrapper absolute right-4 transition-all ease-in-out duration-300
      #{classes(:action_wrapper, :color, assigns)}
      #{classes(:action_wrapper, :position, assigns)}
    ))

    assign(assigns, :action_wrapper_attrs, %{class: class})
  end

  defp build_action_wrapper_attrs(assigns), do: assigns

  ### CSS Classes ##########################

  # Container - Color
  defp classes(:container, :color, %{variant: "filled", severity: color}) do
    "bg-#{color}-600 dark:bg-#{color}-700 text-white"
  end

  defp classes(:container, :color, %{variant: "outlined", severity: color}) do
    "border border-#{color}-500 text-#{color}-800 dark:text-#{color}-200"
  end

  defp classes(:container, :color, %{variant: "standard", severity: color}) do
    "bg-#{color}-500/[.15] text-#{color}-800 dark:text-#{color}-200"
  end

  # Container - Spacing
  defp classes(:container, :spacing, %{icon: false, action: _}), do: "pl-4 pr-14"
  defp classes(:container, :spacing, %{icon: _, action: _}), do: "px-14"
  defp classes(:container, :spacing, %{icon: false}), do: "px-4"
  defp classes(:container, :spacing, %{icon: _}), do: "pl-14 pr-4"
  defp classes(:container, :spacing, %{action: _}), do: "pl-4 pr-14"
  defp classes(:container, :spacing, _assigns), do: "px-4"

  # Icon Wrapper - Color
  defp classes(:icon_wrapper, :color, %{variant: "outlined", severity: color}) do
    "text-#{color}-500"
  end

  defp classes(:icon_wrapper, :color, %{variant: "standard", severity: color}) do
    "text-#{color}-600"
  end

  # Icon Wrapper - Position
  defp classes(:icon_wrapper, :position, %{title: _}), do: "top-4"
  defp classes(:icon_wrapper, :position, _assigns), do: "top-1/2 -translate-y-1/2"

  # Action Wrapper - Color
  defp classes(:action_wrapper, :color, %{variant: "filled"}) do
    "text-white hover:text-white/75"
  end

  defp classes(:action_wrapper, :color, %{variant: "outlined", severity: color}) do
    "hover:text-#{color}-600/75"
  end

  defp classes(:action_wrapper, :color, %{variant: "standard", severity: color}) do
    "hover:text-#{color}-900/50 dark:hover:text-#{color}-200/50"
  end

  # Action Wrapper - Position
  defp classes(:action_wrapper, :position, %{title: _}), do: "top-4"
  defp classes(:action_wrapper, :position, _assigns), do: "top-1/2 -translate-y-1/2"

  defp classes(_element, _rule_group, _assigns), do: nil

  defp icon_mapping("error"), do: "exclamation-circle"
  defp icon_mapping("danger"), do: "exclamation-circle"
  defp icon_mapping("info"), do: "information-circle"
  defp icon_mapping("success"), do: "check-circle"
  defp icon_mapping("warning"), do: "exclamation-triangle"
end