docs(ui): drop sidebar saved-state — server defaults already ARE the standard docs behavior

Simplification per spec: the menu tracks state through the URL alone.
Land on /docs: fully closed, headers only (an explicit home rule now
overrides the contains-URL rule, since the root overview doc lives in
Getting Started and would otherwise auto-open it). Land on any doc page:
its containing section + group render open at first paint, current link
bold — no JS, no jump, arrows consistent. Navigate elsewhere: old
closes, new opens.

Deleted: docs_nav cookie const/parse, ApplyNavOverrides, cookie write +
collect/persist helpers in web/src/docs.ts, data-key attributes in the
sidebar templ, and the first-section-open fallback (docs home used to
force-open the first section — spec'd as closed).

toggleCollapsible remains for in-page peeking; purely ephemeral.
The [data-collapsed=true] CSS from the previous round stays — it is what
makes the server-rendered state actually hide sections at paint (the
original bug was that this CSS never existed).

Tests: TestBuildNavigationNothingCurrent now asserts zero sections open
on unmatched paths; TestBuildNavigation asserts Getting Started closed
on a device page (no site-wide pin). make test green; 146 vitests; live
verified: home 0 expanded, doc pages exactly 2 (section+group).
This commit is contained in:
John O'Keefe committed 2026-10-24 21:17:55 -04:00
1 parent a8f8c4def9
commit 73e8cdef4b
6 files changed
+155 -272

No files matched your search

-29
View File
@@ -2,10 +2,8 @@ package docs
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"bookhoard/internal/database" "bookhoard/internal/database"
@@ -48,37 +46,10 @@ func (h *HTTPHandler) currentUser(c *echo.Context) templates.User {
return templates.User{Theme: "tokyo-night"} return templates.User{Theme: "tokyo-night"}
} }
// docsNavCookie is the browser-saved collapse state of the docs sidebar
// (set by web/src/docs.ts on every toggle). Strictly-necessary UI state:
// first-party, no identifiers, lets the server render the sidebar in the
// visitor's chosen state so there is no post-paint jump.
const docsNavCookie = "docs_nav"
// docsNavOverrides reads the saved collapse state from the cookie.
// Keys are the sidebar's data-key values ("Section", "Section / Group");
// values are the collapsed flag. Malformed cookies are ignored — the
// server-rendered defaults win.
func docsNavOverrides(c *echo.Context) map[string]bool {
cookie, err := c.Cookie(docsNavCookie)
if err != nil || cookie.Value == "" {
return nil
}
raw, err := url.QueryUnescape(cookie.Value)
if err != nil {
return nil
}
var overrides map[string]bool
if err := json.Unmarshal([]byte(raw), &overrides); err != nil {
return nil
}
return overrides
}
// renderDocPage renders a document inside the docs layout. // renderDocPage renders a document inside the docs layout.
func (h *HTTPHandler) renderDocPage(c *echo.Context, doc *templates.Document) error { func (h *HTTPHandler) renderDocPage(c *echo.Context, doc *templates.Document) error {
currentPath := c.Request().URL.Path currentPath := c.Request().URL.Path
nav := h.docs.BuildNavigation(currentPath) nav := h.docs.BuildNavigation(currentPath)
ApplyNavOverrides(nav, docsNavOverrides(c))
var buf bytes.Buffer var buf bytes.Buffer
if err := templates.DocsLayout(*nav, *doc, h.currentUser(c), currentPath).Render(c.Request().Context(), &buf); err != nil { if err := templates.DocsLayout(*nav, *doc, h.currentUser(c), currentPath).Render(c.Request().Context(), &buf); err != nil {
+20 -51
View File
@@ -178,8 +178,8 @@ func docURL(docPath string) string {
// BuildNavigation creates the category → subcategory navigation from the // BuildNavigation creates the category → subcategory navigation from the
// docs filesystem. The section and group containing currentPath render // docs filesystem. The section and group containing currentPath render
// expanded; everything else starts collapsed (with Getting Started as the // expanded; everything else starts collapsed. When nothing matches (docs
// fallback when nothing matches). // home, 404), the sidebar renders fully closed — headers only.
func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation { func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation {
docs, err := h.ListDocuments() docs, err := h.ListDocuments()
if err != nil { if err != nil {
@@ -198,16 +198,17 @@ func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation
} }
type groupBuild struct { type groupBuild struct {
items []templates.NavItem items []templates.NavItem
urls []string urls []string
overview bool // has a pinned index page // overview: the group leads with its "#"Overview landing doc (pinned
// subdirectory index), so it sorts first.
overview bool
} }
type sectionBuild struct { type sectionBuild struct {
items []templates.NavItem items []templates.NavItem
urls []string urls []string
pinned map[string]bool // landing pages lead their section pinned map[string]bool // landing pages lead their section
groups map[string]*groupBuild groups map[string]*groupBuild
overview bool
} }
builds := make(map[string]*sectionBuild, len(sectionOrder)) builds := make(map[string]*sectionBuild, len(sectionOrder))
@@ -231,10 +232,6 @@ func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation
if place.pin < 0 { if place.pin < 0 {
build.pinned[item.URL] = true build.pinned[item.URL] = true
} }
if place.pin == -100 {
// Only the root Overview forces its section open.
build.overview = true
}
} else { } else {
group, ok := build.groups[place.group] group, ok := build.groups[place.group]
if !ok { if !ok {
@@ -301,52 +298,24 @@ func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation
} }
} }
} }
if build.overview {
section.Collapsed = false
}
nav.Sections = append(nav.Sections, section) nav.Sections = append(nav.Sections, section)
} }
// Nothing matches the current page (404, search landing): open the // No default expansion when nothing matches the current page (404,
// first section so the sidebar doesn't render fully closed. // search landing): the sidebar renders fully closed. Same for the docs
anyOpen := false // HOME: although the root overview doc lives there (its contains-URL
for _, s := range nav.Sections { // rule would open "Getting Started"), the spec is "home = all closed,
if !s.Collapsed { // headers only" — the standard docs-site behavior.
anyOpen = true if strings.TrimSuffix(currentPath, "/") == "/docs" {
break for si := range nav.Sections {
nav.Sections[si].Collapsed = true
} }
} return nav
if !anyOpen && len(nav.Sections) > 0 {
nav.Sections[0].Collapsed = false
} }
return nav return nav
} }
// ApplyNavOverrides merges the browser-saved collapse state (docs_nav
// cookie) into the nav so the sidebar first-paints in the visitor's chosen
// state — no post-paint JS correction, no jumping. Keys mirror the
// data-key attributes DocsSidebar renders: section titles, and
// "Section / Group" for groups. Entries for unknown keys (renamed or
// removed sections) are ignored; keys absent from the map keep the
// path-derived default.
func ApplyNavOverrides(nav *templates.Navigation, overrides map[string]bool) {
if len(overrides) == 0 {
return
}
for si := range nav.Sections {
s := &nav.Sections[si]
if v, ok := overrides[s.Title]; ok {
s.Collapsed = v
}
for gi := range s.Groups {
if v, ok := overrides[s.Title+" / "+s.Groups[gi].Title]; ok {
s.Groups[gi].Collapsed = v
}
}
}
}
// containsURL reports whether currentPath is one of the given doc URLs. // containsURL reports whether currentPath is one of the given doc URLs.
func containsURL(currentPath string, urls []string) bool { func containsURL(currentPath string, urls []string) bool {
for _, u := range urls { for _, u := range urls {
+4 -7
View File
@@ -127,8 +127,8 @@ func TestBuildNavigation(t *testing.T) {
} }
getting := nav.Sections[0] getting := nav.Sections[0]
if getting.Collapsed { if !getting.Collapsed {
t.Error("Getting Started should be expanded (root overview pinned)") t.Error("Getting Started should be collapsed: only sections containing the current page open")
} }
if len(getting.Items) != 1 || getting.Items[0].Title != "Overview" || getting.Items[0].URL != "/docs" { if len(getting.Items) != 1 || getting.Items[0].Title != "Overview" || getting.Items[0].URL != "/docs" {
t.Errorf("Getting Started items = %+v, want single Overview → /docs", getting.Items) t.Errorf("Getting Started items = %+v, want single Overview → /docs", getting.Items)
@@ -196,10 +196,7 @@ func TestBuildNavigationNothingCurrent(t *testing.T) {
open++ open++
} }
} }
if open != 1 { if open != 0 {
t.Errorf("expected exactly one section open on a 404 path, got %d", open) t.Errorf("expected zero sections open when nothing matches (docs home / 404 renders fully closed), got %d", open)
}
if nav.Sections[0].Collapsed {
t.Error("fallback should open the first section")
} }
} }
+2 -2
View File
@@ -198,7 +198,7 @@ templ DocsSidebar(nav Navigation, currentPath string) {
@Icon("chevron-down", "h-4 w-4") @Icon("chevron-down", "h-4 w-4")
</span> </span>
</button> </button>
<div class="nav-items pb-1" data-key={ section.Title } data-collapsed={ collapsibleState(section.Collapsed) }> <div class="nav-items pb-1" data-collapsed={ collapsibleState(section.Collapsed) }>
for _, item := range section.Items { for _, item := range section.Items {
<a href={ item.URL } class={ docsNavClass(currentPath, item.URL, "pl-7") }> <a href={ item.URL } class={ docsNavClass(currentPath, item.URL, "pl-7") }>
<span>{ item.Title }</span> <span>{ item.Title }</span>
@@ -217,7 +217,7 @@ templ DocsSidebar(nav Navigation, currentPath string) {
@Icon("chevron-down", "h-3.5 w-3.5") @Icon("chevron-down", "h-3.5 w-3.5")
</span> </span>
</button> </button>
<div class="nav-group-items" data-key={ section.Title + " / " + group.Title } data-collapsed={ collapsibleState(group.Collapsed) }> <div class="nav-group-items" data-collapsed={ collapsibleState(group.Collapsed) }>
for _, item := range group.Items { for _, item := range group.Items {
<a href={ item.URL } class={ docsNavClass(currentPath, item.URL, "pl-9") }> <a href={ item.URL } class={ docsNavClass(currentPath, item.URL, "pl-9") }>
<span class="truncate">{ item.Title }</span> <span class="truncate">{ item.Title }</span>
+124 -150
View File
@@ -588,114 +588,101 @@ func DocsSidebar(nav Navigation, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></button><div class=\"nav-items pb-1\" data-key=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></button><div class=\"nav-items pb-1\" data-collapsed=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.Title) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(collapsibleState(section.Collapsed))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 201, Col: 57} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 201, Col: 85}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" data-collapsed=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(collapsibleState(section.Collapsed))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 201, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range section.Items { for _, item := range section.Items {
var templ_7745c5c3_Var25 = []any{docsNavClass(currentPath, item.URL, "pl-7")} var templ_7745c5c3_Var24 = []any{docsNavClass(currentPath, item.URL, "pl-7")}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 templ.SafeURL var templ_7745c5c3_Var25 templ.SafeURL
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 203, Col: 25} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 203, Col: 25}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var24).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"><span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var25).String()) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 204, Col: 26} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 204, Col: 26}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span></a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, group := range section.Groups { for _, group := range section.Groups {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<div class=\"mt-0.5\"><button type=\"button\" class=\"nav-group-title w-full flex items-center gap-1.5 pl-7 pr-4 py-1.5 cursor-pointer select-none text-xs font-semibold uppercase tracking-wide transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" @click=\"toggleCollapsible($el)\"><span class=\"flex-1 text-left truncate\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<div class=\"mt-0.5\"><button type=\"button\" class=\"nav-group-title w-full flex items-center gap-1.5 pl-7 pr-4 py-1.5 cursor-pointer select-none text-xs font-semibold uppercase tracking-wide transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" @click=\"toggleCollapsible($el)\"><span class=\"flex-1 text-left truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(group.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 215, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span> <span class=\"section-arrow shrink-0 transition-transform\" style=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(group.Title) templ_7745c5c3_Var29, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(chevronStyle(group.Collapsed))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 215, Col: 62} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 216, Col: 104}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</span> <span class=\"section-arrow shrink-0 transition-transform\" style=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(chevronStyle(group.Collapsed))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 216, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -703,97 +690,84 @@ func DocsSidebar(nav Navigation, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span></button><div class=\"nav-group-items\" data-key=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span></button><div class=\"nav-group-items\" data-collapsed=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.Title + " / " + group.Title) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(collapsibleState(group.Collapsed))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 220, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 220, Col: 87}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" data-collapsed=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(collapsibleState(group.Collapsed))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 220, Col: 136}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range group.Items { for _, item := range group.Items {
var templ_7745c5c3_Var33 = []any{docsNavClass(currentPath, item.URL, "pl-9")} var templ_7745c5c3_Var31 = []any{docsNavClass(currentPath, item.URL, "pl-9")}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var33...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var31...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var34 templ.SafeURL var templ_7745c5c3_Var32 templ.SafeURL
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 222, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 222, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var31).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\"><span class=\"truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 223, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var33).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"><span class=\"truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 223, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</span></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</nav></aside>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</nav></aside>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -820,77 +794,77 @@ func DocsSearchResults(results []SearchResultItem) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var37 := templ.GetChildren(ctx) templ_7745c5c3_Var35 := templ.GetChildren(ctx)
if templ_7745c5c3_Var37 == nil { if templ_7745c5c3_Var35 == nil {
templ_7745c5c3_Var37 = templ.NopComponent templ_7745c5c3_Var35 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if len(results) == 0 { if len(results) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<div class=\"px-4 py-3 text-sm\" style=\"color: var(--text-secondary);\">No matching pages.</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<div class=\"px-4 py-3 text-sm\" style=\"color: var(--text-secondary);\">No matching pages.</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<ul class=\"py-1 list-none m-0 p-0\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<ul class=\"py-1 list-none m-0 p-0\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, r := range results { for _, r := range results {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<li><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<li><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var38 templ.SafeURL var templ_7745c5c3_Var36 templ.SafeURL
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinURLErrs(r.URL) templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(r.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 246, Col: 20} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 246, Col: 20}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" onclick=\"this.closest('#docs-search-results').classList.add('hidden')\"><span class=\"block text-sm font-medium truncate\" style=\"color: var(--text-primary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" onclick=\"this.closest('#docs-search-results').classList.add('hidden')\"><span class=\"block text-sm font-medium truncate\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var39 string var templ_7745c5c3_Var37 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 247, Col: 100} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 247, Col: 100}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if r.Category != "" { if r.Category != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<span class=\"block text-xs\" style=\"color: var(--text-secondary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<span class=\"block text-xs\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var40 string var templ_7745c5c3_Var38 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(r.Category) templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.Category)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 249, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 249, Col: 85}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</a></li>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</a></li>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</ul>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -917,12 +891,12 @@ func DocsNotFound(title string, message string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var41 := templ.GetChildren(ctx) templ_7745c5c3_Var39 := templ.GetChildren(ctx)
if templ_7745c5c3_Var41 == nil { if templ_7745c5c3_Var39 == nil {
templ_7745c5c3_Var41 = templ.NopComponent templ_7745c5c3_Var39 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<!doctype html><html lang=\"en\"><head>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<!doctype html><html lang=\"en\"><head>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -930,20 +904,20 @@ func DocsNotFound(title string, message string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var42 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 267, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 267, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, " - Bookhoard Documentation</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"min-h-screen font-sans antialiased\"><div class=\"min-h-screen flex items-center justify-center p-4\"><div class=\"card p-8 max-w-md w-full text-center\"><span class=\"inline-grid place-items-center mb-4\" style=\"color: var(--accent);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " - Bookhoard Documentation</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"min-h-screen font-sans antialiased\"><div class=\"min-h-screen flex items-center justify-center p-4\"><div class=\"card p-8 max-w-md w-full text-center\"><span class=\"inline-grid place-items-center mb-4\" style=\"color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -951,33 +925,33 @@ func DocsNotFound(title string, message string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</span><h1 class=\"text-2xl font-bold mb-3\" style=\"color: var(--text-primary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</span><h1 class=\"text-2xl font-bold mb-3\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var43 string var templ_7745c5c3_Var41 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 277, Col: 84} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 277, Col: 84}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</h1><p class=\"text-sm mb-6\" style=\"color: var(--text-secondary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</h1><p class=\"text-sm mb-6\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var44 string var templ_7745c5c3_Var42 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(message) templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 278, Col: 76} return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 278, Col: 76}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</p><div class=\"flex items-center justify-center gap-3\"><a href=\"/docs\" class=\"btn btn-primary py-2 px-4 text-sm no-underline\">Documentation</a> <a href=\"/\" class=\"btn py-2 px-4 text-sm no-underline\">Bookhoard</a></div></div></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</p><div class=\"flex items-center justify-center gap-3\"><a href=\"/docs\" class=\"btn btn-primary py-2 px-4 text-sm no-underline\">Documentation</a> <a href=\"/\" class=\"btn py-2 px-4 text-sm no-underline\">Bookhoard</a></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+5 -33
View File
@@ -15,39 +15,13 @@ function toggleSidebar(): void {
overlay.classList.toggle("hidden"); overlay.classList.toggle("hidden");
} }
// Docs sidebar collapse persistence. Rendering ownership: the server // Docs sidebar sections toggle open/closed in-page (purely client-local,
// renders what is expanded (navigation defaults + docs_nav cookie // ephemeral — the SERVER decides what is expanded on each page load from
// overrides merged in internal/docs), so first paint is already correct — // navigation defaults, so there is no saved state and nothing to jump).
// no post-paint correction, no jumping. This module only records intent: // The data-collapsed attribute is the single source of truth: the
// every toggle rewrites the cookie with the full current state. // stylesheet hides on it, and the arrow rotates relative to it.
const DOCS_NAV_COOKIE = "docs_nav";
type DocsNavState = Record<string, boolean>;
// collectDocsNavState reads the current collapsed flag of every
// collapsible in the sidebar straight from the DOM.
function collectDocsNavState(): DocsNavState {
const state: DocsNavState = {};
document.querySelectorAll<HTMLElement>("[data-key][data-collapsed]").forEach((el) => {
const key = el.getAttribute("data-key");
if (key) state[key] = el.getAttribute("data-collapsed") === "true";
});
return state;
}
function persistDocsNavCookie(state: DocsNavState): void {
try {
// Strictly-necessary UI state: first-party, no identifiers, one year.
document.cookie = `${DOCS_NAV_COOKIE}=${encodeURIComponent(JSON.stringify(state))}; path=/; max-age=31536000; SameSite=Lax`;
} catch {
// Storage blocked: the sidebar still works, state just doesn't persist.
}
}
function applyCollapsed(items: HTMLElement, collapsed: boolean): void { function applyCollapsed(items: HTMLElement, collapsed: boolean): void {
// The data-collapsed attribute is the single source of truth — the
// stylesheet hides on it, and the arrow is rotated relative to it.
items.setAttribute("data-collapsed", collapsed ? "true" : "false"); items.setAttribute("data-collapsed", collapsed ? "true" : "false");
const arrow = items.previousElementSibling?.querySelector<HTMLElement>(".section-arrow"); const arrow = items.previousElementSibling?.querySelector<HTMLElement>(".section-arrow");
if (arrow) { if (arrow) {
@@ -60,9 +34,7 @@ function toggleCollapsible(header: HTMLElement): void {
if (!items) return; if (!items) return;
const isCollapsed = items.getAttribute("data-collapsed") === "true"; const isCollapsed = items.getAttribute("data-collapsed") === "true";
applyCollapsed(items, !isCollapsed); applyCollapsed(items, !isCollapsed);
persistDocsNavCookie(collectDocsNavState());
} }
let docsSearchTimeout: ReturnType<typeof setTimeout> | null = null; let docsSearchTimeout: ReturnType<typeof setTimeout> | null = null;