Skip to content

Commit ab17d05

Browse files
feat(ui): enhance responsiveness and add scroll features
- Implement responsive design adjustments for mobile and desktop views - Add scroll percentage tracking for better user experience - Introduce a floating scroll-to-top button - Update plot types catalog with new entries and descriptions
1 parent 806f55c commit ab17d05

File tree

5 files changed

+321
-115
lines changed

5 files changed

+321
-115
lines changed

app/src/App.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback, useRef } from 'react';
22
import Box from '@mui/material/Box';
33
import Container from '@mui/material/Container';
44
import Alert from '@mui/material/Alert';
5+
import Fab from '@mui/material/Fab';
6+
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
57

68
import type { PlotImage, LibraryInfo, SpecInfo } from './types';
79
import { API_URL, type ImageSize } from './constants';
@@ -62,6 +64,7 @@ function App() {
6264
const stored = localStorage.getItem('imageSize');
6365
return stored === 'normal' || stored === 'compact' ? stored : 'normal';
6466
});
67+
const [showScrollTop, setShowScrollTop] = useState(false);
6568

6669
// Refs
6770
const searchInputRef = useRef<HTMLInputElement>(null);
@@ -71,6 +74,15 @@ function App() {
7174
localStorage.setItem('imageSize', imageSize);
7275
}, [imageSize]);
7376

