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:
@@ -110,6 +110,7 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error
|
||||
|
||||
// Get endpoint data for the explorer
|
||||
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
|
||||
_ = endpointInfo
|
||||
if err != nil {
|
||||
// Endpoint not found in explorer data, fall back to old behavior
|
||||
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
|
||||
@@ -168,16 +169,10 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error
|
||||
}
|
||||
}
|
||||
|
||||
// Create API explorer data
|
||||
explorerData := templates.APIExplorerData{
|
||||
Endpoint: *endpointInfo,
|
||||
IsLoggedIn: isLoggedIn,
|
||||
}
|
||||
|
||||
// Render API endpoint page with explorer
|
||||
// Render API endpoint page
|
||||
var buf bytes.Buffer
|
||||
currentPath := c.Request().URL.Path
|
||||
err = templates.DocsLayoutWithExplorer(*nav, *doc, user, explorerData, currentPath).Render(c.Request().Context(), &buf)
|
||||
err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf)
|
||||
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
||||
|
||||
+11
-162
@@ -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;
|
||||
|
||||
+8
-356
File diff suppressed because one or more lines are too long
@@ -144,6 +144,24 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Header component styles */
|
||||
.header-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Make header sticky only on docs pages */
|
||||
body:not(.page-docs) .header-nav {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* Add class to body on docs pages */
|
||||
.page-docs .header-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user