refactor(docs): remove inline JS from docs template

- Removed ~400 lines of inline JavaScript from docs.templ
- Moved toggleSection function to docs.ts (now uses Alpine )
- Added highlightCurrentPage function to docs.ts
- Added initializeCodeCopyButtons function to docs.ts
- Updated template to use x-init for initialization
- Functions exported for use in Alpine.data
This commit is contained in:
2026-03-13 12:50:51 -04:00
parent 5b7540be99
commit 7068fabbee
3 changed files with 67 additions and 431 deletions
+2 -421
View File
@@ -13,7 +13,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<link rel="stylesheet" href="/static/highlight-dark.min.css"/>
<script src="/static/main.js" defer></script>
</head>
<body x-data="docs" x-init="initializeSearch" class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
<body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
@Header(user, currentPath)
<!-- Mobile Menu Button -->
<button
@@ -43,7 +43,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<div class="nav-section mb-6">
<div
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
onclick="toggleSection(this)"
@click="toggleSection($el)"
>
<span>{ section.Title }</span>
<svg class="w-4 h-4 transform transition-transform section-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -108,425 +108,6 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
@UnsafeHTML(doc.Content).ToComponent()
</div>
</main>
<script>
// Toggle sidebar section
function toggleSection(el) {
const items = el.nextElementSibling;
const arrow = el.querySelector(".section-arrow");
const isCollapsed = items.getAttribute("data-collapsed") === "true";
if (isCollapsed) {
items.classList.remove("hidden");
items.setAttribute("data-collapsed", "false");
arrow.style.transform = "rotate(0deg)";
} else {
items.classList.add("hidden");
items.setAttribute("data-collapsed", "true");
arrow.style.transform = "rotate(-90deg)";
}
}
// Toggle sidebar on mobile
function toggleSidebar() {
const sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("open");
}
// Highlight current page in nav
document.addEventListener("DOMContentLoaded", function () {
const currentPath = window.location.pathname;
document.querySelectorAll(".nav-item").forEach((item) => {
if (item.getAttribute("href") === currentPath) {
item.classList.add(
"text-accent",
"bg-accent/10",
"border-r-2",
"border-accent",
);
}
});
// Initialize syntax highlighting
if (typeof hljs !== "undefined") {
hljs.highlightAll();
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
}
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
// Search state
let idx;
let searchDocs = [];
// Load index on page load
fetch("/docs/search-index.json")
.then((r) => r.json())
.then((data) => {
idx = lunr(function () {
this.use(lunr.flex);
this.ref("id");
this.field("title", { boost: 10 });
this.field("content", { boost: 1 });
data.forEach((doc) => this.add(doc));
});
searchDocs = data;
console.log("Search index loaded:", data.length, "documents");
})
.catch((err) => console.error("Failed to load search index:", err));
// Search input with debounce
const searchInput = document.getElementById("docs-search");
const searchResultsDiv = document.getElementById("search-results");
let debounceTimer;
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
searchResultsDiv.classList.add("hidden");
return;
}
// Search with fuzzy matching
const results = idx.search(query, {
fields: { title: { boost: 10 }, content: 1 },
expand: true,
});
// Render results
renderSearchResults(results, query);
}, 150);
});
// Render search results
function renderSearchResults(results, query) {
if (results.length === 0) {
searchResultsDiv.innerHTML =
'<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove("hidden");
return;
}
searchResultsDiv.innerHTML = results
.map((r) => {
const doc = searchDocs.find((d) => d.id === r.ref);
if (!doc) return "";
const snippet = getSnippet(doc.content, query);
return `
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
<div class="font-semibold mb-2">${doc.title}</div>
<div class="text-sm text-text-secondary">${snippet}...</div>
</a>
`;
})
.join("");
searchResultsDiv.classList.remove("hidden");
}
function getSnippet(content, query) {
const words = query.toLowerCase().split(/\s+/);
const contentLower = content.toLowerCase();
for (const word of words) {
const idx = contentLower.indexOf(word);
if (idx !== -1) {
const start = Math.max(0, idx - 50);
const end = Math.min(content.length, idx + 100);
return "..." + content.substring(start, end) + "...";
}
}
return content.substring(0, 150) + "...";
}
// Close search results when clicking outside
document.addEventListener("click", (e) => {
if (
!searchResultsDiv.contains(e.target) &&
e.target !== searchInput
) {
searchResultsDiv.classList.add("hidden");
}
});
// Close search on Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
searchResultsDiv.classList.add("hidden");
searchInput.blur();
}
});
</script>
<script>
// Toggle sidebar on mobile
function toggleSidebar() {
const sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("open");
}
// Highlight current page in nav
document.addEventListener("DOMContentLoaded", function () {
const currentPath = window.location.pathname;
document.querySelectorAll(".nav-item").forEach((item) => {
if (item.getAttribute("href") === currentPath) {
item.classList.add(
"text-accent",
"bg-accent/10",
"border-r-2",
"border-accent",
);
}
});
// Initialize syntax highlighting
if (typeof hljs !== "undefined") {
hljs.highlightAll();
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
}
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
// Search state
let idx;
let searchDocs = [];
// Load index on page load
fetch("/docs/search-index.json")
.then((r) => r.json())
.then((data) => {
idx = lunr(function () {
this.use(lunr.flex);
this.ref("id");
this.field("title", { boost: 10 });
this.field("content", { boost: 1 });
data.forEach((doc) => this.add(doc));
});
searchDocs = data;
console.log("Search index loaded:", data.length, "documents");
})
.catch((err) => console.error("Failed to load search index:", err));
// Search input with debounce
const searchInput = document.getElementById("docs-search");
const searchResultsDiv = document.getElementById("search-results");
let debounceTimer;
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
searchResultsDiv.classList.add("hidden");
return;
}
// Search with fuzzy matching
const results = idx.search(query, {
fields: { title: { boost: 10 }, content: 1 },
expand: true,
});
// Render results
renderSearchResults(results, query);
}, 150);
});
// Render search results
function renderSearchResults(results, query) {
if (results.length === 0) {
searchResultsDiv.innerHTML =
'<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove("hidden");
return;
}
searchResultsDiv.innerHTML = results
.map((r) => {
const doc = searchDocs.find((d) => d.id === r.ref);
if (!doc) return "";
const snippet = getSnippet(doc.content, query);
return `
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
<div class="font-semibold mb-2">${doc.title}</div>
<div class="text-sm text-text-secondary">${snippet}...</div>
</a>
`;
})
.join("");
searchResultsDiv.classList.remove("hidden");
}
function getSnippet(content, query) {
const words = query.toLowerCase().split(/\s+/);
const contentLower = content.toLowerCase();
for (const word of words) {
const idx = contentLower.indexOf(word);
if (idx !== -1) {
const start = Math.max(0, idx - 50);
const end = Math.min(content.length, idx + 100);
return "..." + content.substring(start, end) + "...";
}
}
return content.substring(0, 150) + "...";
}
// Close search results when clicking outside
document.addEventListener("click", (e) => {
if (
!searchResultsDiv.contains(e.target) &&
e.target !== searchInput
) {
searchResultsDiv.classList.add("hidden");
}
});
// Close search on Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
searchResultsDiv.classList.add("hidden");
searchInput.blur();
}
});
</script>
</body>
</html>
}
File diff suppressed because one or more lines are too long
+60 -5
View File
@@ -11,6 +11,24 @@ function toggleSidebar(): void {
}
}
function toggleSection(el: HTMLElement): void {
const items = el.nextElementSibling as HTMLElement;
const arrow = el.querySelector(".section-arrow") as HTMLElement;
if (!items || !arrow) return;
const isCollapsed = items.getAttribute("data-collapsed") === "true";
if (isCollapsed) {
items.classList.remove("hidden");
items.setAttribute("data-collapsed", "false");
arrow.style.transform = "rotate(0deg)";
} else {
items.classList.add("hidden");
items.setAttribute("data-collapsed", "true");
arrow.style.transform = "rotate(-90deg)";
}
}
function initializeDocsSearch(): void {
const searchInput = document.getElementById(
"docs-search",
@@ -89,14 +107,51 @@ function performDocsSearch(query: string): void {
}
}
Alpine.data("docs", () => ({
toggleSidebar,
initializeSearch: initializeDocsSearch,
}));
function highlightCurrentPage(): void {
const currentPath = window.location.pathname;
document.querySelectorAll(".nav-item").forEach((item) => {
if (item.getAttribute("href") === currentPath) {
item.classList.add("text-accent", "bg-accent/10", "border-r-2", "border-accent");
}
});
}
export { toggleSidebar, initializeDocsSearch };
function initializeCodeCopyButtons(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hljs = (window as any).hljs;
if (hljs) {
hljs.highlightAll();
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
if (!pre) return;
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 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent || "");
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
}
}
Alpine.data("docs", () => ({
toggleSidebar,
toggleSection,
initializeSearch: initializeDocsSearch,
highlightCurrentPage,
initializeCodeCopyButtons,
}));
export { toggleSidebar, toggleSection, initializeDocsSearch, highlightCurrentPage, initializeCodeCopyButtons };