// Project detail — full-width page (not a slide-over). // Driven by the parent via prop; closes via onClose, switches via onSelect. // Hacker palette: dark terminal-paper, phosphor green primary. // Shared responsive hook — used by both ProjectDetail and DossierMain function useIsMobile(breakpoint = 768) { const [mobile, setMobile] = React.useState( typeof window !== 'undefined' && window.innerWidth <= breakpoint ); React.useEffect(() => { const check = () => setMobile(window.innerWidth <= breakpoint); window.addEventListener('resize', check); return () => window.removeEventListener('resize', check); }, [breakpoint]); return mobile; } const detailStyles = { page: { minHeight: '100vh', background: 'var(--bg)', fontFamily: "'JetBrains Mono', monospace", color: 'var(--ink)', }, topbar: { position: 'sticky', top: 0, zIndex: 20, background: '#000', borderBottom: '1px solid var(--green)', padding: '12px 24px', display: 'flex', alignItems: 'center', gap: 14, fontSize: 11, letterSpacing: 1.4, color: 'var(--green)', }, toolbar: { background: 'var(--paper-2)', borderBottom: '1px solid var(--line-2)', padding: '14px 32px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap', }, toolGroup: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }, // Floating back button — stays in view as page scrolls floatingBack: { position: 'fixed', bottom: 32, left: 32, zIndex: 100, background: 'var(--green)', color: '#000', fontFamily: "'JetBrains Mono', monospace", fontSize: 12, fontWeight: 600, letterSpacing: 0.8, padding: '10px 18px', border: 'none', cursor: 'pointer', boxShadow: '4px 4px 0 rgba(57,255,122,0.25)', transition: 'transform .1s, box-shadow .1s', }, body: { maxWidth: 980, margin: '0 auto', padding: '40px 32px 80px', }, docHead: { paddingBottom: 24, borderBottom: '2px solid var(--line-2)', marginBottom: 28, }, twId: { fontSize: 11, letterSpacing: 1.4, color: 'var(--green)', marginBottom: 12, }, title: { fontFamily: "'Inter', sans-serif", fontSize: 44, fontWeight: 700, letterSpacing: -1.2, margin: 0, lineHeight: 1.05, color: '#fff', }, blurb: { fontFamily: "'Inter', sans-serif", fontSize: 17, lineHeight: 1.65, color: 'var(--ink)', marginTop: 16, maxWidth: 760, }, metaRow: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 18 }, tag: { fontSize: 12, letterSpacing: 0.4, color: 'var(--green)', border: '1px solid var(--line-2)', background: '#000', padding: '3px 9px', borderRadius: 2, fontFamily: "'JetBrains Mono', monospace", }, callout: { fontFamily: "'Inter', sans-serif", fontSize: 16, lineHeight: 1.7, background: 'var(--paper-2)', borderLeft: '3px solid var(--green)', padding: '16px 20px', color: 'var(--ink)', border: '1px solid var(--line-2)', }, body2: { fontFamily: "'Inter', sans-serif", fontSize: 16, lineHeight: 1.7, color: 'var(--ink)', }, list: { margin: '4px 0 0', padding: '0 0 0 20px', fontFamily: "'Inter', sans-serif", fontSize: 16, lineHeight: 1.75, color: 'var(--ink)', }, artifactRow: { display: 'flex', flexWrap: 'wrap', gap: 10 }, artifact: { fontSize: 12, color: 'var(--green)', border: '1px solid var(--green)', background: '#000', padding: '8px 12px', borderRadius: 2, fontFamily: "'JetBrains Mono', monospace", }, // Rich content section styles figure: { margin: '18px 0', background: '#0a0c10', border: '1px solid var(--line-2)', borderRadius: 6, overflow: 'hidden', }, figImg: { width: '100%', height: 'auto', display: 'block', }, figCaption: { padding: '10px 14px', fontSize: 13, color: 'var(--dim)', borderTop: '1px dashed var(--line-2)', fontFamily: "'JetBrains Mono', monospace", }, codeBlock: { background: '#0a0c10', color: '#c5f0c8', padding: '16px 18px', borderRadius: 6, overflow: 'auto', border: '1px solid var(--line-2)', fontSize: 13, lineHeight: 1.6, fontFamily: "'JetBrains Mono', monospace", whiteSpace: 'pre-wrap', wordBreak: 'break-word', }, codeTitle: { fontSize: 11, letterSpacing: 0.6, color: 'var(--dim)', marginBottom: 6, fontFamily: "'JetBrains Mono', monospace", }, disclosure: { margin: '12px 0', border: '1px solid var(--line-2)', borderRadius: 6, overflow: 'hidden', }, disclosureSummary: { padding: '12px 16px', background: 'var(--paper-2)', cursor: 'pointer', fontSize: 13, color: 'var(--green)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: 0.4, userSelect: 'none', }, // Slider styles sliderFrame: { position: 'relative', }, sliderControls: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: '10px 14px', background: 'var(--paper-2)', borderTop: '1px solid var(--line-2)', }, sliderBtn: { border: '1px solid var(--line-2)', background: '#000', color: 'var(--green)', borderRadius: 2, padding: '6px 14px', fontSize: 11, cursor: 'pointer', fontFamily: "'JetBrains Mono', monospace", letterSpacing: 0.6, }, sliderDots: { display: 'flex', gap: 6, alignItems: 'center', }, sliderDot: { width: 10, height: 10, borderRadius: '50%', border: '1px solid var(--line-2)', background: 'transparent', padding: 0, cursor: 'pointer', transition: 'background .15s', }, sliderDotActive: { background: 'var(--green)', }, video: { width: '100%', borderRadius: 6, border: '1px solid var(--line-2)', background: '#000', }, footerNav: { marginTop: 56, paddingTop: 24, borderTop: '2px solid var(--line-2)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, }, navCard: { border: '1px solid var(--line-2)', background: 'var(--paper-2)', padding: '16px 18px', cursor: 'pointer', transition: 'border-color .12s, background .12s, transform .12s', }, navCardLabel: { fontSize: 10, letterSpacing: 1.4, color: 'var(--dim)', textTransform: 'uppercase', marginBottom: 6, }, navCardTitle: { fontFamily: "'Inter', sans-serif", fontSize: 16, color: '#fff', fontWeight: 600, }, }; // ─── Image Slider sub-component ─────────────────────────────────────── function ImageSlider({ images }) { const [idx, setIdx] = React.useState(0); const ds = detailStyles; const count = images.length; const show = (i) => setIdx((i + count) % count); return (
{images.map((img, i) => ( {img.alt ))}
{count > 1 && (
{images.map((_, i) => (
)} {images[idx]?.caption && (
{images[idx].caption}
)}
); } // ─── Code Disclosure sub-component ──────────────────────────────────── function CodeDisclosure({ label, code, language }) { const [open, setOpen] = React.useState(false); const ds = detailStyles; return (
setOpen(!open)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open); } }} > {open ? '▾' : '▸'} {label || 'View source code'}
{open && (
          {code}
        
)}
); } // ─── Rich section renderer ──────────────────────────────────────────── function RichContentBlock({ block, mobile }) { const ds = detailStyles; switch (block.type) { case 'paragraph': return

