Add Navigation organism — responsive site header with mobile drawer

- Maps to Figma Main Nav (14:108) desktop + Mobile Header (2391:41508)
- Desktop: logo left, nav links right, optional CTA button
- Mobile: hamburger + drawer with nav items, CTA, and help footer
- Sticky header with warm surface background and border
- Composes AppBar, Link, IconButton, Button, Divider, Drawer
- 6 stories: Default, WithCTA, WithPageContent (sticky scroll demo),
  Minimal, ExtendedNavigation, MobilePriceTracker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:03:34 +11:00
parent 43e7191ead
commit 177c3b20e3
3 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
import React from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import useMediaQuery from '@mui/material/useMediaQuery';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import type { SxProps, Theme } from '@mui/material/styles';
import { IconButton } from '../../atoms/IconButton';
import { Link } from '../../atoms/Link';
import { Button } from '../../atoms/Button';
import { Typography } from '../../atoms/Typography';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** A navigation link item */
export interface NavItem {
/** Display label */
label: string;
/** URL to navigate to */
href: string;
/** Click handler (alternative to href for SPA navigation) */
onClick?: () => void;
}
/** Props for the FA Navigation organism */
export interface NavigationProps {
/** Site logo — rendered on the left (desktop) or centre (mobile) */
logo: React.ReactNode;
/** Click handler for the logo (navigate to home) */
onLogoClick?: () => void;
/** Navigation links displayed in the header */
items?: NavItem[];
/** Optional CTA button (e.g. "Start planning") on desktop */
ctaLabel?: string;
/** Click handler for the CTA button */
onCtaClick?: () => void;
/** Optional right-aligned content for mobile (e.g. price tracker) */
mobileTrailing?: React.ReactNode;
/** MUI sx prop for the root AppBar */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Site header navigation for the FA design system.
*
* Responsive header with logo, navigation links, and optional CTA.
* Desktop shows links inline; mobile collapses to hamburger + drawer.
*
* Maps to Figma "Main Nav" (14:108) desktop and "Mobile Header"
* (2391:41508) mobile patterns.
*
* Composes AppBar + Link + IconButton + Button + Divider + Drawer.
*
* Usage:
* ```tsx
* <Navigation
* logo={<img src="/logo.svg" alt="Funeral Arranger" height={40} />}
* onLogoClick={() => navigate('/')}
* items={[
* { label: 'FAQ', href: '/faq' },
* { label: 'Contact Us', href: '/contact' },
* { label: 'Log in', href: '/login' },
* ]}
* ctaLabel="Start planning"
* onCtaClick={() => navigate('/arrange')}
* />
* ```
*/
export const Navigation = React.forwardRef<HTMLDivElement, NavigationProps>(
({ logo, onLogoClick, items = [], ctaLabel, onCtaClick, mobileTrailing, sx }, ref) => {
const [drawerOpen, setDrawerOpen] = React.useState(false);
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
const handleDrawerToggle = () => setDrawerOpen((prev) => !prev);
return (
<>
<AppBar
ref={ref}
position="sticky"
elevation={0}
sx={[
{
bgcolor: 'var(--fa-color-surface-warm)',
color: 'text.primary',
borderBottom: '1px solid',
borderColor: 'divider',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Toolbar
sx={{
minHeight: { xs: 56, md: 80 },
px: { xs: 2, md: 4 },
justifyContent: 'space-between',
}}
>
{/* Left: hamburger (mobile) + logo */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{isMobile && (
<IconButton
aria-label={drawerOpen ? 'Close menu' : 'Open menu'}
onClick={handleDrawerToggle}
size="medium"
edge="start"
>
<MenuIcon />
</IconButton>
)}
<Box
onClick={onLogoClick}
sx={{
display: 'flex',
alignItems: 'center',
cursor: onLogoClick ? 'pointer' : 'default',
}}
role={onLogoClick ? 'link' : undefined}
aria-label={onLogoClick ? 'Home' : undefined}
>
{logo}
</Box>
</Box>
{/* Right: nav links (desktop) or trailing content (mobile) */}
{!isMobile ? (
<Box
component="nav"
aria-label="Main navigation"
sx={{ display: 'flex', alignItems: 'center', gap: 3.5 }}
>
{items.map((item) => (
<Link
key={item.label}
href={item.href}
onClick={item.onClick}
underline="hover"
sx={{
color: 'var(--fa-color-brand-900)',
fontWeight: 600,
fontSize: '1rem',
'&:hover': {
color: 'primary.main',
},
}}
>
{item.label}
</Link>
))}
{ctaLabel && (
<Button
variant="contained"
size="medium"
onClick={onCtaClick}
>
{ctaLabel}
</Button>
)}
</Box>
) : (
mobileTrailing && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{mobileTrailing}
</Box>
)
)}
</Toolbar>
</AppBar>
{/* Mobile drawer */}
{isMobile && (
<Drawer
anchor="left"
open={drawerOpen}
onClose={handleDrawerToggle}
PaperProps={{
sx: {
width: 300,
bgcolor: 'background.default',
},
}}
>
{/* Drawer header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
py: 1.5,
bgcolor: 'var(--fa-color-surface-warm)',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{logo}
</Box>
<IconButton
aria-label="Close menu"
onClick={handleDrawerToggle}
size="small"
>
<CloseIcon />
</IconButton>
</Box>
<Divider />
{/* Nav items */}
<List component="nav" aria-label="Main navigation">
{items.map((item) => (
<ListItemButton
key={item.label}
component="a"
href={item.href}
onClick={(e: React.MouseEvent) => {
if (item.onClick) {
e.preventDefault();
item.onClick();
}
setDrawerOpen(false);
}}
sx={{
py: 1.5,
px: 3,
'&:hover': {
bgcolor: 'var(--fa-color-brand-100)',
},
}}
>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontWeight: 500,
fontSize: '1rem',
}}
/>
</ListItemButton>
))}
</List>
{ctaLabel && (
<Box sx={{ px: 3, py: 2 }}>
<Button
variant="contained"
size="large"
fullWidth
onClick={() => {
if (onCtaClick) onCtaClick();
setDrawerOpen(false);
}}
>
{ctaLabel}
</Button>
</Box>
)}
{/* Footer */}
<Box sx={{ mt: 'auto' }}>
<Divider />
<Box sx={{ px: 3, py: 2, bgcolor: 'var(--fa-color-surface-warm)' }}>
<Typography variant="body2" color="text.secondary">
Need help? Call us on
</Typography>
<Link
href="tel:1800987888"
sx={{
fontWeight: 600,
fontSize: '1rem',
}}
>
1800 987 888
</Link>
</Box>
</Box>
</Drawer>
)}
</>
);
},
);
Navigation.displayName = 'Navigation';
export default Navigation;