chrisgreg / bloom

The opinionated extension to Phoenix core_components
https://bloom-ui.fly.dev
MIT License
347 stars 18 forks source link

Proposal: layout component #19

Open lessless opened 6 months ago

lessless commented 6 months ago

Hi,

The layout component should implement at least the stack and the sidebar layouts, ref: https://every-layout.dev The cover would be an excellent addition.

Petal's approach to structure is a decent start https://docs.petal.build/petal-pro-documentation/fundamentals/layouts-and-menus

Here is what I did to port the sidebar layout from the https://github.com/themesberg/flowbite-astro-admin-dashboard/tree/main (it also implements the stack) into one of my experiments.

  1. The root layout didn't change, but I removed all classes from the main tag
  <.flash_group flash={@flash} />
  <%= @inner_content %>
</main>
  1. Added components/astro.ex
defmodule SesameWeb.AstroComponents do
  use Phoenix.Component
  import SesameWeb.AstroComponents.NavBarSidebar
  import SesameWeb.AstroComponents.Sidebar

  attr :main_menu_items, :list
  attr :user_menu_items, :list
  attr :user, :map
  attr :current_page, :atom, required: true
  slot(:inner_block)

  def layout_sidebar(assigns) do
    assigns =
      assigns
      |> assign_new(:main_menu_items, fn -> SesameWeb.Menus.main_menu_items(assigns[:user]) end)
      |> assign_new(:user_menu_items, fn -> SesameWeb.Menus.user_menu_items(assigns[:user]) end)

    ~H"""
    <.navbar_sidebar user={@user} user_menu_items={@user_menu_items} />
    <.sidebar main_menu_items={@main_menu_items} current_page={@current_page} />

    <div class="flex pt-16 overflow-hidden  dark:bg-gray-900">
      <div
        id="main-content"
        class="relative w-full h-full overflow-y-auto  lg:ml-64 dark:bg-gray-900 min-h-[calc(100vh-64px)]"
      >
        <%= render_slot(@inner_block) %>
      </div>
    </div>
    """
  end
end

I don't remember much now, but astro/sidebar.ex and astro/navbar_sidebar.ex basically loop over the menu items and render them accordingly:

defmodule SesameWeb.AstroComponents.Sidebar do
  use Phoenix.Component
  use SesameWeb, :verified_routes
  import SesameWeb.CoreComponents, only: [icon: 1]

  attr :main_menu_items, :list, required: true
  attr :current_page, :atom, required: true

  def sidebar(assigns) do
    ~H"""
    <aside
      id="sidebar"
      class="fixed top-0 left-0 z-20 flex flex-col flex-shrink-0 hidden w-64 h-full pt-16 font-normal duration-75 lg:flex transition-width"
      aria-label="Sidebar"
      phx-hook="SideBar"
    >
      <div class="relative flex flex-col flex-1 min-h-0 pt-0 bg-white border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700">
        <div class="flex flex-col flex-1 pt-5 pb-28 overflow-y-auto scrollbar scrollbar-w-2 scrollbar-thumb-rounded-[0.1667rem] scrollbar-thumb-slate-200 scrollbar-track-gray-400 dark:scrollbar-thumb-slate-900 dark:scrollbar-track-gray-800">
          <div class="flex-1 px-3 space-y-1 bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
            <ul class="pb-2 space-y-2">
              <%= for menu_item <- @main_menu_items do %>
                <li>
                  <.link class={main_menu_item_class(@current_page, menu_item.name)} navigate={menu_item.path}>
                    <%= if is_binary(menu_item.icon) do %>
                      <.icon
                        name={"hero-#{menu_item.icon}"}
                        class="w-6 h-6 text-gray-500 transition duration-75 group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white"
                      />
                    <% end %>

                    <span class="ml-3" sidebar-toggle-item><%= menu_item.label %></span>
                  </.link>
                </li>
              <% end %>
            </ul>
          </div>
        </div>
      </div>
    </aside>

    <div class="fixed inset-0 z-10 hidden bg-gray-900/50 dark:bg-gray-900/90" id="sidebarBackdrop"></div>
    """
  end

  def main_menu_item_class(current_page, current_page) do
    main_menu_item_class_base_class() <> " bg-gray-100 dark:bg-gray-700"
  end

  def main_menu_item_class(_current_page, _page) do
    main_menu_item_class_base_class()
  end

  def main_menu_item_class_base_class() do
    "flex items-center p-2 text-base text-gray-900 rounded-lg hover:bg-gray-100 group dark:text-gray-200 dark:hover:bg-gray-700"
  end
