diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index cd68a1d70..96a4667d1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -25,11 +25,13 @@ jobs: # and should no longer be modified in app/. # Calculator components (report builder, household, charts, etc.) are # still legitimately modified in app/ since calculator-app/ imports from there. + # Note: HomeHeader.tsx and homeHeader/ were ported to website but + # are still actively used by the calculator-app (StandardLayout → + # HomeHeader). They are now de facto calculator components, so + # they're intentionally NOT in this guard list. WEBSITE_BLOCKED=$(echo "$ALL_CHANGED" | grep -E \ -e '^app/src/components/home/' \ -e '^app/src/components/shared/static/' \ - -e '^app/src/components/shared/HomeHeader\.tsx' \ - -e '^app/src/components/homeHeader/' \ -e '^app/src/components/Footer\.tsx' \ -e '^app/src/components/FooterSubscribe\.tsx' \ -e '^app/src/components/blog/BlogPostCard\.tsx' \ diff --git a/CLAUDE.md b/CLAUDE.md index 719a38e56..a766ad8b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,8 @@ The `app/` directory contains the legacy Vite build. Some parts have been ported to Next.js, others are still active. **Ported to website/ (do NOT modify in app/):** -- Website components: `home/`, `shared/static/`, `homeHeader/`, `Footer.tsx`, `FooterSubscribe.tsx`, `blog/BlogPostCard.tsx`, `blog/BlogPostGrid.tsx`, `blog/ResearchFilters.tsx` +- Website components: `home/`, `shared/static/`, `Footer.tsx`, `FooterSubscribe.tsx`, `blog/BlogPostCard.tsx`, `blog/BlogPostGrid.tsx`, `blog/ResearchFilters.tsx` + - Note: `homeHeader/` and `shared/HomeHeader.tsx` were also ported to `website/src/components/Header.tsx`, but the calculator-app still renders `HomeHeader` via `StandardLayout`, so these remain editable in `app/`. Keep the calculator header in sync with the website header when the website one changes. - Website pages: `Home`, `Research`, `Blog`, `Team`, `Supporters`, `Donate`, `Privacy`, `Terms`, `Brand*`, `Citations`, `AppPage` - `vercel.json` (root) — new rewrites go in `website/next.config.ts` diff --git a/app/src/components/homeHeader/MobileMenu.tsx b/app/src/components/homeHeader/MobileMenu.tsx index e23cc37d4..9d62652a3 100644 --- a/app/src/components/homeHeader/MobileMenu.tsx +++ b/app/src/components/homeHeader/MobileMenu.tsx @@ -58,7 +58,7 @@ export default function MobileMenu({ opened, onOpen, onClose, navItems }: Mobile className="tw:flex tw:flex-col" style={{ gap: spacing.xs, paddingLeft: spacing.md }} > - {item.dropdownItems.map((dropdownItem) => ( + {item.dropdownItems.flatMap((dropdownItem) => [ {dropdownItem.label} - - ))} + , + ...(dropdownItem.children ?? []).map((grandchild) => ( + + {grandchild.label} + + )), + ])} ) : ( diff --git a/app/src/components/homeHeader/NavItem.tsx b/app/src/components/homeHeader/NavItem.tsx index 2dd4ecc01..d1b2d1f84 100644 --- a/app/src/components/homeHeader/NavItem.tsx +++ b/app/src/components/homeHeader/NavItem.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { IconChevronDown } from '@tabler/icons-react'; import { AppLink } from '@/components/AppLink'; +import { useAppPathname } from '@/contexts/LocationContext'; import { useAppNavigate } from '@/contexts/NavigationContext'; import { colors, typography } from '@/designTokens'; @@ -8,6 +9,8 @@ export interface DropdownItem { label: string; onClick?: () => void; href?: string; + /** Nested children rendered indented under this item. One level deep only. */ + children?: DropdownItem[]; } export interface NavItemSetup { @@ -22,47 +25,130 @@ interface NavItemProps { setup: NavItemSetup; } +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: React.CSSProperties = { 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: React.MouseEvent) => { - e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.12)'; - }, - onMouseLeave: (e: React.MouseEvent) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }, -}; +function NavUnderline({ visible }: { visible: boolean }) { + return ( +