mirror of
https://codeberg.org/JasterV/intisync.ex.git
synced 2026-04-26 18:10:07 +00:00
282 lines
8.3 KiB
Elixir
282 lines
8.3 KiB
Elixir
defmodule IntisyncWeb.CoreComponents do
|
||
@moduledoc """
|
||
Provides core UI components.
|
||
|
||
At first glance, this module may seem daunting, but its goal is to provide
|
||
core building blocks for your application, such as modals, tables, and
|
||
forms. The components consist mostly of markup and are well-documented
|
||
with doc strings and declarative assigns. You may customize and style
|
||
them in any way you want, based on your application growth and needs.
|
||
|
||
The default components use Tailwind CSS, a utility-first CSS framework.
|
||
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
|
||
how to customize them or feel free to swap in another framework altogether.
|
||
|
||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||
"""
|
||
use Phoenix.Component
|
||
|
||
alias Phoenix.LiveView.JS
|
||
|
||
@doc """
|
||
Renders flash notices.
|
||
|
||
## Examples
|
||
|
||
<.flash kind={:info} flash={@flash} />
|
||
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
|
||
"""
|
||
attr :id, :string, doc: "the optional id of flash container"
|
||
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
|
||
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 container"
|
||
|
||
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"""
|
||
<div
|
||
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
||
id={@id}
|
||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> 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}
|
||
>
|
||
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
||
<.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}
|
||
</p>
|
||
<p class="mt-2 text-sm leading-5">{msg}</p>
|
||
<button type="button" class="group absolute top-1 right-1 p-2" aria-label="close">
|
||
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
||
</button>
|
||
</div>
|
||
"""
|
||
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"
|
||
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
||
|
||
def flash_group(assigns) do
|
||
~H"""
|
||
<div id={@id}>
|
||
<.flash kind={:info} title="Success!" flash={@flash} />
|
||
<.flash kind={:error} title="Error!" flash={@flash} />
|
||
<.flash
|
||
id="client-error"
|
||
kind={:error}
|
||
title="We can't find the internet"
|
||
phx-disconnected={show(".phx-client-error #client-error")}
|
||
phx-connected={hide("#client-error")}
|
||
hidden
|
||
>
|
||
Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||
</.flash>
|
||
|
||
<.flash
|
||
id="server-error"
|
||
kind={:error}
|
||
title="Something went wrong!"
|
||
phx-disconnected={show(".phx-server-error #server-error")}
|
||
phx-connected={hide("#server-error")}
|
||
hidden
|
||
>
|
||
Hang in there while we get back on track
|
||
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||
</.flash>
|
||
</div>
|
||
"""
|
||
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 :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}>
|
||
<div class="mt-10 space-y-8 bg-white">
|
||
{render_slot(@inner_block, f)}
|
||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||
{render_slot(action, f)}
|
||
</div>
|
||
</div>
|
||
</.form>
|
||
"""
|
||
end
|
||
|
||
@doc """
|
||
Renders a button.
|
||
|
||
## Examples
|
||
|
||
<.button>Send!</.button>
|
||
<.button phx-click="go" class="ml-2">Send!</.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"""
|
||
<button
|
||
type={@type}
|
||
class={[
|
||
"phx-submit-loading:opacity-75 rounded-lg px-8 py-3",
|
||
"bg-indigo-500 hover:bg-indigo-400 shadow-lg shadow-indigo-500/50",
|
||
"font-semibold leading-6 text-white active:text-white/80",
|
||
@class
|
||
]}
|
||
{@rest}
|
||
>
|
||
{render_slot(@inner_block)}
|
||
</button>
|
||
"""
|
||
end
|
||
|
||
@doc """
|
||
Renders a badge
|
||
"""
|
||
attr :id, :string, default: nil
|
||
attr :class, :string, default: nil
|
||
slot :inner_block, required: true
|
||
|
||
def badge(assigns) do
|
||
~H"""
|
||
<span
|
||
id={@id}
|
||
class={[
|
||
"inline-flex size-fit items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||
@class
|
||
]}
|
||
>
|
||
{render_slot(@inner_block)}
|
||
</span>
|
||
"""
|
||
end
|
||
|
||
@doc """
|
||
Generates a generic error message.
|
||
"""
|
||
slot :inner_block, required: true
|
||
|
||
def error(assigns) do
|
||
~H"""
|
||
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600 phx-no-feedback:hidden">
|
||
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
||
{render_slot(@inner_block)}
|
||
</p>
|
||
"""
|
||
end
|
||
|
||
@doc """
|
||
Renders a back navigation link.
|
||
|
||
## Examples
|
||
|
||
<.back navigate={~p"/posts"}>Back to posts</.back>
|
||
"""
|
||
attr :navigate, :any, required: true
|
||
slot :inner_block, required: true
|
||
|
||
def back(assigns) do
|
||
~H"""
|
||
<div class="mt-16">
|
||
<.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)}
|
||
</.link>
|
||
</div>
|
||
"""
|
||
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"""
|
||
<span class={[@name, @class]} />
|
||
"""
|
||
end
|
||
|
||
## JS Commands
|
||
|
||
def show(js \\ %JS{}, selector) do
|
||
JS.show(js,
|
||
to: selector,
|
||
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
|
||
end
|