bloom
bloom copied to clipboard
Proposal: layout component
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.
- The root layout didn't change, but I removed all classes from the
maintag
<.flash_group flash={@flash} />
<%= @inner_content %>
</main>
- 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!
@lessless this is excellent, would you like to raise a PR to implement this in Bloom?