Skip to content

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

LayoutKeyDescription
BerylliumberylliumDefault - Clean header, minimal chrome
HydrogenhydrogenFull sidebar layout
HeliumheliumCompact navigation
LithiumlithiumWide content area
BoronboronMinimal chrome, invitation modal support
CarboncarbonAlternative 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 items

Layout 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>
  );
}
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>
  );
}
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

BreakpointWidthDescription
xs480pxExtra small devices
sm640pxSmall devices
md768pxMedium devices (tablets)
lg1024pxLarge devices (laptops)
xl1280pxExtra large devices
2xl1536px2X large devices
3xl1920pxFull HD
4xl2560px4K displays

Hiding Layout Elements

For special pages (onboarding, full-screen views):

typescript
const hideHeaderOn = ['/onboarding', '/new-artist'];
const shouldShowHeader = !hideHeaderOn.includes(pathname);

{shouldShowHeader && <Header />}
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:

  1. Create folder: src/layouts/new-layout/
  2. Create layout component: new-layout.tsx
  3. Add to enum: LAYOUT_OPTIONS.NEW_LAYOUT = 'new-layout'
  4. Add case in layout provider switch statement
  5. Optionally add layout icon: new-layout-icon.tsx

Ctrl-Audio Platform Documentation