E-Crystal Architecture

Providers & Layouts

Complete guide for providers, layouts, theme system, and sidebar management.

Overview

Provider hierarchy:

<Providers>
  <LayoutProvider>        ← Sidebar state
    <ThemeProvider>        ← Dark/Light mode
      <AppLayout>          ← Header + Sidebar shell
        {children}
      </AppLayout>
    </ThemeProvider>
  </LayoutProvider>
</Providers>
🎨ThemeProvider

Dark/Light mode via next-themes

providers/theme-provider.tsx
📐LayoutProvider

Sidebar open/close state

providers/layout-provider.tsx
🖼️AppLayout

Header + Sidebar shell

components/layouts/app-layout/index.tsx

Providers

Central compositor that wraps all global context providers.

providers/index.tsx
"use client";

import { LayoutProvider } from "./layout-provider";
import { ThemeProvider } from "./theme-provider";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LayoutProvider>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
        disableTransitionOnChange
      >
        {children}
      </ThemeProvider>
    </LayoutProvider>
  );
}

Theme Provider

Manages dark/light mode using next-themes. Press 'D' to toggle.

Basic Usage
import { useTheme } from "next-themes";

function MyComponent() {
  const { resolvedTheme, setTheme } = useTheme();

  // resolvedTheme: "light" | "dark"

  return (
    <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
      Current: {resolvedTheme}
    </button>
  );
}
With Icon Toggle
import { useTheme } from "next-themes";
import { Sun, Moon } from "lucide-react";

function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();

  return (
    <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
      {resolvedTheme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
    </button>
  );
}

Available Values

theme— User choice: "light", "dark", or "system"

resolvedTheme— Actual applied: "light" or "dark"

setTheme("dark") — Set to dark

setTheme("light") — Set to light

setTheme("system") — Follow system preference

Layout Provider

Manages sidebar open/close state using useReducer.

Basic Usage
import { useLayout } from "@/providers/layout-provider";

function MyComponent() {
  const { state, toggleLeftSidebar, toggleAgentsPanel } = useLayout();

  // state.isLeftSidebarOpen: boolean
  // state.isAgentsPanelOpen: boolean

  return (
    <div>
      <p>Left sidebar: {state.isLeftSidebarOpen ? "Open" : "Closed"}</p>
      <button onClick={toggleLeftSidebar}>Toggle Left</button>
      <button onClick={toggleAgentsPanel}>Toggle AI Agents</button>
    </div>
  );
}
Available Actions
const { state, dispatch, toggleLeftSidebar, toggleAgentsPanel } = useLayout();

// Toggle sidebar / agents panel
toggleLeftSidebar();
toggleAgentsPanel();

// Or use dispatch directly
dispatch({ type: "TOGGLE_LEFT_SIDEBAR" });
dispatch({ type: "TOGGLE_AGENTS_PANEL" });
dispatch({ type: "SET_LEFT_SIDEBAR", payload: true });
dispatch({ type: "SET_AGENTS_PANEL", payload: false });
dispatch({ type: "SET_ACTIVE_ROUTE", payload: "/dashboard" });
ActionDescription
toggleLeftSidebar()Toggle left sidebar open/close
toggleAgentsPanel()Collapse/expand the AI agents panel
SET_LEFT_SIDEBARSet left sidebar state (payload: boolean)
SET_AGENTS_PANELSet AI agents panel state (payload: boolean)
SET_ACTIVE_ROUTESet active route (payload: string)

Layout Components

App shell with header, sidebars, and content area.

AppLayout Structure
<AppLayout title="Page Title" subtitle="Optional subtitle">
  {/* Your page content */}
</AppLayout>

Layout Files

app-layout.tsx

Main layout wrapper. Auth routes get no chrome, app routes get header + sidebars.

top-navbar.tsx

Sticky header with title, search, theme toggle, avatar, sidebar toggle.

left-sidebar.tsx

Left navigation with primary and secondary nav items.

right-sidebar.tsx

Right panel with AI agents or other contextual content.

Excluded Routes (No Layout)
// These routes render children without the layout shell
const EXCLUDED_PATHS = [
  "/login",
  "/register",
  "/auth/*",
  "/forgot-password",
  "/reset-password",
];

Usage Examples

Page with Layout
// app/dashboard/page.tsx
import { AppLayout } from "@/components/layouts/app-layout";

export default function DashboardPage() {
  return (
    <AppLayout title="Dashboard" subtitle="Welcome back!">
      <div className="p-6">
        {/* Your dashboard content */}
      </div>
    </AppLayout>
  );
}
Conditional Layout
// app/(auth)/login/page.tsx — No layout
export default function LoginPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      {/* Auth pages bypass AppLayout automatically */}
      <LoginForm />
    </div>
  );
}
Custom Header Title
import { AppLayout } from "@/components/layouts/app-layout";

export default function SettingsPage() {
  return (
    <AppLayout title="Settings" subtitle="Manage your preferences">
      {/* Header shows "Settings" and subtitle */}
    </AppLayout>
  );
}
Sidebar State in Components
import { useLayout } from "@/providers/layout-provider";

function ContentArea() {
  const { state } = useLayout();

  return (
    <div
      className="transition-all duration-300"
      style={{
        marginLeft: state.isLeftSidebarOpen ? "256px" : "64px",
      }}
    >
      {/* Content adjusts based on sidebar state */}
    </div>
  );
}
E-Crystal Providers & Layouts Guide