defmodule PlausibleWeb.Components.Generic do @moduledoc """ Generic reusable components """ use Phoenix.Component, global_prefixes: ~w(x-) @notice_themes %{ gray: %{ bg: "bg-gray-100 dark:bg-gray-800", icon: "text-gray-600 dark:text-gray-300", title_text: "text-sm text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-300 leading-5" }, yellow: %{ bg: "bg-yellow-100/60 dark:bg-yellow-900/40", icon: "text-yellow-500", title_text: "text-sm text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" }, red: %{ bg: "bg-red-100 dark:bg-red-900/30", icon: "text-red-600 dark:text-red-500", title_text: "text-sm text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" } } @button_themes %{ "primary" => "bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:outline-indigo-600 disabled:bg-indigo-400/60 disabled:dark:bg-indigo-600/30 disabled:dark:text-white/35", "secondary" => "border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:text-gray-900 hover:shadow-sm dark:hover:bg-gray-600 dark:hover:text-white disabled:text-gray-700/40 disabled:hover:shadow-none dark:disabled:text-gray-500 dark:disabled:bg-gray-800 dark:disabled:border-gray-800", "danger" => "border border-gray-300 dark:border-gray-800 text-red-600 bg-white dark:bg-gray-800 hover:text-red-700 hover:shadow-sm dark:hover:text-red-400 dark:text-red-500 active:text-red-800 disabled:text-red-700/40 disabled:hover:shadow-none dark:disabled:text-red-500/35 dark:disabled:bg-gray-800" } @button_base_class "whitespace-nowrap truncate inline-flex items-center justify-center gap-x-2 font-medium rounded-md px-3.5 py-2.5 text-sm transition-all duration-150 cursor-pointer disabled:cursor-not-allowed" attr(:type, :string, default: "button") attr(:theme, :string, default: "primary") attr(:class, :string, default: "") attr(:disabled, :boolean, default: false) attr(:mt?, :boolean, default: true) attr(:rest, :global, include: ~w(name)) slot(:inner_block) def button(assigns) do assigns = assign(assigns, button_base_class: @button_base_class, theme_class: @button_themes[assigns.theme] ) ~H""" """ end attr(:href, :string, required: true) attr(:class, :string, default: "") attr(:theme, :string, default: "primary") attr(:disabled, :boolean, default: false) attr(:method, :string, default: "get") attr(:mt?, :boolean, default: true) attr(:rest, :global) slot(:inner_block) def button_link(assigns) do extra = if assigns.method == "get" do [] else [ "data-csrf": Phoenix.Controller.get_csrf_token(), "data-method": assigns.method, "data-to": assigns.href ] end assigns = assign(assigns, extra: extra) theme_class = if assigns.disabled do "bg-gray-400 text-white transition-colors duration-150 dark:text-white dark:text-gray-400 dark:bg-gray-700 cursor-not-allowed" else @button_themes[assigns.theme] end onclick = if assigns.disabled do "return false;" else assigns[:onclick] end assigns = assign(assigns, onclick: onclick, button_base_class: @button_base_class, theme_class: theme_class ) ~H""" <.link href={@href} onclick={@onclick} class={[ @mt? && "mt-6", @button_base_class, @theme_class, @class ]} {@extra} {@rest} > {render_slot(@inner_block)} """ end attr(:slug, :string, required: true) attr(:class, :string, default: nil) def docs_info(assigns) do ~H"""
<.tooltip enabled?={true} centered?={true}> <:tooltip_content> Learn more
""" end attr(:title, :any, default: "") attr(:class, :string, default: "") attr(:rest, :global) slot(:icon, required: true) slot(:inner_block) def upgrade(assigns) do ~H"""
{render_slot(@icon)}

{@title}

{render_slot(@inner_block)}

""" end attr(:title, :any, default: nil) attr(:theme, :atom, default: :yellow) attr(:dismissable_id, :any, default: nil) attr(:class, :string, default: "") attr(:rest, :global) slot(:inner_block) def notice(assigns) do assigns = assign(assigns, :theme, Map.fetch!(@notice_themes, assigns.theme)) ~H"""

{@title}

{render_slot(@inner_block)}

""" end attr(:href, :string, default: "#") attr(:new_tab, :boolean, default: false) attr(:class, :string, default: "") attr(:rest, :global, include: ~w(patch)) attr(:method, :string, default: "get") slot(:inner_block) def styled_link(assigns) do ~H""" <.unstyled_link new_tab={@new_tab} href={@href} method={@method} class={"text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-400 transition-colors duration-150 " <> @class} {@rest} > {render_slot(@inner_block)} """ end attr :class, :string, default: "" attr :id, :string, default: nil slot :button, required: true do attr(:class, :string) end slot :menu, required: true do attr(:class, :string) end def dropdown(assigns) do assigns = assign(assigns, :menu_class, assigns.menu |> List.first() |> Map.get(:class, "")) ~H"""
""" end attr(:href, :string) attr(:class, :string, default: "") attr(:id, :string, default: nil) attr(:new_tab, :boolean, default: false) attr(:disabled, :boolean, default: false) attr(:rest, :global, include: ~w(method)) slot(:inner_block, required: true) @base_class "block rounded-md text-sm/6 text-gray-900 ui-disabled:text-gray-500 dark:text-gray-100 dark:ui-disabled:text-gray-400 px-3 py-1.5" @clickable_class "hover:bg-gray-100 dark:hover:bg-gray-700/80" def dropdown_item(assigns) do assigns = if assigns[:disabled] do assign(assigns, :state, "disabled") else assign(assigns, :state, "") end if assigns[:href] && !assigns[:disabled] do assigns = assign(assigns, :class, [assigns[:class], @base_class, @clickable_class]) ~H""" <.unstyled_link id={@id} class={@class} new_tab={@new_tab} href={@href} x-on:click="close()" data-ui-state={@state} {@rest} > {render_slot(@inner_block)} """ else assigns = assign(assigns, :class, [assigns[:class], @base_class]) ~H"""
{render_slot(@inner_block)}
""" end end def dropdown_divider(assigns) do ~H""" """ end attr(:href, :string, required: true) attr(:new_tab, :boolean, default: false) attr(:class, :string, default: nil) attr(:rest, :global) attr(:method, :string, default: "get") slot(:inner_block) def unstyled_link(assigns) do extra = if assigns.method == "get" do [] else [ "data-csrf": Phoenix.Controller.get_csrf_token(), "data-method": assigns.method, "data-to": assigns.href ] end assigns = assign(assigns, extra: extra) if assigns[:new_tab] do assigns = assign(assigns, :icon_class, icon_class(assigns)) ~H""" <.link class={[ "inline-flex items-center gap-x-0.5", @class ]} href={@href} target="_blank" rel="noopener noreferrer" {@extra} {@rest} > {render_slot(@inner_block)} """ else ~H""" <.link class={@class} href={@href} {@extra} {@rest}>{render_slot(@inner_block)} """ end end attr(:class, :any, default: "") attr(:rest, :global) def spinner(assigns) do ~H""" """ end attr :id, :string, required: true attr :js_active_var, :string, required: true attr :id_suffix, :string, default: "" attr :disabled, :boolean, default: false attr(:rest, :global) def toggle_switch(assigns) do ~H""" """ end def settings_tiles(assigns) do ~H"""
{render_slot(@inner_block)}
""" end attr :docs, :string, default: nil slot :inner_block, required: true slot :title, required: true slot :subtitle, required: false attr :feature_mod, :atom, default: nil attr :feature_toggle?, :boolean, default: false attr :current_role, :atom, default: nil attr :current_team, :any, default: nil attr :site, :any attr :conn, :any def tile(assigns) do ~H"""
<.title> {render_slot(@title)} <.docs_info :if={@docs} slug={@docs} class="absolute top-4 right-4 z-1" />
{render_slot(@subtitle)}
<%= if @feature_mod do %>
{render_slot(@inner_block)}
<% else %>
{render_slot(@inner_block)}
<% end %>
""" end attr(:sticky?, :boolean, default: true) attr(:enabled?, :boolean, default: true) attr(:centered?, :boolean, default: false) attr(:testid, :string, default: nil) slot(:inner_block, required: true) slot(:tooltip_content, required: true) def tooltip(assigns) do wrapper_data = if assigns[:sticky?], do: "{sticky: false, hovered: false}", else: "{hovered: false}" show_inner = if assigns[:sticky?], do: "hovered || sticky", else: "hovered" base_classes = [ "absolute", "pb-2", "top-0", "-translate-y-full", "z-[1000]", "sm:max-w-72", "w-max" ] tooltip_position_classes = if assigns.centered? do base_classes ++ ["left-1/2", "-translate-x-1/2"] else base_classes end assigns = assign(assigns, wrapper_data: wrapper_data, show_inner: show_inner, tooltip_position_classes: tooltip_position_classes ) if assigns.enabled? do ~H"""
{render_slot(@tooltip_content)}
{render_slot(@inner_block)}
""" else ~H"{render_slot(@inner_block)}" end end slot :inner_block, required: true def accordion_menu(assigns) do ~H"""
{render_slot(@inner_block)}
""" end attr :id, :string, required: true attr :title, :string, required: true attr :open_by_default, :boolean, default: false attr :title_class, :string, default: "" slot :inner_block, required: true def accordion_item(assigns) do ~H"""
{render_slot(@inner_block)}
""" end attr(:rest, :global, include: ~w(fill stroke stroke-width)) attr(:name, :atom, required: true) attr(:outline, :boolean, default: true) attr(:solid, :boolean, default: false) attr(:mini, :boolean, default: false) def dynamic_icon(assigns) do apply(Heroicons, assigns.name, [assigns]) end attr(:width, :integer, default: 100) attr(:height, :integer, default: 100) attr(:id, :string, default: "shuttle") defp icon_class(link_assigns) do classes = List.wrap(link_assigns[:class]) |> Enum.join(" ") if String.contains?(classes, "text-sm") or String.contains?(classes, "text-xs") do ["w-3 h-3"] else ["w-4 h-4"] end end slot(:item, required: true) def focus_list(assigns) do ~H"""
  1. {render_slot(item)}
""" end slot :title slot :subtitle slot :inner_block, required: true slot :footer attr :padding?, :boolean, default: true attr :rest, :global def focus_box(assigns) do ~H"""
<.title :if={@title != []}> {render_slot(@title)}
{render_slot(@subtitle)}
{render_slot(@inner_block)}
{render_slot(@inner_block)}
{render_slot(@footer)}
""" end attr :rest, :global attr :width, :string, default: "min-w-full" attr :rows, :list, default: [] attr :row_attrs, :any, default: nil slot :thead, required: false slot :tbody, required: true slot :inner_block, required: false def table(assigns) do ~H""" {render_slot(@thead)} {render_slot(@tbody, item)} {render_slot(@inner_block)}
""" end slot :inner_block, required: true attr :truncate, :boolean, default: false attr :max_width, :string, default: "" attr :height, :string, default: "" attr :class, :string, default: "" attr :actions, :boolean, default: nil attr :hide_on_mobile, :boolean, default: nil attr :rest, :global def td(assigns) do max_width = cond do assigns.max_width != "" -> assigns.max_width assigns.truncate -> "max-w-sm" true -> "" end assigns = assign(assigns, max_width: max_width) ~H"""
{render_slot(@inner_block)}
{render_slot(@inner_block)}
""" end slot :inner_block, required: true attr :invisible, :boolean, default: false attr :hide_on_mobile, :boolean, default: nil def th(assigns) do class = if assigns[:invisible] do "invisible" else "px-6 first:pl-0 last:pr-0 py-3 text-left text-sm font-semibold" end assigns = assign(assigns, class: class) ~H""" {render_slot(@inner_block)} """ end attr :set_to, :boolean, default: false attr :disabled?, :boolean, default: false slot :inner_block, required: true def toggle_submit(assigns) do ~H"""
{render_slot(@inner_block)}
""" end attr :href, :string, default: nil attr :icon, :atom, default: :pencil_square attr :rest, :global, include: ~w(method disabled) def edit_button(assigns) do if assigns[:href] do ~H""" <.unstyled_link href={@href} {@rest}> <.dynamic_icon name={@icon} class="size-5 text-indigo-700 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" /> """ else ~H""" """ end end attr :href, :string, default: nil attr :icon, :atom, default: :trash attr :rest, :global, include: ~w(method disabled) def delete_button(assigns) do if assigns[:href] do ~H""" <.unstyled_link href={@href} {@rest}> <.dynamic_icon name={@icon} class="size-5 text-red-700 hover:text-red-500 dark:text-red-500 dark:hover:text-red-400" /> """ else ~H""" """ end end attr :filter_text, :string, default: "" attr :placeholder, :string, default: "" attr :filtering_enabled?, :boolean, default: true slot :inner_block, required: false def filter_bar(assigns) do ~H"""
{render_slot(@inner_block)}
""" end slot :inner_block, required: true attr :class, :any, default: nil def h2(assigns) do ~H"""

{render_slot(@inner_block)}

""" end slot :inner_block, required: true attr :class, :any, default: nil def title(assigns) do ~H""" <.h2 class={["text-lg font-medium text-gray-900 dark:text-gray-100 leading-7", @class]}> {render_slot(@inner_block)} """ end alias Phoenix.LiveView.JS slot :inner_block, required: true def disclosure(assigns) do ~H"""
{render_slot(@inner_block)}
""" end slot :inner_block, required: true attr :class, :any, default: nil def disclosure_button(assigns) do ~H""" """ end slot :inner_block, required: true def disclosure_panel(assigns) do ~H""" """ end def settings_badge(%{type: :new} = assigns) do ~H""" NEW 🔥 """ end def settings_badge(assigns), do: ~H"" end