diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx
new file mode 100644
index 00000000..b00da9f4
--- /dev/null
+++ b/ui/src/components/layout/app-sidebar.tsx
@@ -0,0 +1,167 @@
+import { Link, useLocation } from 'react-router-dom';
+import {
+ Home,
+ Key,
+ Zap,
+ Users,
+ Settings,
+ Activity,
+ FolderOpen,
+ ChevronRight,
+ BarChart3,
+ Gauge,
+ Github,
+} from 'lucide-react';
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+ SidebarMenuSub,
+ SidebarMenuSubItem,
+ SidebarMenuSubButton,
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarFooter,
+ SidebarTrigger,
+ SidebarGroupContent,
+} from '@/components/ui/sidebar';
+import { CcsLogo } from '@/components/shared/ccs-logo';
+import { useSidebar } from '@/hooks/use-sidebar';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+
+// Define navigation groups
+const navGroups = [
+ {
+ title: 'General',
+ items: [
+ { path: '/', icon: Home, label: 'Home' },
+ { path: '/analytics', icon: BarChart3, label: 'Analytics' },
+ ],
+ },
+ {
+ title: 'Identity & Access',
+ items: [
+ { path: '/api', icon: Key, label: 'API Profiles' },
+ {
+ path: '/cliproxy',
+ icon: Zap,
+ label: 'CLIProxy',
+ isCollapsible: true,
+ children: [
+ { path: '/cliproxy', label: 'Overview' },
+ { path: '/cliproxy/control-panel', icon: Gauge, label: 'Control Panel' },
+ ],
+ },
+ { path: '/copilot', icon: Github, label: 'GitHub Copilot' },
+ {
+ path: '/accounts',
+ icon: Users,
+ label: 'Accounts',
+ isCollapsible: true,
+ children: [
+ { path: '/accounts', label: 'All Accounts' },
+ { path: '/shared', icon: FolderOpen, label: 'Shared Data' },
+ ],
+ },
+ ],
+ },
+ {
+ title: 'System',
+ items: [
+ { path: '/health', icon: Activity, label: 'Health' },
+ { path: '/settings', icon: Settings, label: 'Settings' },
+ ],
+ },
+];
+
+export function AppSidebar() {
+ const location = useLocation();
+ const { state } = useSidebar();
+
+ // Helper to check if a route is active (exact match)
+ const isRouteActive = (path: string) => location.pathname === path;
+
+ // Helper to check if a group/parent should be open based on active child
+ // Also handles sub-routes (e.g., /cliproxy/control-panel matches /cliproxy)
+ const isParentActive = (children: { path: string }[]) => {
+ return children.some(
+ (child) => isRouteActive(child.path) || location.pathname.startsWith(child.path + '/')
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ {navGroups.map((group, index) => (
+
+ {group.title && {group.title}}
+
+
+ {group.items.map((item) => (
+
+ {item.isCollapsible && item.children ? (
+
+
+
+
+ {item.icon && }
+
+ {item.label}
+
+
+
+
+
+
+ {item.children.map((child) => (
+
+
+
+ {child.label}
+
+
+
+ ))}
+
+
+
+
+ ) : (
+
+
+ {item.icon && }
+ {item.label}
+
+
+ )}
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/layout/hero-section.tsx b/ui/src/components/layout/hero-section.tsx
new file mode 100644
index 00000000..409900ff
--- /dev/null
+++ b/ui/src/components/layout/hero-section.tsx
@@ -0,0 +1,23 @@
+import { Badge } from '@/components/ui/badge';
+import { CcsLogo } from '@/components/shared/ccs-logo';
+
+interface HeroSectionProps {
+ version?: string;
+}
+
+export function HeroSection({ version = '5.0.0' }: HeroSectionProps) {
+ return (
+
+
+
+
+
CCS Config
+
+ v{version}
+
+
+
Claude Code Switch Dashboard
+
+
+ );
+}
diff --git a/ui/src/components/layout/hub-footer.tsx b/ui/src/components/layout/hub-footer.tsx
new file mode 100644
index 00000000..a81539f8
--- /dev/null
+++ b/ui/src/components/layout/hub-footer.tsx
@@ -0,0 +1,71 @@
+import { Separator } from '@/components/ui/separator';
+import { Button } from '@/components/ui/button';
+import { FileTextIcon, SettingsIcon, GithubIcon, ExternalLinkIcon } from 'lucide-react';
+
+export function HubFooter() {
+ const currentYear = new Date().getFullYear();
+
+ const footerLinks = [
+ {
+ icon: ,
+ label: 'Logs',
+ href: '#logs',
+ onClick: () => console.log('Navigate to Logs'),
+ },
+ {
+ icon: ,
+ label: 'Settings',
+ href: '#settings',
+ onClick: () => console.log('Navigate to Settings'),
+ },
+ {
+ icon: ,
+ label: 'GitHub',
+ href: 'https://github.com/kaitranntt/ccs',
+ external: true,
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/ui/src/components/layout/index.ts b/ui/src/components/layout/index.ts
new file mode 100644
index 00000000..d5ad15e9
--- /dev/null
+++ b/ui/src/components/layout/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Layout Components Barrel Export
+ */
+
+export { AppSidebar } from './app-sidebar';
+export { HeroSection } from './hero-section';
+export { HubFooter } from './hub-footer';
+export { Layout } from './layout';
+export { ThemeProvider } from './theme-provider';
+export { ThemeToggle } from './theme-toggle';
diff --git a/ui/src/components/layout/layout.tsx b/ui/src/components/layout/layout.tsx
new file mode 100644
index 00000000..d04ec791
--- /dev/null
+++ b/ui/src/components/layout/layout.tsx
@@ -0,0 +1,69 @@
+import { Suspense } from 'react';
+import { Outlet } from 'react-router-dom';
+import { SidebarProvider } from '@/components/ui/sidebar';
+import { AppSidebar } from './app-sidebar';
+import { ThemeToggle } from './theme-toggle';
+import { PrivacyToggle } from '@/components/shared/privacy-toggle';
+import { GitHubLink } from '@/components/shared/github-link';
+import { DocsLink } from '@/components/shared/docs-link';
+import { ConnectionIndicator } from '@/components/shared/connection-indicator';
+import { LocalhostDisclaimer } from '@/components/shared/localhost-disclaimer';
+import { Skeleton } from '@/components/ui/skeleton';
+import { ClaudeKitBadge } from '@/components/shared/claudekit-badge';
+import { SponsorButton } from '@/components/shared/sponsor-button';
+import { ProjectSelectionDialog } from '@/components/shared/project-selection-dialog';
+import { useProjectSelection } from '@/hooks/use-project-selection';
+
+function PageLoader() {
+ return (
+
+
+
+
+ );
+}
+
+export function Layout() {
+ const { isOpen, prompt, onSelect, onClose } = useProjectSelection();
+
+ return (
+
+
+
+
+
+ }>
+
+
+
+
+
+
+ {/* Global project selection dialog for OAuth flows */}
+ {prompt && (
+
+ )}
+
+ );
+}
diff --git a/ui/src/components/layout/theme-provider.tsx b/ui/src/components/layout/theme-provider.tsx
new file mode 100644
index 00000000..d113fe46
--- /dev/null
+++ b/ui/src/components/layout/theme-provider.tsx
@@ -0,0 +1,61 @@
+import { useEffect, useState } from 'react';
+import { ThemeProviderContext } from '@/contexts/theme-context';
+import type { Theme } from '@/contexts/theme-context';
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'vite-ui-theme',
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ );
+
+ // Track system preference separately
+ // Initialize with window.matchMedia value if available
+ const [systemIsDark, setSystemIsDark] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+ return false;
+ });
+
+ // Listen for system theme changes
+ useEffect(() => {
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+ const listener = (e: MediaQueryListEvent) => {
+ setSystemIsDark(e.matches);
+ };
+
+ media.addEventListener('change', listener);
+ return () => media.removeEventListener('change', listener);
+ }, []);
+
+ // Derive effective theme
+ const isDark = theme === 'dark' || (theme === 'system' && systemIsDark);
+
+ // Update DOM when effective theme changes
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove('light', 'dark');
+ root.classList.add(isDark ? 'dark' : 'light');
+ }, [isDark]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ isDark,
+ };
+
+ return {children};
+}
diff --git a/ui/src/components/layout/theme-toggle.tsx b/ui/src/components/layout/theme-toggle.tsx
new file mode 100644
index 00000000..619b3a11
--- /dev/null
+++ b/ui/src/components/layout/theme-toggle.tsx
@@ -0,0 +1,19 @@
+import { Moon, Sun } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useTheme } from '@/hooks/use-theme';
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+ );
+}