defmodule Backpex.HTML.Layout do @moduledoc """ Contains all Backpex layout components. """ use BackpexWeb, :html alias Backpex.Router @doc """ Renders an app shell representing the base of your layout. """ @doc type: :component attr :fluid, :boolean, default: false, doc: "toggles fluid layout" slot :inner_block slot :topbar, doc: "content to be displayed in the topbar" do attr :class, :string, doc: "additional class that will be added to the component" end slot :sidebar, doc: "content to be displayed in the sidebar" do attr :class, :string, doc: "additional class that will be added to the component" end slot :footer, doc: "content to be displayed in the footer" def app_shell(assigns) do ~H"""
<%= for sidebar <- @sidebar do %> <% end %>
0, do: "md:pl-64", else: ""} flex flex-1 flex-col"}>
<.topbar class={for topbar <- @topbar, do: topbar[:class] || ""}> <%= render_slot(@topbar) %> <%= for _ <- @sidebar do %> <% end %>
<%= render_slot(@inner_block) %>
<%= render_slot(@footer) %> <.footer :if={@footer == []} />
""" end @doc """ Renders a topbar. """ @doc type: :component attr :class, :string, default: "", doc: "additional class to be added to the component" slot :inner_block def topbar(assigns) do ~H"""
<%= render_slot(@inner_block) %>
""" end @doc """ Renders flash messages. """ @doc type: :component attr :flash, :map, required: true, doc: "flash map that will be passed to `Phoenix.Flash.get/2`" def flash_messages(assigns) do ~H"""
<%= Phoenix.Flash.get(@flash, :info) %>
<%= Phoenix.Flash.get(@flash, :error) %>
""" end @doc """ Renders a footer. It provides a default look when no content is provided. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" slot :inner_block def footer(assigns) do ~H""" """ end defp version, do: Application.spec(:backpex, :vsn) |> to_string() @doc """ Renders the topbar branding. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" attr :title, :string, default: "Backpex", doc: "title that will be displayed next to the logo" attr :hide_title, :boolean, default: false, doc: "if the title should be hidden" slot :logo, doc: "the logo of the branding" def topbar_branding(assigns) do ~H"""
<%= if @logo === [] do %> <.backpex_logo class="w-8" /> <% else %> <%= render_slot(@logo) %> <% end %> <%= unless @hide_title do %>

<%= @title %>

<% end %>
""" end @doc """ Renders a theme selector. """ @doc type: :component attr :socket, :any, required: true attr :themes, :list, doc: "A list of tuples with {theme_label, theme_name} format", examples: [[{"Light", "light"}, {"Dark", "dark"}]] def theme_selector(assigns) do ~H""" """ end @doc """ Get the Backpex logo SVG. """ @doc type: :component attr :class, :string, required: false, default: nil, doc: "class that will be added to the SVG element" def backpex_logo(assigns) do ~H""" """ end @doc """ Renders a topbar dropdown. """ @doc type: :component attr :class, :string, required: false, default: "", doc: "additional class that will be added to the component" slot :label, required: true, doc: "label of the dropdown" def topbar_dropdown(assigns) do ~H""" """ end @doc """ Container to wrap main elements and add margin. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" slot :inner_block def main_container(assigns) do ~H"""
<%= render_slot(@inner_block) %>
""" end @doc """ Renders a title. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" slot :inner_block def main_title(assigns) do ~H"""

<%= render_slot(@inner_block) %>

