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 (
{/* 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.
redesign-v2 refinements 2026-06-04: UserProfileDropdown
removed for authenticated users (the TopSearchBar's
user-menu chip is the canonical entry point now). For
logged-out visitors, the Sign-in CTA still renders here
so the drawer surfaces the auth path. */}
{!user && (
setIsMobileMenuOpen(false)}
/>
)}
{/* Icon Buttons Row - Support and Dark Mode */}
{/* Desktop Sidebar - Hidden on mobile. Liquid Glass mid-tint, ambient
elevation + rim-light edges; the page background visibly cools
through the rail. */}
{/* Desktop Header — flame logomark + two-tone wordmark.
redesign-v2 sub-convoy #2 (2026-06-04): replaces the prior
"DH" monogram + plain text. Mockup shows a gradient flame
icon on the left and "Deck" + "Hearth" where "Hearth" is
gradient-text-flame so the wordmark reads as a single brand
unit with the ember accent. */}
DeckHearth
{/* Desktop Navigation Content */}
{/* Desktop Bottom Section.
redesign-v2 refinements 2026-06-04: the UserProfileDropdown
that used to live here has moved to the TopSearchBar's
user-menu chip (top-right). Keeping both was redundant.
For logged-out visitors, render the Sign-in CTA here so
the sidebar still surfaces the auth path (the top bar
renders null when user is null). */}
{user && }
{!user && (
{}}
/>
)}
{/* Icon Buttons Row - Support and Dark Mode */}
{/* 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}