diff --git a/src/components/shell/AppShellLayout.tsx b/src/components/shell/AppShellLayout.tsx
new file mode 100644
index 00000000..0e947e4e
--- /dev/null
+++ b/src/components/shell/AppShellLayout.tsx
@@ -0,0 +1,11 @@
+import type { ReactNode } from 'react';
+import { AppSidebar } from './AppSidebar';
+
+export function AppShellLayout({ children }: { children: ReactNode }) {
+ return (
+
+ );
+}
diff --git a/src/components/shell/AppSidebar.test.tsx b/src/components/shell/AppSidebar.test.tsx
new file mode 100644
index 00000000..2d8fed90
--- /dev/null
+++ b/src/components/shell/AppSidebar.test.tsx
@@ -0,0 +1,33 @@
+/** @vitest-environment happy-dom */
+
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { AppSidebar } from './AppSidebar';
+
+vi.mock('next/navigation', () => ({ usePathname: () => '/analytics' }));
+
+describe('AppSidebar', () => {
+ afterEach(() => {
+ cleanup();
+ localStorage.clear();
+ });
+
+ it('persists collapsed state and keeps labels accessible', () => {
+ render();
+ const toggle = screen.getByRole('button', { name: 'Collapse sidebar' });
+
+ fireEvent.click(toggle);
+
+ expect(screen.getByRole('button', { name: 'Expand sidebar' })).toHaveAttribute(
+ 'aria-expanded',
+ 'false',
+ );
+ expect(screen.getByRole('link', { name: 'Analytics' })).toHaveAttribute('title', 'Analytics');
+ expect(localStorage.getItem('commitlabs:sidebar-collapsed')).toBe('true');
+ });
+
+ it('marks the active route', () => {
+ render();
+ expect(screen.getByRole('link', { name: 'Analytics' })).toHaveAttribute('aria-current', 'page');
+ });
+});
diff --git a/src/components/shell/AppSidebar.tsx b/src/components/shell/AppSidebar.tsx
new file mode 100644
index 00000000..353f75ca
--- /dev/null
+++ b/src/components/shell/AppSidebar.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { FiBarChart2, FiChevronLeft, FiHome, FiMenu, FiPlus, FiSettings } from 'react-icons/fi';
+
+const STORAGE_KEY = 'commitlabs:sidebar-collapsed';
+
+const navigation = [
+ { href: '/', label: 'Overview', icon: FiHome },
+ { href: '/analytics', label: 'Analytics', icon: FiBarChart2 },
+ { href: '/create', label: 'Create commitment', icon: FiPlus },
+ { href: '/settings', label: 'Settings', icon: FiSettings },
+] as const;
+
+export function AppSidebar() {
+ const pathname = usePathname();
+ const [collapsed, setCollapsed] = useState(false);
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ setCollapsed(window.localStorage.getItem(STORAGE_KEY) === 'true');
+ setHydrated(true);
+ }, []);
+
+ useEffect(() => {
+ if (hydrated) window.localStorage.setItem(STORAGE_KEY, String(collapsed));
+ }, [collapsed, hydrated]);
+
+ return (
+
+ );
+}
diff --git a/src/components/shell/index.ts b/src/components/shell/index.ts
new file mode 100644
index 00000000..184cee39
--- /dev/null
+++ b/src/components/shell/index.ts
@@ -0,0 +1,2 @@
+export { AppShellLayout } from './AppShellLayout';
+export { AppSidebar } from './AppSidebar';