/* 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); const useIsMobile = (breakpoint = 768) => { const [mobile, setMobile] = useState(() => window.innerWidth < breakpoint); useEffect(() => { const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`); const handler = (e) => setMobile(e.matches); mql.addEventListener('change', handler); return () => mql.removeEventListener('change', handler); }, [breakpoint]); return mobile; }; /* ---------- Honest-fact helpers for the tool drawer + recent strip ---------- */ const STATUS_LABEL = { live: 'Live', beta: 'Beta', planned: 'Planned' }; const SHORT_MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; // "2026-04-23" → "Apr 23"; today's year is omitted for compactness. const formatShortDate = (iso) => { if (!iso) return '—'; const d = new Date(iso + (iso.length === 10 ? 'T00:00:00' : '')); if (isNaN(d)) return iso; return `${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}`; }; // Coarse "x ago" — exact minutes for fresh, days/weeks otherwise. No false precision. const formatRelativeTime = (ts) => { const diff = Math.max(0, Date.now() - ts); const m = Math.floor(diff / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const d = Math.floor(h / 24); if (d < 7) return d === 1 ? 'yesterday' : `${d}d ago`; const w = Math.floor(d / 7); if (w < 5) return `${w}w ago`; return formatShortDate(new Date(ts).toISOString().slice(0, 10)); }; /* ---------- "Your recent tools" — local-only, never sent anywhere ---------- * Honest replacement for the removed cross-user activity feed. The data is the * user's own clicks, capped at RECENT_MAX, deduped by slug, MRU first. */ const RECENT_KEY = 'pm:recent-tools'; const RECENT_MAX = 8; const recordRecent = (slug) => { try { const prev = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]') .filter(e => e && e.slug && e.slug !== slug); const next = [{ slug, at: Date.now() }, ...prev].slice(0, RECENT_MAX); localStorage.setItem(RECENT_KEY, JSON.stringify(next)); } catch {} // private mode / quota — skip silently; the strip just won't update }; const readRecent = () => { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch { return []; } }; const PINNED_KEY = 'pm:pinned-slugs'; const readPinned = () => { try { return JSON.parse(localStorage.getItem(PINNED_KEY) || '[]'); } catch { return []; } }; const writePinned = (slugs) => { try { localStorage.setItem(PINNED_KEY, JSON.stringify(slugs)); } catch {} }; /* ---------- 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, pinnedSlugs }) => { 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 => pinnedSlugs.includes(t.slug)).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, onTogglePin, isPinned }) => { 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:'Status', value: STATUS_LABEL[tool.status] || tool.status }, { label:'Max file size', value: `${tool.maxFileSizeMB ?? window.PM_HUB_MAX_FILE_SIZE_MB} MB` }, { label:'Output', value: tool.output || '—' }, { label:'Last updated', value: formatShortDate(tool.updatedAt || window.PM_HUB_DEFAULT_UPDATED_AT) }, ].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, isPinned }) => { const grp = window.PM_GROUPS.find(g => g.id === tool.group); const tone = grp?.tone || '#7cb0ff'; const [hover, setHover] = useState(false); const pinned = isPinned && isPinned(tool.slug); 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, isMobile }) => { 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 — hidden on mobile */} {!isMobile && (
{stack.map((s,i) => (
))}
)} {!isMobile && (
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, isMobile, isPinned }) => { const pops = __pmVisible(window.PM_TOOLS).filter(t => t.popular); return (
Popular right now
{pops.map(t => )}
); }; /* ---------- Grouped catalog ---------- */ const GroupedCatalog = ({ onOpen, activeGroup, onSetGroup, isMobile, isPinned }) => { 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, isMobile }) => (
{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}
))}
); }; /* ---------- Your recent tools — local-only browsing history ---------- * Honest replacement for the removed cross-user activity feed. Reads from * localStorage on mount + on focus (so it refreshes when the user comes back * from a tool tab). Hidden entirely when empty — no fake placeholder rows. */ const YourRecentTools = ({ onOpen, isMobile }) => { const [items, setItems] = useState([]); useEffect(() => { setItems(readRecent()); const refresh = () => setItems(readRecent()); window.addEventListener('focus', refresh); return () => window.removeEventListener('focus', refresh); }, []); const resolved = useMemo(() => items .map(it => ({ ...it, tool: window.PM_TOOLS.find(t => t.slug === it.slug) })) .filter(it => it.tool && !it.tool.hidden), [items]); if (!resolved.length) return null; return (
Your recent tools
· stored in your browser, never sent
{resolved.map(({ tool, at }) => { const grp = window.PM_GROUPS.find(g => g.id === tool.group); const tone = grp?.tone || '#7cb0ff'; return ( ); })}
); }; /* ---------- Tweaks panel ---------- */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "dark", "density": "comfortable", "accent": "#7cb0ff" }/*EDITMODE-END*/; const TweaksPanel = ({ open, values, onChange, onClose, isMobile }) => { 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 isMobile = useIsMobile(); const [view, setView] = useState('home'); const [paletteOpen, setPaletteOpen] = useState(false); const [shortcutsOpen, setShortcutsOpen] = useState(false); const [tweaksOpen, setTweaksOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = 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; } }); const [pinnedSlugs, setPinnedSlugs] = useState(() => { const stored = readPinned(); if (stored.length > 0) return stored; const defaults = window.PM_TOOLS.filter(t => t.pin).map(t => t.slug); writePinned(defaults); return defaults; }); const togglePin = useCallback((slug) => { setPinnedSlugs(prev => { const next = prev.includes(slug) ? prev.filter(s => s !== slug) : [...prev, slug]; writePinned(next); return next; }); }, []); const isPinned = useCallback((slug) => pinnedSlugs.includes(slug), [pinnedSlugs]); 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']; })(); const pinnedTools = __pmVisible(window.PM_TOOLS).filter(t => pinnedSlugs.includes(t.slug)); 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} isMobile={isMobile} open={sidebarOpen} onClose={() => setSidebarOpen(false)} pinnedSlugs={pinnedSlugs}/>
setPaletteOpen(true)} onOpenTweaks={() => setTweaksOpen(true)} breadcrumb={breadcrumb} isMobile={isMobile} onToggleSidebar={() => setSidebarOpen(v => !v)}/>
{view === 'home' && (
setPaletteOpen(true)} isMobile={isMobile}/> {}} isMobile={isMobile} isPinned={isPinned}/>
)} {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

{pinnedTools.length === 0 ? (
No pinned tools yet
Open any tool and click Pin to add it here.
) : (
{pinnedTools.map(t => )}
)}
)} {view === 'map' && (

Platform

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

Settings

Use the Tweaks panel (bottom-right) to adjust theme, density, and accent.
)}
setPaletteOpen(false)} onRun={onRun} pinnedSlugs={pinnedSlugs}/> setShortcutsOpen(false)}/> setSelectedTool(null)} onTogglePin={togglePin} isPinned={isPinned}/> setTweaksOpen(false)} isMobile={isMobile}/>
); }; /* ---------- 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);