+ {/* ── Search Bar ── */}
+
+ {/* Search icon */}
+
+
{
+ setSearchFocused(true);
+ }}
+ onBlur={() => {
+ setSearchFocused(false);
+ }}
+ style={{
+ flex: 1,
+ background: "none",
+ border: "none",
+ outline: "none",
+ color: "var(--text)",
+ fontSize: "0.83rem",
+ fontFamily: "inherit",
+ }}
+ aria-label="Search projects, agents, tasks"
+ />
+
+
+ {/* ── Spacer ── */}
+
+
+ {/* ── Right side controls ── */}
+
+ {/* System Status */}
+
+
+
All Systems
+
Operational
+
+
+ {/* Terminal Toggle */}
+
+
+ {/* Notifications */}
+
+
+ {/* Theme Toggle */}
- {user && (
-
- {user.name || user.email}
-
- )}
+ {/* User Avatar + Dropdown */}
+
+
-
+ {/* Dropdown Menu */}
+ {dropdownOpen && (
+
+ {/* User info header */}
+
+
+ {user?.name ?? "User"}
+
+ {user?.email && (
+
+ {user.email}
+
+ )}
+
+
+ {/* Divider */}
+
+
+ {/* Profile link */}
+
{
+ setDropdownOpen(false);
+ }}
+ >
+
+ Profile
+
+
+ {/* Account Settings link */}
+
{
+ setDropdownOpen(false);
+ }}
+ >
+
+ Account Settings
+
+
+ {/* Divider */}
+
+
+ {/* Sign Out */}
+
+
+ )}
+
);
}
+
+// ---------------------------------------------------------------------------
+// Sub-components
+// ---------------------------------------------------------------------------
+
+/** Terminal toggle button — visual only; no panel wired yet. */
+function TerminalToggleButton(): React.JSX.Element {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
+ );
+}
+
+interface DropdownItemProps {
+ href: string;
+ onClick: () => void;
+ children: React.ReactNode;
+}
+
+/** A navigation link styled as a dropdown menu item. */
+function DropdownItem({ href, onClick, children }: DropdownItemProps): React.JSX.Element {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
{
+ setHovered(true);
+ }}
+ onMouseLeave={() => {
+ setHovered(false);
+ }}
+ >
+ {children}
+
+ );
+}
diff --git a/apps/web/src/components/layout/AppSidebar.tsx b/apps/web/src/components/layout/AppSidebar.tsx
index 6f27c8e..ef3ecb3 100644
--- a/apps/web/src/components/layout/AppSidebar.tsx
+++ b/apps/web/src/components/layout/AppSidebar.tsx
@@ -2,159 +2,709 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
+import Image from "next/image";
+import { useAuth } from "@/lib/auth/auth-context";
+import { useSidebar } from "./SidebarContext";
-interface NavItem {
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface NavBadge {
+ label: string;
+ pulse?: boolean;
+}
+
+interface NavItemConfig {
href: string;
label: string;
icon: React.JSX.Element;
+ badge?: NavBadge;
}
-const navItems: NavItem[] = [
+interface NavGroup {
+ label: string;
+ items: NavItemConfig[];
+}
+
+// ---------------------------------------------------------------------------
+// SVG Icons (16x16 viewBox, stroke="currentColor", strokeWidth="1.5")
+// ---------------------------------------------------------------------------
+
+function IconDashboard(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconProjects(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconProjectWorkspace(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconKanban(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconFileManager(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconLogs(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconTerminal(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconSettings(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconChevronLeft(): React.JSX.Element {
+ return (
+
+ );
+}
+
+function IconChevronRight(): React.JSX.Element {
+ return (
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Nav groups definition
+// ---------------------------------------------------------------------------
+
+const NAV_GROUPS: NavGroup[] = [
{
- href: "/",
- label: "Dashboard",
- icon: (
-
- ),
+ label: "Overview",
+ items: [
+ {
+ href: "/",
+ label: "Dashboard",
+ icon:
,
+ badge: { label: "live", pulse: true },
+ },
+ ],
},
{
- href: "/tasks",
- label: "Tasks",
- icon: (
-
- ),
+ label: "Workspace",
+ items: [
+ {
+ href: "/projects",
+ label: "Projects",
+ icon:
,
+ },
+ {
+ href: "/workspace",
+ label: "Project Workspace",
+ icon:
,
+ },
+ {
+ href: "/kanban",
+ label: "Kanban",
+ icon:
,
+ },
+ {
+ href: "/files",
+ label: "File Manager",
+ icon:
,
+ },
+ ],
},
{
- href: "/calendar",
- label: "Calendar",
- icon: (
-
- ),
+ label: "Operations",
+ items: [
+ {
+ href: "/logs",
+ label: "Logs & Telemetry",
+ icon:
,
+ badge: { label: "live", pulse: true },
+ },
+ {
+ href: "#terminal",
+ label: "Terminal",
+ icon:
,
+ },
+ ],
},
{
- href: "/knowledge",
- label: "Knowledge",
- icon: (
-
- ),
- },
- {
- href: "/usage",
- label: "Usage",
- icon: (
-
- ),
+ label: "System",
+ items: [
+ {
+ href: "/settings",
+ label: "Settings",
+ icon:
,
+ },
+ ],
},
];
+// ---------------------------------------------------------------------------
+// Helper: derive initials from display name
+// ---------------------------------------------------------------------------
+
+function getInitials(name: string): string {
+ const parts = name.trim().split(/\s+/);
+ const first = parts[0] ?? "";
+ if (parts.length === 1) {
+ return first.slice(0, 2).toUpperCase();
+ }
+ const last = parts[parts.length - 1] ?? "";
+ return ((first[0] ?? "") + (last[0] ?? "")).toUpperCase();
+}
+
+// ---------------------------------------------------------------------------
+// NavBadge component
+// ---------------------------------------------------------------------------
+
+interface NavBadgeProps {
+ badge: NavBadge;
+}
+
+function NavBadgeChip({ badge }: NavBadgeProps): React.JSX.Element {
+ const pulseStyle: React.CSSProperties = badge.pulse
+ ? {
+ background: "rgba(47,128,255,0.15)",
+ color: "var(--primary-l)",
+ }
+ : {
+ background: "var(--surface-2)",
+ color: "var(--muted)",
+ };
+
+ return (
+
+ {badge.pulse && (
+
+ )}
+ {badge.label}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// NavItem component
+// ---------------------------------------------------------------------------
+
+interface NavItemProps {
+ item: NavItemConfig;
+ isActive: boolean;
+ collapsed: boolean;
+}
+
+function NavItem({ item, isActive, collapsed }: NavItemProps): React.JSX.Element {
+ const baseStyle: React.CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: "11px",
+ padding: "9px 10px",
+ borderRadius: "6px",
+ fontSize: "0.875rem",
+ fontWeight: 500,
+ color: isActive ? "var(--text)" : "var(--muted)",
+ background: isActive ? "var(--surface)" : "transparent",
+ position: "relative",
+ transition: "background 0.12s ease, color 0.12s ease",
+ textDecoration: "none",
+ justifyContent: collapsed ? "center" : undefined,
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ };
+
+ const iconStyle: React.CSSProperties = {
+ width: "16px",
+ height: "16px",
+ flexShrink: 0,
+ opacity: isActive ? 1 : 0.7,
+ transition: "opacity 0.12s ease",
+ };
+
+ const content = (
+ <>
+ {/* Active left accent bar */}
+ {isActive && (
+
+ )}
+
+ {/* Icon */}
+
{item.icon}
+
+ {/* Label and badge — hidden when collapsed */}
+ {!collapsed && (
+ <>
+
+ {item.label}
+
+ {item.badge !== undefined &&
}
+ >
+ )}
+ >
+ );
+
+ const sharedProps = {
+ style: baseStyle,
+ "aria-current": isActive ? ("page" as const) : undefined,
+ title: collapsed ? item.label : undefined,
+ onMouseEnter: (e: React.MouseEvent
): void => {
+ if (!isActive) {
+ (e.currentTarget as HTMLElement).style.background = "var(--surface)";
+ (e.currentTarget as HTMLElement).style.color = "var(--text-2)";
+ const iconEl = (e.currentTarget as HTMLElement).querySelector(
+ "[data-nav-icon]"
+ );
+ if (iconEl) iconEl.style.opacity = "1";
+ }
+ },
+ onMouseLeave: (e: React.MouseEvent): void => {
+ if (!isActive) {
+ (e.currentTarget as HTMLElement).style.background = "transparent";
+ (e.currentTarget as HTMLElement).style.color = "var(--muted)";
+ const iconEl = (e.currentTarget as HTMLElement).querySelector(
+ "[data-nav-icon]"
+ );
+ if (iconEl) iconEl.style.opacity = "0.7";
+ }
+ },
+ };
+
+ if (item.href.startsWith("#")) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// UserCard component
+// ---------------------------------------------------------------------------
+
+interface UserCardProps {
+ collapsed: boolean;
+}
+
+function UserCard({ collapsed }: UserCardProps): React.JSX.Element {
+ const { user } = useAuth();
+
+ const displayName = user?.name ?? "User";
+ const initials = getInitials(displayName);
+ const role = user?.workspaceRole ?? "Member";
+
+ return (
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// CollapseToggle component
+// ---------------------------------------------------------------------------
+
+interface CollapseToggleProps {
+ collapsed: boolean;
+ onToggle: () => void;
+}
+
+function CollapseToggle({ collapsed, onToggle }: CollapseToggleProps): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main AppSidebar component
+// ---------------------------------------------------------------------------
+
/**
- * Application sidebar — navigation only, no brand/logo.
+ * Application sidebar — navigation groups, collapse toggle, and user card.
* Logo lives in AppHeader per MS15 design spec.
*/
export function AppSidebar(): React.JSX.Element {
const pathname = usePathname();
+ const { collapsed, toggleCollapsed } = useSidebar();
return (
-
);
}
diff --git a/apps/web/src/components/layout/SidebarContext.tsx b/apps/web/src/components/layout/SidebarContext.tsx
new file mode 100644
index 0000000..ca83270
--- /dev/null
+++ b/apps/web/src/components/layout/SidebarContext.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
+
+interface SidebarContextValue {
+ collapsed: boolean;
+ toggleCollapsed: () => void;
+}
+
+const SidebarContext = createContext(undefined);
+
+export function SidebarProvider({ children }: { children: ReactNode }): React.JSX.Element {
+ const [collapsed, setCollapsed] = useState(false);
+
+ const toggleCollapsed = useCallback((): void => {
+ setCollapsed((prev) => !prev);
+ }, []);
+
+ const value: SidebarContextValue = { collapsed, toggleCollapsed };
+
+ return {children};
+}
+
+export function useSidebar(): SidebarContextValue {
+ const context = useContext(SidebarContext);
+ if (context === undefined) {
+ throw new Error("useSidebar must be used within SidebarProvider");
+ }
+ return context;
+}