/* ProofMark Studio — main app: tile-led, grouped, SmallPDF-ish */ const { useState, useEffect, useMemo, useRef, useCallback } = React; /* ---------- Display filter ---------- * Server sets t.hidden = true for beta/planned/flag-off tiles when the catalog * is in live-only mode (PROOFMARK_SHOW_ALL_TILES=false, the default). Every * render surface goes through __pmVisible() so only tiles that work fully show * up. Roadmap mode (server env var flipped) leaves t.hidden falsy, so the full * catalog renders. */ const __pmVisible = (arr) => arr.filter(t => !t.hidden); /* ---------- Shortcuts cheat-sheet ---------- * `?` opens this modal; it lists every keyboard binding the hub registers * so users don't have to guess. Kept alongside the palette because they * share a similar chrome shape. */ const SHORTCUTS = [ { combo: ['Cmd+K', 'Ctrl+K'], label: 'Open command palette' }, { combo: ['?'], label: 'Open this cheat-sheet' }, { combo: ['H'], label: 'Go to Home' }, { combo: ['G'], label: 'Go to All tools' }, { combo: ['P'], label: 'Go to Pinned' }, { combo: ['M'], label: 'Go to Platform' }, { combo: ['Esc'], label: 'Close dialogs / drawers' }, ]; const ShortcutsModal = ({ open, onClose }) => { useEffect(() => { if (!open) return; const h = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()} role="dialog" aria-label="Keyboard shortcuts" style={{ width:'min(540px, 92vw)', borderRadius:16, background:'var(--bg-elev)', border:'1px solid var(--border-strong)', boxShadow:'var(--shadow-lg)', overflow:'hidden', }}>
Keyboard shortcuts Esc
{SHORTCUTS.map((s, i) => (
{s.combo.map((c, j) => {c})}
{s.label}
))}
); }; /* ---------- Command Palette ---------- */ const CommandPalette = ({ open, onClose, onRun }) => { const [q, setQ] = useState(''); const [idx, setIdx] = useState(0); const inputRef = useRef(null); useEffect(() => { if (open) { setQ(''); setIdx(0); setTimeout(() => inputRef.current?.focus(), 20); } }, [open]); const results = useMemo(() => { const tools = __pmVisible(window.PM_TOOLS); const term = q.trim().toLowerCase(); if (!term) { return [ { group:'Popular', items: tools.filter(t => t.popular).slice(0,6) }, { group:'Pinned', items: tools.filter(t => t.pin).slice(0,4) }, ]; } const hits = tools.filter(t => t.title.toLowerCase().includes(term) || t.desc.toLowerCase().includes(term) || t.cat.includes(term) || t.slug.includes(term) ); return [{ group:`${hits.length} result${hits.length===1?'':'s'}`, items: hits.slice(0, 14) }]; }, [q]); const flat = results.flatMap(g => g.items); useEffect(() => { if (!open) return; const h = (e) => { if (e.key === 'Escape') onClose(); else if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(i+1, flat.length-1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(i-1, 0)); } else if (e.key === 'Enter') { e.preventDefault(); const t = flat[idx]; if (t) { onRun(t); onClose(); } } }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [open, flat, idx, onClose, onRun]); if (!open) return null; let running = -1; return (
e.stopPropagation()} style={{ width:'min(640px, 92vw)', borderRadius:16, background:'var(--bg-elev)', border:'1px solid var(--border-strong)', boxShadow:'var(--shadow-lg)', overflow:'hidden', }}>
{setQ(e.target.value); setIdx(0);}} placeholder="Search 50+ tools, workflows…" style={{ flex:1, border:0, outline:0, background:'transparent', color:'var(--text)', fontSize:15, fontFamily:'inherit' }}/> Esc
{results.map((g, gi) => (
{g.group}
{g.items.length === 0 &&
No matches.
} {g.items.map(t => { running++; const sel = running === idx; const myIdx = running; const grp = window.PM_GROUPS.find(x=>x.id===t.group); return ( ); })}
))}
navigate open Esc close ProofMark Studio · v0.3.4
); }; /* ---------- Tool drawer ---------- */ const ToolDrawer = ({ tool, onClose }) => { if (!tool) return null; const grp = window.PM_GROUPS.find(g => g.id === tool.group); const tone = grp?.tone || '#7cb0ff'; return (
e.stopPropagation()} style={{ width:'min(520px, 94vw)', height:'100%', background:'var(--bg-elev)', borderLeft:'1px solid var(--border-strong)', boxShadow:'var(--shadow-lg)', display:'flex', flexDirection:'column', }}>
{grp?.label}
{tool.title}
/tools/{tool.slug}

{tool.desc}

{[ { label:'Avg. run time', value:'1.2s' }, { label:'Max file size', value:'512 MB' }, { label:'Docs processed', value:'14.2k' }, { label:'Last updated', value:'Apr 16' }, ].map(m => (
{m.label}
{m.value}
))}
How it works
    {['Drop your files into the workspace','Configure options (preserved between runs)','Download or forward the result'].map((s,i)=>(
  1. {i+1}
    {s}
  2. ))}
); }; /* ---------- Big icon tile (SmallPDF-style) ---------- */ const ToolTile = ({ tool, onOpen }) => { const grp = window.PM_GROUPS.find(g => g.id === tool.group); const tone = grp?.tone || '#7cb0ff'; const [hover, setHover] = useState(false); return ( ); }; /* ---------- Group section header ---------- */ const GroupHeader = ({ group, count, onViewAll }) => (
{String(count).padStart(2,'0')} tools
{group.id}

{group.label}

{group.desc}
); /* ---------- Hero: visual-first ---------- */ const HeroPanel = ({ onOpenPalette }) => { const visible = __pmVisible(window.PM_TOOLS); const liveCount = visible.filter(t=>t.status==='live').length; // The flying mini-tile cluster behind the hero copy const stack = [ { ic:'merge', tone:'#ff7a45', x: 62, y: 6, r:-8 }, { ic:'docx', tone:'#7cb0ff', x: 72, y: 44, r:5 }, { ic:'sig', tone:'#ff6b8a', x: 86, y: 18, r:10 }, { ic:'aa', tone:'#62e0d9', x: 54, y: 42, r:-4 }, { ic:'ai', tone:'#f0c674', x: 80, y: 64, r:-6 }, ]; return (
{/* Floating tiles cluster */}
{stack.map((s,i) => (
))}
Workspace · online
ProofMark Studio · Working hub

Every PDF tool
you need, in one studio.

Merge, split, convert, sign, compress, and proofread — organized like a real workspace. Private, keyboard-first, built for document craft.

{liveCount}
tools live
); }; /* ---------- Popular strip ---------- */ const PopularStrip = ({ onOpen }) => { const pops = __pmVisible(window.PM_TOOLS).filter(t => t.popular); return (
Popular right now
{pops.map(t => )}
); }; /* ---------- Grouped catalog ---------- */ const GroupedCatalog = ({ onOpen, activeGroup, onSetGroup }) => { const groups = activeGroup === 'all' ? window.PM_GROUPS : window.PM_GROUPS.filter(g => g.id === activeGroup); return (
{groups.map(g => { const items = __pmVisible(window.PM_TOOLS).filter(t => t.group === g.id); if (items.length === 0) return null; return (
{items.map(t => )}
); })}
); }; /* ---------- Group chips with colored dots ---------- */ const GroupChips = ({ active, onSelect }) => (
{window.PM_GROUPS.map(g => { const isActive = active === g.id; const count = __pmVisible(window.PM_TOOLS).filter(t => t.group === g.id).length; if (count === 0) return null; return ( ); })}
); /* RecentStream + Throughput removed — they were hardcoded mock data (fake users, fake doc names, fake throughput numbers). Real activity tracking and metrics ship with Phase 17 (database). */ /* ---------- Platform map (real components only) ---------- */ const PlatformMap = () => { const spokes = [ { id:'pdf', title:'ProofMark PDF', desc:'Merge, split, compress, convert, sign, watermark, redact.', status:'live', url:'/go/proofmark-pdf' }, { id:'text', title:'Text Inspection', desc:'Surface hidden characters, normalize whitespace, review typography.', status:'live', url:'/go/text-inspection' }, { id:'site', title:'ProofMark Site', desc:'Public brand entry point and project home.', status:'live', url:'/go/proofmark-site' }, ]; return (
Platform
{spokes.length} components
{spokes.map((s, i) => ( {String(i+1).padStart(2,'0')}
{s.title}
{s.desc}
))}
); }; /* ---------- Tweaks panel ---------- */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "dark", "density": "comfortable", "accent": "#7cb0ff" }/*EDITMODE-END*/; const TweaksPanel = ({ open, values, onChange, onClose }) => { if (!open) return null; const opt = (current, choices, onPick, label) => (
{label}
{choices.map(c => ( ))}
); const accents = ['#7cb0ff','#5ee59b','#ffb366','#ff6b8a','#a57cff','#1e4fd6']; return (
Tweaks
Live
{opt(values.theme, [{ v:'dark', l:'Console (dark)' }, { v:'light', l:'Editorial (light)' }], v => onChange({ theme: v }), 'Theme')} {opt(values.density, [{ v:'comfortable', l:'Comfortable' }, { v:'compact', l:'Compact' }], v => onChange({ density: v }), 'Density')}
Accent
{accents.map(a => (
); }; /* ---------- Main App ---------- */ const App = () => { const [view, setView] = useState('home'); const [paletteOpen, setPaletteOpen] = useState(false); const [shortcutsOpen, setShortcutsOpen] = useState(false); const [tweaksOpen, setTweaksOpen] = useState(false); const [selectedTool, setSelectedTool] = useState(null); const [group, setGroup] = useState('all'); const [tweaks, setTweaks] = useState(() => { try { return { ...TWEAK_DEFAULTS, ...JSON.parse(localStorage.getItem('pm_tweaks')||'{}') }; } catch { return TWEAK_DEFAULTS; } }); useEffect(() => { document.body.dataset.theme = tweaks.theme; document.body.style.setProperty('--accent', tweaks.accent); document.body.style.setProperty('--accent-glow', tweaks.accent + '22'); try { localStorage.setItem('pm_tweaks', JSON.stringify(tweaks)); } catch {} }, [tweaks]); useEffect(() => { const handler = (e) => { if (!e.data || typeof e.data !== 'object') return; if (e.data.type === '__activate_edit_mode') setTweaksOpen(true); if (e.data.type === '__deactivate_edit_mode') setTweaksOpen(false); }; window.addEventListener('message', handler); window.parent.postMessage({ type:'__edit_mode_available' }, '*'); return () => window.removeEventListener('message', handler); }, []); const updateTweak = (patch) => { setTweaks(prev => ({ ...prev, ...patch })); window.parent.postMessage({ type:'__edit_mode_set_keys', edits: patch }, '*'); }; useEffect(() => { const h = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setPaletteOpen(true); return; } if (document.activeElement?.tagName === 'INPUT') return; if (paletteOpen) return; if (e.key === '?') { e.preventDefault(); setShortcutsOpen(v => !v); return; } if (shortcutsOpen) return; if (e.key === 'g' || e.key === 'G') setView('tools'); if (e.key === 'h' || e.key === 'H') setView('home'); if (e.key === 'p' || e.key === 'P') setView('pinned'); if (e.key === 'm' || e.key === 'M') setView('map'); }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [paletteOpen, shortcutsOpen]); const onRun = (tool) => setSelectedTool(tool); const breadcrumb = (() => { if (view === 'home') return ['Workspace', 'Home']; if (view === 'tools') return ['Workspace', 'All tools', group === 'all' ? 'All' : (window.PM_GROUPS.find(g=>g.id===group)?.label || 'All')]; if (view === 'pinned') return ['Workspace', 'Pinned']; if (view === 'map') return ['Workspace', 'Platform']; return ['Workspace']; })(); return (
{ if (v.startsWith('tool:')) { const slug = v.slice(5); const tool = window.PM_TOOLS.find(t => t.slug === slug); if (tool) setSelectedTool(tool); } else setView(v); }} onOpenPalette={() => setPaletteOpen(true)} density={tweaks.density}/>
setPaletteOpen(true)} onOpenTweaks={() => setTweaksOpen(true)} breadcrumb={breadcrumb}/>
{view === 'home' && (
setPaletteOpen(true)}/> {}}/>
)} {view === 'tools' && (
Catalog

All tools · {__pmVisible(window.PM_TOOLS).length}

Click a category to filter, or scroll through grouped sections below.

)} {view === 'pinned' && (

Pinned tools

{__pmVisible(window.PM_TOOLS).filter(t=>t.pin).map(t => )}
)} {view === 'map' && (

Platform

)} {view === 'settings' && (

Settings

Use the Tweaks panel (bottom-right) to adjust theme, density, and accent.
)}
setPaletteOpen(false)} onRun={onRun}/> setShortcutsOpen(false)}/> setSelectedTool(null)}/> setTweaksOpen(false)}/>
); }; /* ---------- Backend registry sync ---------- * Hub serves /api/tools as source of truth for { status, url } per slug. * We merge that into the React catalog (window.PM_TOOLS) BEFORE first render * so status pills + drawer targets reflect whatever the backend currently says. * Falls back to hardcoded values after a 600ms timeout so offline dev still works. */ const __pmSyncFromBackend = async () => { try { const res = await fetch('/api/tools', { cache: 'no-store' }); if (!res.ok) return; const payload = await res.json(); const serverTools = payload.tools || {}; window.PM_TOOLS.forEach(t => { const s = serverTools[t.slug]; if (!s) return; if (s.status) t.status = s.status; if (s.url) t.url = s.url; // drawer keeps this for potential deep-link // Flag-downgraded live tools carry `paused: true` so the pill can show it. if (s.paused) t.paused = true; // Display filter: beta/planned tiles hide by default (live-only catalog). // Server flips `display:true` in roadmap mode (PROOFMARK_SHOW_ALL_TILES=true). t.hidden = s.display === false; }); } catch (err) { console.warn('[registry] sync failed, using hardcoded catalog', err); } }; const __pmMount = () => { ReactDOM.createRoot(document.getElementById('root')).render(); }; Promise.race([ __pmSyncFromBackend(), new Promise(resolve => setTimeout(resolve, 600)), ]).then(__pmMount);