Skip to content

Commit 74d9841

Browse files
feat(navbar): add navigation bar component
- implement navigation links for catalog, specs, palette, and mcp - integrate search functionality for catalog - style navigation links with hover effects
1 parent d7e46ea commit 74d9841

File tree

14 files changed

+1228
-522
lines changed

14 files changed

+1228
-522
lines changed

app/src/components/HeroSection.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { buildSrcSet, getFallbackSrc } from '../utils/responsiveImage';
99

1010
interface HeroSectionProps {
1111
stats: { specs: number; plots: number; libraries: number } | null;
12-
onBrowse: () => void;
1312
}
1413

1514
interface PlotOfTheDayData {
@@ -20,7 +19,7 @@ interface PlotOfTheDayData {
2019
preview_url: string | null;
2120
}
2221

23-
export function HeroSection({ stats, onBrowse }: HeroSectionProps) {
22+
export function HeroSection({ stats }: HeroSectionProps) {
2423
const [potd, setPotd] = useState<PlotOfTheDayData | null>(null);
2524

2625
useEffect(() => {
@@ -97,17 +96,31 @@ export function HeroSection({ stats, onBrowse }: HeroSectionProps) {
9796
mb: 5,
9897
fontWeight: 300,
9998
}}>
100-
A curated catalogue of plotting examples across nine Python libraries —
101-
each reproducible, colorblind-safe, and calibrated to the same scientific palette.
99+
A curated catalogue of visualization examples — AI-generated, open source, and built for every library you already use.
100+
</Box>
101+
102+
{/* Tagline — prominent */}
103+
<Box sx={{
104+
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.25s backwards',
105+
fontFamily: typography.serif,
106+
fontSize: { xs: '1.125rem', md: '1.5rem' },
107+
lineHeight: 1.3,
108+
color: 'var(--ink)',
109+
maxWidth: '52ch',
110+
mb: 5,
111+
fontWeight: 400,
112+
fontStyle: 'italic',
113+
}}>
114+
Get inspired. Grab the code. Make it yours.
102115
</Box>
103116

104117
{/* CTAs */}
105118
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.3s backwards' }}>
106119
<Box
107-
component="button"
108-
onClick={onBrowse}
120+
component={RouterLink}
121+
to="/catalog"
109122
sx={{
110-
all: 'unset',
123+
textDecoration: 'none',
111124
boxSizing: 'border-box',
112125
fontFamily: typography.mono,
113126
fontSize: '13px',
@@ -131,13 +144,13 @@ export function HeroSection({ stats, onBrowse }: HeroSectionProps) {
131144
},
132145
}}
133146
>
134-
Browse the catalogue <Box component="span" sx={{ transition: 'transform 0.2s', '.MuiBox-root:hover > &': { transform: 'translateX(3px)' } }}></Box>
147+
Browse the catalogue <Box component="span" sx={{ transition: 'transform 0.2s' }}></Box>
135148
</Box>
136-
<Link
149+
<Box
137150
component={RouterLink}
138151
to="/mcp"
139152
sx={{
140-
all: 'unset',
153+
textDecoration: 'none',
141154
boxSizing: 'border-box',
142155
fontFamily: typography.mono,
143156
fontSize: '13px',
@@ -151,7 +164,7 @@ export function HeroSection({ stats, onBrowse }: HeroSectionProps) {
151164
}}
152165
>
153166
use via MCP
154-
</Link>
167+
</Box>
155168
</Box>
156169

157170
{/* Meta stats */}
@@ -170,9 +183,9 @@ export function HeroSection({ stats, onBrowse }: HeroSectionProps) {
170183
letterSpacing: '0.1em',
171184
}}>
172185
{[
173-
{ value: stats.plots.toLocaleString(), label: 'examples' },
186+
{ value: String(stats.specs), label: 'specs' },
187+
{ value: stats.plots.toLocaleString(), label: 'implementations' },
174188
{ value: String(stats.libraries), label: 'libraries' },
175-
{ value: '100%', label: 'colorblind-safe' },
176189
].map((item, i) => (
177190
<Box key={i}>
178191
<Box sx={{

app/src/components/Layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ export function AppDataProvider({ children }: { children: ReactNode }) {
8080
);
8181
}
8282

83-
// Layout component for pages with standard layout (HomePage, SpecPage, CatalogPage)
83+
// Layout component — kept for backward compat with tests. Not used as route wrapper.
8484
export function Layout() {
8585
return (
8686
<>
8787
<Helmet>
8888
<meta name="robots" content="index, follow" />
8989
</Helmet>
90-
<Box component="main" sx={{ minHeight: '100vh', bgcolor: 'var(--bg-page)', py: 5, position: 'relative' }}>
91-
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 8, lg: 12, xl: 16 }, maxWidth: 2200, mx: 'auto' }}>
90+
<Box component="main" sx={{ minHeight: '100vh', bgcolor: 'var(--bg-page)', position: 'relative' }}>
91+
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 8, lg: 12, xl: 16 }, maxWidth: 1600, mx: 'auto' }}>
9292
<Outlet />
9393
</Container>
9494
</Box>

app/src/components/MastheadRule.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function MastheadRule() {
1212
gridTemplateColumns: '1fr auto 1fr',
1313
alignItems: 'center',
1414
py: 1.5,
15-
mb: 4,
15+
mb: 0,
1616
borderBottom: '1px solid var(--rule)',
1717
fontFamily: typography.mono,
1818
fontSize: '11px',

app/src/components/NavBar.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { useCallback } from 'react';
2+
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
3+
import Box from '@mui/material/Box';
4+
import { colors, typography } from '../theme';
5+
6+
interface NavBarProps {
7+
/** Ref to FilterBar search input -- when on /catalog, focuses it; otherwise navigates to /catalog */
8+
searchInputRef?: React.RefObject<HTMLInputElement | null>;
9+
}
10+
11+
const NAV_LINKS = [
12+
{ label: 'catalog', to: '/catalog' },
13+
{ label: 'specs', to: '/specs' },
14+
{ label: 'palette', to: '/palette' },
15+
{ label: 'mcp', to: '/mcp' },
16+
];
17+
18+
const linkSx = {
19+
color: 'var(--ink-soft)',
20+
textDecoration: 'none',
21+
position: 'relative' as const,
22+
padding: '4px 0',
23+
transition: 'color 0.2s',
24+
'&::after': {
25+
content: '""',
26+
position: 'absolute' as const,
27+
bottom: 0,
28+
left: 0,
29+
right: 0,
30+
height: '1px',
31+
background: colors.primary,
32+
transform: 'scaleX(0)',
33+
transformOrigin: 'left',
34+
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
35+
},
36+
'&:hover': {
37+
color: 'var(--ink)',
38+
'&::after': { transform: 'scaleX(1)' },
39+
},
40+
} as const;
41+
42+
const activeLinkSx = {
43+
...linkSx,
44+
color: 'var(--ink)',
45+
'&::before': {
46+
content: '"•"',
47+
color: colors.primary,
48+
position: 'absolute' as const,
49+
left: -12,
50+
fontSize: '18px',
51+
lineHeight: 1,
52+
top: '3px',
53+
},
54+
} as const;
55+
56+
export function NavBar({ searchInputRef }: NavBarProps) {
57+
const navigate = useNavigate();
58+
const location = useLocation();
59+
60+
const handleSearch = useCallback(() => {
61+
if (location.pathname === '/catalog' && searchInputRef?.current) {
62+
searchInputRef.current.focus();
63+
} else {
64+
navigate('/catalog');
65+
}
66+
}, [location.pathname, searchInputRef, navigate]);
67+
68+
return (
69+
<Box component="nav" sx={{
70+
display: 'grid',
71+
gridTemplateColumns: 'auto 1fr auto',
72+
alignItems: 'center',
73+
py: 2,
74+
gap: 4,
75+
borderBottom: '1px solid var(--rule)',
76+
}}>
77+
{/* Logo */}
78+
<Box
79+
component={RouterLink}
80+
to="/"
81+
sx={{
82+
fontFamily: typography.mono,
83+
fontWeight: 700,
84+
fontSize: '22px',
85+
letterSpacing: '-0.02em',
86+
textDecoration: 'none',
87+
color: 'var(--ink)',
88+
}}
89+
>
90+
any<Box component="span" sx={{ color: colors.primary, display: 'inline-block', transform: 'scale(1.45)', mx: '2px' }}>.</Box>plot<Box component="span" sx={{ fontWeight: 400, opacity: 0.45 }}>()</Box>
91+
</Box>
92+
93+
{/* Nav links */}
94+
<Box component="ul" sx={{
95+
display: { xs: 'none', sm: 'flex' },
96+
gap: 3.5,
97+
listStyle: 'none',
98+
m: 0,
99+
p: 0,
100+
fontFamily: typography.mono,
101+
fontSize: '13px',
102+
}}>
103+
{NAV_LINKS.map(link => (
104+
<li key={link.to}>
105+
<Box
106+
component={RouterLink}
107+
to={link.to}
108+
sx={location.pathname === link.to ? activeLinkSx : linkSx}
109+
>
110+
{link.label}
111+
</Box>
112+
</li>
113+
))}
114+
</Box>
115+
116+
{/* Search pill */}
117+
<Box
118+
component="button"
119+
onClick={handleSearch}
120+
sx={{
121+
all: 'unset',
122+
boxSizing: 'border-box',
123+
fontFamily: typography.mono,
124+
fontSize: '12px',
125+
padding: '8px 14px',
126+
bgcolor: 'var(--bg-surface)',
127+
border: '1px solid var(--rule)',
128+
borderRadius: '99px',
129+
color: 'var(--ink-muted)',
130+
cursor: 'pointer',
131+
display: 'flex',
132+
alignItems: 'center',
133+
gap: 1,
134+
transition: 'all 0.2s',
135+
'&:hover': {
136+
borderColor: 'var(--ink-soft)',
137+
color: 'var(--ink)',
138+
},
139+
}}
140+
>
141+
<span>⌕ search plots</span>
142+
<Box component="span" sx={{
143+
fontFamily: typography.mono,
144+
fontSize: '10px',
145+
padding: '1px 5px',
146+
bgcolor: 'var(--bg-page)',
147+
border: '1px solid var(--rule)',
148+
borderRadius: '3px',
149+
letterSpacing: 0,
150+
display: { xs: 'none', md: 'inline' },
151+
}}>
152+
⌘ K
153+
</Box>
154+
</Box>
155+
</Box>
156+
);
157+
}

app/src/hooks/useUrlSync.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,28 +60,28 @@ describe('buildFilterUrl', () => {
6060

6161
it('builds single filter URL', () => {
6262
const filters: ActiveFilters = [{ category: 'lib', values: ['matplotlib'] }];
63-
expect(buildFilterUrl(filters)).toBe('?lib=matplotlib');
63+
expect(buildFilterUrl(filters)).toBe('/?lib=matplotlib');
6464
});
6565

6666
it('builds comma-separated OR values', () => {
6767
const filters: ActiveFilters = [{ category: 'lib', values: ['matplotlib', 'seaborn'] }];
68-
expect(buildFilterUrl(filters)).toBe('?lib=matplotlib%2Cseaborn');
68+
expect(buildFilterUrl(filters)).toBe('/?lib=matplotlib%2Cseaborn');
6969
});
7070

7171
it('builds multiple AND groups', () => {
7272
const filters: ActiveFilters = [
7373
{ category: 'lib', values: ['matplotlib'] },
7474
{ category: 'plot', values: ['scatter'] },
7575
];
76-
expect(buildFilterUrl(filters)).toBe('?lib=matplotlib&plot=scatter');
76+
expect(buildFilterUrl(filters)).toBe('/?lib=matplotlib&plot=scatter');
7777
});
7878

7979
it('skips groups with empty values', () => {
8080
const filters: ActiveFilters = [
8181
{ category: 'lib', values: [] },
8282
{ category: 'plot', values: ['scatter'] },
8383
];
84-
expect(buildFilterUrl(filters)).toBe('?plot=scatter');
84+
expect(buildFilterUrl(filters)).toBe('/?plot=scatter');
8585
});
8686
});
8787

app/src/hooks/useUrlSync.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export function buildFilterUrl(filters: ActiveFilters): string {
4646
}
4747
});
4848
const queryString = params.toString();
49-
return queryString ? `?${queryString}` : '/';
49+
const basePath = window.location.pathname;
50+
return queryString ? `${basePath}?${queryString}` : basePath;
5051
}
5152

5253
interface UseUrlSyncOptions {

0 commit comments

Comments
 (0)