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.
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. 2026-06-04 floating-chrome pass: rounded-3xl
+ overflow-hidden so the rail reads as its own floating chip;
`var(--rim-light-outer)` dropped from the box-shadow stack
because that 1px outer ring was the "vertical divider between
the sidebar and the right side" the operator wanted gone. The
chip's edge is now defined solely by `var(--elevation-ambient)`
(drop shadow) + the glass-surface-mid tint against the body
gradient that shows through `md:gap-4` on the outer flex. */}
{/* 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 */}
{/* 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}