Skip to content

Commit f12d08c

Browse files
feat: enhance landing page and hero section
- Implement Plot of the Day feature with terminal-style display - Add TypewriterText component for dynamic text effect - Refactor HeroSection to include Plot of the Day and improve layout - Update LibrariesSection to support new width tiers and header styles - Adjust MastheadRule and SectionHeader for improved styling
1 parent 966bc65 commit f12d08c

File tree

12 files changed

+806
-381
lines changed

12 files changed

+806
-381
lines changed

app/src/components/HeroSection.tsx

Lines changed: 193 additions & 244 deletions
Large diffs are not rendered by default.

app/src/components/LibrariesSection.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,43 @@ import { LIBRARIES } from '../constants';
77
interface LibrariesSectionProps {
88
libraries: LibraryInfo[];
99
onLibraryClick: (library: string) => void;
10+
/** Width tier per style-guide §6.1 — `paper` = 1240px, `catalog` = 2200px. */
11+
widthTier?: 'paper' | 'catalog';
12+
/** Header style per style-guide §6.3 — `prompt` uses `❯ libraries`, `number` keeps `§ 01`. */
13+
headerStyle?: 'prompt' | 'number';
1014
}
1115

12-
export function LibrariesSection({ libraries, onLibraryClick }: LibrariesSectionProps) {
16+
export function LibrariesSection({
17+
libraries,
18+
onLibraryClick,
19+
widthTier = 'paper',
20+
headerStyle = 'number',
21+
}: LibrariesSectionProps) {
1322
// Use known library order, with counts from stats if available
1423
const libList = LIBRARIES.map(name => {
1524
const info = libraries.find(l => l.id === name);
1625
return { name, count: info ? undefined : undefined }; // counts come from API if available
1726
});
1827

28+
const maxWidth = widthTier === 'catalog' ? 'var(--max-catalog)' : 'var(--max)';
29+
1930
return (
20-
<Box sx={{ maxWidth: 'var(--max)', mx: 'auto', py: { xs: 6, md: 10 } }}>
21-
<SectionHeader
22-
number="§ 01"
23-
title={<>The <em>libraries</em></>}
24-
linkText="view all"
25-
linkTo="/plots"
26-
/>
31+
<Box sx={{ maxWidth, mx: 'auto', py: { xs: 6, md: 10 } }}>
32+
{headerStyle === 'prompt' ? (
33+
<SectionHeader
34+
prompt="❯"
35+
title={<em>libraries</em>}
36+
linkText="view all"
37+
linkTo="/plots"
38+
/>
39+
) : (
40+
<SectionHeader
41+
number="§ 01"
42+
title={<>The <em>libraries</em></>}
43+
linkText="view all"
44+
linkTo="/plots"
45+
/>
46+
)}
2747

2848
<Box sx={{
2949
display: 'grid',

app/src/components/MastheadRule.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { typography } from '../theme';
33
import { ThemeToggle } from './ThemeToggle';
44
import { useTheme } from '../hooks';
55

6+
/**
7+
* Top masthead bar (style-guide §6.4).
8+
*
9+
* Lowercase, monospace, three slots separated by │. Reads as a status line
10+
* from a tool — positions the site as a curated publication that lives
11+
* inside a terminal.
12+
*/
613
export function MastheadRule() {
714
const { isDark, toggle } = useTheme();
815

@@ -11,27 +18,31 @@ export function MastheadRule() {
1118
display: 'grid',
1219
gridTemplateColumns: '1fr auto 1fr',
1320
alignItems: 'center',
14-
py: 1.5,
21+
py: 1.25,
1522
mb: 0,
1623
borderBottom: '1px solid var(--rule)',
1724
fontFamily: typography.mono,
1825
fontSize: '11px',
1926
color: 'var(--ink-muted)',
20-
textTransform: 'uppercase',
21-
letterSpacing: '0.12em',
27+
letterSpacing: '0.04em',
2228
}}>
2329
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
24-
Vol. 1 · 2026
30+
~/anyplot · v1 · spring 2026
2531
</Box>
2632
<Box sx={{
2733
px: 2,
2834
fontFeatureSettings: '"tnum"',
2935
textAlign: 'center',
3036
display: { xs: 'none', md: 'block' },
3137
}}>
32-
anyplot.ai — a catalogue of scientific plotting
38+
any library. one plot.
3339
</Box>
34-
<Box sx={{ textAlign: 'right', gridColumn: { xs: '1 / -1', sm: 'auto' }, display: 'flex', justifyContent: { xs: 'center', sm: 'flex-end' } }}>
40+
<Box sx={{
41+
textAlign: 'right',
42+
gridColumn: { xs: '1 / -1', sm: 'auto' },
43+
display: 'flex',
44+
justifyContent: { xs: 'center', sm: 'flex-end' },
45+
}}>
3546
<ThemeToggle isDark={isDark} onToggle={toggle} />
3647
</Box>
3748
</Box>
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Link as RouterLink } from 'react-router-dom';
2+
import Box from '@mui/material/Box';
3+
4+
import { colors, typography } from '../theme';
5+
import { buildSrcSet, getFallbackSrc } from '../utils/responsiveImage';
6+
import { specPath } from '../utils/paths';
7+
import type { PlotOfTheDayData } from '../hooks/usePlotOfTheDay';
8+
9+
interface PlotOfTheDayTerminalProps {
10+
potd: PlotOfTheDayData | null;
11+
/** Sizes attribute for the responsive picture sources. */
12+
sizes?: string;
13+
/** Optional max-width override (defaults to `100%`, i.e. fills parent). */
14+
maxWidth?: number | string;
15+
/**
16+
* Hard cap on the plot frame height. With `aspectRatio: 16/10` this also
17+
* caps the width via `calc(height * 1.6)`. Use viewport units so the hero
18+
* always fits one screen. Default `55vh`.
19+
*/
20+
maxPlotHeight?: string;
21+
}
22+
23+
/**
24+
* Terminal-framed plot-of-the-day card.
25+
*
26+
* Looks like a terminal window rendered on the editorial paper: thin border,
27+
* `~/anyplot` tab label, green prompt + filename header, plot image as console
28+
* output, status footer. Fills its container by default so parents control width.
29+
*/
30+
export function PlotOfTheDayTerminal({
31+
potd,
32+
sizes = '(max-width: 899px) 86vw, 1200px',
33+
maxWidth = '100%',
34+
maxPlotHeight = '55vh',
35+
}: PlotOfTheDayTerminalProps) {
36+
if (!potd?.preview_url) return null;
37+
38+
const filename = `plots/${potd.spec_id}/${potd.library_id}.py`;
39+
40+
return (
41+
<Box
42+
sx={{
43+
position: 'relative',
44+
width: '100%',
45+
maxWidth,
46+
border: '1px solid var(--ink-muted)',
47+
borderRadius: '6px',
48+
bgcolor: 'var(--bg-surface)',
49+
fontFamily: typography.mono,
50+
animation: 'rise 1s cubic-bezier(0.2, 0.8, 0.2, 1) 0.3s backwards',
51+
'&::before': {
52+
content: '"~/anyplot"',
53+
position: 'absolute',
54+
top: '-0.7em',
55+
left: 24,
56+
px: 1.25,
57+
bgcolor: 'var(--bg-page)',
58+
fontFamily: typography.mono,
59+
fontSize: '12px',
60+
color: 'var(--ink-muted)',
61+
letterSpacing: '0.05em',
62+
},
63+
}}
64+
>
65+
{/* Prompt header */}
66+
<Box
67+
component={RouterLink}
68+
to={specPath(potd.spec_id, potd.library_id)}
69+
sx={{
70+
display: 'flex',
71+
alignItems: 'center',
72+
gap: 0.75,
73+
px: { xs: 2, md: 3 },
74+
pt: { xs: 2.5, md: 3 },
75+
pb: 1.5,
76+
textDecoration: 'none',
77+
color: 'var(--ink-muted)',
78+
fontSize: '12px',
79+
transition: 'color 0.2s',
80+
'&:hover': { color: colors.primary },
81+
}}
82+
>
83+
<Box component="span" sx={{ color: colors.primary, fontWeight: 700 }}>$</Box>
84+
<Box
85+
component="span"
86+
sx={{
87+
flex: 1,
88+
whiteSpace: 'nowrap',
89+
overflow: 'hidden',
90+
textOverflow: 'ellipsis',
91+
}}
92+
>
93+
python {filename}
94+
</Box>
95+
<Box component="span" sx={{ fontSize: '11px' }}>↗ open</Box>
96+
</Box>
97+
98+
{/* Plot image — 16:10 frame, contain so square plots are letterboxed */}
99+
<Box
100+
sx={{
101+
mx: 'auto',
102+
width: { xs: 'calc(100% - 32px)', md: 'calc(100% - 48px)' },
103+
maxWidth: `calc(${maxPlotHeight} * 1.6)`,
104+
maxHeight: maxPlotHeight,
105+
borderTop: '1px dashed var(--rule)',
106+
borderBottom: '1px dashed var(--rule)',
107+
bgcolor: 'var(--bg-elevated)',
108+
aspectRatio: '16 / 10',
109+
position: 'relative',
110+
overflow: 'hidden',
111+
}}
112+
>
113+
<Box
114+
component="picture"
115+
sx={{
116+
position: 'absolute',
117+
inset: 0,
118+
display: 'flex',
119+
alignItems: 'center',
120+
justifyContent: 'center',
121+
}}
122+
>
123+
<source type="image/webp" srcSet={buildSrcSet(potd.preview_url, 'webp')} sizes={sizes} />
124+
<source type="image/png" srcSet={buildSrcSet(potd.preview_url, 'png')} sizes={sizes} />
125+
<Box
126+
component="img"
127+
src={getFallbackSrc(potd.preview_url)}
128+
alt={`${potd.spec_title}${potd.library_name}`}
129+
sx={{
130+
maxWidth: '100%',
131+
maxHeight: '100%',
132+
width: 'auto',
133+
height: 'auto',
134+
display: 'block',
135+
objectFit: 'contain',
136+
}}
137+
/>
138+
</Box>
139+
</Box>
140+
141+
{/* Status footer */}
142+
<Box
143+
sx={{
144+
display: 'flex',
145+
alignItems: 'center',
146+
gap: 1.5,
147+
flexWrap: 'wrap',
148+
px: { xs: 2, md: 3 },
149+
py: 1.5,
150+
fontSize: '11px',
151+
color: 'var(--ink-muted)',
152+
}}
153+
>
154+
<Box component="span" sx={{ color: colors.primary }}>&gt;&gt;&gt;</Box>
155+
<Box component="span" sx={{ color: 'var(--ink-soft)' }}>{potd.spec_title}</Box>
156+
<Box component="span">·</Box>
157+
<Box component="span">{potd.library_name}</Box>
158+
<Box sx={{ flex: 1 }} />
159+
<Box component="span" sx={{ opacity: 0.8 }}>// plot of the day</Box>
160+
</Box>
161+
</Box>
162+
);
163+
}

app/src/components/SectionHeader.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,50 @@ import { Link } from 'react-router-dom';
33
import { colors, typography } from '../theme';
44

55
interface SectionHeaderProps {
6-
number: string;
6+
/** Shell-prompt prefix per style-guide §6.3 — e.g. `❯`, `$`, `~/anyplot/`. Preferred. */
7+
prompt?: string;
8+
/** Legacy editorial section number — e.g. `§ 01`. Kept for non-landing pages. */
9+
number?: string;
710
title: React.ReactNode;
811
linkText?: string;
912
linkTo?: string;
1013
}
1114

12-
export function SectionHeader({ number, title, linkText, linkTo }: SectionHeaderProps) {
15+
export function SectionHeader({ prompt, number, title, linkText, linkTo }: SectionHeaderProps) {
16+
const isPrompt = !!prompt;
17+
1318
return (
1419
<Box sx={{
1520
display: 'grid',
1621
gridTemplateColumns: 'auto 1fr auto',
1722
alignItems: 'baseline',
18-
gap: 3,
23+
gap: { xs: 1.5, md: 2 },
1924
mb: 6,
2025
pb: 2.5,
2126
borderBottom: `1px solid var(--rule)`,
2227
}}>
23-
<Box sx={{
28+
<Box sx={isPrompt ? {
29+
fontFamily: typography.mono,
30+
fontSize: { xs: '0.95rem', sm: '1.15rem', md: '1.4rem' },
31+
fontWeight: 500,
32+
color: 'var(--ink-muted)',
33+
whiteSpace: 'nowrap',
34+
} : {
2435
fontFamily: typography.mono,
2536
fontSize: '11px',
2637
color: 'var(--ink-muted)',
2738
textTransform: 'uppercase',
2839
letterSpacing: '0.15em',
2940
}}>
30-
{number}
41+
{isPrompt ? prompt : number}
3142
</Box>
3243
<Box component="h2" sx={{
3344
fontFamily: typography.serif,
3445
fontWeight: 400,
35-
fontSize: { xs: '1.75rem', sm: '2.25rem', md: 'clamp(2.25rem, 4.5vw, 3.5rem)' },
36-
lineHeight: 1,
46+
fontSize: isPrompt
47+
? { xs: '1.5rem', sm: '1.875rem', md: 'clamp(1.875rem, 3.5vw, 2.5rem)' }
48+
: { xs: '1.75rem', sm: '2.25rem', md: 'clamp(2.25rem, 4.5vw, 3.5rem)' },
49+
lineHeight: isPrompt ? 1.15 : 1,
3750
letterSpacing: '-0.02em',
3851
color: 'var(--ink)',
3952
m: 0,

app/src/components/ThemeToggle.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ export function ThemeToggle({ isDark, onToggle }: ThemeToggleProps) {
1414
aria-label={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
1515
sx={{
1616
background: 'none',
17-
border: `1px solid ${colors.gray[200]}`,
17+
border: '1px solid var(--rule)',
1818
cursor: 'pointer',
19-
padding: '4px 10px',
20-
borderRadius: '99px',
19+
padding: '3px 9px',
20+
borderRadius: '4px',
2121
fontFamily: typography.mono,
22-
fontSize: '10px',
23-
letterSpacing: '0.12em',
24-
color: colors.gray[500],
25-
textTransform: 'uppercase',
26-
transition: 'all 0.2s',
22+
fontSize: '11px',
23+
letterSpacing: '0.02em',
24+
color: 'var(--ink-muted)',
25+
textTransform: 'none',
26+
transition: 'color 0.2s, border-color 0.2s',
2727
'&:hover': {
28-
color: colors.gray[800],
29-
borderColor: colors.gray[800],
28+
color: colors.primary,
29+
borderColor: 'var(--ink-muted)',
3030
},
3131
}}
3232
>

0 commit comments

Comments
 (0)