import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { useTheme } from '../lib/theme-context';
import MobileNavigation from './MobileNavigation';
import DailyEmberWidget from './DailyEmberWidget';
import TopSearchBar from './ui/TopSearchBar';
import CommandPaletteModal from './ui/CommandPaletteModal';
// User Profile Dropdown Component
function UserProfileDropdown({ user, onMobileMenuClose }) {
// Hook order is fixed for both branches; do not move this below the
// null-user early return — see rules-of-hooks (AGENTS.md Gotcha #11.5).
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Logged-out: replace avatar + email + dropdown with a Sign-in CTA.
if (!user) {
return (
))}
>
);
}
export default function Layout({ children, user = null, showSearch = false }) {
const router = useRouter();
const { theme, toggleTheme } = useTheme();
const [searchQuery, setSearchQuery] = useState('');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Command palette (redesign-v2 sub-convoy #3, 2026-06-04). Opens
// globally on ⌘K / Ctrl+K for authenticated users. The keyboard
// listener is attached at Layout-scope (not _app.js) so it doesn't
// fire on the public marketing pages that don't mount .
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
useEffect(() => {
if (!user) return undefined;
const handler = (event) => {
const isShortcut =
(event.metaKey || event.ctrlKey) &&
!event.shiftKey &&
!event.altKey &&
event.key.toLowerCase() === 'k';
if (isShortcut) {
event.preventDefault();
setIsCommandPaletteOpen((prev) => !prev);
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [user]);
// Mouse-tracking radial spotlight for nav items (redesign-v2
// spotlight-hover refinement, 2026-06-04). Delegated single
// mousemove listener at the document level — cheaper than
// attaching per-item React onMouseMove handlers to the 10+
// nav surfaces in NavigationContent. Writes --mouse-x and
// --mouse-y as percentages onto the closest matching element;
// the CSS ::before in globals.css consumes those vars to
// position the radial-gradient. closest() returns null when
// the cursor isn't over a nav item, which short-circuits 99%
// of pointer events so the listener cost is negligible.
// prefers-reduced-motion users still see the bg-color hover
// tint (the ::before opacity transition is collapsed to 0.01ms
// by the global reduced-motion sweep, but the spotlight itself
// is visually static once the mouse stops moving — that's a
// positional update, not an animation, so this respects WCAG
// SC 2.3.3 without special-casing).
useEffect(() => {
const selector = '.nav-item, .nav-item-bottom, .nav-item-hover';
const handler = (event) => {
const el = event.target.closest?.(selector);
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
el.style.setProperty('--mouse-x', `${x}%`);
el.style.setProperty('--mouse-y', `${y}%`);
};
document.addEventListener('mousemove', handler);
return () => document.removeEventListener('mousemove', handler);
}, []);
return (
// Outer shell — `md:p-4 md:gap-4` pulls both the sidebar and the
// main column in from the viewport edges so the sidebar and the
// top header read as floating chrome chips (operator feedback
// 2026-06-04: "pull out the left nav to make it look like it is
// its own floating section versus attached to the top-left"). The
// `md:gap-4` on this row also creates the visible page-body
// background between the sidebar's right edge and the main
// column, which replaces the previous hard divider — "all feels
// like one large page" is achieved by the hearth gradient
// showing through the gap. Mobile (< 768px) keeps the current
// flush layout because the sidebar is `hidden md:flex` anyway.
{/* Mobile Navigation - Bottom bar for mobile */}
setIsMobileMenuOpen(true)}
/>
{/* Mobile Overlay — Liquid Glass scrim consistent with . */}
{isMobileMenuOpen && (
setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Menu Drawer - Slides in from left when "More" is tapped */}
{/* Mobile Header with Close Button */}
DeckHearth
{/* Mobile Navigation Content */}
{/* Mobile Bottom Section. 2026-06-04 dropdown-migration pass:
Support + Theme moved into the TopSearchBar
dropdown for authenticated users (the avatar chip in the
top bar is reachable from mobile too — the chevron/name
are hidden via `md:` but the chip itself is always
visible). Logged-out visitors keep the Sign-in CTA +
Support + Theme here since they have no top-bar menu. */}
{!user && (
setIsMobileMenuOpen(false)}
/>
)}
{/* Desktop Sidebar — Hidden on mobile. 2026-06-04 split-into-two-
chips pass: per operator feedback ("make the navigation its own
section, separated from the badges and progress"), the sidebar
column now holds TWO independent floating glass chips with a
visible body-gradient gap between them:
1. nav-chip (flex-1) — logo, wordmark, NavigationContent,
and the Sign-in CTA when logged-out.
2. bottom-chip — for authenticated users this is the
(which is already a glass-panel and
renders as its own chip without further wrapping). For
logged-out visitors, this is a small glass-panel that
holds the Support + Theme icon row (since logged-out
users have no TopSearchBar dropdown to host those).
Support + Theme were removed from the authenticated path
entirely — they live in TopSearchBar's dropdown
now. The shared `boxShadow` stack is rim-light-inner +
elevation-ambient (matches the prior floating-chrome pass —
rim-light-outer stays dropped so there's no 1px ring that
would re-introduce a divider against the body gradient). */}
{/* Main Content. `md:gap-4` adds vertical breathing room between
the floating TopSearchBar header and the page content so they
read as two distinct floating chrome elements over the body
gradient (rather than the header sitting flush against the
content). `min-w-0` prevents flex children from blowing past
the column width when long card titles or table cells refuse
to wrap. */}
{/* Global top bar — redesign-v2 sub-convoy #3 (2026-06-04).
Rendered for ALL authenticated pages so the search +
notifications + user-menu chrome is consistent everywhere
(operator decision § 7.3 of the umbrella convoy: "sweep to
all authenticated pages in this convoy"). Unauthenticated
visitors see no top bar — public landing has its own
marketing header. The legacy `showSearch` prop is honored
via fall-through but no longer drives visibility; it can
be removed in a cleanup convoy along with the dead
`searchQuery` state. */}
{user && (
setIsCommandPaletteOpen(true)}
/>
)}
{/* Page Content */}
{children}