1- import { useState , useEffect , useRef } from 'react'
1+ import { useState , useEffect , useRef , createContext , useContext } from 'react'
22import {
33 BarChart , Bar , XAxis , YAxis , CartesianGrid , Tooltip , Legend ,
44 PieChart , Pie , Cell , ResponsiveContainer ,
@@ -25,6 +25,10 @@ const T = {
2525 link : '#38bdf8' ,
2626}
2727
28+ // ─── Mobile context ───────────────────────────────────────────────────────────
29+ const MobileCtx = createContext ( false )
30+ function useIsMobile ( ) { return useContext ( MobileCtx ) }
31+
2832// ─── Data ─────────────────────────────────────────────────────────────────────
2933const tokenTable = [
3034 { cmd : 'pip install' , before : 1787 , after : 9 , pct : 99 } ,
@@ -256,9 +260,11 @@ function Callout({ type = 'tip', children }: { type?: 'tip' | 'note' | 'warning'
256260}
257261
258262function StatRow ( { items } : { items : { value : string ; label : string ; color ?: string } [ ] } ) {
263+ const isMobile = useIsMobile ( )
264+ const cols = isMobile ? 2 : items . length
259265 return (
260266 < div style = { {
261- display : 'grid' , gridTemplateColumns : `repeat(${ items . length } , 1fr)` ,
267+ display : 'grid' , gridTemplateColumns : `repeat(${ cols } , 1fr)` ,
262268 gap : 1 , background : T . border , borderRadius : 10 , overflow : 'hidden' ,
263269 marginBottom : 28 ,
264270 } } >
@@ -280,9 +286,9 @@ function DataTable({ headers, rows, highlight }: {
280286 return (
281287 < div style = { {
282288 background : T . card , border : `1px solid ${ T . border } ` , borderRadius : 10 ,
283- overflow : 'hidden ' , marginBottom : 28 ,
289+ overflow : 'auto ' , marginBottom : 28 ,
284290 } } >
285- < table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 13 } } >
291+ < table style = { { width : '100%' , minWidth : 480 , borderCollapse : 'collapse' , fontSize : 13 } } >
286292 < thead >
287293 < tr style = { { background : T . sidebar } } >
288294 { headers . map ( ( h , i ) => (
@@ -533,6 +539,7 @@ panda gain # see token savings from this session
533539}
534540
535541function SectionAgents ( ) {
542+ const isMobile = useIsMobile ( )
536543 const agents = [
537544 {
538545 name : 'Claude Code' ,
@@ -605,7 +612,7 @@ function SectionAgents() {
605612 verifies the installation automatically.
606613 </ P >
607614
608- < div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 , marginBottom : 28 } } >
615+ < div style = { { display : 'grid' , gridTemplateColumns : isMobile ? '1fr' : '1fr 1fr' , gap : 12 , marginBottom : 28 } } >
609616 { agents . map ( ( { name, cmd, color, config, script, desc, logo } ) => (
610617 < div key = { name } style = { {
611618 background : T . card , border : `1px solid ${ T . border } ` , borderRadius : 10 ,
@@ -717,6 +724,7 @@ function SectionPipeline() {
717724}
718725
719726function SectionBert ( ) {
727+ const isMobile = useIsMobile ( )
720728 const useCases = [
721729 {
722730 role : 'Output summarization' ,
@@ -752,7 +760,7 @@ function SectionBert() {
752760
753761 < H3 > Model</ H3 >
754762 < div style = { {
755- display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 1 ,
763+ display : 'grid' , gridTemplateColumns : isMobile ? '1fr' : '1fr 1fr' , gap : 1 ,
756764 background : T . border , borderRadius : 10 , overflow : 'hidden' , marginBottom : 24 ,
757765 } } >
758766 { [
@@ -837,7 +845,7 @@ function SectionBert() {
837845 </ P >
838846
839847 < div style = { {
840- display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 20 ,
848+ display : 'grid' , gridTemplateColumns : isMobile ? '1fr' : '1fr 1fr' , gap : 16 , marginBottom : 20 ,
841849 } } >
842850 { [
843851 {
@@ -888,16 +896,16 @@ function SectionBert() {
888896 { range : '0.10 ≤ entropy ≤ 0.35' , budget : 'Linear interpolation' , label : 'Mixed content' , color : T . amber } ,
889897 { range : 'entropy > 0.35' , budget : '100% of max budget' , label : 'Diverse / rich output' , color : T . emerald } ,
890898 ] . map ( ( { range, budget, label, color } ) => (
891- < div key = { range } style = { { display : 'flex' , alignItems : 'center' , gap : 16 } } >
892- < code style = { { width : 200 , flexShrink : 0 , fontSize : 12 , color, fontFamily : 'JetBrains Mono, monospace' } } > { range } </ code >
893- < div style = { { flex : 1 , height : 6 , background : T . border , borderRadius : 3 , overflow : 'hidden' } } >
899+ < div key = { range } style = { { display : 'flex' , flexWrap : 'wrap' , alignItems : 'center' , gap : isMobile ? 8 : 16 } } >
900+ < code style = { { width : isMobile ? '100%' : 200 , flexShrink : 0 , fontSize : 12 , color, fontFamily : 'JetBrains Mono, monospace' } } > { range } </ code >
901+ < div style = { { flex : 1 , minWidth : 60 , height : 6 , background : T . border , borderRadius : 3 , overflow : 'hidden' } } >
894902 < div style = { {
895903 height : '100%' , borderRadius : 3 , background : color ,
896904 width : color === T . red ? '5%' : color === T . amber ? '50%' : '100%' ,
897905 } } />
898906 </ div >
899- < span style = { { fontSize : 12.5 , color : T . sub , width : 160 , flexShrink : 0 } } > { budget } </ span >
900- < span style = { { fontSize : 12 , color : T . muted } } > { label } </ span >
907+ < span style = { { fontSize : 12.5 , color : T . sub , width : isMobile ? 'auto' : 160 , flexShrink : 0 } } > { budget } </ span >
908+ { ! isMobile && < span style = { { fontSize : 12 , color : T . muted } } > { label } </ span > }
901909 </ div >
902910 ) ) }
903911 </ div >
@@ -1734,9 +1742,18 @@ export default function App() {
17341742 const [ query , setQuery ] = useState ( '' )
17351743 const [ searchFocused , setSearchFocused ] = useState ( false )
17361744 const [ selectedIdx , setSelectedIdx ] = useState ( 0 )
1745+ const [ isMobile , setIsMobile ] = useState ( false )
1746+ const [ sidebarOpen , setSidebarOpen ] = useState ( false )
17371747 const searchRef = useRef < HTMLInputElement > ( null )
17381748 const contentRef = useRef < HTMLDivElement > ( null )
17391749
1750+ useEffect ( ( ) => {
1751+ const check = ( ) => setIsMobile ( window . innerWidth < 768 )
1752+ check ( )
1753+ window . addEventListener ( 'resize' , check )
1754+ return ( ) => window . removeEventListener ( 'resize' , check )
1755+ } , [ ] )
1756+
17401757 const results = query . trim ( ) . length > 0
17411758 ? SEARCH_INDEX . filter ( item => {
17421759 const q = query . toLowerCase ( )
@@ -1751,6 +1768,7 @@ export default function App() {
17511768 function goTo ( id : string ) {
17521769 setQuery ( '' )
17531770 setSearchFocused ( false )
1771+ setSidebarOpen ( false )
17541772 document . getElementById ( id ) ?. scrollIntoView ( { behavior : 'smooth' } )
17551773 }
17561774
@@ -1771,6 +1789,7 @@ export default function App() {
17711789 } , [ ] )
17721790
17731791 return (
1792+ < MobileCtx . Provider value = { isMobile } >
17741793 < div style = { { background : T . bg , color : T . text , minHeight : '100vh' , fontFamily : 'Inter, system-ui, sans-serif' } } >
17751794 { /* Top nav */ }
17761795 < nav style = { {
@@ -1779,20 +1798,38 @@ export default function App() {
17791798 padding : '0 28px' , background : T . sidebar , borderBottom : `1px solid ${ T . border } ` ,
17801799 } } >
17811800 < div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
1801+ { /* Hamburger — mobile only */ }
1802+ { isMobile && (
1803+ < button
1804+ onClick = { ( ) => setSidebarOpen ( o => ! o ) }
1805+ style = { {
1806+ background : 'none' , border : 'none' , cursor : 'pointer' ,
1807+ color : T . sub , padding : '4px 6px' , borderRadius : 6 ,
1808+ display : 'flex' , flexDirection : 'column' , gap : 4 ,
1809+ } }
1810+ aria-label = "Toggle menu"
1811+ >
1812+ < span style = { { display : 'block' , width : 18 , height : 2 , background : T . sub , borderRadius : 1 } } />
1813+ < span style = { { display : 'block' , width : 18 , height : 2 , background : T . sub , borderRadius : 1 } } />
1814+ < span style = { { display : 'block' , width : 18 , height : 2 , background : T . sub , borderRadius : 1 } } />
1815+ </ button >
1816+ ) }
17821817 < img src = { `${ import . meta. env . BASE_URL } logo.png` } alt = "PandaFilter" style = { { width : 32 , height : 32 , objectFit : 'contain' } } />
17831818 < span style = { { fontWeight : 700 , fontSize : 15 , color : T . text } } > PandaFilter</ span >
1784- < span style = { {
1785- fontSize : 11 , padding : '2px 8px' , borderRadius : 999 ,
1786- background : 'rgba(34,211,238,0.1)' , color : T . cyan , border : `1px solid rgba(34,211,238,0.25)` ,
1787- marginLeft : 4 ,
1788- } } > docs</ span >
1819+ { ! isMobile && (
1820+ < span style = { {
1821+ fontSize : 11 , padding : '2px 8px' , borderRadius : 999 ,
1822+ background : 'rgba(34,211,238,0.1)' , color : T . cyan , border : `1px solid rgba(34,211,238,0.25)` ,
1823+ marginLeft : 4 ,
1824+ } } > docs</ span >
1825+ ) }
17891826 </ div >
1790- < div style = { { display : 'flex' , gap : 24 , alignItems : 'center' } } >
1827+ < div style = { { display : 'flex' , gap : isMobile ? 12 : 24 , alignItems : 'center' } } >
17911828 { [
17921829 { label : 'GitHub' , href : 'https://github.com/AssafWoo/PandaFilter' } ,
17931830 { label : 'Discord' , href : 'https://discord.com/invite/FFQC3bxYQ' } ,
17941831 ] . map ( ( { label, href } ) => (
1795- < a key = { label } href = { href } style = { { fontSize : 13.5 , color : T . sub , textDecoration : 'none' } }
1832+ < a key = { label } href = { href } style = { { fontSize : isMobile ? 12 : 13.5 , color : T . sub , textDecoration : 'none' } }
17961833 onMouseEnter = { e => ( e . currentTarget . style . color = T . text ) }
17971834 onMouseLeave = { e => ( e . currentTarget . style . color = T . sub ) } >
17981835 { label }
@@ -1801,14 +1838,31 @@ export default function App() {
18011838 </ div >
18021839 </ nav >
18031840
1841+ { /* Mobile sidebar backdrop */ }
1842+ { isMobile && sidebarOpen && (
1843+ < div
1844+ onClick = { ( ) => setSidebarOpen ( false ) }
1845+ style = { {
1846+ position : 'fixed' , inset : 0 , zIndex : 149 ,
1847+ background : 'rgba(0,0,0,0.5)' ,
1848+ } }
1849+ />
1850+ ) }
1851+
18041852 { /* Body */ }
18051853 < div style = { { display : 'flex' , paddingTop : 56 , minHeight : '100vh' } } >
18061854
18071855 { /* Left sidebar */ }
18081856 < aside style = { {
1809- width : 240 , flexShrink : 0 , position : 'fixed' , top : 56 , bottom : 0 ,
1857+ width : 240 , flexShrink : 0 ,
1858+ position : 'fixed' , top : 56 , bottom : 0 ,
18101859 background : T . sidebar , borderRight : `1px solid ${ T . border } ` ,
18111860 overflowY : 'auto' , display : 'flex' , flexDirection : 'column' ,
1861+ zIndex : 150 ,
1862+ ...( isMobile ? {
1863+ transform : sidebarOpen ? 'translateX(0)' : 'translateX(-100%)' ,
1864+ transition : 'transform 0.25s ease' ,
1865+ } : { } ) ,
18121866 } } >
18131867 { /* Search */ }
18141868 < div style = { { padding : '16px 16px 12px' , position : 'relative' , flexShrink : 0 } } >
@@ -1924,8 +1978,10 @@ export default function App() {
19241978
19251979 { /* Main content */ }
19261980 < main ref = { contentRef } style = { {
1927- flex : 1 , marginLeft : 240 , marginRight : 200 ,
1928- padding : '48px 56px 96px' ,
1981+ flex : 1 ,
1982+ marginLeft : isMobile ? 0 : 240 ,
1983+ marginRight : isMobile ? 0 : 200 ,
1984+ padding : isMobile ? '32px 20px 80px' : '48px 56px 96px' ,
19291985 minWidth : 0 ,
19301986 } } >
19311987 < div id = "overview" >
@@ -1957,8 +2013,8 @@ export default function App() {
19572013 </ div >
19582014 </ main >
19592015
1960- { /* Right TOC */ }
1961- < aside style = { {
2016+ { /* Right TOC — hidden on mobile */ }
2017+ { ! isMobile && < aside style = { {
19622018 width : 200 , flexShrink : 0 , position : 'fixed' , top : 56 , right : 0 , bottom : 0 ,
19632019 padding : '28px 20px' , overflowY : 'auto' ,
19642020 borderLeft : `1px solid ${ T . border } ` ,
@@ -1984,8 +2040,9 @@ export default function App() {
19842040 </ a >
19852041 )
19862042 } ) }
1987- </ aside >
2043+ </ aside > }
19882044 </ div >
19892045 </ div >
2046+ </ MobileCtx . Provider >
19902047 )
19912048}
0 commit comments