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>ThemeProviderDark/Light mode via next-themes
providers/theme-provider.tsxLayoutProviderSidebar open/close state
providers/layout-provider.tsxAppLayoutHeader + Sidebar shell
components/layouts/app-layout/index.tsxProviders
Central compositor that wraps all global context providers.
"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.
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>
);
}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.
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>
);
}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" });| Action | Description |
|---|---|
| toggleLeftSidebar() | Toggle left sidebar open/close |
| toggleAgentsPanel() | Collapse/expand the AI agents panel |
| SET_LEFT_SIDEBAR | Set left sidebar state (payload: boolean) |
| SET_AGENTS_PANEL | Set AI agents panel state (payload: boolean) |
| SET_ACTIVE_ROUTE | Set active route (payload: string) |
Layout Components
App shell with header, sidebars, and content area.
<AppLayout title="Page Title" subtitle="Optional subtitle">
{/* Your page content */}
</AppLayout>Layout Files
app-layout.tsxMain layout wrapper. Auth routes get no chrome, app routes get header + sidebars.
top-navbar.tsxSticky header with title, search, theme toggle, avatar, sidebar toggle.
left-sidebar.tsxLeft navigation with primary and secondary nav items.
right-sidebar.tsxRight panel with AI agents or other contextual content.
// These routes render children without the layout shell
const EXCLUDED_PATHS = [
"/login",
"/register",
"/auth/*",
"/forgot-password",
"/reset-password",
];Usage Examples
// 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>
);
}// 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>
);
}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>
);
}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>
);
}