defmodule LiveDebuggerWeb.Components do
@moduledoc """
This module provides reusable components for LiveDebugger.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
alias LiveDebuggerWeb.Helpers.RoutesHelper
@report_issue_url "https://github.com/software-mansion/live-debugger/issues/new/choose"
@doc """
Renders an alert
Right now we have styles only for `danger` variant, but it'll change soon
"""
attr(:variant, :string, required: true, values: ["danger"])
attr(:class, :any, default: nil, doc: "Additional classes to add to the alert.")
attr(:with_icon, :boolean, default: false, doc: "Whether to show an icon.")
attr(:heading, :string, default: nil, doc: "Heading for the alert.")
attr(:rest, :global)
slot(:inner_block, required: true)
def alert(assigns) do
~H"""
"""
end
@doc """
Renders a button.
"""
attr(:variant, :string, default: "primary", values: ["primary", "secondary"])
attr(:size, :string, default: "md", values: ["md", "sm"])
attr(:class, :any, default: nil, doc: "Additional classes to add to the button.")
attr(:rest, :global)
slot(:inner_block, required: true)
def button(assigns) do
~H"""
"""
end
@doc """
Collapsible element that can be toggled open and closed.
It uses the `details` and `summary` HTML elements.
`hide-on-open` and `show-on-open` css classes are used to hide or show elements based on the open state of the collapsible.
## Examples
<.collapsible id="collapsible" open={true}>
<:label>
Collapsible
Info when closed
Content
"""
attr(:id, :string, required: true)
attr(:class, :any, default: nil, doc: "CSS class for parent container")
attr(:label_class, :any, default: nil, doc: "CSS class for the label")
attr(:chevron_class, :any, default: nil, doc: "CSS class for the chevron icon")
attr(:open, :boolean, default: false, doc: "Whether the collapsible is open by default")
attr(:icon, :string,
default: "icon-chevron-right",
doc: "Icon for chevron. It will be rotated 90 degrees when the collapsible is open"
)
attr(:rest, :global)
slot(:label, required: true)
slot(:inner_block, required: true)
def collapsible(assigns) do
~H"""
summary>.rotate-icon]:open:rotate-90 [&>summary_.hide-on-open]:open:hidden [&>summary_.show-on-open]:open:flex"
| List.wrap(@class)
]}
{show_collapsible_assign(@open)}
>
"-summary"}
class={["flex items-center cursor-pointer" | List.wrap(@label_class)]}
{@rest}
>
<.icon name={@icon} class={["rotate-icon shrink-0" | List.wrap(@chevron_class)]} />
<%= render_slot(@label) %>
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders flash notices.
## Examples
<.flash flash={@flash} />
<.flash phx-mounted={show("#flash")}>Welcome Back!
"""
attr(:id, :string, doc: "the optional id of flash container")
attr(:flash, :map, default: %{}, doc: "the map of flash messages to display")
attr(:rest, :global, doc: "the arbitrary HTML attributes to add to the flash container")
def flash(assigns) do
message = Phoenix.Flash.get(assigns.flash, :error)
assigns =
assigns
|> assign_new(:id, fn -> "flash" end)
|> assign(:message, message)
~H"""
"""
end
@doc """
Used to add CollapsibleOpen hook to element based on condition.
"""
def show_collapsible_assign(true), do: %{:"phx-hook" => "CollapsibleOpen"}
def show_collapsible_assign(_), do: %{}
@doc """
Typography component to render headings.
"""
attr(:class, :any, default: nil, doc: "Additional classes to add to the heading.")
attr(:rest, :global)
slot(:inner_block, required: true)
def h1(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders an icon.
Not all icons are available. If you want to use an icon check if it exists in the `assets/icons` folder.
`name` must start with `icon-`
## Examples
<.icon name="icon-play" />
"""
attr(:name, :string, required: true, doc: "The name of the icon. Must start with `icon-`.")
attr(:class, :any, default: nil, doc: "Additional classes to add to the icon.")
attr(:rest, :global)
def icon(%{name: "icon-" <> _} = assigns) do
~H"""
"""
end
@doc """
Renders a button with an icon in it.
"""
attr(:icon, :string, required: true, doc: "Icon to be displayed as a button.")
attr(:size, :string,
default: "md",
values: ["md", "sm"],
doc: "Size of the button."
)
attr(:variant, :string,
default: "primary",
values: ["primary", "secondary"],
doc: "Variant of the button."
)
attr(:class, :any, default: nil, doc: "Additional classes to add to the button.")
attr(:rest, :global, include: ~w(id))
def icon_button(assigns) do
{button_class, icon_class} =
case assigns.size do
"md" -> {"w-8! h-8! px-[0.25rem] py-[0.25rem]", "h-6 w-6"}
"sm" -> {"w-7! h-7! px-[0.375rem] py-[0.375rem]", "h-4 w-4"}
end
assigns =
assigns
|> assign(:button_class, button_class)
|> assign(:icon_class, icon_class)
|> assign(:aria_label, assigns[:"aria-label"] || icon_label(assigns.icon))
~H"""
<.button
aria-label={@aria_label}
class={[@button_class | List.wrap(@class)]}
variant={@variant}
{@rest}
>
<.icon name={@icon} class={@icon_class} />
"""
end
attr(:icon, :string, required: true, doc: "Icon to be displayed.")
attr(:class, :any, default: nil, doc: "Additional classes to add to the nav icon.")
attr(:rest, :global, include: ~w(id))
def nav_icon(assigns) do
~H"""
"""
end
attr(:elements, :list,
required: true,
doc: "Elements that will be displayed in the list's `item` slot."
)
attr(:class, :any, default: nil, doc: "Additional classes.")
attr(:item_class, :any, default: nil, doc: "Additional classes for each item.")
slot(:item, required: true)
def list(assigns) do
~H"""
<%= render_slot(@item, elem) %>
"""
end
@doc """
Renders a fullscreen using Fullscreen hook.
It can be opened and via browser "open" event (by default) with JS.dispatch or via server event (check example in fullscreen button).
You can use `fullscreen_button` to open this fullscreen.
You can close the fullscreen using X button or by pressing ESC key.
"""
attr(:id, :string, required: true)
attr(:title, :string, default: "", doc: "Title of the fullscreen.")
attr(:class, :any,
default: nil,
doc: "Additional classes to be added to the fullscreen element."
)
slot(:inner_block, required: true)
def fullscreen(assigns) do
~H"""
"""
end
@doc """
Renders a button which will show a fullscreen when clicked.
You can override `phx-click` value, but remember to push correct event at the end of `handle_event` function.
## Examples
<.fullscreen_button
id="my-fullscreen"
phx-click="open-fullscreen"
icon="icon-expand"
/>
@impl true
def handle_event("open-fullscreen", _, socket) do
trace_id = String.to_integer(string_id)
socket
|> push_event("my-fullscreen-open", %{})
|> noreply()
end
"""
attr(:id, :string, required: true, doc: "Same as `id` of the fullscreen.")
attr(:class, :any, default: nil, doc: "Additional classes to be added to the button.")
attr(:icon, :string,
default: "icon-expand",
doc: "Icon to be displayed as a button"
)
attr(:rest, :global)
def fullscreen_button(assigns) do
~H"""
<.icon_button
id={"#{@id}-button"}
phx-click={@rest[:"phx-click"] || JS.dispatch("open", to: "##{@id}")}
icon={@icon}
size="sm"
data-fullscreen-id={@id}
class={@class}
variant="secondary"
{@rest}
/>
"""
end
attr(:class, :any, default: nil, doc: "CSS class")
attr(:size, :string,
default: "md",
values: ["xs", "sm", "md", "lg", "xl"],
doc: "Size of the spinner"
)
attr(:show, :boolean, default: true, doc: "show or hide spinner")
attr(:rest, :global)
def spinner(assigns) do
size_class =
case assigns.size do
"xs" -> "h-4 w-4"
"sm" -> "h-6 w-6"
"md" -> "h-8 w-8"
"lg" -> "h-10 w-10"
"xl" -> "h-12 w-12"
end
assigns = assign(assigns, :size_class, size_class)
~H"""
"""
end
attr(:text, :string, required: true)
attr(:icon, :string, required: true)
def badge(assigns) do
~H"""
"""
end
@doc """
Renders navbar with possible link to return to the main page.
"""
attr(:return_link?, :boolean,
required: true,
doc: "Whether to show a link to return to the main page."
)
slot(:inner_block)
def navbar(assigns) do
~H"""
<.link :if={@return_link?} patch={RoutesHelper.live_views_dashboard()}>
<.nav_icon icon="icon-arrow-left" />
<.icon name="icon-logo-text" class="h-6 w-32" />
<%= if LiveDebugger.Feature.enabled?(:dark_mode) do %>
<.nav_icon
id="light-mode-switch"
class="dark:hidden"
icon="icon-moon"
phx-hook="ToggleTheme"
/>
<.nav_icon
id="dark-mode-switch"
class="hidden dark:block"
icon="icon-sun"
phx-hook="ToggleTheme"
/>
<% end %>
<%= @inner_block && render_slot(@inner_block) %>
"""
end
attr(:class, :any, default: nil)
attr(:text, :string, default: "See any issues?")
def report_issue(assigns) do
assigns = assign(assigns, :report_issue_url, @report_issue_url)
~H"""
<%= @text %>
<.link
href={@report_issue_url}
target="_blank"
class="text-link-primary hover:text-link-primary-hover"
>
Report it here
"""
end
@doc """
Renders a switch component.
Based on [Tailwind CSS Toggle - Flowbite](https://flowbite.com/docs/forms/toggle)
"""
attr(:checked, :boolean, default: false, doc: "Whether the switch is checked.")
attr(:label, :string, default: "", doc: "Label for the switch.")
attr(:rest, :global)
def toggle_switch(assigns) do
~H"""
"""
end
attr(:variant, :string, required: true, values: ["danger"])
defp alert_icon(assigns) do
{icon_name, icon_class} =
case assigns.variant do
"danger" -> {"icon-x-circle", "text-error-icon"}
end
assigns = assign(assigns, name: icon_name, class: icon_class)
~H"""
<.icon name={@name} class={@class} />
"""
end
defp button_color_classes(variant) do
case variant do
"primary" ->
"bg-button-primary-bg text-button-primary-content hover:bg-button-primary-bg-hover hover:text-button-primary-content-hover"
"secondary" ->
"bg-button-secondary-bg text-button-secondary-content border-button-secondary-border border hover:bg-button-secondary-bg-hover hover:text-button-secondary-content-hover hover:border-button-secondary-border-hover"
end
end
defp button_size_classes("md"), do: "py-2 px-3"
defp button_size_classes("sm"), do: "py-1.5 px-2"
defp icon_label("icon-" <> icon_name) do
icon_name
|> String.capitalize()
|> String.replace("-", " ")
end
end