import { Alpine } from "./alpine"; // Documentation site behavior. Rendering ownership (AGENTS.md): the server // renders what things look like — nav markup, search results — and this // module only orchestrates client-local state (open/closed panels, the // search dropdown) and swaps in the server-rendered search partial. function toggleSidebar(): void { const sidebar = document.getElementById("docs-sidebar"); const overlay = document.getElementById("docs-overlay"); if (!sidebar || !overlay) return; sidebar.classList.toggle("translate-x-0"); sidebar.classList.toggle("-translate-x-full"); overlay.classList.toggle("hidden"); } // Docs sidebar sections toggle open/closed in-page (purely client-local, // ephemeral — the SERVER decides what is expanded on each page load from // navigation defaults, so there is no saved state and nothing to jump). // The data-collapsed attribute is the single source of truth: the // stylesheet hides on it, and the arrow rotates relative to it. function applyCollapsed(items: HTMLElement, collapsed: boolean): void { items.setAttribute("data-collapsed", collapsed ? "true" : "false"); const arrow = items.previousElementSibling?.querySelector(".section-arrow"); if (arrow) { arrow.style.transform = collapsed ? "rotate(-90deg)" : "rotate(0deg)"; } } function toggleCollapsible(header: HTMLElement): void { const items = header.nextElementSibling as HTMLElement | null; if (!items) return; const isCollapsed = items.getAttribute("data-collapsed") === "true"; applyCollapsed(items, !isCollapsed); } let docsSearchTimeout: ReturnType | null = null; let docsSearchAbort: AbortController | null = null; function initializeDocsSearch(): void { const searchInput = document.getElementById("docs-search") as HTMLInputElement | null; const searchResults = document.getElementById("docs-search-results"); if (!searchInput || !searchResults) return; searchInput.addEventListener("input", () => { const query = searchInput.value.trim(); if (docsSearchTimeout) { clearTimeout(docsSearchTimeout); } if (query.length < 2) { hideDocsSearchResults(); return; } docsSearchTimeout = setTimeout(() => { void performDocsSearch(query); }, 250); }); } function hideDocsSearchResults(): void { const searchResults = document.getElementById("docs-search-results"); if (!searchResults) return; searchResults.replaceChildren(); searchResults.classList.add("hidden"); } // performDocsSearch fetches the server-rendered results partial and swaps it // in verbatim. The partial is templ-rendered (DocsSearchResults); this module // never builds result markup itself. async function performDocsSearch(query: string): Promise { const searchResults = document.getElementById("docs-search-results"); if (!searchResults) return; docsSearchAbort?.abort(); docsSearchAbort = new AbortController(); try { const response = await fetch(`/docs/search/partial?q=${encodeURIComponent(query)}`, { signal: docsSearchAbort.signal, }); if (!response.ok) { throw new Error(`search failed: ${response.status}`); } searchResults.innerHTML = await response.text(); searchResults.classList.remove("hidden"); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") return; console.error("Docs search error:", error); searchResults.textContent = "Search failed — try again."; searchResults.classList.add("px-4", "py-3", "text-sm"); searchResults.style.color = "var(--text-secondary)"; searchResults.classList.remove("hidden"); } } function initializeCodeCopyButtons(): void { document.querySelectorAll(".prose pre").forEach((pre) => { const wrapper = document.createElement("div"); wrapper.className = "relative group"; pre.parentNode?.insertBefore(wrapper, pre); wrapper.appendChild(pre); const button = document.createElement("button"); button.className = "absolute top-2 right-2 btn px-2 py-1 rounded-lg text-xs opacity-0 group-hover:opacity-100 transition-opacity"; button.textContent = "Copy"; button.addEventListener("click", async () => { await navigator.clipboard.writeText(pre.textContent || ""); button.textContent = "Copied!"; setTimeout(() => { button.textContent = "Copy"; }, 2000); }); wrapper.appendChild(button); }); } Alpine.data("docs", () => ({ toggleSidebar, toggleCollapsible, initializeSearch: initializeDocsSearch, initializeCodeCopyButtons, }));