defmodule SuperintelligenceWeb.CoreComponents do @moduledoc """ Provides core UI components. """ use Phoenix.Component alias Phoenix.LiveView.JS use Gettext, backend: SuperintelligenceWeb.Gettext @doc """ Renders flash notices. """ attr :flash, :map, default: %{}, doc: "the map of flash messages" attr :title, :string, default: nil attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash" slot :inner_block, doc: "the optional inner block that renders the flash message" def flash(assigns) do assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end) ~H"""
hide("##{@id}")} role="alert" class={[ "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1", @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900", @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900" ]} {@rest} >

<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" /> <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" /> <%= @title %>

<%= msg %>

""" end @doc """ Shows the flash group with standard titles and content. """ attr :flash, :map, required: true, doc: "the map of flash messages" attr :id, :string, default: "flash-group", doc: "the optional id of flash container" def flash_group(assigns) do ~H"""
<.flash kind={:info} title={gettext("Success!")} flash={@flash} /> <.flash kind={:error} title={gettext("Error!")} flash={@flash} /> <.flash id="client-error" kind={:error} title={gettext("We can't find the internet")} phx-disconnected={show(".phx-client-error #client-error")} phx-connected={hide("#client-error")} hidden > <%= gettext("Attempting to reconnect") %> <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> <.flash id="server-error" kind={:error} title={gettext("Something went wrong!")} phx-disconnected={show(".phx-server-error #server-error")} phx-connected={hide("#server-error")} hidden > <%= gettext("Hang in there while we get back on track") %> <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
""" end @doc """ Renders a simple form. """ attr :for, :any, required: true, doc: "the datastructure for the form" attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" attr :rest, :global, include: ~w(autocomplete name rel action enctype method novalidate target multipart), 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 ~H""" <.form :let={f} for={@for} as={@as} {@rest}>
<%= render_slot(@inner_block, f) %>
<%= render_slot(action, f) %>
""" end @doc """ Renders a button. """ attr :type, :string, default: nil attr :class, :string, default: nil attr :rest, :global, include: ~w(disabled form name value) slot :inner_block, required: true def button(assigns) do ~H""" """ end @doc """ Renders an input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :type, :string, default: "text", values: ~w(checkbox color date datetime-local email file month number password range search select tel text textarea time url week) attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]" attr :errors, :list, default: [] attr :checked, :boolean, doc: "the checked flag for checkbox inputs" attr :prompt, :string, default: nil, doc: "the prompt for select inputs" attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength multiple pattern placeholder readonly required rows size step) def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] assigns |> assign(field: nil, id: assigns.id || field.id) |> assign(:errors, Enum.map(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(%{type: "checkbox"} = assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
<.error :for={msg <- @errors}><%= msg %>
""" end def input(%{type: "select"} = assigns) do ~H"""
<.label for={@id}><%= @label %> <.error :for={msg <- @errors}><%= msg %>
""" end def input(%{type: "textarea"} = assigns) do ~H"""
<.label for={@id}><%= @label %> <.error :for={msg <- @errors}><%= msg %>
""" end def input(assigns) do ~H"""
<.label for={@id}><%= @label %> <.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders a label. """ attr :for, :string, default: nil slot :inner_block, required: true def label(assigns) do ~H""" """ end @doc """ Generates a generic error message. """ slot :inner_block, required: true def error(assigns) do ~H"""

<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> <%= render_slot(@inner_block) %>

""" end @doc """ Renders a header with title. """ attr :class, :string, default: nil slot :inner_block, required: true slot :subtitle slot :actions def header(assigns) do ~H"""

<%= render_slot(@inner_block) %>

<%= render_slot(@subtitle) %>

<%= render_slot(@actions) %>
""" end @doc ~S""" Renders a table with generic styling. """ attr :id, :string, required: true attr :rows, :list, required: true attr :row_id, :any, default: nil, doc: "the function for generating the row id" attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" attr :row_item, :any, default: &Function.identity/1, doc: "the function for mapping each row before calling the :col and :action slots" 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 = with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) end ~H"""
<%= col[:label] %> <%= gettext("Actions") %>
<%= render_slot(col, @row_item.(row)) %>
<%= render_slot(action, @row_item.(row)) %>
""" end @doc """ Renders a list of items. """ slot :item, required: true do attr :title, :string, required: true end def list(assigns) do ~H"""
<%= item.title %>
<%= render_slot(item) %>
""" end @doc """ Renders a back navigation link. """ attr :navigate, :any, required: true slot :inner_block, required: true def back(assigns) do ~H"""
<.link navigate={@navigate} class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" > <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> <%= render_slot(@inner_block) %>
""" end @doc """ Renders a [Heroicon](https://heroicons.com). Heroicons come in three styles – outline, solid, and mini. By default, the outline style is used, but solid and mini may be applied by using the `-solid` and `-mini` suffix. You can customize the size and colors of the icons by setting width, height, and background color classes. Icons are extracted from the `deps/heroicons` directory and bundled within your compiled app.css by the plugin in your `assets/tailwind.config.js`. ## Examples <.icon name="hero-x-mark-solid" /> <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> """ attr :name, :string, required: true attr :class, :string, default: nil def icon(%{name: "hero-" <> _} = assigns) do ~H""" """ end ## JS Commands def show(js \\ %JS{}, selector) do JS.show(js, to: selector, time: 300, 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"} ) end def hide(js \\ %JS{}, selector) do JS.hide(js, to: selector, time: 200, 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"} ) end def show_modal(js \\ %JS{}, id) when is_binary(id) do js |> JS.show(to: "##{id}") |> JS.show( to: "##{id}-bg", time: 300, transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} ) |> show("##{id}-container") |> JS.add_class("overflow-hidden", to: "body") |> JS.focus_first(to: "##{id}-content") end def hide_modal(js \\ %JS{}, id) do js |> JS.hide( to: "##{id}-bg", transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} ) |> hide("##{id}-container") |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) |> JS.remove_class("overflow-hidden", to: "body") |> JS.pop_focus() end @doc """ Translates an error message using gettext. """ def translate_error({msg, opts}) do # When using gettext, we typically pass the strings we want # to translate as a static argument: # # # Translate the number of files with plural rules # dngettext("errors", "1 file", "%{count} files", count) # # However the error messages in our forms and APIs are generated # dynamically, so we need to translate them by calling Gettext # with our gettext backend as first argument. Translations are # available in the errors.po file (as we use the "errors" domain). if count = opts[:count] do Gettext.dngettext(SuperintelligenceWeb.Gettext, "errors", msg, msg, count, opts) else Gettext.dgettext(SuperintelligenceWeb.Gettext, "errors", msg, opts) end end @doc """ Translates the errors for a field from a keyword list of errors. """ def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end end