77+
// Show/hide scroll-to-top button based on scroll position
78+
useEffect(() => {
79+
const handleScroll = () => {
80+
setShowScrollTop(window.scrollY > 300);
81+
};
82+
window.addEventListener('scroll', handleScroll);
83+
return () => window.removeEventListener('scroll', handleScroll);
84+
}, []);
85+
7486
// Handle card click - open modal
7587
const handleCardClick = useCallback(
7688
(img: PlotImage) => {
@@ -214,6 +226,25 @@ function App() {
214226
onClose={() => setModalImage(null)}
215227
onTrackEvent={trackEvent}
216228
/>
229+
230+
{/* Floating scroll-to-top button */}
231+
<Fab
232+
size="small"
233+
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
234+
sx={{
235+
position: 'fixed',
236+
bottom: 24,
237+
right: 24,
238+
bgcolor: '#f3f4f6',
239+
color: '#6b7280',
240+
opacity: showScrollTop ? 1 : 0,
241+
visibility: showScrollTop ? 'visible' : 'hidden',
242+
transition: 'opacity 0.3s, visibility 0.3s',
243+
'&:hover': { bgcolor: '#e5e7eb', color: '#3776AB' },
244+
}}
245+
>
246+
<KeyboardArrowUpIcon />
247+
</Fab>
217248
</Box>
218249
);
219250
}

app/src/components/FilterBar.tsx

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import SearchIcon from '@mui/icons-material/Search';
1212
import AddIcon from '@mui/icons-material/Add';
1313
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
1414
import ViewModuleIcon from '@mui/icons-material/ViewModule';
15+
import useMediaQuery from '@mui/material/useMediaQuery';
16+
import { useTheme } from '@mui/material/styles';
1517

1618
import type { FilterCategory, ActiveFilters, FilterCounts } from '../types';
1719
import { FILTER_LABELS, FILTER_CATEGORIES } from '../types';
@@ -51,6 +53,37 @@ export function FilterBar({
5153
onRemoveGroup,
5254
onTrackEvent,
5355
}: FilterBarProps) {
56+
const theme = useTheme();
57+
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
58+
59+
// Scroll percentage - estimate based on total plots, not just loaded ones
60+
const [scrollPercent, setScrollPercent] = useState(0);
61+
62+
useEffect(() => {
63+
const calculatePercent = () => {
64+
const scrollY = window.scrollY;
65+
const docHeight = document.documentElement.scrollHeight;
66+
const windowHeight = window.innerHeight;
67+
68+
// Estimate total height based on ratio of loaded vs total plots
69+
const loadRatio = displayedCount > 0 && currentTotal > 0
70+
? currentTotal / displayedCount
71+
: 1;
72+
const estimatedTotalHeight = (docHeight - windowHeight) * loadRatio;
73+
74+
const percent = Math.round((scrollY / estimatedTotalHeight) * 100);
75+
setScrollPercent(Math.min(100, Math.max(0, percent || 0)));
76+
};
77+
calculatePercent();
78+
window.addEventListener('scroll', calculatePercent);
79+
const resizeObserver = new ResizeObserver(calculatePercent);
80+
resizeObserver.observe(document.body);
81+
return () => {
82+
window.removeEventListener('scroll', calculatePercent);
83+
resizeObserver.disconnect();
84+
};
85+
}, [displayedCount, currentTotal]);
86+
5487
// Search/dropdown state
5588
const [searchQuery, setSearchQuery] = useState('');
5689
const [dropdownAnchor, setDropdownAnchor] = useState<HTMLElement | null>(null);
@@ -260,11 +293,11 @@ export function FilterBar({
260293
gap: 1,
261294
justifyContent: 'center',
262295
alignItems: 'center',
263-
position: 'relative',
296+
position: { xs: 'static', md: 'relative' },
264297
}}
265298
>
266-
{/* Progress counter - absolute left */}
267-
{currentTotal > 0 && (
299+
{/* Progress counter - absolute left (desktop only) */}
300+
{!isMobile && currentTotal > 0 && (
268301
<Typography
269302
sx={{
270303
position: 'absolute',
@@ -275,31 +308,33 @@ export function FilterBar({
275308
whiteSpace: 'nowrap',
276309
}}
277310
>
278-
{Math.min(displayedCount, currentTotal)} / {currentTotal}
311+
{scrollPercent}% · {currentTotal}
279312
</Typography>
280313
)}
281-
{/* Grid size toggle - absolute right */}
282-
<Box
283-
onClick={() => onImageSizeChange(imageSize === 'normal' ? 'compact' : 'normal')}
284-
sx={{
285-
position: 'absolute',
286-
right: 0,
287-
display: 'flex',
288-
alignItems: 'center',
289-
justifyContent: 'center',
290-
width: 32,
291-
height: 32,
292-
cursor: 'pointer',
293-
color: '#9ca3af',
294-
'&:hover': { color: '#3776AB' },
295-
}}
296-
>
297-
{imageSize === 'normal' ? (
298-
<ViewAgendaIcon sx={{ fontSize: '1.25rem' }} />
299-
) : (
300-
<ViewModuleIcon sx={{ fontSize: '1.25rem' }} />
301-
)}
302-
</Box>
314+
{/* Grid size toggle - absolute right (desktop only) */}
315+
{!isMobile && (
316+
<Box
317+
onClick={() => onImageSizeChange(imageSize === 'normal' ? 'compact' : 'normal')}
318+
sx={{
319+
position: 'absolute',
320+
right: 0,
321+
display: 'flex',
322+
alignItems: 'center',
323+
justifyContent: 'center',
324+
width: 32,
325+
height: 32,
326+
cursor: 'pointer',
327+
color: '#9ca3af',
328+
'&:hover': { color: '#3776AB' },
329+
}}
330+
>
331+
{imageSize === 'normal' ? (
332+
<ViewAgendaIcon sx={{ fontSize: '1.25rem' }} />
333+
) : (
334+
<ViewModuleIcon sx={{ fontSize: '1.25rem' }} />
335+
)}
336+
</Box>
337+
)}
303338
{/* Active filter chips */}
304339
{activeFilters.map((group, index) => {
305340
const isAnimating = randomAnimation?.index === index;
@@ -363,8 +398,8 @@ export function FilterBar({
363398
gap: 0.5,
364399
px: isSearchExpanded ? 1.5 : 0,
365400
height: 32,
366-
width: isSearchExpanded ? 'auto' : 32,
367-
minWidth: isSearchExpanded ? 120 : 32,
401+
width: isSearchExpanded ? { xs: 80, sm: 160, md: 'auto' } : 32,
402+
minWidth: isSearchExpanded ? { xs: 80, sm: 160, md: 120 } : 32,
368403
border: isSearchExpanded ? '1px dashed #9ca3af' : 'none',
369404
borderRadius: '16px',
370405
bgcolor: isDropdownOpen ? '#f9fafb' : 'transparent',
@@ -441,6 +476,52 @@ export function FilterBar({
441476
)}
442477
</Box>
443478

479+
{/* Counter and toggle row (mobile only) */}
480+
{isMobile && (
481+
<Box
482+
sx={{
483+
display: 'flex',
484+
justifyContent: 'space-between',
485+
alignItems: 'center',
486+
mt: 1,
487+
}}
488+
>
489+
{currentTotal > 0 ? (
490+
<Typography
491+
sx={{
492+
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
493+
fontSize: '0.75rem',
494+
color: '#9ca3af',
495+
whiteSpace: 'nowrap',
496+
}}
497+
>
498+
{scrollPercent}% · {currentTotal}
499+
</Typography>
500+
) : (
501+
<Box />
502+
)}
503+
<Box
504+
onClick={() => onImageSizeChange(imageSize === 'normal' ? 'compact' : 'normal')}
505+
sx={{
506+
display: 'flex',
507+
alignItems: 'center',
508+
justifyContent: 'center',
509+
width: 32,
510+
height: 32,
511+
cursor: 'pointer',
512+
color: '#9ca3af',
513+
'&:hover': { color: '#3776AB' },
514+
}}
515+
>
516+
{imageSize === 'normal' ? (
517+
<ViewAgendaIcon sx={{ fontSize: '1.25rem' }} />
518+
) : (
519+
<ViewModuleIcon sx={{ fontSize: '1.25rem' }} />
520+
)}
521+
</Box>
522+
</Box>
523+
)}
524+
444525
{/* Dropdown menu */}
445526
<Menu
446527
anchorEl={dropdownAnchor}

app/src/components/Header.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ interface HeaderProps {
1515

1616
export const Header = memo(function Header({ stats, onRandom }: HeaderProps) {
1717
const theme = useTheme();
18-
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
18+
const isXs = useMediaQuery(theme.breakpoints.down('sm'));
19+
const isSm = useMediaQuery(theme.breakpoints.between('sm', 'md'));
1920
const [tooltipOpen, setTooltipOpen] = useState(false);
2021
const [pinned, setPinned] = useState(false); // true = opened via click, stays open
2122
const tooltipText = stats
@@ -52,7 +53,7 @@ export const Header = memo(function Header({ stats, onRandom }: HeaderProps) {
5253
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
5354
mb: { xs: 2, md: 3 },
5455
letterSpacing: '-0.02em',
55-
fontSize: { xs: '2rem', md: '3.75rem' },
56+
fontSize: { xs: '2rem', sm: '2.75rem', md: '3.75rem' },
5657
}}
5758
>
5859
<Link
@@ -119,7 +120,7 @@ export const Header = memo(function Header({ stats, onRandom }: HeaderProps) {
119120
fontSize: { xs: '0.875rem', md: '1rem' },
120121
}}
121122
>
122-
{isMobile ? 'ai-powered python plots' : 'library-agnostic, ai-powered python plotting examples.'}
123+
{isXs ? 'ai-powered python plots' : isSm ? 'library-agnostic, ai-powered python plotting.' : 'library-agnostic, ai-powered python plotting examples.'}
123124
</Typography>
124125
<Typography
125126
variant="body1"
@@ -184,7 +185,7 @@ export const Header = memo(function Header({ stats, onRandom }: HeaderProps) {
184185
185186
</Box>
186187
)}
187-
{isMobile ? '. copy. create.' : '. grab the code. make it yours.'}
188+
{isXs ? '. copy. create.' : '. grab the code. make it yours.'}
188189
</Typography>
189190
</Box>
190191
);

0 commit comments

Comments
 (0)