ui: fix admin/docs theme flash + persist sidebar open/closed state

Theme flash (admin pages, docs, stats, progress — anywhere SSR'd):
the JWT middlewares built the context user WITHOUT the Theme column,
so every page rendering theme-{user.Theme} from c.Get("user") painted
an empty class (:root tokyo-night default) and only settled on the real
theme after initializeTheme -> loadUserTheme round-tripped the server.
Both middlewares now carry liveUser.Theme into the context user;
verified live: /admin /stats /progress /docs /docs-deep /dashboard
/collections all SSR theme-hackerman.

Defense in depth for the same bug: loadTheme() no longer re-applies
localStorage over a server-rendered body theme (applyTheme records even
the default, so any signed-out visit baked tokyo-night into localStorage
and every signed-in page flipped to it post-paint). Public auth/landing
bodies dropped their hardcoded theme classes so ThemeFlashGuard's
pre-paint html class actually governs them; docs theme <select> now
syncs from the body class and re-syncs after the profile fetch.

Sidebar state (the unpleasant part): the app sidebar's openPanel was
reinitialized from the pathname on every load; it now restores from
localStorage ('sidebar-open-panel') with the pathname default as
fallback and -persists every toggle. Docs sidebar sections/groups
carry data-key attributes and toggleCollapsible persists collapsed
state per key ('docs-nav-state'); restoreDocsNav() applies it at init —
server-rendered defaults still govern first visit.

make test green; rendering ownership OK (no new inline scripts); 146
vitests pass; live-verified SSR classes on 7 pages.
This commit is contained in:
John O'Keefe committed 2026-10-24 19:44:34 -04:00
1 parent 983980d07a
commit f1a6de7cd7
50 files changed
+915 -803

No files matched your search

+7 -7
View File
@@ -36,7 +36,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 9, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 9, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -58,7 +58,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 1, Col: 0}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -71,7 +71,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("initCollectionRules('" + collection.ID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 14, Col: 120}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 14, Col: 120}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -100,7 +100,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: var(--accent-muted); color: " + collection.Color)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 23, Col: 159}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 23, Col: 159}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -113,7 +113,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 23, Col: 179}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 23, Col: 179}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -126,7 +126,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 25, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 25, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -147,7 +147,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 49, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 49, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {