Skip to content

Commit 974ed57

Browse files
feat: enhance filter bar and layout components
- Add sticky detection and scroll percentage calculation to FilterBar - Refactor Layout component to serve as a global provider - Implement breadcrumb navigation in InteractivePage and SpecPage
1 parent d959b87 commit 974ed57

5 files changed

Lines changed: 134 additions & 50 deletions

File tree

app/src/components/FilterBar.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ export function FilterBar({
5656
const theme = useTheme();
5757
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
5858

59-
// Scroll percentage - estimate based on total plots, not just loaded ones
59+
// Scroll percentage and sticky detection
6060
const [scrollPercent, setScrollPercent] = useState(0);
61+
const [isSticky, setIsSticky] = useState(false);
62+
const filterBarRef = useRef<HTMLDivElement>(null);
6163

6264
useEffect(() => {
6365
const calculatePercent = () => {
@@ -73,6 +75,10 @@ export function FilterBar({
7375

7476
const percent = Math.round((scrollY / estimatedTotalHeight) * 100);
7577
setScrollPercent(Math.min(100, Math.max(0, percent || 0)));
78+
79+
// Detect if bar is in sticky mode (scrolled past threshold)
80+
// The bar becomes sticky when scrollY > ~200px (header height)
81+
setIsSticky(scrollY > 200);
7682
};
7783
calculatePercent();
7884
window.addEventListener('scroll', calculatePercent);
@@ -288,16 +294,27 @@ export function FilterBar({
288294

289295
return (
290296
<Box
297+
ref={filterBarRef}
291298
sx={{
292299
mb: 4,
293-
px: 2,
294-
py: 1.5,
295300
position: 'sticky',
296301
top: 0,
297302
zIndex: 100,
298-
bgcolor: '#fafafa',
299-
backdropFilter: 'blur(8px)',
300-
backgroundColor: 'rgba(250, 250, 250, 0.9)',
303+
py: 1,
304+
transition: 'background-color 0.2s, border-color 0.2s, margin 0.2s, padding 0.2s',
305+
// Only apply full-width styling when sticky
306+
...(isSticky
307+
? {
308+
mx: { xs: -2, sm: -4, md: -8, lg: -12 },
309+
px: { xs: 2, sm: 4, md: 8, lg: 12 },
310+
bgcolor: '#f3f4f6',
311+
borderBottom: '1px solid #e5e7eb',
312+
}
313+
: {
314+
px: 2,
315+
bgcolor: 'transparent',
316+
borderBottom: '1px solid transparent',
317+
}),
301318
}}
302319
>
303320
{/* Filter chips row */}

app/src/components/Layout.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, createContext, useContext, useRef, useCallback } from 'react';
1+
import { useState, useEffect, createContext, useContext, useRef, useCallback, type ReactNode } from 'react';
22
import { Outlet } from 'react-router-dom';
33
import Box from '@mui/material/Box';
44
import Container from '@mui/material/Container';
@@ -50,20 +50,21 @@ const HomeStateContext = createContext<HomeStateContext | null>(null);
5050
export function useAppData() {
5151
const context = useContext(AppDataContext);
5252
if (!context) {
53-
throw new Error('useAppData must be used within Layout');
53+
throw new Error('useAppData must be used within AppDataProvider');
5454
}
5555
return context;
5656
}
5757

5858
export function useHomeState() {
5959
const context = useContext(HomeStateContext);
6060
if (!context) {
61-
throw new Error('useHomeState must be used within Layout');
61+
throw new Error('useHomeState must be used within AppDataProvider');
6262
}
6363
return context;
6464
}
6565

66-
export function Layout() {
66+
// Global provider that wraps the entire router (persists across all pages including InteractivePage)
67+
export function AppDataProvider({ children }: { children: ReactNode }) {
6768
const [specsData, setSpecsData] = useState<SpecInfo[]>([]);
6869
const [librariesData, setLibrariesData] = useState<LibraryInfo[]>([]);
6970
const [stats, setStats] = useState<{ specs: number; plots: number; libraries: number } | null>(null);
@@ -117,12 +118,19 @@ export function Layout() {
117118
return (
118119
<AppDataContext.Provider value={{ specsData, librariesData, stats }}>
119120
<HomeStateContext.Provider value={{ homeState, homeStateRef, setHomeState, saveScrollPosition }}>
120-
<Box sx={{ minHeight: '100vh', bgcolor: '#fafafa', py: 5, position: 'relative' }}>
121-
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 8, lg: 12 } }}>
122-
<Outlet />
123-
</Container>
124-
</Box>
121+
{children}
125122
</HomeStateContext.Provider>
126123
</AppDataContext.Provider>
127124
);
128125
}
126+
127+
// Layout component for pages with standard layout (HomePage, SpecPage, CatalogPage)
128+
export function Layout() {
129+
return (
130+
<Box sx={{ minHeight: '100vh', bgcolor: '#fafafa', py: 5, position: 'relative' }}>
131+
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 8, lg: 12 } }}>
132+
<Outlet />
133+
</Container>
134+
</Box>
135+
);
136+
}

app/src/pages/InteractivePage.tsx

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useRef, useCallback } from 'react';
2-
import { useParams, useNavigate } from 'react-router-dom';
2+
import { useParams, useNavigate, Link } from 'react-router-dom';
33
import { Helmet } from 'react-helmet-async';
44
import Box from '@mui/material/Box';
55
import IconButton from '@mui/material/IconButton';
@@ -186,7 +186,7 @@ export function InteractivePage() {
186186
flexDirection: 'column',
187187
}}
188188
>
189-
{/* Top bar with controls */}
189+
{/* Breadcrumb navigation */}
190190
<Box
191191
sx={{
192192
display: 'flex',
@@ -196,29 +196,56 @@ export function InteractivePage() {
196196
py: 1,
197197
bgcolor: '#f3f4f6',
198198
borderBottom: '1px solid #e5e7eb',
199+
fontFamily: '"MonoLisa", monospace',
200+
fontSize: '0.85rem',
199201
}}
200202
>
201-
<Box
202-
sx={{
203-
fontFamily: '"MonoLisa", monospace',
204-
fontSize: '0.85rem',
205-
color: '#4b5563',
206-
}}
207-
>
208-
{specId} · {library}
209-
</Box>
210-
<Box sx={{ display: 'flex', gap: 0.5 }}>
211-
<Tooltip title="View Raw HTML">
212-
<IconButton onClick={handleOpenExternal} size="small">
213-
<OpenInNewIcon fontSize="small" />
214-
</IconButton>
215-
</Tooltip>
216-
<Tooltip title="Close">
217-
<IconButton onClick={handleClose} size="small">
218-
<CloseIcon fontSize="small" />
219-
</IconButton>
220-
</Tooltip>
203+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
204+
<Box
205+
component={Link}
206+
to="/"
207+
sx={{
208+
color: '#3776AB',
209+
textDecoration: 'none',
210+
'&:hover': { textDecoration: 'underline' },
211+
}}
212+
>
213+
pyplots.ai
214+
</Box>
215+
<Box component="span" sx={{ mx: 1, color: '#9ca3af' }}></Box>
216+
<Box
217+
component={Link}
218+
to={`/${specId}`}
219+
sx={{
220+
color: '#3776AB',
221+
textDecoration: 'none',
222+
'&:hover': { textDecoration: 'underline' },
223+
}}
224+
>
225+
{specId}
226+
</Box>
227+
<Box component="span" sx={{ mx: 1, color: '#9ca3af' }}></Box>
228+
<Box
229+
component={Link}
230+
to={`/${specId}/${library}`}
231+
sx={{
232+
color: '#3776AB',
233+
textDecoration: 'none',
234+
'&:hover': { textDecoration: 'underline' },
235+
}}
236+
>
237+
{library}
238+
</Box>
239+
<Box component="span" sx={{ mx: 1, color: '#9ca3af' }}></Box>
240+
<Box component="span" sx={{ color: '#4b5563' }}>
241+
interactive
242+
</Box>
221243
</Box>
244+
<Tooltip title="View Raw HTML">
245+
<IconButton onClick={handleOpenExternal} size="small">
246+
<OpenInNewIcon fontSize="small" />
247+
</IconButton>
248+
</Tooltip>
222249
</Box>
223250

224251
{/* Fullscreen iframe - scaled to fit container */}

app/src/pages/SpecPage.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import IconButton from '@mui/material/IconButton';
88
import Tooltip from '@mui/material/Tooltip';
99
import Skeleton from '@mui/material/Skeleton';
1010
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
11+
import CloseIcon from '@mui/icons-material/Close';
1112
import DownloadIcon from '@mui/icons-material/Download';
1213
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
1314
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
@@ -196,21 +197,50 @@ export function SpecPage() {
196197
</Helmet>
197198

198199
<Box sx={{ pb: 4 }}>
199-
{/* Back Button */}
200-
<Button
201-
component={Link}
202-
to="/"
203-
startIcon={<ArrowBackIcon />}
200+
{/* Breadcrumb navigation */}
201+
<Box
204202
sx={{
205-
color: '#6b7280',
203+
display: 'flex',
204+
alignItems: 'center',
205+
mx: { xs: -2, sm: -4, md: -8, lg: -12 },
206+
mt: -5, // Compensate for Layout padding
207+
px: 2,
208+
py: 1,
206209
mb: 2,
210+
bgcolor: '#f3f4f6',
211+
borderBottom: '1px solid #e5e7eb',
207212
fontFamily: '"MonoLisa", monospace',
208-
textTransform: 'none',
209-
'&:hover': { color: '#3776AB', bgcolor: 'transparent' },
213+
fontSize: '0.85rem',
210214
}}
211215
>
212-
Back
213-
</Button>
216+
<Box
217+
component={Link}
218+
to="/"
219+
sx={{
220+
color: '#3776AB',
221+
textDecoration: 'none',
222+
'&:hover': { textDecoration: 'underline' },
223+
}}
224+
>
225+
pyplots.ai
226+
</Box>
227+
<Box component="span" sx={{ mx: 1, color: '#9ca3af' }}></Box>
228+
<Box
229+
component={Link}
230+
to={`/${specId}`}
231+
sx={{
232+
color: '#3776AB',
233+
textDecoration: 'none',
234+
'&:hover': { textDecoration: 'underline' },
235+
}}
236+
>
237+
{specId}
238+
</Box>
239+
<Box component="span" sx={{ mx: 1, color: '#9ca3af' }}></Box>
240+
<Box component="span" sx={{ color: '#4b5563' }}>
241+
{selectedLibrary}
242+
</Box>
243+
</Box>
214244

215245
{/* Title */}
216246
<Typography

app/src/router.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
22
import { HelmetProvider } from 'react-helmet-async';
3-
import { Layout } from './components/Layout';
3+
import { Layout, AppDataProvider } from './components/Layout';
44
import { HomePage } from './pages/HomePage';
55
import { SpecPage } from './pages/SpecPage';
66
import { CatalogPage } from './pages/CatalogPage';
@@ -17,14 +17,16 @@ const router = createBrowserRouter([
1717
{ path: ':specId/:library', element: <SpecPage /> },
1818
],
1919
},
20-
// Fullscreen interactive view (outside Layout)
20+
// Fullscreen interactive view (outside Layout but inside AppDataProvider)
2121
{ path: 'interactive/:specId/:library', element: <InteractivePage /> },
2222
]);
2323

2424
export function AppRouter() {
2525
return (
2626
<HelmetProvider>
27-
<RouterProvider router={router} />
27+
<AppDataProvider>
28+
<RouterProvider router={router} />
29+
</AppDataProvider>
2830
</HelmetProvider>
2931
);
3032
}

0 commit comments

Comments
 (0)