docs: add sticky header support for documentation pages

- Add CSS rules to web/static/input.css for header positioning
- Header is sticky only on /docs pages via .page-docs body class
- Add page-docs class to body element in templates/docs.templ
- Non-docs pages have static header position
This commit is contained in:
2026-02-03 16:16:14 -05:00
parent d9c6be1429
commit 79373dd225
5 changed files with 41 additions and 527 deletions
+11 -162
View File
@@ -18,7 +18,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<script src="/static/lunr-flex.min.js"></script>
<script src="/static/header.js"></script>
</head>
<body class={ "theme-" + user.Theme + " bg-background-primary text-text-primary font-sans antialiased" }>
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
@Header(user, currentPath)
<!-- Mobile Menu Button -->
<button
@@ -35,7 +35,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
<!-- Sidebar -->
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto z-40 mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
<!-- Search -->
<div class="p-4 border-b border-border">
<input
@@ -96,11 +96,14 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<!-- Title -->
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
<!-- Table of Contents -->
if len(doc.TOC) > 0 {
<div class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
<strong class="block mb-2 text-text-primary font-semibold">On this page</strong>
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
<summary class="cursor-pointer hover:opacity-80 transition-opacity">
<strong class="text-text-primary font-semibold">On this page</strong>
<span class="ml-2 text-text-secondary text-xs"></span>
</summary>
for _, item := range doc.TOC {
<a
href={ "#" + item.Anchor }
@@ -110,7 +113,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
{ item.Title }
</a>
}
</div>
</details>
}
<!-- Content -->
@@ -143,14 +146,6 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
sidebar.classList.toggle('open');
}
// Close sidebar when clicking main content on mobile
document.querySelector('.main-content')?.addEventListener('click', () => {
const sidebar = document.querySelector('.sidebar');
if (window.innerWidth < 1024) {
sidebar.classList.remove('open');
}
});
// Highlight current page in nav
document.addEventListener('DOMContentLoaded', function() {
const currentPath = window.location.pathname;
@@ -163,7 +158,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
// 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;
@@ -331,160 +326,14 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
}
});
</script>
</body>
</html>
}
templ DocsLayoutWithExplorer(nav Navigation, doc Document, user User, explorer APIExplorerData, currentPath string) {
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{ doc.Title } - Bookhoard Documentation</title>
<link href="/static/style.css" rel="stylesheet">
<link rel="stylesheet" href="/static/highlight-dark.min.css">
<script src="/static/highlight.min.js"></script>
<script src="/static/lunr.min.js"></script>
<script src="/static/lunr-flex.min.js"></script>
<script src="/static/header.js"></script>
</head>
<body class={ "theme-" + user.Theme + " bg-background-primary text-text-primary font-sans antialiased" }>
@Header(user, currentPath)
<!-- Mobile Menu Button -->
<button
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
onclick="toggleSidebar()"
aria-label="Toggle menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- Search Results Overlay -->
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-4 lg:p-8 transition-opacity duration-200 ease-in-out"></div>
<!-- Sidebar -->
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto z-40 mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
<!-- Search -->
<div class="p-4 border-b border-border">
<input
type="text"
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
placeholder="Search documentation..."
id="docs-search"
>
</div>
<!-- Navigation Sections -->
for _, section := range nav.Sections {
<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)"
>
<span>{ section.Title }</span>
<svg class="w-4 h-4 transform transition-transform section-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
if section.Collapsed {
<div class="nav-items hidden" data-collapsed="true">
for _, item := range section.Items {
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline">
<span class="mr-2">{ item.Icon }</span>{ item.Title }
</a>
}
</div>
} else {
<div class="nav-items" data-collapsed="false">
for _, item := range section.Items {
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline">
<span class="mr-2">{ item.Icon }</span>{ item.Title }
</a>
}
</div>
}
</div>
}
</div>
<!-- Main Content -->
<main class="main-content lg:ml-72 p-4 lg:p-8 max-w-4xl mx-auto">
<!-- Breadcrumb -->
if len(doc.Breadcrumb) > 0 {
<nav class="breadcrumb flex flex-wrap gap-2 text-sm text-text-secondary mb-8" aria-label="Breadcrumb">
for i, crumb := range doc.Breadcrumb {
if i > 0 {
<span class="text-text-secondary/50"></span>
}
<a href={ crumb.URL } class="text-accent hover:underline no-underline">{ crumb.Title }</a>
}
</nav>
}
<!-- Title -->
<h1 class="text-3xl lg:text-4xl font-bold mb-6 lg:mb-8 text-text-primary">{ doc.Title }</h1>
<!-- Table of Contents -->
if len(doc.TOC) > 0 {
<div class="toc bg-background-secondary p-4 rounded-lg mb-6 lg:mb-8 border border-border lg:static">
<strong class="block mb-2 text-text-primary font-semibold">On this page</strong>
for _, item := range doc.TOC {
<a
href={ "#" + item.Anchor }
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline"
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
>
{ item.Title }
</a>
}
</div>
}
<!-- Content -->
<div class="prose prose-invert prose-sm lg:prose-base max-w-none">
@UnsafeHTML(doc.Content).ToComponent()
</div>
<!-- API Explorer -->
@APIExplorer(explorer)
</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');
}
// Close sidebar when clicking main content on mobile
document.querySelector('.main-content')?.addEventListener('click', () => {
const sidebar = document.querySelector('.sidebar');
if (window.innerWidth < 1024) {
sidebar.classList.remove('open');
}
});
// Highlight current page in nav
document.addEventListener('DOMContentLoaded', function() {
const currentPath = window.location.pathname;