defmodule LivebookWeb.CoreComponents do use Phoenix.Component alias Phoenix.LiveView.JS @doc """ Renders [Remix](https://remixicon.com) icon. ## Examples <.remix_icon icon="cpu-line" /> <.remix_icon icon="cpu-line" class="align-middle mr-1" /> """ attr :icon, :string, required: true attr :class, :any, default: nil attr :rest, :global def remix_icon(assigns) do ~H""" """ end @doc """ Renders flash notices. ## Examples <.flash kind={:info} flash={@flash} /> """ attr :flash, :map, default: %{}, doc: "the map of flash messages to display" attr :kind, :atom, values: [:info, :success, :warning, :error], doc: "used for styling and flash lookup" attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" def flash(assigns) do ~H""" """ end @doc """ Shows the flash group with standard titles and content. ## Examples <.flash_group flash={@flash} /> """ attr :flash, :map, required: true, doc: "the map of flash messages" def flash_group(assigns) do ~H"""
<.flash kind={:info} flash={@flash} /> <.flash kind={:success} flash={@flash} /> <.flash kind={:warning} flash={@flash} /> <.flash kind={:error} flash={@flash} />
""" end @doc """ Renders a message notice. Similar to `flash/1`, but for permanent messages on the page. ## Examples <.message_box kind={:info} message="🦊 in a 📦" /> <.message_box kind={:info}> 🦊 in a 📦 """ attr :message, :string, default: nil attr :kind, :atom, values: [:info, :success, :warning, :error] slot :inner_block def message_box(assigns) do ~H"""
<%= @message %>
<%= render_slot(@inner_block) %>
""" end @doc """ Creates a live region with the given role. ## Examples <.live_region role="alert" /> <.live_region role="status" /> """ def live_region(assigns) do ~H"""
""" end @doc """ Sends a message to be read by the screen reader by changing the text content of the live region """ def sr_message(js \\ %JS{}, message) do JS.dispatch(js, "lb:set_text", to: "#live-region", detail: %{value: message}) end @doc """ Wraps the given content in a modal dialog. ## Example <.modal id="edit-modal" patch={...}> <.live_component module={MyComponent} /> """ attr :id, :string, required: true attr :show, :boolean, default: false attr :patch, :string, default: nil attr :navigate, :string, default: nil attr :class, :string, default: nil attr :width, :atom, values: [:small, :medium, :big, :large], required: true attr :rest, :global slot :inner_block, required: true def modal(assigns) do ~H""" """ end defp modal_width_class(:small), do: "max-w-sm" defp modal_width_class(:medium), do: "max-w-xl" defp modal_width_class(:big), do: "max-w-4xl" defp modal_width_class(:large), do: "max-w-6xl" defp modal_on_cancel(nil, nil), do: JS.exec("phx-remove") defp modal_on_cancel(nil, navigate), do: JS.patch(navigate) defp modal_on_cancel(patch, nil), do: JS.patch(patch) @doc """ Shows a modal rendered with `modal/1`. """ def show_modal(js \\ %JS{}, id) do js |> JS.show(to: "##{id}") |> JS.transition( {"ease-out duration-200", "opacity-0", "opacity-100"}, to: "##{id}-container" ) end @doc """ Hides a modal rendered with `modal/1`. """ def hide_modal(js \\ %JS{}, id) do js |> JS.transition( {"ease-in duration-200", "opacity-100", "opacity-0"}, to: "##{id}-container" ) |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) end @doc """ Renders a popup menu that shows up on toggle click. ## Examples <.menu id="my-menu"> <:toggle> <.menu_item> <.menu_item> """ attr :id, :string, required: true attr :disabled, :boolean, default: false, doc: "whether the menu is active" attr :position, :atom, default: :bottom_right, values: [:top_left, :top_right, :bottom_left, :bottom_right] attr :sm_position, :atom, default: nil, values: [nil, :top_left, :top_right, :bottom_left, :bottom_right] attr :md_position, :atom, default: nil, values: [nil, :top_left, :top_right, :bottom_left, :bottom_right] attr :distant, :boolean, default: false, doc: "whether the menu should be further from the anchor element" attr :secondary_click, :boolean, default: false, doc: "whether secondary click (usually right mouse click) should open the menu" slot :toggle, required: true slot :inner_block, required: true def menu(assigns) do ~H"""
<%= render_slot(@toggle) %>
<%= render_slot(@inner_block) %>
""" end defp show_menu(id) do JS.show(to: "##{id}-overlay") |> JS.show(to: "##{id}-content", display: "flex") |> JS.dispatch("lb:scroll_into_view", to: "##{id}-content") end defp hide_menu(id) do JS.hide(to: "##{id}-overlay") |> JS.hide(to: "##{id}-content") end defp menu_position_class(:top_left), do: "top-0 left-0 transform -translate-y-full -mt-1" defp menu_position_class(:top_right), do: "top-0 right-0 transform -translate-y-full -mt-1" defp menu_position_class(:bottom_left), do: "bottom-0 left-0 transform translate-y-full -mb-1" defp menu_position_class(:bottom_right), do: "bottom-0 right-0 transform translate-y-full -mb-1" defp menu_md_position_class(:top_left) do "md:top-0 md:left-0 md:bottom-auto md:right-auto md:transform md:-translate-y-full md:-mt-1 md:mb-0" end defp menu_md_position_class(:top_right) do "md:top-0 md:right-0 md:bottom-auto md:left-auto md:transform md:-translate-y-full md:-mt-1 md:mb-0" end defp menu_md_position_class(:bottom_left) do "md:bottom-0 md:left-0 md:top-auto md:right-auto md:transform md:translate-y-full md:-mb-1 md:mt-0" end defp menu_md_position_class(:bottom_right) do "md:bottom-0 md:right-0 md:top-auto md:left-auto md:transform md:translate-y-full md:-mb-1 md:mt-0" end defp menu_sm_position_class(:top_left) do "sm:top-0 sm:left-0 sm:bottom-auto sm:right-auto sm:transform sm:-translate-y-full sm:-mt-1 sm:mb-0" end defp menu_sm_position_class(:top_right) do "sm:top-0 sm:right-0 sm:bottom-auto sm:left-auto sm:transform sm:-translate-y-full sm:-mt-1 sm:mb-0" end defp menu_sm_position_class(:bottom_left) do "sm:bottom-0 sm:left-0 sm:top-auto sm:right-auto sm:transform sm:translate-y-full sm:-mb-1 sm:mt-0" end defp menu_sm_position_class(:bottom_right) do "sm:bottom-0 sm:right-0 sm:top-auto sm:left-auto sm:transform sm:translate-y-full sm:-mb-1 sm:mt-0" end defp menu_distant_class(position) when position in [:top_left, :top_right], do: "-mt-2" defp menu_distant_class(position) when position in [:bottom_left, :bottom_right], do: "-mb-2" @doc """ Wraps a menu item that shows a submenu on hover. This component should be used within `menu/1` content. ## Example <.submenu> <:primary> <.menu_item> """ slot :primary, required: true slot :inner_block, required: true def submenu(assigns) do ~H"""
<%= render_slot(@primary) %>
""" end @doc """ Renders a menu item used in `menu/1` and `submenu/1`. """ attr :disabled, :boolean, default: false attr :variant, :atom, default: :default, values: [:default, :selected, :danger] slot :inner_block, required: true def menu_item(assigns) do ~H"""
  • :first-child]:w-full [&>:first-child]:flex [&>:first-child]:space-x-3 [&>:first-child]:px-5 [&>:first-child]:py-2 [&>:first-child]:items-center [&>:first-child:hover]:bg-gray-100 [&>:first-child:focus]:bg-gray-100 [&>:first-child]:whitespace-nowrap font-medium", menu_item_class(@variant), @disabled && "pointer-events-none opacity-50" ]}> <%= render_slot(@inner_block) %>
  • """ end defp menu_item_class(:default), do: "text-gray-500" defp menu_item_class(:selected), do: "text-gray-900" defp menu_item_class(:danger), do: "text-red-600" @doc """ Renders a text content skeleton. """ attr :empty, :boolean, default: false, doc: "if the source is empty" attr :bg_class, :string, default: "bg-gray-200", doc: "the skeleton background color" def content_skeleton(assigns) do ~H""" <%= if @empty do %>
    <% else %>
    <% end %> """ end @doc """ Renders a highlighted code snippet. ## Examples <.code_preview source_id="my-snippet" language="elixir" source="System.version()" /> """ attr :source_id, :string, required: true attr :language, :string, required: true attr :source, :string, required: true attr :wrap, :boolean, default: false def code_preview(assigns) do ~H"""
    <%= @source %>
    """ end @doc """ Renders text with a tiny label. ## Examples <.labeled_text label="Name">Sherlock Holmes """ attr :label, :string, required: true attr :one_line, :boolean, default: false, doc: "whether to force the text into a single scrollable line" attr :class, :string, default: nil slot :inner_block, required: true def labeled_text(assigns) do ~H"""
    <%= @label %> <%= render_slot(@inner_block) %>
    """ end @doc """ Renders a choice button that is either active or not. ## Examples <.choice_button active={@tab == "my_tab"} phx-click="set_my_tab"> My tab """ attr :active, :boolean, required: true attr :disabled, :boolean attr :class, :string, default: nil attr :rest, :global slot :inner_block, required: true def choice_button(assigns) do assigns = assigns |> assign_new(:disabled, fn -> assigns.active end) ~H""" """ end @doc """ Renders an status indicator circle. """ attr :variant, :atom, required: true, values: [:success, :warning, :error, :inactive, :waiting, :progressing] def status_indicator(assigns) do ~H""" """ end @doc """ Returns background class based on the given variant. See `status_indicator/1` for available variants. """ def status_circle_class(variant) def status_circle_class(:success), do: "bg-green-bright-400" def status_circle_class(:warning), do: "bg-yellow-bright-200" def status_circle_class(:error), do: "bg-red-400" def status_circle_class(:inactive), do: "bg-gray-500" def status_circle_class(:waiting), do: "bg-gray-400" def status_circle_class(:progressing), do: "bg-blue-500" defp animated_status_circle_class(:waiting), do: "bg-gray-300" defp animated_status_circle_class(:progressing), do: "bg-blue-400" defp animated_status_circle_class(_other), do: nil @doc """ Renders an informative box as a placeholder for a list. """ slot :inner_block, required: true slot :actions def no_entries(assigns) do ~H"""
    <.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
    <%= render_slot(@inner_block) %>
    <%= render_slot(@actions) %>
    """ end @doc """ Renders a circular spinner. """ attr :class, :string, default: nil attr :rest, :global def spinner(assigns) do ~H""" """ end @doc """ Returns the text in singular or plural depending on the quantity. ## Examples <.listing items={@packages}> <:item :let={package}><%= package.name %> <:singular_suffix>package <:plural_suffix>packages """ attr :items, :list, required: true slot :item, required: true slot :plural_suffix slot :singular_suffix def listing(%{items: [_]} = assigns) do ~H""" <%= render_slot(@item, hd(@items)) %> <%= render_slot(@singular_suffix) %> """ end def listing(%{items: [_, _ | _]} = assigns) do {items, assigns} = Map.pop!(assigns, :items) {leading, [second_to_last, last]} = Enum.split(items, -2) assigns = assign(assigns, leading: leading, second_to_last: second_to_last, last: last) ~H""" <%= for item <- @leading do %> <%= render_slot(@item, item) %>, <% end %> <%= render_slot(@item, @second_to_last) %> and <%= render_slot(@item, @last) %> <%= render_slot(@plural_suffix) %> """ end @doc ~S""" Renders a table with generic styling. ## Examples <.table id="users" rows={@users}> <:col :let={user} label="id"><%= user.id %> <:col :let={user} label="username"><%= user.username %> """ 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] %> Actions
    <%= render_slot(col, @row_item.(row)) %>
    <%= render_slot(action, @row_item.(row)) %>
    """ end @doc ~S""" Renders a button. ## Examples <.button>Click <.button color="gray" outlined>Click <.button color="gray" small>Click """ attr :disabled, :boolean, default: false attr :color, :string, default: "blue", values: ~w(blue gray red) attr :outlined, :boolean, default: false attr :small, :boolean, default: false attr :class, :string, default: nil attr :rest, :global, include: ~w(href patch navigate download name) slot :inner_block def button(assigns) when is_map_key(assigns.rest, :href) or is_map_key(assigns.rest, :patch) or is_map_key(assigns.rest, :navigate) do ~H""" <.link class={[button_classes(@small, @disabled, @color, @outlined), @class]} {@rest}> <%= render_slot(@inner_block) %> """ end def button(assigns) do ~H""" """ end defp button_classes(small, disabled, color, outlined) do [ if small do "px-2 py-1 font-normal text-xs" else "px-5 py-2 font-medium text-sm" end, "inline-flex rounded-lg border whitespace-nowrap items-center justify-center gap-1.5", if disabled do "cursor-default pointer-events-none border-transparent bg-gray-100 text-gray-400" else case {color, outlined} do {"blue", false} -> "border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700" {"red", false} -> "border-transparent bg-red-600 text-white hover:bg-red-700 focus:bg-red-700" {"gray", false} -> "border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200 focus:bg-gray-200" {"blue", true} -> "bg-blue-50 border-blue-600 text-blue-600 hover:bg-blue-100 focus:bg-blue-100" {"red", true} -> "bg-red-50 border-red-600 text-red-600 hover:bg-red-100 focus:bg-red-100" {"gray", true} -> "bg-transparent border-gray-300 text-gray-600 hover:bg-gray-100 focus:bg-gray-100" end end ] end @doc ~S""" Renders an icon button. ## Examples <.icon_button> <.remix_icon icon="refresh-line" /> """ attr :disabled, :boolean, default: false attr :small, :boolean, default: false attr :class, :string, default: nil attr :rest, :global, include: ~w(href patch navigate download name) slot :inner_block def icon_button(assigns) when is_map_key(assigns.rest, :href) or is_map_key(assigns.rest, :patch) or is_map_key(assigns.rest, :navigate) do ~H""" <.link class={[icon_button_classes(@small, @disabled), @class]} {@rest}> <%= render_slot(@inner_block) %> """ end def icon_button(assigns) do ~H""" """ end defp icon_button_classes(small, disabled) do [ unless small do "text-xl" end, "p-1 flex items-center justify-center rounded-full leading-none", if disabled do "cursor-default text-gray-300" else "text-gray-500 hover:text-gray-900 focus:bg-gray-100" end ] end # JS commands @doc """ Toggles classes on elements. """ def toggle_class(js \\ %JS{}, names, opts \\ []) do opts = Keyword.validate!(opts, [:to]) to = Keyword.fetch!(opts, :to) names |> String.split() |> Enum.reduce(js, fn name, js -> js |> JS.remove_class(name, to: "#{to}.#{name}") |> JS.add_class(name, to: "#{to}:not(.#{name})") end) end @doc """ Pushes and executes the given `%Phoenix.LiveView.JS{}` on the client. ## Options * `:to` - selector for elements to execute against. Defaults to document body """ def exec_js(socket, js, opts \\ []) do opts = Keyword.validate!(opts, [:to]) Phoenix.LiveView.push_event(socket, "lb:exec_js", %{js: Jason.encode!(js.ops), to: opts[:to]}) end @doc """ Encodes value for hook prop attribute. ## Examples
    """ def hook_prop(value) def hook_prop(%Phoenix.LiveComponent.CID{} = value) do hook_prop(to_string(value)) end def hook_prop(value) do Jason.encode!(value) end end