{block.text}

; case 'list': const ListTag = block.ordered ? 'ol' : 'ul'; return ( {block.items.map((item, i) =>
  • {item}
  • )}
    ); case 'image': return (
    {block.alt {block.caption &&
    {block.caption}
    }
    ); case 'slider': return ; case 'code': return (
    {block.title &&
    {block.title}
    }
                {block.code}
              
    ); case 'disclosure': return ; case 'video': return (
    {block.caption &&
    {block.caption}
    }
    ); default: return null; } } function ProjectDetail({ project, projects, onClose, onSelect }) { const idx = projects.findIndex(p => p.id === project.id); const prev = projects[idx - 1]; const next = projects[idx + 1]; const d = project.detail || {}; const ds = detailStyles; const mobile = useIsMobile(); // Esc to close, ←/→ to switch React.useEffect(() => { const handler = (e) => { if (e.key === 'Escape') onClose(); else if (e.key === 'ArrowLeft' && prev) onSelect(prev.id); else if (e.key === 'ArrowRight' && next) onSelect(next.id); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [project.id, prev?.id, next?.id]); // Reset scroll when switching projects React.useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, [project.id]); return (
    // TECHNICAL WORK · TW-{String(idx + 1).padStart(3, '0')} {!mobile && ESC · ← → to navigate}
    {/* Floating back button */}
    TW-{String(idx + 1).padStart(3, '0')} · {project.kind.toUpperCase()}

    {project.title}

    {project.blurb}

    {project.tags.map(t => {t})}
    {d.objective ? ( <>
    § Objective
    {d.objective}
    ) : null} {d.approach?.length ? ( <>
    § Approach{d.approach.length} steps
      {d.approach.map((step, i) =>
    1. {step}
    2. )}
    ) : null} {d.outcomes?.length ? ( <>
    § Outcomes
      {d.outcomes.map((o, i) =>
    • {o}
    • )}
    ) : null} {d.learned ? ( <>
    § What I learned
    {d.learned}
    ) : null} {/* ─── Rich content sections ──────────────────────────────── */} {d.sections?.map((section, si) => (
    {section.heading && (
    § {section.heading}
    )} {section.content?.map((block, bi) => ( ))}
    ))} {d.artifacts?.length ? ( <>
    § Artifacts
    {d.artifacts.map((a, i) => {a})}
    ) : null}
    {prev ? (
    onSelect(prev.id)} onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--green)'; e.currentTarget.style.transform = 'translateX(-3px)'; }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.transform = 'translateX(0)'; }} >
    ← Previous · TW-{String(idx).padStart(3, '0')}
    {prev.title}
    ) : !mobile ?
    : null} {next ? (
    onSelect(next.id)} onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--green)'; e.currentTarget.style.transform = mobile ? 'translateY(-2px)' : 'translateX(3px)'; }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line-2)'; e.currentTarget.style.transform = 'none'; }} >
    Next · TW-{String(idx + 2).padStart(3, '0')} →
    {next.title}
    ) : !mobile ?
    : null}
    ); } window.ProjectDetail = ProjectDetail;