""" end @doc """ Renders a sidebar section. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" attr :id, :string, default: "section", doc: "The id for this section. It will be used to save and load the opening state of this section from local storage." slot :inner_block slot :label, required: true, doc: "label to be displayed on the section." def sidebar_section(assigns) do ~H"""
localStorage.setItem('section-opened-#{@id}', val))"} >
<%= render_slot(@label) %>
<%= render_slot(@inner_block) %>
""" end @doc """ Renders a sidebar item. It uses `Phoenix.Component.link/1` component, so you can can use link and href navigation. """ @doc type: :component attr :class, :string, default: "", doc: "additional class that will be added to the component" attr :current_url, :string, doc: "the current url" attr :navigate, :string attr :patch, :string attr :href, :any slot :inner_block def sidebar_item(assigns) do path = case assigns do %{navigate: to} -> to %{patch: to} -> to %{href: href} -> href end highlight = if Router.active?(assigns.current_url, path) do "bg-base-300 text-base-content" else "text-base-content/95 hover:bg-base-100" end base_class = "group flex items-center gap-2 rounded-btn px-2 py-2 space-x-2 hover:cursor-pointer" extra = assigns_to_attributes(assigns) assigns = assigns |> assign(:class, [base_class, highlight, assigns.class]) |> assign(:extra, extra) ~H""" <.link class={@class} {@extra}> <%= render_slot(@inner_block) %> """ end @doc """ Renders the form label and input with corresponding margin and alignment. """ @doc type: :component attr :class, :string, default: "", doc: "extra classes to be added" slot :label, required: true do attr :align, :atom, values: [:top, :center, :bottom] end slot :inner_block def field_container(assigns) do ~H"""
<%= render_slot(@label) %>
<%= render_slot(@inner_block) %>
""" end @doc """ Renders a modal. """ @doc type: :component attr :title, :string, default: nil, doc: "modal title" attr :target, :string, default: nil, doc: "live component for the close event to go to" attr :close_event_name, :string, default: "close-modal", doc: "close event name" attr :max_width, :string, default: "md", values: ["sm", "md", "lg", "xl", "2xl", "full"], doc: "modal max width" attr :open, :boolean, default: true, doc: "modal open" attr :rest, :global slot :inner_block, required: false def modal(assigns) do assigns = assigns |> assign(:classes, get_modal_classes(assigns)) ~H""" """ end def hide_modal(target \\ nil, close_event) def hide_modal(_target, nil) do %JS{} |> JS.hide(to: "#modal-overlay") |> JS.hide(to: "#modal-content") end def hide_modal(target, close_event) do case target do nil -> JS.push(%JS{}, close_event) target -> JS.push(%JS{}, close_event, target: target) end end defp get_modal_classes(assigns) do base_classes = "animate-fade-in-scale w-full max-h-full overflow-auto bg-base-100 rounded-box shadow-lg" max_width_class = case Map.get(assigns, :max_width, "md") do "sm" -> "max-w-sm" "md" -> "max-w-xl" "lg" -> "max-w-3xl" "xl" -> "max-w-5xl" "2xl" -> "max-w-7xl" "full" -> "max-w-full" end [base_classes, max_width_class] end @doc """ Renders a text to be used as a label for an input. """ @doc type: :component attr :text, :string, doc: "text of the label" def input_label(assigns) do ~H"""

<%= @text %>

""" end @doc """ Filters fields by certain panel. ## Examples iex> Backpex.HTML.Layout.visible_fields_by_panel([field1: %{panel: :default}, field2: %{panel: :panel}], :default, nil) [field1: %{panel: :default}] iex> Backpex.HTML.Layout.visible_fields_by_panel([field1: %{panel: :default, visible: fn _assigns -> false end}, field2: %{panel: :panel}], :default, nil) [] iex> Backpex.HTML.Layout.visible_fields_by_panel([field1: %{panel: :default}], :panel, nil) [] """ def visible_fields_by_panel(fields, panel, assigns) do fields |> Keyword.filter(fn {_name, field_options} -> get_panel(field_options) == panel and visible?(field_options, assigns) end) end defp get_panel(%{panel: panel} = _field), do: panel defp get_panel(_field), do: :default defp visible?(%{visible: visible} = _field_options, assigns), do: visible.(assigns) defp visible?(_field_options, _assigns), do: true defp get_align_class(align) do case align do :top -> "sm:self-start" :center -> "sm:self-center" :bottom -> "sm:self-end" _align -> "sm:self-center" end end end