From 39d6575a77a8f23a65a9235ff7f61fdbff411a75 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 26 May 2026 10:14:14 -0400 Subject: [PATCH] Header: align with website redesign (hover-open, underline, Model dropdown) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the website header changes (PolicyEngine/policyengine-app-v2#1059) so users see consistent navigation across zones. - Dropdowns open on hover with a 100ms intent delay and a 200ms close grace; a transparent paddingTop "bridge" inside the same hover container fills the 10px gap so the cursor doesn't trigger close while travelling to the panel. Click toggles, Escape, and outside-click still work for touch / keyboard. - Per-item center-out underline grow on hover. The active page (or any child of a dropdown trigger) keeps the underline lit so the bar shows where you are. Active-state walks dropdown children recursively. - Model is now a dropdown with the full sidebar mirror: Rules (Coverage / Parameters / Variables), Data (Pipeline / Calibration / Validation), Behavioral responses. Children are indented one level. - Per-property transition delays on dropdown rows: the cascading entry reveal staggers opacity only; hover background/color stay instant. - Logo→nav gap bumped from spacing.md (12px) to 40px so the logo reads as an anchor instead of another menu entry. - MobileMenu now walks nested children too and indents them. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/Header.jsx | 25 +- src/components/homeHeader/HeaderLogo.jsx | 6 +- src/components/homeHeader/MobileMenu.jsx | 23 +- src/components/homeHeader/NavItem.jsx | 313 ++++++++++++++--------- 4 files changed, 244 insertions(+), 123 deletions(-) diff --git a/src/components/Header.jsx b/src/components/Header.jsx index a88f3dc..d29b9f1 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -14,6 +14,7 @@ function useDisclosure(initialState = false) { } function buildNavItems(country) { + const modelBase = country.modelUrl; return [ { label: 'Research', @@ -22,8 +23,28 @@ function buildNavItems(country) { }, { label: 'Model', - href: country.modelUrl, - hasDropdown: false, + hasDropdown: true, + dropdownItems: [ + { + label: 'Rules', + href: `${modelBase}/rules`, + children: [ + { label: 'Coverage', href: `${modelBase}/rules/coverage` }, + { label: 'Parameters', href: `${modelBase}/rules/parameters` }, + { label: 'Variables', href: `${modelBase}/rules/variables` }, + ], + }, + { + label: 'Data', + href: `${modelBase}/data`, + children: [ + { label: 'Pipeline', href: `${modelBase}/data/pipeline` }, + { label: 'Calibration', href: `${modelBase}/data/calibration` }, + { label: 'Validation', href: `${modelBase}/data/validation` }, + ], + }, + { label: 'Behavioral responses', href: `${modelBase}/behavioral` }, + ], }, { label: 'API', diff --git a/src/components/homeHeader/HeaderLogo.jsx b/src/components/homeHeader/HeaderLogo.jsx index 501177b..289ff97 100644 --- a/src/components/homeHeader/HeaderLogo.jsx +++ b/src/components/homeHeader/HeaderLogo.jsx @@ -1,25 +1,25 @@ 'use client'; -import { spacing } from '@policyengine/ui-kit/legacy/tokens'; const PolicyEngineLogo = 'https://www.policyengine.org/assets/logos/policyengine/white.svg'; const logoContainerStyles = { display: 'flex', alignItems: 'center', cursor: 'pointer', + // Wider gap than between nav items so the logo reads as an anchor + marginRight: '40px', }; const logoImageStyles = { height: '24px', width: 'auto', - marginRight: 12, }; export default function HeaderLogo({ country }) { const logoImage = PolicyEngine; return ( - + {logoImage} ); diff --git a/src/components/homeHeader/MobileMenu.jsx b/src/components/homeHeader/MobileMenu.jsx index 35ecaf5..5e1e8bb 100644 --- a/src/components/homeHeader/MobileMenu.jsx +++ b/src/components/homeHeader/MobileMenu.jsx @@ -151,7 +151,7 @@ export default function MobileMenu({ country, opened, onOpen, onClose, navItems className="flex flex-col" style={{ gap: spacing.xs, paddingLeft: spacing.md }} > - {item.dropdownItems.map((dropdownItem) => ( + {item.dropdownItems.flatMap((dropdownItem) => [ {dropdownItem.label} - - ))} + , + ...(dropdownItem.children ?? []).map((grand) => ( + + {grand.label} + + )), + ])} ) : ( diff --git a/src/components/homeHeader/NavItem.jsx b/src/components/homeHeader/NavItem.jsx index a2f32d4..e472a31 100644 --- a/src/components/homeHeader/NavItem.jsx +++ b/src/components/homeHeader/NavItem.jsx @@ -1,41 +1,112 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { usePathname } from 'next/navigation'; import { IconChevronDown } from '@tabler/icons-react'; import { colors, typography } from '@policyengine/ui-kit/legacy/tokens'; +const NAV_ITEM_PADDING_X = 14; +const NAV_UNDERLINE_INSET = 10; +const DROPDOWN_GAP = 10; +const HOVER_OPEN_DELAY_MS = 100; +const HOVER_CLOSE_DELAY_MS = 200; + const navItemStyle = { color: colors.text.inverse, fontWeight: typography.fontWeight.medium, fontSize: '15px', fontFamily: typography.fontFamily.primary, textDecoration: 'none', - padding: '6px 14px', - borderRadius: '6px', - transition: 'background-color 0.15s ease', + padding: `8px ${NAV_ITEM_PADDING_X}px`, letterSpacing: '0.01em', + position: 'relative', }; -const hoverHandlers = { - onMouseEnter: (e) => { - e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.12)'; - }, - onMouseLeave: (e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }, -}; +function NavUnderline({ visible }) { + return ( +