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
|
// Get endpoint data for the explorer
|
||||||
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
|
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
|
||||||
|
_ = endpointInfo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Endpoint not found in explorer data, fall back to old behavior
|
// Endpoint not found in explorer data, fall back to old behavior
|
||||||
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
|
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
|
||||||
@@ -168,16 +169,10 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create API explorer data
|
// Render API endpoint page
|
||||||
explorerData := templates.APIExplorerData{
|
|
||||||
Endpoint: *endpointInfo,
|
|
||||||
IsLoggedIn: isLoggedIn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render API endpoint page with explorer
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
currentPath := c.Request().URL.Path
|
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 {
|
if err != nil {
|
||||||
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
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/lunr-flex.min.js"></script>
|
||||||
<script src="/static/header.js"></script>
|
<script src="/static/header.js"></script>
|
||||||
</head>
|
</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)
|
@Header(user, currentPath)
|
||||||
<!-- Mobile Menu Button -->
|
<!-- Mobile Menu Button -->
|
||||||
<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>
|
<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 -->
|
<!-- 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 -->
|
<!-- Search -->
|
||||||
<div class="p-4 border-b border-border">
|
<div class="p-4 border-b border-border">
|
||||||
<input
|
<input
|
||||||
@@ -96,11 +96,14 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
||||||
|
|
||||||
<!-- Table of Contents -->
|
<!-- Table of Contents -->
|
||||||
if len(doc.TOC) > 0 {
|
if len(doc.TOC) > 0 {
|
||||||
<div class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
<details 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>
|
<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 {
|
for _, item := range doc.TOC {
|
||||||
<a
|
<a
|
||||||
href={ "#" + item.Anchor }
|
href={ "#" + item.Anchor }
|
||||||
@@ -110,7 +113,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
{ item.Title }
|
{ item.Title }
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</details>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
@@ -143,14 +146,6 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
sidebar.classList.toggle('open');
|
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
|
// Highlight current page in nav
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
@@ -163,7 +158,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
// Initialize syntax highlighting
|
// Initialize syntax highlighting
|
||||||
if (typeof hljs !== 'undefined') {
|
if (typeof hljs !== 'undefined') {
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
|
|
||||||
// Add copy buttons to all code blocks
|
// Add copy buttons to all code blocks
|
||||||
document.querySelectorAll('pre code').forEach((block) => {
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
const pre = block.parentElement;
|
const pre = block.parentElement;
|
||||||
@@ -331,160 +326,14 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<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
|
// Toggle sidebar on mobile
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
sidebar.classList.toggle('open');
|
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
|
// Highlight current page in nav
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const currentPath = window.location.pathname;
|
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);
|
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 {
|
.card {
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user