Remove redundant <script src="/static/main.js" defer></script> tags from 17+ templates that include the @Header component, eliminating duplicate script loading that was causing Alpine.js to initialize twice per page load. The header.templ component now serves as the single source of truth for main.js inclusion, following the DRY principle and ensuring consistent script loading across all pages that use the header navigation. Additionally, add type="button" attribute to all buttons in header navigation to prevent default form submission behavior when buttons are clicked. Changes: - Remove main.js script tag from templates using @Header component - Keep main.js in header.templ (line 279) as universal inclusion point - Preserve main.js in special pages: index.templ, login.templ, register.templ (these don't use @Header and are standalone entry points) - Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons to prevent unwanted form submissions or page navigation Benefits: - Eliminates Alpine.js double-initialization bug - Reduces HTTP requests (one script load instead of two) - Improves maintainability (add header, get scripts automatically) - Fixes broken @click handlers on collections, devices, and other pages - Prevents buttons from triggering default form submission behavior Technical notes: - Templates affected: admin, analytics, bookshelf, collection_rules, collections, conflicts, custom_section, dashboard, devices, docs, library, profile, progress, queue, unlinked_books - No changes to entry pages (index, login, register) which don't use @Header - HTMX script remains in individual templates (stateless, no double-load issue) - All interactive buttons in header now explicitly marked type="button" to prevent default browser form submission behavior Related to: previous commit fixing Vite code-splitting
113 lines
4.7 KiB
Templ
113 lines
4.7 KiB
Templ
package templates
|
||
|
||
import "fmt"
|
||
|
||
templ DocsLayout(nav Navigation, doc Document, user User, 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"/>
|
||
</head>
|
||
<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
|
||
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-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 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"
|
||
@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">
|
||
<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-8 max-w-4xl mx-auto">
|
||
<!-- Breadcrumb -->
|
||
if len(doc.Breadcrumb) > 0 {
|
||
<nav class="breadcrumb flex 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-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
||
<!-- Table of Contents -->
|
||
if len(doc.TOC) > 0 {
|
||
<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 }
|
||
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>
|
||
}
|
||
</details>
|
||
}
|
||
<!-- Content -->
|
||
<div class="prose prose-invert max-w-none">
|
||
@UnsafeHTML(doc.Content).ToComponent()
|
||
</div>
|
||
</main>
|
||
</body>
|
||
</html>
|
||
}
|