11import { Helmet } from 'react-helmet-async' ;
22import Box from '@mui/material/Box' ;
3- import Container from '@mui/material/Container' ;
43import { Link as RouterLink } from 'react-router-dom' ;
54
6- import { MastheadRule } from '../components/MastheadRule' ;
7- import { NavBar } from '../components/NavBar' ;
85import { HeroSection } from '../components/HeroSection' ;
96import { NumbersStrip } from '../components/NumbersStrip' ;
107import { LibrariesSection } from '../components/LibrariesSection' ;
118import { SectionHeader } from '../components/SectionHeader' ;
12- import { Footer } from '../components/Footer' ;
13- import { useAppData , useAnalytics } from '../hooks' ;
9+ import { useAppData } from '../hooks' ;
1410import { usePlotOfTheDay } from '../hooks/usePlotOfTheDay' ;
11+ import { useFeaturedSpecs , type FeaturedImpl } from '../hooks/useFeaturedSpecs' ;
1512import { GITHUB_URL } from '../constants' ;
16- import { colors , typography } from '../theme' ;
13+ import { specPath } from '../utils/paths' ;
14+ import { buildSrcSet , getFallbackSrc } from '../utils/responsiveImage' ;
15+ import { colors , fontSize , semanticColors , typography } from '../theme' ;
1716
1817export function LandingPage ( ) {
1918 const { librariesData, stats } = useAppData ( ) ;
20- const { trackEvent } = useAnalytics ( ) ;
2119 const potd = usePlotOfTheDay ( ) ;
22-
23- // Every section on the landing page lives on the catalog tier so the grid
24- // stays consistent from hero to footer on ultrawide displays.
25- const catalogContainerSx = {
26- px : { xs : 2 , sm : 4 , md : 8 , lg : 12 } ,
27- maxWidth : 'var(--max-catalog)' ,
28- mx : 'auto' ,
29- } as const ;
20+ const featured = useFeaturedSpecs ( 4 ) ;
3021
3122 return (
3223 < >
@@ -39,58 +30,41 @@ export function LandingPage() {
3930 < link rel = "canonical" href = "https://anyplot.ai/" />
4031 </ Helmet >
4132
42- < Box sx = { { minHeight : '100vh' , bgcolor : 'var(--bg-page)' } } >
43- < Container maxWidth = { false } sx = { catalogContainerSx } >
44- < MastheadRule />
45- < NavBar />
46- </ Container >
47-
48- { /* Hero — fills the viewport so the fold shows only the hero */ }
49- < Box
50- component = "section"
51- sx = { {
52- display : 'flex' ,
53- flexDirection : 'column' ,
54- justifyContent : 'center' ,
55- minHeight : { xs : 'auto' , md : 'calc(88svh - 88px)' } ,
56- } }
57- >
58- < Container maxWidth = { false } sx = { catalogContainerSx } >
59- < HeroSection potd = { potd } />
60- < NumbersStrip stats = { stats } />
61- </ Container >
62- </ Box >
63-
64- < Container maxWidth = { false } sx = { catalogContainerSx } >
65- < LibrariesSection
66- libraries = { librariesData }
67- onLibraryClick = { ( ) => { } }
68- widthTier = "catalog"
69- headerStyle = "prompt"
70- />
71- </ Container >
33+ { /* Hero — fills the viewport so the fold shows only the hero */ }
34+ < Box
35+ component = "section"
36+ sx = { {
37+ display : 'flex' ,
38+ flexDirection : 'column' ,
39+ justifyContent : 'center' ,
40+ minHeight : { xs : 'auto' , md : 'calc(88svh - 88px)' } ,
41+ } }
42+ >
43+ < HeroSection potd = { potd } />
44+ < NumbersStrip stats = { stats } />
45+ </ Box >
7246
73- < Container maxWidth = { false } sx = { catalogContainerSx } >
74- < SpecsSection specCount = { stats ?. specs } />
75- </ Container >
47+ < SpecsSection specCount = { stats ?. specs } featured = { featured } />
7648
77- < Container maxWidth = { false } sx = { catalogContainerSx } >
78- < Footer onTrackEvent = { trackEvent } />
79- </ Container >
80- </ Box >
49+ < LibrariesSection
50+ libraries = { librariesData }
51+ onLibraryClick = { ( ) => { } }
52+ widthTier = "catalog"
53+ headerStyle = "prompt"
54+ />
8155 </ >
8256 ) ;
8357}
8458
8559/**
86- * Specs section — catalog-tier layout. On narrow screens it stacks; on wide
87- * screens the prompt/title sits left and the narrative + action chips sit
88- * right, so the section doesn't leave a rectangle of whitespace on 2200px .
60+ * Specs section — catalog-tier layout. Description sits on the left, a 2×2
61+ * preview grid of featured implementations on the right so visitors see what
62+ * specs become .
8963 */
90- function SpecsSection ( { specCount } : { specCount ?: number } ) {
64+ function SpecsSection ( { specCount, featured } : { specCount ?: number ; featured : FeaturedImpl [ ] | null } ) {
9165 return (
9266 < Box sx = { { py : { xs : 6 , md : 10 } } } >
93- < SectionHeader prompt = "$" title = { < em > specs </ em > } linkText = "view all" linkTo = "/specs" />
67+ < SectionHeader prompt = "$" title = { < em > specifications </ em > } linkText = "view all" linkTo = "/specs" />
9468
9569 < Box
9670 sx = { {
@@ -100,72 +74,58 @@ function SpecsSection({ specCount }: { specCount?: number }) {
10074 alignItems : 'start' ,
10175 } }
10276 >
103- < Box
104- sx = { {
105- fontFamily : typography . serif ,
106- fontSize : { xs : '1rem' , md : '1.25rem' } ,
107- lineHeight : 1.55 ,
108- color : 'var(--ink-soft)' ,
109- fontWeight : 300 ,
110- maxWidth : '52ch' ,
111- } }
112- >
113- Every plot lives as a library-agnostic markdown spec.{ ' ' }
114- < Box component = "span" sx = { { color : 'var(--ink)' } } >
115- One source, nine libraries.
116- </ Box > { ' ' }
117- Drafted by AI from a human idea, approved before any code is
118- generated — so the intent stays with the humans.
119- </ Box >
120-
121- < Box
122- sx = { {
123- display : 'flex' ,
124- flexDirection : 'column' ,
125- gap : 2 ,
126- fontFamily : typography . mono ,
127- } }
128- >
129- < Box sx = { { fontSize : '12px' , color : 'var(--ink-muted)' , mb : 1 } } >
130- // spec pipeline
77+ < Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 3 } } >
78+ < Box
79+ sx = { {
80+ fontFamily : typography . serif ,
81+ fontSize : { xs : '1rem' , md : '1.25rem' } ,
82+ lineHeight : 1.55 ,
83+ color : 'var(--ink-soft)' ,
84+ fontWeight : 300 ,
85+ maxWidth : '52ch' ,
86+ } }
87+ >
88+ a spec is a short, library-agnostic markdown document —{ ' ' }
89+ < Box component = "span" sx = { { color : 'var(--ink)' } } >
90+ what the plot shows, what data it needs, and when to use it.
91+ </ Box > { ' ' }
92+ from that single source, implementations are generated for every
93+ supported library. new specs come from github issues; anyone can
94+ propose one.
13195 </ Box >
132- { [
133- [ 'idea ' , 'human-submitted' ] ,
134- [ 'spec ' , 'ai-drafted, human-approved' ] ,
135- [ 'code ' , 'ai-generated' ] ,
136- [ 'review ' , 'ai-evaluated' ] ,
137- ] . map ( ( [ k , v ] ) => (
138- < Box
139- key = { k }
140- sx = { {
141- display : 'flex' ,
142- gap : 1.5 ,
143- fontSize : '13px' ,
144- lineHeight : 1.6 ,
145- } }
146- >
147- < Box component = "span" sx = { { color : 'var(--ink)' , whiteSpace : 'pre' } } >
148- { k }
149- </ Box >
150- < Box component = "span" sx = { { color : 'var(--ink-muted)' } } >
151- →
152- </ Box >
153- < Box component = "span" sx = { { color : 'var(--ink-soft)' } } >
154- { v }
155- </ Box >
156- </ Box >
157- ) ) }
158-
159- < Box sx = { { display : 'flex' , gap : 1 , flexWrap : 'wrap' , mt : 2 } } >
96+
97+ < Box sx = { { display : 'flex' , flexWrap : 'wrap' , gap : 1 } } >
16098 < ActionChip to = "/specs" label = { `browse ${ specCount ?? '' } specs` . trim ( ) } />
161- < ActionChip href = { GITHUB_URL } label = "github" external />
99+ < ActionChip
100+ href = { `${ GITHUB_URL } /issues/new?template=request-new-plot.yml` }
101+ label = "suggest spec"
102+ external
103+ />
162104 </ Box >
163105 </ Box >
106+
107+ < FeaturedGrid featured = { featured } />
164108 </ Box >
165109 </ Box >
166110 ) ;
167111}
168112
113+ function FeaturedGrid ( { featured } : { featured : FeaturedImpl [ ] | null } ) {
114+ return (
115+ < Box
116+ sx = { {
117+ display : 'grid' ,
118+ gridTemplateColumns : { xs : 'repeat(2, 1fr)' , sm : 'repeat(2, 1fr)' } ,
119+ gap : 2 ,
120+ } }
121+ >
122+ { ( featured ?? Array . from ( { length : 4 } , ( ) => null ) ) . map ( ( item , i ) => (
123+ < FeaturedThumb key = { item ?. spec_id ? `${ item . spec_id } -${ item . library_id } ` : `skel-${ i } ` } item = { item } />
124+ ) ) }
125+ </ Box >
126+ ) ;
127+ }
128+
169129interface ActionChipProps {
170130 to ?: string ;
171131 href ?: string ;
@@ -188,8 +148,8 @@ function ActionChip({ to, href, label, external }: ActionChipProps) {
188148 display : 'inline-flex' ,
189149 alignItems : 'baseline' ,
190150 transition : 'color 0.2s, background 0.2s' ,
191- '&:hover' : { color : colors . primary , bgcolor : 'var(--bg-elevated)' } ,
192151 '&::before' : { content : '"."' , color : 'inherit' } ,
152+ '&:hover' : { color : colors . primary , bgcolor : 'var(--bg-elevated)' } ,
193153 } as const ;
194154
195155 if ( href ) {
@@ -211,3 +171,110 @@ function ActionChip({ to, href, label, external }: ActionChipProps) {
211171 </ Box >
212172 ) ;
213173}
174+
175+ /**
176+ * Compact terminal-window card — mirrors PlotOfTheDay's shape (top bar with
177+ * `$` prompt, square-ish image body, bottom bar with `>>>` output) so the
178+ * featured grid feels part of the same language as the hero POTD.
179+ */
180+ function FeaturedThumb ( { item } : { item : FeaturedImpl | null } ) {
181+ const cardSx = {
182+ display : 'flex' ,
183+ flexDirection : 'column' as const ,
184+ borderRadius : 2 ,
185+ overflow : 'hidden' ,
186+ border : `1px solid ${ colors . gray [ 200 ] } ` ,
187+ boxShadow : '0 2px 12px rgba(0,0,0,0.04)' ,
188+ transition : 'box-shadow 0.25s, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)' ,
189+ textDecoration : 'none' ,
190+ color : 'inherit' ,
191+ bgcolor : 'var(--bg-surface)' ,
192+ '&:hover' : {
193+ transform : 'translateY(-2px)' ,
194+ boxShadow : '0 6px 24px rgba(0,0,0,0.08)' ,
195+ } ,
196+ } as const ;
197+
198+ const barSx = {
199+ display : 'flex' ,
200+ alignItems : 'center' ,
201+ px : 1.25 ,
202+ py : 0.5 ,
203+ bgcolor : colors . gray [ 100 ] ,
204+ gap : 0.75 ,
205+ fontFamily : typography . mono ,
206+ fontSize : fontSize . xxs ,
207+ } as const ;
208+
209+ if ( ! item || ! item . preview_url ) {
210+ return (
211+ < Box sx = { cardSx } >
212+ < Box sx = { { ...barSx , borderBottom : `1px solid ${ colors . gray [ 200 ] } ` } } >
213+ < Box component = "span" sx = { { color : colors . gray [ 300 ] } } > $</ Box >
214+ < Box component = "span" sx = { { color : colors . gray [ 300 ] } } > </ Box >
215+ </ Box >
216+ < Box sx = { { aspectRatio : '16 / 10' , bgcolor : 'var(--bg-elevated)' } } />
217+ < Box sx = { { ...barSx , borderTop : `1px solid ${ colors . gray [ 200 ] } ` } } >
218+ < Box component = "span" sx = { { color : colors . gray [ 300 ] } } > >>></ Box >
219+ </ Box >
220+ </ Box >
221+ ) ;
222+ }
223+
224+ return (
225+ < Box component = { RouterLink } to = { specPath ( item . spec_id , item . library_id ) } sx = { cardSx } >
226+ { /* Top bar — mimics POTD's `$ python …` prompt */ }
227+ < Box sx = { { ...barSx , borderBottom : `1px solid ${ colors . gray [ 200 ] } ` } } >
228+ < Box component = "span" sx = { { color : colors . primary , fontWeight : 600 } } > $</ Box >
229+ < Box
230+ component = "span"
231+ sx = { {
232+ color : semanticColors . mutedText ,
233+ flex : 1 ,
234+ whiteSpace : 'nowrap' ,
235+ overflow : 'hidden' ,
236+ textOverflow : 'ellipsis' ,
237+ } }
238+ >
239+ { item . spec_id } .py
240+ </ Box >
241+ </ Box >
242+
243+ { /* Image body */ }
244+ < Box sx = { { aspectRatio : '16 / 10' , overflow : 'hidden' } } >
245+ < Box component = "picture" sx = { { display : 'block' , width : '100%' , height : '100%' } } >
246+ < source type = "image/webp" srcSet = { buildSrcSet ( item . preview_url , 'webp' ) } sizes = "(max-width: 599px) 50vw, 25vw" />
247+ < source type = "image/png" srcSet = { buildSrcSet ( item . preview_url , 'png' ) } sizes = "(max-width: 599px) 50vw, 25vw" />
248+ < Box
249+ component = "img"
250+ src = { getFallbackSrc ( item . preview_url ) }
251+ alt = { item . spec_title }
252+ loading = "lazy"
253+ sx = { { display : 'block' , width : '100%' , height : '100%' , objectFit : 'cover' } }
254+ />
255+ </ Box >
256+ </ Box >
257+
258+ { /* Bottom bar — mimics POTD's `>>> plot.png saved | library` */ }
259+ < Box sx = { { ...barSx , borderTop : `1px solid ${ colors . gray [ 200 ] } ` } } >
260+ < Box component = "span" sx = { { color : colors . primary } } > >>></ Box >
261+ < Box
262+ component = "span"
263+ sx = { {
264+ color : semanticColors . mutedText ,
265+ flex : 1 ,
266+ whiteSpace : 'nowrap' ,
267+ overflow : 'hidden' ,
268+ textOverflow : 'ellipsis' ,
269+ } }
270+ >
271+ plot.png saved
272+ </ Box >
273+ < Box component = "span" sx = { { color : colors . gray [ 300 ] } } > │</ Box >
274+ < Box component = "span" sx = { { color : semanticColors . mutedText , flexShrink : 0 } } >
275+ { item . library_id }
276+ </ Box >
277+ </ Box >
278+ </ Box >
279+ ) ;
280+ }
0 commit comments