")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/partials_render_test.go b/templates/partials_render_test.go
index 42bccbfd..f4ffb906 100644
--- a/templates/partials_render_test.go
+++ b/templates/partials_render_test.go
@@ -130,3 +130,38 @@ func TestLibrarySwitcherExcludesPodcasts(t *testing.T) {
t.Error("switcher missing the books library")
}
}
+
+// The Appearance panel's theme buttons interpolate their click handlers
+// from Go loop vars. The CSP-build migration (eb57b720) shipped
+// `@click="selectTheme(opt.Name)"` as LITERAL text (templ gotcha #1:
+// quoted attribute values are never interpolated), so Alpine threw
+// "Undefined variable: opt" on every theme click and themes were
+// unchangeable until the user report, 2026-10-31. The wood-paneling
+// sibling always had the correct expression form, which is why the
+// panel half-worked. Pin both halves: handlers must carry real theme
+// names, and the raw Go identifier must never reach the HTML.
+func TestThemePickerHandlersAreInterpolated(t *testing.T) {
+ var b strings.Builder
+ user := User{ID: "u1", Username: "testuser", Role: "admin", Theme: "tokyo-night", ShowPodcastsNav: true}
+ if err := Header(user, "/collections").Render(context.Background(), &b); err != nil {
+ t.Fatalf("Header: %v", err)
+ }
+ out := b.String()
+ // templ escapes `'` as ' inside attribute values; the HTML parser
+ // decodes entities before Alpine evaluates, so normalize for matching.
+ decoded := strings.ReplaceAll(out, "'", "'")
+ if strings.Contains(out, "selectTheme(opt.Name)") {
+ t.Error("theme buttons leaked the raw Go loop var into the click handler (themes unchangeable)")
+ }
+ if strings.Contains(out, "changeWoodPaneling(w.Name)") {
+ t.Error("wood buttons leaked the raw Go loop var into the click handler")
+ }
+ // At least the SSR'd current theme and one wood option must appear
+ // as real string-literal arguments.
+ if !strings.Contains(decoded, "selectTheme('tokyo-night')") {
+ t.Error("theme buttons missing the interpolated current-theme handler")
+ }
+ if !strings.Contains(decoded, "changeWoodPaneling('none')") {
+ t.Error("wood buttons missing the interpolated handler")
+ }
+}
diff --git a/web/src/collections.ts b/web/src/collections.ts
index 5f013243..68dcd9b7 100644
--- a/web/src/collections.ts
+++ b/web/src/collections.ts
@@ -54,13 +54,17 @@ function setupHTMXAuth(): void {
});
}
-function navigateToCollection(element: HTMLElement): void {
- const event = window.event as Event;
- if (event && event.target instanceof HTMLElement) {
- const target = event.target as HTMLElement;
- if (target.tagName === "BUTTON" || target.closest("button") !== null) {
- return;
- }
+function navigateToCollection(element: HTMLElement, event?: Event): void {
+ // ANY ancestor-or-self button swallows the click. The guard used to be
+ // `event.target instanceof HTMLElement` — false for SVG elements — so
+ // a click landing exactly on the pencil glyph's