// Cmd+K / Ctrl+K command palette. Hits /api/search and renders editorial-card // results. Vanilla JS, no deps; styles come from output.css (Tailwind). (function () { const overlay = document.getElementById("pete-search"); if (!overlay) return; const input = overlay.querySelector("[data-search-input]"); const list = overlay.querySelector("[data-search-list]"); const meta = overlay.querySelector("[data-search-meta]"); let activeIndex = -1; let items = []; let inflight = null; let debounceTimer = 0; let lastQuery = ""; function open() { if (!overlay.classList.contains("hidden")) return; overlay.classList.remove("hidden"); document.body.classList.add("overflow-hidden"); requestAnimationFrame(() => input.focus()); } function close() { overlay.classList.add("hidden"); document.body.classList.remove("overflow-hidden"); input.value = ""; list.innerHTML = ""; meta.textContent = ""; items = []; activeIndex = -1; lastQuery = ""; } function escapeHTML(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function render(results) { items = results || []; activeIndex = items.length > 0 ? 0 : -1; if (items.length === 0) { list.innerHTML = ""; return; } const html = items.map((r, i) => { const thumb = r.thumb_url ? `` : ``; const postedRing = r.posted ? ` border-theme-${escapeHTML(r.channel_theme)}` : ""; return ` ${thumb}
${escapeHTML(r.channel_title)} ${escapeHTML(r.source)} ${escapeHTML(r.time_ago)}

${escapeHTML(r.headline)}

${r.lede ? `

${escapeHTML(r.lede)}

` : ""}
`; }).join(""); list.innerHTML = html; paintActive(); } function paintActive() { const nodes = list.querySelectorAll(".search-result"); nodes.forEach((n, i) => { if (i === activeIndex) { n.classList.add("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15"); n.scrollIntoView({ block: "nearest" }); } else { n.classList.remove("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15"); } }); } async function runQuery(q) { if (q === lastQuery) return; lastQuery = q; if (!q) { render([]); meta.textContent = "type to search · esc to close"; return; } if (inflight) inflight.abort(); const ctrl = new AbortController(); inflight = ctrl; meta.textContent = "searching…"; try { const res = await fetch("/api/search?q=" + encodeURIComponent(q), { signal: ctrl.signal }); if (!res.ok) throw new Error("search failed: " + res.status); const data = await res.json(); if (ctrl !== inflight) return; render(data.results || []); const n = (data.results || []).length; meta.textContent = n === 0 ? "no matches" : `${n} result${n === 1 ? "" : "s"}`; } catch (err) { if (err.name === "AbortError") return; console.error(err); meta.textContent = "search error"; } finally { if (ctrl === inflight) inflight = null; } } input.addEventListener("input", () => { clearTimeout(debounceTimer); const q = input.value.trim(); debounceTimer = window.setTimeout(() => runQuery(q), 120); }); input.addEventListener("keydown", (e) => { if (e.key === "ArrowDown") { e.preventDefault(); if (items.length === 0) return; activeIndex = (activeIndex + 1) % items.length; paintActive(); } else if (e.key === "ArrowUp") { e.preventDefault(); if (items.length === 0) return; activeIndex = (activeIndex - 1 + items.length) % items.length; paintActive(); } else if (e.key === "Enter") { if (activeIndex >= 0 && items[activeIndex]) { e.preventDefault(); window.open(items[activeIndex].article_url, "_blank", "noopener"); } } }); overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); document.addEventListener("keydown", (e) => { const isK = e.key === "k" || e.key === "K"; if ((e.metaKey || e.ctrlKey) && isK) { e.preventDefault(); if (overlay.classList.contains("hidden")) open(); else close(); return; } if (e.key === "Escape" && !overlay.classList.contains("hidden")) { close(); } if (e.key === "/" && overlay.classList.contains("hidden")) { const tag = (document.activeElement && document.activeElement.tagName) || ""; if (tag === "INPUT" || tag === "TEXTAREA") return; e.preventDefault(); open(); } }); document.querySelectorAll("[data-search-trigger]").forEach((btn) => { btn.addEventListener("click", (e) => { e.preventDefault(); open(); }); }); meta.textContent = "type to search · esc to close"; })();