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 = ;
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 (
+
+ );
+}
-/**
- * Check if href is a relative path (internal) vs absolute URL (external).
- */
-function isInternalHref(href) {
- return !!href && href.startsWith('/');
+function isItemActive(setup, currentPath) {
+ if (!currentPath) return false;
+ const matches = (href) => {
+ if (!href) return false;
+ // External absolute URLs (https://...) never match the local pathname
+ if (href.startsWith('http')) return false;
+ return currentPath === href || currentPath.startsWith(`${href}/`);
+ };
+ if (setup.hasDropdown && setup.dropdownItems) {
+ const walk = (items) =>
+ items.some(
+ (c) => matches(c.href) || (c.children ? walk(c.children) : false),
+ );
+ return walk(setup.dropdownItems);
+ }
+ return matches(setup.href);
}
-/**
- * Apple-style dropdown panel with smooth height reveal and content fade.
- */
-function AppleDropdown({ items, open, onClose }) {
+function DropdownRow({ item, depth, index, visible, onClose }) {
+ const isChild = depth > 0;
+ return (
+ {
+ e.currentTarget.style.backgroundColor = colors.primary[500];
+ e.currentTarget.style.color = colors.text.inverse;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ e.currentTarget.style.color = isChild
+ ? colors.primary[700]
+ : colors.primary[800];
+ }}
+ >
+ {item.label}
+
+ );
+}
+
+function DropdownPanel({ items, open, onClose }) {
const contentRef = useRef(null);
const [contentHeight, setContentHeight] = useState(0);
const [visible, setVisible] = useState(false);
@@ -51,45 +122,34 @@ function AppleDropdown({ items, open, onClose }) {
}
}, [open]);
- const handleSelect = useCallback(
- (item) => {
- onClose();
- if (item.href) {
- window.location.href = item.href;
- } else if (item.onClick) {
- item.onClick();
- }
- },
- [onClose]
- );
+ if (!open && contentHeight === 0) return null;
- if (!open && contentHeight === 0) {
- return null;
+ // Flatten one level of children for the cascading reveal
+ const rows = [];
+ for (const item of items) {
+ rows.push({ item, depth: 0 });
+ if (item.children) {
+ for (const child of item.children) {
+ rows.push({ item: child, depth: 1 });
+ }
+ }
}
return (
- <>
- {/* Invisible click-away layer (no dim) — sits below the header's z-index */}
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
-