08 - Layouts
Overview
Wavic WebApp features a multi-layout system with 6 different layout options. This allows for A/B testing, user preferences, and incremental UI updates.
Layout Options
| Layout | Key | Description |
|---|---|---|
| Beryllium | beryllium | Default - Clean header, minimal chrome |
| Hydrogen | hydrogen | Full sidebar layout |
| Helium | helium | Compact navigation |
| Lithium | lithium | Wide content area |
| Boron | boron | Minimal chrome, invitation modal support |
| Carbon | carbon | Alternative style |
Layout Architecture
src/layouts/
├── beryllium/ # Default layout
│ ├── beryllium-layout.tsx # Main layout component
│ ├── beryllium-header.tsx # Header component
│ └── beryllium-utils.ts # Sidebar state utilities
│
├── hydrogen/
│ └── layout.tsx
│
├── helium/
│ └── helium-layout.tsx
│
├── lithium/
│ └── lithium-layout.tsx
│
├── boron/
│ └── boron-layout.tsx
│
├── carbon/
│ └── carbon-layout.tsx
│
├── nav-menu/ # Shared navigation
├── profile-menu.tsx # Profile dropdown
├── notification-dropdown.tsx # Notifications
├── messages-dropdown.tsx # Messages
├── sticky-header.tsx # Sticky header wrapper
└── header-menu-right.tsx # Right header itemsLayout Switching
Layout Enum
typescript
// src/config/enums.ts
export enum LAYOUT_OPTIONS {
HYDROGEN = 'hydrogen',
HELIUM = 'helium',
LITHIUM = 'lithium',
BERYLLIUM = 'beryllium', // Default
BORON = 'boron',
CARBON = 'carbon',
}Layout Hook
typescript
// src/hooks/use-layout.ts
import { useLocalStorage } from '@/hooks/use-local-storage';
import { LAYOUT_OPTIONS } from '@/config/enums';
export function useLayout() {
const [layout, setLayout] = useLocalStorage(
'layout',
LAYOUT_OPTIONS.BERYLLIUM
);
return { layout, setLayout };
}Dynamic Layout Provider
typescript
// src/app/(hydrogen)/layout.tsx
'use client';
import { LAYOUT_OPTIONS } from '@/config/enums';
import { useLayout } from '@/hooks/use-layout';
import BerylLiumLayout from '@/layouts/beryllium/beryllium-layout';
import HeliumLayout from '@/layouts/helium/helium-layout';
import HydrogenLayout from '@/layouts/hydrogen/layout';
import BoronLayout from '@/layouts/boron/boron-layout';
import CarbonLayout from '@/layouts/carbon/carbon-layout';
import LithiumLayout from '@/layouts/lithium/lithium-layout';
export default function DefaultLayout({ children }) {
return <LayoutProvider>{children}</LayoutProvider>;
}
function LayoutProvider({ children }) {
const { layout } = useLayout();
switch (layout) {
case LAYOUT_OPTIONS.HELIUM:
return <HeliumLayout>{children}</HeliumLayout>;
case LAYOUT_OPTIONS.LITHIUM:
return <LithiumLayout>{children}</LithiumLayout>;
case LAYOUT_OPTIONS.BERYLLIUM:
return <BerylLiumLayout>{children}</BerylLiumLayout>;
case LAYOUT_OPTIONS.HYDROGEN:
return <HydrogenLayout>{children}</HydrogenLayout>;
case LAYOUT_OPTIONS.CARBON:
return <CarbonLayout>{children}</CarbonLayout>;
default:
return <BoronLayout>{children}</BoronLayout>;
}
}Beryllium Layout (Default)
The primary layout used in production:
typescript
// src/layouts/beryllium/beryllium-layout.tsx
'use client';
import Header from '@/layouts/beryllium/beryllium-header';
import { useBerylliumSidebars } from '@/layouts/beryllium/beryllium-utils';
import cn from '@/utils/class-names';
import { usePathname } from 'next/navigation';
export default function BerylliumLayout({ children }) {
const pathname = usePathname();
const { expandedLeft } = useBerylliumSidebars();
// Pages without header
const hideHeaderOn = ['/onboarding', '/new-artist'];
const shouldShowHeader = !hideHeaderOn.includes(pathname);
return (
<main className={cn('flex min-h-screen flex-grow')}>
<div className="flex w-full flex-col">
{shouldShowHeader && <Header className="xl:ms-[88px]" />}
<div className="flex flex-grow flex-col gap-4 duration-200">
<div className={cn(shouldShowHeader ? 'grow md:px-3 xl:mt-4' : 'grow')}>
{children}
</div>
</div>
</div>
</main>
);
}Boron Layout
Used as fallback with invitation modal support:
typescript
// src/layouts/boron/boron-layout.tsx
export default function BoronLayout({ children }) {
return (
<>
{children}
<InvitationModal
isInviteModalOpen={isInviteModalOpen}
setIsInviteModalOpen={setIsInviteModalOpen}
linkId={resourceId}
/>
</>
);
}Header Components
Main Header
typescript
// src/layouts/beryllium/beryllium-header.tsx
export default function Header({ className }) {
return (
<StickyHeader className={className}>
<div className="flex items-center gap-4">
<Logo />
<Navigation />
</div>
<HeaderMenuRight />
</StickyHeader>
);
}Header Right Menu
typescript
// src/layouts/header-menu-right.tsx
import ProfileMenu from '@/layouts/profile-menu';
import NotificationDropdown from '@/layouts/notification-dropdown';
import MessagesDropdown from '@/layouts/messages-dropdown';
export default function HeaderMenuRight() {
return (
<div className="flex items-center gap-3">
<NotificationDropdown />
<MessagesDropdown />
<ProfileMenu />
</div>
);
}Profile Menu
typescript
// src/layouts/profile-menu.tsx
import { useSession, signOut } from 'next-auth/react';
import { Avatar, Dropdown } from 'rizzui';
export default function ProfileMenu() {
const { data: session } = useSession();
return (
<Dropdown>
<Dropdown.Trigger>
<Avatar src={session?.user?.image} name={session?.user?.fullName} />
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item href="/profile">Profile</Dropdown.Item>
<Dropdown.Item href="/user/profile-settings">Settings</Dropdown.Item>
<Dropdown.Item onClick={() => signOut()}>Sign Out</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}Sticky Header
typescript
// src/layouts/sticky-header.tsx
export default function StickyHeader({ children, className }) {
return (
<header
className={cn(
'sticky top-0 z-50 bg-white/80 backdrop-blur-md',
'border-b border-gray-200',
className
)}
>
{children}
</header>
);
}Navigation Menu
typescript
// src/layouts/nav-menu/
// Main navigation items
const menuItems = [
{ label: 'Dashboard', href: '/', icon: HomeIcon },
{ label: 'Library', href: '/library', icon: LibraryIcon },
{ label: 'Transfers', href: '/transfers', icon: ShareIcon },
{ label: 'Trash', href: '/trash', icon: TrashIcon },
];Notification Dropdown
typescript
// src/layouts/notification-dropdown.tsx
export default function NotificationDropdown() {
const { notifications } = useNotificationContext();
return (
<Dropdown>
<Dropdown.Trigger>
<Button variant="ghost">
<BellIcon />
{unreadCount > 0 && <Badge>{unreadCount}</Badge>}
</Button>
</Dropdown.Trigger>
<Dropdown.Menu>
{notifications.map((notification) => (
<NotificationItem key={notification._id} {...notification} />
))}
</Dropdown.Menu>
</Dropdown>
);
}Responsive Design
All layouts are responsive:
typescript
// Mobile-first approach with Tailwind breakpoints
<div className={cn(
'px-4', // Mobile: 16px padding
'sm:px-6', // 640px+: 24px padding
'md:px-8', // 768px+: 32px padding
'lg:px-12', // 1024px+: 48px padding
'xl:px-16', // 1280px+: 64px padding
)} />Breakpoints
| Breakpoint | Width | Description |
|---|---|---|
xs | 480px | Extra small devices |
sm | 640px | Small devices |
md | 768px | Medium devices (tablets) |
lg | 1024px | Large devices (laptops) |
xl | 1280px | Extra large devices |
2xl | 1536px | 2X large devices |
3xl | 1920px | Full HD |
4xl | 2560px | 4K displays |
Hiding Layout Elements
For special pages (onboarding, full-screen views):
typescript
const hideHeaderOn = ['/onboarding', '/new-artist'];
const shouldShowHeader = !hideHeaderOn.includes(pathname);
{shouldShowHeader && <Header />}Sidebar State (Beryllium)
typescript
// src/layouts/beryllium/beryllium-utils.ts
import { atom, useAtom } from 'jotai';
const expandedLeftAtom = atom(false);
export function useBerylliumSidebars() {
const [expandedLeft, setExpandedLeft] = useAtom(expandedLeftAtom);
return {
expandedLeft,
toggleLeft: () => setExpandedLeft(!expandedLeft),
};
}Customizing Layouts
To add a new layout:
- Create folder:
src/layouts/new-layout/ - Create layout component:
new-layout.tsx - Add to enum:
LAYOUT_OPTIONS.NEW_LAYOUT = 'new-layout' - Add case in layout provider switch statement
- Optionally add layout icon:
new-layout-icon.tsx