/* FreeToolzHub — app shell: hash router, theme, toasts, search overlay */ function SearchOverlay({ open, onClose, go }) { const { Icon } = window.DesignSystem_3a7ab0; const { TOOLS } = window.FTH; const [q, setQ] = React.useState(''); const inputRef = React.useRef(null); React.useEffect(() => { if (open) { setQ(''); setTimeout(()=>inputRef.current && inputRef.current.focus(), 60); } }, [open]); if (!open) return null; const list = (q ? TOOLS.filter(t => (t.title+' '+t.desc+' '+t.cat).toLowerCase().includes(q.toLowerCase())) : TOOLS.filter(t=>t.working)).slice(0,9); return (
e.stopPropagation()} style={{ width:'min(640px,92vw)', background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:'var(--radius-2xl)', boxShadow:'var(--shadow-xl)', padding:14 }}>
setQ(e.target.value)} placeholder="Search tools — e.g. compress, merge, summarize…" style={{ flex:1, border:'none', outline:'none', background:'none', fontFamily:'var(--font-heading)', fontSize:19, color:'var(--text-strong)' }} />
{list.length ? list.map(t => { const [bg,fg] = window.FTH.TONE_MAP[t.tone]; return ( { onClose(); go('/tool/'+t.id); }} style={{ display:'flex', alignItems:'center', gap:14, padding:'11px 12px', borderRadius:'var(--radius-lg)', cursor:'pointer' }} onMouseEnter={e=>e.currentTarget.style.background='var(--bg-subtle)'} onMouseLeave={e=>e.currentTarget.style.background='transparent'}> {t.title} {t.desc} {window.FTH.catLabel(t.cat)} ); }) :
No tools match “{q}”.
}
); } function parseHash() { const h = (location.hash || '#/').replace(/^#/, ''); const parts = h.split('/').filter(Boolean); // ['tool','merge-pdf'] if (!parts.length) return { name:'home' }; if (parts[0]==='tools') return { name:'tools', cat: parts[1] || 'all' }; if (parts[0]==='tool') return { name:'tool', id: parts[1] }; return { name:'home' }; } function App() { const [route, setRoute] = React.useState(parseHash()); const [dark, setDark] = React.useState(localStorage.getItem('fth-theme')==='dark'); const [search, setSearch] = React.useState(false); const [toasts, setToasts] = React.useState([]); const { Toast } = window.DesignSystem_3a7ab0; React.useEffect(() => { document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); localStorage.setItem('fth-theme', dark ? 'dark' : 'light'); }, [dark]); React.useEffect(() => { const onHash = () => { setRoute(parseHash()); window.scrollTo({ top:0 }); }; window.addEventListener('hashchange', onHash); const onKey = (e) => { if (e.key==='/' && !/input|textarea/i.test(document.activeElement.tagName)) { e.preventDefault(); setSearch(true); } if (e.key==='Escape') setSearch(false); }; window.addEventListener('keydown', onKey); return () => { window.removeEventListener('hashchange', onHash); window.removeEventListener('keydown', onKey); }; }, []); const go = (path) => { location.hash = '#'+path; }; const pushToast = (message) => { const id = Math.random().toString(36).slice(2); setToasts(t => [...t, { id, message }]); setTimeout(() => setToasts(t => t.filter(x => x.id!==id)), 2600); }; let page; if (route.name==='tools') page = ; else if (route.name==='tool') page = ; else page = ; return (
setSearch(true)} /> {page} setSearch(false)} go={go} />
{toasts.map(t => )}
); } ReactDOM.createRoot(document.getElementById('root')).render();