/** * issue-detail.ts — Issue detail page module. * * Responsibilities: * 1. Comment entrance animations (IntersectionObserver stagger). * 2. Re-animate comments after HTMX swap (new comment submitted). * 3. Copy issue URL to clipboard (keyboard shortcut). * 4. Syntax-highlight CLI / MCP / REST code snippets (highlight.js). * 5. Inject per-snippet copy buttons. */ import hljs from 'highlight.js/lib/core'; import bash from 'highlight.js/lib/languages/bash'; import javascript from 'highlight.js/lib/languages/javascript'; hljs.registerLanguage('bash', bash); hljs.registerLanguage('javascript', javascript); // ── Comment entrance animations ─────────────────────────────────────────────── function animateComments(root: Element | Document = document): void { const comments = root.querySelectorAll(".id-comment, .id-reply"); if (!comments.length) return; const io = new IntersectionObserver((entries) => { entries.forEach((entry, i) => { if (!entry.isIntersecting) return; const el = entry.target as HTMLElement; el.style.animationDelay = `${i * 30}ms`; io.unobserve(el); }); }, { threshold: 0.05 }); comments.forEach(el => io.observe(el)); } // ── HTMX: re-animate on comment swap ───────────────────────────────────────── function bindHtmxSwap(): void { document.body.addEventListener("htmx:afterSwap", (e: Event) => { const target = (e as CustomEvent).detail?.target as HTMLElement | undefined; if (!target) return; if (target.id === "issue-comments" || target.closest("#issue-comments")) { animateComments(target); } }); } // ── Copy issue URL (keyboard shortcut) ─────────────────────────────────────── function bindCopyUrl(): void { document.addEventListener("keydown", async (e: KeyboardEvent) => { if (e.key !== "y" || e.ctrlKey || e.metaKey || e.altKey) return; const active = document.activeElement; if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) return; try { await navigator.clipboard.writeText(window.location.href); } catch { /* clipboard unavailable */ } }); } // ── Syntax highlighting ─────────────────────────────────────────────────────── function highlightSnippets(): void { document.querySelectorAll('pre.isd-cli-snippet code').forEach(el => { hljs.highlightElement(el); }); } // ── Copy buttons ────────────────────────────────────────────────────────────── const COPY_ICON = ``; const CHECK_ICON = ``; function injectCopyButtons(): void { document.querySelectorAll('.isd-snippet-wrap').forEach(wrap => { if (wrap.querySelector('.isd-cli-copy')) return; const pre = wrap.querySelector('pre.isd-cli-snippet'); if (!pre) return; const btn = document.createElement('button'); btn.className = 'isd-cli-copy'; btn.setAttribute('aria-label', 'Copy to clipboard'); btn.innerHTML = `${COPY_ICON}copy`; btn.addEventListener('click', async () => { const code = pre.querySelector('code'); const text = (code?.textContent ?? pre.textContent ?? '').trim(); try { await navigator.clipboard.writeText(text); btn.classList.add('copied'); btn.innerHTML = `${CHECK_ICON}copied`; setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = `${COPY_ICON}copy`; }, 2000); } catch { /* clipboard unavailable */ } }); wrap.appendChild(btn); }); } // ── Entry point ─────────────────────────────────────────────────────────────── export function initIssueDetail(_data?: Record): void { animateComments(); bindHtmxSwap(); bindCopyUrl(); highlightSnippets(); injectCopyButtons(); }