"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useAuth } from "@/lib/auth/auth-context"; import { ThemeToggle } from "./ThemeToggle"; import { useSidebar } from "./SidebarContext"; /** * Full-width application header (topbar). * Logo/brand MUST live here — not in the sidebar — per MS15 design spec. * Spans grid-column 1 / -1 in the app shell grid layout. * * Layout (left → right): * [Logo/Brand] [Breadcrumb] [Search] [spacer] * [System Status] [Terminal Toggle] [Notifications] [Theme Toggle] [User Avatar+Dropdown] */ export function AppHeader(): React.JSX.Element { const { user, signOut } = useAuth(); const { isMobile, mobileOpen, setMobileOpen, toggleCollapsed } = useSidebar(); const pathname = usePathname(); const [dropdownOpen, setDropdownOpen] = useState(false); const [searchFocused, setSearchFocused] = useState(false); const dropdownRef = useRef(null); // Close dropdown on outside click const handleOutsideClick = useCallback((event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setDropdownOpen(false); } }, []); useEffect(() => { if (dropdownOpen) { document.addEventListener("mousedown", handleOutsideClick); } else { document.removeEventListener("mousedown", handleOutsideClick); } return (): void => { document.removeEventListener("mousedown", handleOutsideClick); }; }, [dropdownOpen, handleOutsideClick]); // Derive breadcrumb segments from pathname const breadcrumbSegments = pathname .split("/") .filter(Boolean) .map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, " ")); // User initials for avatar fallback const initials = user?.name ? user.name .split(" ") .slice(0, 2) .map((part) => part[0]) .join("") .toUpperCase() : user?.email ? (user.email[0] ?? "?").toUpperCase() : "?"; const handleHamburgerClick = useCallback((): void => { if (isMobile) { setMobileOpen(!mobileOpen); } else { toggleCollapsed(); } }, [isMobile, mobileOpen, setMobileOpen, toggleCollapsed]); return (
{/* ── Hamburger — visible below lg ── */} {/* ── Brand / Logo ── */} {/* Mosaic logo mark: four colored squares + center dot */} Mosaic Stack {/* ── Breadcrumb ── */} {/* ── 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 */}
{/* Terminal Toggle */} {/* Notifications */} {/* Theme Toggle */} {/* User Avatar + Dropdown */}
{/* Dropdown Menu */} {dropdownOpen && (
{/* User info header */}
{user?.name ?? "User"}
{user?.email && (
{user.email}
)}
{/* Divider */}
); } // --------------------------------------------------------------------------- // 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} ); }