lib/pyro/components/core.ex

defmodule Pyro.Components.Core do
  use Pyro.Component

  import Pyro.Gettext

  @moduledoc """
  Drop-in (prop/API compatible) replacement (and enhancement) of `core_components.ex` as generated by `Phoenix`, providing core UI components.

  Compared to the generated components, Pyro's implementation adds:

  - Maintenance/bugfixes/new features, since it's a library
  - A powerful [override system](#module-overridable-component-attributes) for customization
  - A special `:tails_classes` type that merges [Tailwind CSS](https://tailwindcss.com) classes via `Tails`
  - The button component implements both button and anchor tags (button-styled links!)
  - Inputs
    - `autofocus` prop to enable a hook for reliable focus on mount
    - `hidden` input type with a slot for custom content
  - A rich flash experience
    - Auto-remove after (configurable) timeout
    - Progress bar for auto-removed flash messages
    - Define which flashes are included in which trays (supports multiple trays)
  - Slightly cleaner, more semantic markup

  But wait, *there's more*! Those are just the `core_components` drop-in replacements  — Pyro has lots of other components on top of that. Check out the components section of the docs for the other modules.
  """

  @doc """
  TODO: This component is not fully converted to use overrides/properly styled.
  Renders a modal.

  ## Examples

      <.modal id="confirm-modal">
        Are you sure?
        <:confirm>OK</:confirm>
        <:cancel>Cancel</:cancel>
      </.modal>

  JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
  for the caller to react to each button press, for example:

      <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
        Are you sure you?
        <:confirm>OK</:confirm>
        <:cancel>Cancel</:cancel>
      </.modal>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :id, :string, required: true
  attr :show, :boolean, default: false
  attr :on_cancel, JS, default: %JS{}
  attr :on_confirm, JS, default: %JS{}
  attr :show_js, :any, overridable: true, required: true
  attr :hide_js, :any, overridable: true, required: true
  attr :class, :tails_classes, overridable: true, required: true
  slot :inner_block, required: true
  slot :title
  slot :subtitle
  slot :confirm
  slot :cancel

  def modal(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <div
      id={@id}
      phx-mounted={@show && apply(@show_js, [%JS{}, @id])}
      phx-remove={apply(@hide_js, [%JS{}, @id])}
      class={@class}
    >
      <div id={"#{@id}-bg"} class="fixed inset-0 bg-zinc-50/90 transition-opacity" aria-hidden="true" />
      <div
        class="fixed inset-0 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-center justify-center">
          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
            <.focus_wrap
              id={"#{@id}-container"}
              phx-mounted={@show && apply(@show_js, [%JS{}, @id])}
              phx-window-keydown={apply(@hide_js, [@on_cancel, @id])}
              phx-key="escape"
              phx-click-away={apply(@hide_js, [@on_cancel, @id])}
              class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition"
            >
              <div class="absolute top-6 right-5">
                <button
                  phx-click={apply(@hide_js, [@on_cancel, @id])}
                  type="button"
                  class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
                  aria-label={gettext("close")}
                >
                  <.icon name="hero-x-mark-solid" class="h-5 w-5 stroke-current" />
                </button>
              </div>
              <div id={"#{@id}-content"}>
                <header :if={@title != []}>
                  <h1 id={"#{@id}-title"} class={@header_title_class}>
                    <%= render_slot(@title) %>
                  </h1>
                  <p
                    :if={@subtitle != []}
                    id={"#{@id}-description"}
                    class="mt-2 text-sm leading-6 text-zinc-600"
                  >
                    <%= render_slot(@subtitle) %>
                  </p>
                </header>
                <%= render_slot(@inner_block) %>
                <div :if={@confirm != [] or @cancel != []} class="ml-6 mb-4 flex items-center gap-5">
                  <.button
                    :for={confirm <- @confirm}
                    overrides={@overrides}
                    id={"#{@id}-confirm"}
                    phx-click={@on_confirm}
                    phx-disable-with
                    class="py-2 px-3"
                  >
                    <%= render_slot(confirm) %>
                  </.button>
                  <.link
                    :for={cancel <- @cancel}
                    phx-click={apply(@hide_js, [@on_cancel, @id])}
                    class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
                  >
                    <%= render_slot(cancel) %>
                  </.link>
                </div>
              </div>
            </.focus_wrap>
          </div>
        </div>
      </div>
    </div>
    """
  end

  @doc """
  Renders flash notices.

  ## Examples

      <.flash kind={:info} flash={@flash} />
      <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :flash, :map, default: %{}, doc: "The map of flash messages to display."

  attr :autoshow, :boolean,
    overridable: true,
    required: true,
    doc: "Whether to auto show the flash on mount"

  attr :close, :boolean, overridable: true, required: true, doc: "Whether the flash can be closed"
  attr :close_icon_name, :string, overridable: true, required: true
  attr :icon_name, :string, overridable: true, required: true
  attr :hide_js, :any, overridable: true, required: true
  attr :show_js, :any, overridable: true, required: true
  attr :title, :string, overridable: true
  attr :ttl, :integer, overridable: true, required: true
  attr :kind, :string, overridable: true, required: true, doc: "Used for styling and flash lookup"

  attr :style_for_kind, :string,
    overridable: true,
    doc: "Used for styling a flash with a different kind"

  attr :class, :tails_classes, overridable: true, required: true
  attr :control_class, :tails_classes, overridable: true
  attr :close_button_class, :tails_classes, overridable: true
  attr :close_icon_class, :tails_classes, overridable: true
  attr :message_class, :tails_classes, overridable: true, required: true
  attr :progress_class, :tails_classes, overridable: true, required: true
  attr :title_class, :tails_classes, overridable: true, required: true
  attr :title_icon_class, :tails_classes, overridable: true
  attr :rest, :global, doc: "The arbitrary HTML attributes to add to the flash container"

  slot :inner_block, doc: "The optional inner block to render the flash message"

  def flash(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <div
      :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
      id={"phx-flash-#{@kind}"}
      phx-hook="PyroFlashComponent"
      phx-click={
        @close &&
          apply(@hide_js, [JS.push("lv:clear-flash", value: %{key: @kind}), "#phx-flash-#{@kind}"])
      }
      data-show-exec-js={apply(@show_js, [%JS{}, "#phx-flash-#{@kind}"])}
      data-autoshow={@autoshow}
      data-ttl={@ttl}
      data-hide-exec-js={
        apply(@hide_js, [JS.push("lv:clear-flash", value: %{key: @kind}), "#phx-flash-#{@kind}"])
      }
      role="alert"
      class={@class}
      {@rest}
    >
      <section :if={@ttl > 0 || @close} class={@control_class}>
        <Pyro.Components.Extra.progress
          :if={@ttl > 0}
          overrides={@overrides}
          value={@ttl}
          max={@ttl}
          color={@kind}
          class={@progress_class}
          size="xs"
        />
        <div :if={@ttl <= 0} />
        <button :if={@close} type="button" class={@close_button_class} aria-label={gettext("close")}>
          <.icon overrides={@overrides} name={@close_icon_name} class={@close_icon_class} />
        </button>
      </section>
      <p :if={@title} class={@title_class}>
        <.icon :if={@icon_name} overrides={@overrides} name={@icon_name} class={@title_icon_class} />
        <%= @title %>
      </p>
      <p id={"phx-flash-#{@kind}-message"} class={@message_class}><%= msg %></p>
    </div>
    """
  end

  @doc """
  Shows the flash group with titles and content.

  ## Examples
      <.flash_group flash={@flash} />
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :flash, :map, required: true, doc: "The map of flash messages to display"

  attr :include_kinds, :list,
    overridable: true,
    required: true,
    doc: "The kinds of flashes to display"

  attr :class, :tails_classes, overridable: true, required: true
  attr :rest, :global, doc: "The arbitrary HTML attributes to add to the flash tray"

  def flash_group(assigns) do
    assigns = assign_overridables(assigns)
    assigns = assign(assigns, :flash, filter_flash(assigns[:flash], assigns[:include_kinds]))

    ~H"""
    <div :if={any_flash?(@flash)} overrides={@overrides} class={@class}>
      <.flash :for={{kind, _message} <- @flash} overrides={@overrides} {parse_flash(@flash, kind)} />
    </div>
    """
  end

  # defp filter_flash(nil, _kinds), do: %{}
  defp filter_flash(_flash, nil), do: %{}

  defp filter_flash(flash, kinds) do
    flash
    |> Enum.filter(fn {kind, _msg} ->
      kind in kinds
    end)
    |> Map.new()
  end

  defp any_flash?(flash), do: !Enum.empty?(Map.keys(flash))

  defp parse_flash(flash, kind) do
    flash
    |> Phoenix.Flash.get(kind)
    |> Jason.decode()
    |> case do
      {:ok, %{"message" => message} = parsed} ->
        Enum.reduce(parsed, [flash: Map.put(%{}, kind, message), kind: kind], fn
          {"message", _}, acc ->
            acc

          {_key, nil}, acc ->
            acc

          {"icon_name", value}, acc ->
            Keyword.put(acc, :icon_name, value)

          {"ttl", value}, acc ->
            Keyword.put(acc, :ttl, value)

          {"title", value}, acc ->
            Keyword.put(acc, :title, value)

          {"close", value}, acc ->
            Keyword.put(acc, :close, value)

          {"style_for_kind", value}, acc ->
            Keyword.put(acc, :style_for_kind, value)
        end)

      _ ->
        [flash: Map.put(%{}, kind, Map.get(flash, kind)), kind: kind]
    end
  end

  @doc """
  Renders a simple form.

  ## Examples

      <.simple_form for={@form} phx-change="validate" phx-submit="save">
        <.input field={@form[:email]} label="Email"/>
        <.input field={@form[:username]} label="Username" />
        <:actions>
          <.button>Save</.button>
        </:actions>
      </.simple_form>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :as, :any, default: nil, doc: "The server side parameter to collect all input under"
  attr :for, :any, required: true, doc: "The datastructure for the form"

  attr :class, :tails_classes, overridable: true, required: true
  attr :actions_class, :tails_classes, overridable: true, required: true

  attr :rest, :global,
    include: ~w(autocomplete name rel action enctype method novalidate target),
    doc: "The arbitrary HTML attributes to apply to the form tag"

  slot :inner_block, required: true
  slot :actions, doc: "The slot for form actions, such as a submit button"

  def simple_form(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <.form :let={f} for={@for} as={@as} class={@class} {@rest}>
      <%= render_slot(@inner_block, f) %>
      <section :for={action <- @actions} class={@actions_class}>
        <%= render_slot(action, f) %>
      </section>
    </.form>
    """
  end

  @doc """
  Renders a button.

  Supports:

  - Any button type
  - Any anchor type
    - LivePatch
    - LiveRedirect
    - External href links

  ## Examples

      <.button>Send!</.button>
      <.button phx-click="go" class="ml-2">Send!</.button>
      <.button navigate={~p"/home"}>Home</.button>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc

  attr :confirm, :string,
    default: nil,
    doc: "Text to display in a confirm dialog before emitting click event"

  attr :csrf_token, :string, default: nil
  attr :disabled, :boolean, default: false
  attr :href, :any

  attr :icon_name, :string,
    default: nil,
    doc:
      "The name of the icon to display (nil for none); see [`icon/1`](`Pyro.Components.Core.icon/1`) for details"

  attr :loading, :boolean, default: false, doc: "Display a loading spinner"
  attr :method, :string, default: "get"
  attr :navigate, :string
  attr :patch, :string
  attr :ping, :boolean, default: false, doc: "Show a ping indicator"
  attr :replace, :boolean, default: false
  attr :rest, :global, include: ~w[download hreflang referrerpolicy rel target type]

  attr :type, :string,
    default: "button",
    values: ~w[button reset submit],
    doc: "Type of the button"

  attr :case, :string,
    overridable: true,
    required: true,
    values: ~w[uppercase normal-case lowercase capitalize],
    doc: "The case of the text"

  attr :color, :string, overridable: true, required: true, doc: "The color of the button"
  attr :shape, :string, overridable: true, required: true, doc: "Shape of the button"
  attr :size, :string, overridable: true, required: true, doc: "The size of the button"
  attr :variant, :string, overridable: true, required: true, doc: "Style of button"
  attr :class, :tails_classes, overridable: true, required: true
  attr :icon_class, :tails_classes, overridable: true, required: true
  attr :ping_class, :tails_classes, overridable: true, required: true

  slot :inner_block, required: true, doc: "The content of the button"

  def button(assigns) do
    assigns
    |> assign_overridables()
    |> render_button()
  end

  defp render_button(%{href: _href} = assigns) do
    ~H"""
    <.link
      href={@href}
      replace={@replace}
      method={@method}
      csrf_token={@csrf_token}
      data-confirm={@confirm}
      data-submit={!!@confirm}
      class={@class}
      {@rest}
    >
      <Pyro.Components.Extra.spinner :if={@loading} overrides={@overrides} size={@size} />
      <.icon
        :if={!@loading && @icon_name}
        overrides={@overrides}
        name={@icon_name}
        class={@icon_class}
      />
      <%= render_slot(@inner_block) %>
      <%= if @ping do %>
        <span class={@ping_class <> " animate-ping opacity-75"} />
        <span class={@ping_class} />
      <% end %>
    </.link>
    """
  end

  defp render_button(%{patch: _patch} = assigns) do
    ~H"""
    <.link
      patch={@patch}
      replace={@replace}
      data-confirm={@confirm}
      data-submit={!!@confirm}
      class={@class}
      {@rest}
    >
      <Pyro.Components.Extra.spinner :if={@loading} overrides={@overrides} size={@size} />
      <.icon
        :if={!@loading && @icon_name}
        overrides={@overrides}
        name={@icon_name}
        class={@icon_class}
      />
      <%= render_slot(@inner_block) %>
      <%= if @ping do %>
        <span class={@ping_class <> " animate-ping opacity-75"} />
        <span class={@ping_class} />
      <% end %>
    </.link>
    """
  end

  defp render_button(%{navigate: _navigate} = assigns) do
    ~H"""
    <.link
      navigate={@navigate}
      replace={@replace}
      data-confirm={@confirm}
      data-submit={!!@confirm}
      class={@class}
      {@rest}
    >
      <Pyro.Components.Extra.spinner :if={@loading} overrides={@overrides} size={@size} />
      <.icon
        :if={!@loading && @icon_name}
        overrides={@overrides}
        name={@icon_name}
        class={@icon_class}
      />
      <%= render_slot(@inner_block) %>
      <%= if @ping do %>
        <span class={@ping_class <> " animate-ping opacity-75"} />
        <span class={@ping_class} />
      <% end %>
    </.link>
    """
  end

  defp render_button(assigns) do
    ~H"""
    <button
      type={@type}
      disabled={@disabled}
      data-confirm={@confirm}
      data-submit={!!@confirm}
      class={@class}
      {@rest}
    >
      <Pyro.Components.Extra.spinner :if={@loading} overrides={@overrides} size={@size} />
      <.icon
        :if={!@loading && @icon_name}
        overrides={@overrides}
        name={@icon_name}
        class={@icon_class}
      />
      <%= render_slot(@inner_block) %>
      <%= if @ping do %>
        <span class={@ping_class <> " animate-ping opacity-75"} />
        <span class={@ping_class} />
      <% end %>
    </button>
    """
  end

  @doc """
  Renders an input with label and error messages.

  A `%Phoenix.HTML.Form{}` and field name may be passed to the input
  to build input names and error messages, or all the attributes and
  errors may be passed explicitly.


  ## Examples

      <.input field={@form[:email]} type="email" />
      <.input name="my-input" errors={["oh no!"]} />
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc

  attr :autofocus, :boolean,
    default: false,
    doc: "Enable autofocus hook to reliably focus input on mount"

  attr :checked, :boolean, doc: "The checked flag for checkbox inputs"
  attr :description, :string, default: nil
  attr :errors, :list, default: []

  attr :field, Phoenix.HTML.FormField,
    doc: "A form field struct retrieved from the form, for example: @form[:email]"

  attr :id, :any, default: nil

  attr :label, :string, default: nil
  attr :multiple, :boolean, default: false, doc: "The multiple flag for select inputs"
  attr :name, :any
  attr :options, :list, doc: "The options to pass to Phoenix.HTML.Form.options_for_select/2"
  attr :prompt, :string, default: nil, doc: "The prompt for select inputs"

  attr :type, :string,
    default: "text",
    values:
      ~w[checkbox color date datetime-local email file hidden month number password range radio search select tel text textarea time url week]

  attr :value, :any

  attr :clear_on_escape, :boolean,
    overridable: true,
    required: true,
    doc: "Clear input value on pressing Escape"

  attr :class, :tails_classes,
    overridable: true,
    required: true,
    doc: "Class of the field container element"

  attr :input_class, :tails_classes,
    overridable: true,
    required: true,
    doc: "Class of the input element"

  attr :input_check_label_class, :tails_classes,
    overridable: true,
    required: true,
    doc: "Class of the label element for a check input"

  attr :description_class, :tails_classes,
    overridable: true,
    required: true,
    doc: "Class of the field description"

  attr :rest, :global, include: ~w(autocomplete cols disabled form max maxlength min minlength
                                   pattern placeholder readonly required rows size step)
  slot :inner_block

  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
    assigns
    |> assign(field: nil, id: assigns.id || field.id)
    |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
    |> assign_new(:value, fn -> field.value end)
    |> input()
  end

  def input(assigns) do
    assigns
    |> assign_overridables()
    |> render_input()
  end

  defp render_input(%{type: "checkbox", value: value} = assigns) do
    assigns =
      assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)

    ~H"""
    <div class={@class} phx-feedback-for={@name}>
      <label class={@input_check_label_class}>
        <input type="hidden" name={@name} value="false" />
        <input
          type="checkbox"
          id={@id || @name}
          name={@name}
          value="true"
          checked={@checked}
          class={@input_class}
          phx-mounted={!@autofocus || JS.focus()}
          {@rest}
        />
        <%= @label %>
      </label>
      <.error :for={msg <- @errors} overrides={@overrides}><%= msg %></.error>
    </div>
    """
  end

  defp render_input(%{type: "select"} = assigns) do
    ~H"""
    <div class={@class} phx-feedback-for={@name}>
      <.label for={@id} overrides={@overrides}><%= @label %></.label>
      <select
        id={@id || @name}
        name={@name}
        class={@input_class}
        multiple={@multiple}
        phx-mounted={!@autofocus || JS.focus()}
        {@rest}
      >
        <option :if={@prompt} value=""><%= @prompt %></option>
        <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
      </select>
      <.error :for={msg <- @errors} overrides={@overrides}><%= msg %></.error>
    </div>
    """
  end

  defp render_input(%{type: "textarea"} = assigns) do
    ~H"""
    <div phx-feedback-for={@name} class={@class}>
      <.label for={@id} overrides={@overrides}><%= @label %></.label>
      <textarea
        id={@id || @name}
        name={@name}
        class={@input_class}
        phx-keydown={!@clear_on_escape || JS.dispatch("pyro:clear")}
        phx-key={!@clear_on_escape || "Escape"}
        phx-mounted={!@autofocus || JS.focus()}
        {@rest}
      ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
      <p :if={@description} class={@description_class}>
        <%= @description %>
      </p>
      <.error :for={msg <- @errors} overrides={@overrides}><%= msg %></.error>
    </div>
    """
  end

  defp render_input(%{type: "hidden"} = assigns) do
    ~H"""
    <div phx-feedback-for={@name} class={@class}>
      <.label for={@id} overrides={@overrides}><%= @label %></.label>
      <input
        type="hidden"
        name={@name}
        id={@id || @name}
        value={Phoenix.HTML.Form.normalize_value(@type, @value)}
        {@rest}
      />
      <%= render_slot(@inner_block) %>
      <p :if={@description} class={@description_class}>
        <%= @description %>
      </p>
      <.error :for={msg <- @errors} overrides={@overrides}><%= msg %></.error>
    </div>
    """
  end

  defp render_input(assigns) do
    ~H"""
    <div phx-feedback-for={@name} class={@class}>
      <.label for={@id} overrides={@overrides}><%= @label %></.label>
      <input
        type={@type}
        name={@name}
        id={@id || @name}
        value={Phoenix.HTML.Form.normalize_value(@type, @value)}
        class={@input_class}
        phx-keydown={!@clear_on_escape || JS.dispatch("pyro:clear")}
        phx-key={!@clear_on_escape || "Escape"}
        phx-mounted={!@autofocus || JS.focus()}
        {@rest}
      />
      <p :if={@description} class={@description_class}>
        <%= @description %>
      </p>
      <.error :for={msg <- @errors} overrides={@overrides}><%= msg %></.error>
    </div>
    """
  end

  @doc """
  Renders a label.
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :class, :tails_classes, overridable: true, required: true
  attr :for, :string, default: nil
  slot :inner_block, required: true

  def label(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <label for={@for} class={@class}>
      <%= render_slot(@inner_block) %>
    </label>
    """
  end

  @doc """
  Generates a generic error message.
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc

  attr :icon_name, :string,
    overridable: true,
    required: true,
    doc: "The name of the icon; see [`icon/1`](`Pyro.Components.Core.icon/1`) for details"

  attr :icon_class, :tails_classes, overridable: true
  attr :class, :tails_classes, overridable: true, required: true

  slot :inner_block, required: true

  def error(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <p class={@class}>
      <.icon overrides={@overrides} name={@icon_name} class={@icon_class} />
      <%= render_slot(@inner_block) %>
    </p>
    """
  end

  @doc """
  Renders a header with title and optional subtitle/actions.
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :class, :tails_classes, overridable: true, required: true
  attr :title_class, :tails_classes, overridable: true, required: true
  attr :subtitle_class, :tails_classes, overridable: true, required: true
  attr :actions_class, :tails_classes, overridable: true, required: true
  slot :inner_block, required: true
  slot :subtitle
  slot :actions

  def header(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <header class={@class}>
      <div>
        <h1 class={@title_class}>
          <%= render_slot(@inner_block) %>
        </h1>
        <p :if={@subtitle != []} class={@subtitle_class}>
          <%= render_slot(@subtitle) %>
        </p>
      </div>
      <div :if={@actions != []} class={@actions_class}>
        <%= render_slot(@actions) %>
      </div>
    </header>
    """
  end

  @doc ~S"""
  Renders a simple table with generic styling.

  ## Examples

      <.table id="users" rows={@users}>
        <:col :let={user} label="id"><%= user.id %></:col>
        <:col :let={user} label="username"><%= user.username %></:col>
      </.table>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :id, :string
  attr :row_click, :any, default: nil

  attr :row_id, :any,
    default: nil,
    doc: "The function for generating the row id (will automatically extract from a LiveStream)"

  attr :row_item, :any,
    default: &Function.identity/1,
    doc: "The function for mapping each row before calling the :col and :action slots"

  attr :rows, :list, required: true, doc: "Supports a list or LiveStream"

  attr :class, :tails_classes, overridable: true, required: true
  attr :action_class, :tails_classes, overridable: true, required: true
  attr :action_td_class, :tails_classes, overridable: true, required: true
  attr :action_wrapper_class, :tails_classes, overridable: true, required: true
  attr :tbody_class, :tails_classes, overridable: true, required: true
  attr :td_class, :tails_classes, overridable: true, required: true
  attr :th_action_class, :tails_classes, overridable: true, required: true
  attr :th_label_class, :tails_classes, overridable: true, required: true
  attr :thead_class, :tails_classes, overridable: true, required: true
  attr :tr_class, :tails_classes, overridable: true, required: true

  slot :col, required: true do
    attr :label, :string
  end

  slot(:action, doc: "The slot for showing user actions in the last table column")

  def table(assigns) do
    assigns = assign_overridables(assigns)

    assigns =
      with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
        assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
      end

    ~H"""
    <table class={@class}>
      <thead class={@thead_class}>
        <tr>
          <th :for={col <- @col} class={@th_label_class}><%= col[:label] %></th>
          <th class={@th_action_class}>
            <span class="sr-only"><%= gettext("Actions") %></span>
          </th>
        </tr>
      </thead>
      <tbody
        id={@id}
        class={@tbody_class}
        phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
      >
        <tr :for={row <- @rows} id={@row_id && @row_id.(row)} class={@tr_class}>
          <td :for={col <- @col} phx-click={@row_click && @row_click.(row)} class={@td_class}>
            <%= render_slot(col, @row_item.(row)) %>
          </td>
          <td :if={@action != []} class={@action_td_class}>
            <div class={@action_wrapper_class}>
              <span :for={action <- @action} class={@action_class}>
                <%= render_slot(action, @row_item.(row)) %>
              </span>
            </div>
          </td>
        </tr>
      </tbody>
    </table>
    """
  end

  @doc """
  Renders a description list.

  ## Examples

      <.list>
        <:item title="Title"><%= @post.title %></:item>
        <:item title="Views"><%= @post.views %></:item>
      </.list>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :class, :tails_classes, overridable: true, required: true
  attr :dd_class, :tails_classes, overridable: true, required: true
  attr :dt_class, :tails_classes, overridable: true, required: true

  slot :item, required: true do
    attr :title, :string, required: true
  end

  def list(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <dl class={@class}>
      <%= for item <- @item do %>
        <dt class={@dt_class}><%= item.title %></dt>
        <dd class={@dd_class}><%= render_slot(item) %></dd>
      <% end %>
    </dl>
    """
  end

  @doc """
  Renders a back navigation link.

  ## Examples

      <.back navigate={~p"/posts"}>Back to posts</.back>
      <.back icon_name="hero-arrow-left" navigate={~p"/"}>
        Go back to the about page.
      </.back>
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc

  attr :icon_name, :string,
    overridable: true,
    required: true,
    doc: "The name of the icon; see [`icon/1`](`Pyro.Components.Core.icon/1`) for details"

  attr :class, :tails_classes, overridable: true, required: true
  attr :icon_class, :tails_classes, overridable: true, required: true

  attr :navigate, :any, required: true
  slot :inner_block, required: true

  def back(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <.link navigate={@navigate} class={@class}>
      <.icon overrides={@overrides} name={@icon_name} class={@icon_class} />
      <%= render_slot(@inner_block) %>
    </.link>
    """
  end

  @icons_dir Path.join(File.cwd!(), "priv/static/heroicons/optimized")

  @outline [@icons_dir, "24/outline"]
           |> Path.join()
           |> File.ls!()
           |> Enum.map(fn name ->
             "hero-#{Path.basename(name, ".svg")}"
           end)

  @solid [@icons_dir, "24/solid"]
         |> Path.join()
         |> File.ls!()
         |> Enum.map(fn name ->
           "hero-#{Path.basename(name, ".svg")}-solid"
         end)

  @mini [@icons_dir, "20/solid"]
        |> Path.join()
        |> File.ls!()
        |> Enum.map(fn name ->
          "hero-#{Path.basename(name, ".svg")}-mini"
        end)

  @icon_name_options Enum.zip([@outline, @solid, @mini])
                     |> Enum.flat_map(fn {o, s, m} -> [o, s, m] end)

  @doc """
  Renders an icon.

  > #### Tip {: .info}
  >
  > See the [Heroicons website](https://heroicons.com/) to preview/search the available icons.

  Additionally, there are long-term plans to add more icon libraries.

  ## Examples

  ```heex
  <.icon name="hero-arrow-left" />
  ```
  ```heex
  <.icon name="hero-arrow-right-solid" class="block" />
  ```
  """

  attr :overrides, :list, default: nil, doc: @overrides_attr_doc
  attr :class, :tails_classes, overridable: true, required: true

  attr :name, :string,
    required: true,
    values: @icon_name_options,
    doc: "The icon name"

  attr :rest, :global,
    doc: "The arbitrary HTML attributes for the svg container",
    include: ~w(fill stroke stroke-width)

  def icon(assigns) do
    assigns = assign_overridables(assigns)

    ~H"""
    <span class={[@name, @class]} />
    """
  end
end