end

defmodule SesameWeb.AstroComponents.NavBarSidebar do
  use Phoenix.Component
  import SesameWeb.CoreComponents, only: [icon: 1]
  import PetalComponents.Dropdown, only: [dropdown_menu_item: 1]

  attr :user, :map, required: true
  attr :user_menu_items, :list, required: true

  def navbar_sidebar(assigns) do
    ~H"""
    <nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
      <div class="px-3 py-3 lg:px-5 lg:pl-3">
        <div class="flex items-center justify-between">
          <div class="flex items-center justify-start">
            <button
              id="toggleSidebarMobile"
              aria-expanded="true"
              aria-controls="sidebar"
              class="p-2 text-gray-600 rounded cursor-pointer lg:hidden hover:text-gray-900 hover:bg-gray-100 focus:bg-gray-100 dark:focus:bg-gray-700 focus:ring-2 focus:ring-gray-100 dark:focus:ring-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
            >
              <.icon id="toggleSidebarMobileHamburger" class="w-6 h-6" name="hero-bars-3" />
              <.icon id="toggleSidebarMobileClose" class="hidden w-6 h-6" name="hero-x-mark" />
            </button>
            <a href="/" class="flex ml-2 md:mr-24">
              <%!-- <img src="images/logo.svg" class="h-8 mr-3" alt="FlowBite Logo" /> --%>
              <span class="self-center text-xl  font-black sm:text-2xl whitespace-nowrap dark:text-white">
                🏔️Sesame
              </span>
            </a>

            <%!-- <SearchInput /> --%>
          </div>

          <div class="flex items-center">
            <.notifications />
            <.apps />

            <%!-- <ColorModeSwitcher /> --%>
            <!-- Profile -->
            <.user_menu user={@user} user_menu_items={@user_menu_items} />
          </div>
        </div>
      </div>
    </nav>
    """
  end

  defp user_menu(assigns) do
    ~H"""
    <div class="flex items-center ml-3">
      <div>
        <button
          type="button"
          class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
          id="user-menu-button-2"
          aria-expanded="false"
          data-dropdown-toggle="dropdown-2"
        >
          <span class="sr-only">Open user menu</span>
          <%!-- <img
            class="w-8 h-8 rounded-full"
            src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
            alt="user photo"
          /> --%>

          <.icon class="w-8 h-8 bg-stone-50" name="hero-user-circle-solid" />
        </button>
      </div>
      <!-- Dropdown menu -->
      <div
        class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
        id="dropdown-2"
      >
        <div class="px-4 py-3" role="none">
          <p class="text-sm text-gray-900 dark:text-white" role="none">
            <%= @user.name %>
          </p>
          <p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
            <%= @user.email %>
          </p>
        </div>
        <ul class="py-1" role="none">

          <%= for menu_item <-  @user_menu_items do %>
            <li>
              <.dropdown_menu_item
                link_type={if menu_item[:method], do: "a", else: "live_redirect"}
                method={if menu_item[:method], do: menu_item[:method], else: nil}
                to={menu_item.path}
                class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
              >
                <%= if is_binary(menu_item.icon) do %>
                  <.icon name={"hero-#{menu_item.icon}"} class="w-5 h-5 text-gray-500 dark:text-gray-400" />
                <% end %>

                <%= menu_item.label %>
              </.dropdown_menu_item>
            </li>
          <% end %>
        </ul>
      </div>
    </div>
    """
  end
end

Then, I was able to choose which layout I wanted to use on a LiveView basis:

<.layout_sidebar user={@current_user} current_page={:participants}>
   Content
</.layout_sidebar>

The SesameWeb.Menus contains definitions of menus as described in Petal docs https://docs.petal.build/petal-pro-documentation/fundamentals/layouts-and-menus#menus


defmodule SesameWeb.Menus do

  use SesameWeb, :verified_routes

  # Public menu 
  def public_menu_items(_user \\ nil),
    do: [
      %{label: "Features", path: "/#features"},
    ]

  # Signed out main menu
  def main_menu_items(nil) do 
    []
  end

  # Signed in main menu
  def main_menu_items(current_user) do
     build_menu([:my_notifications, :participants], current_user)
  end
end

Hope this helps!

chrisgreg commented 1 month ago

@lessless this is excellent, would you like to raise a PR to implement this in Bloom?