feat(ui): accordion sidebar panels + wood texture preview swatches
Release / build-and-push (push) Canceled after 42s

Sidebar appearance menu improvements:

Accordion behavior:
- Lift panel open/close state to a shared 'openPanel' variable on the
  parent container so only one sidebar panel (User, Appearance, Admin,
  Sign In) can be open at a time; all can be closed.
- Admin panel still auto-opens on /admin/* pages via initial state.
- Add chevron rotation to User and Appearance panels (previously only
  Admin rotated); add a chevron to the Sign In panel for consistency.

Wood texture previews:
- Generate 48x48 WebP thumbnails (~200 bytes each) from the full-size
  PNG textures (873 KB – 1.9 MB) so the bookshelf option circles show
  the actual wood grain instead of a flat grey dot.
- Use unquoted url() in the inline style to avoid templ's double-HTML
  escaping of single quotes (SanitizeStyleAttributeValues + EscapeString
  turned url('...') into url('...) which is invalid CSS).
- 'None' keeps the flat neutral circle.

CSS resilience:
- Move the wood background-image: url() rules from the compiled
  style.css into input.css (the Tailwind source) so they survive CSS
  rebuilds instead of being silently lost.
This commit is contained in:
2026-08-10 14:22:46 -04:00
parent 9a62c10803
commit 05c7431d86
7 changed files with 158 additions and 101 deletions
+29 -17
View File
@@ -74,6 +74,7 @@ templ Header(user User, currentPath string) {
</nav>
<!-- Bottom: theme picker + user -->
<div
x-data="{ openPanel: window.location.pathname.startsWith('/admin') ? 'admin' : null }"
class="shrink-0 px-3 py-3 space-y-2 border-t"
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
>
@@ -83,21 +84,23 @@ templ Header(user User, currentPath string) {
@SidebarSignIn(currentPath)
}
<!-- Theme picker -->
<div x-data="{ themeOpen: false }" class="sidebar-panel">
<div class="sidebar-panel">
<button
type="button"
@click="themeOpen = !themeOpen"
@click="openPanel = openPanel === 'appearance' ? null : 'appearance'"
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover"
>
@Icon("palette", "h-5 w-5 shrink-0")
<span>Appearance</span>
@Icon("chevron-down", "h-4 w-4 ml-auto transition-transform")
<span class="ml-auto h-4 w-4 transition-transform" :class="{ 'rotate-180': openPanel === 'appearance' }">
@Icon("chevron-down", "h-4 w-4")
</span>
</button>
<div x-show="themeOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
<div x-show="openPanel === 'appearance'" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
for _, opt := range ThemeOptions {
<button
type="button"
@click={ "changeTheme('" + opt.Name + "'); themeOpen = false" }
@click={ "changeTheme('" + opt.Name + "'); openPanel = null" }
class="theme-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
data-theme={ opt.Name }
style="color: var(--text-secondary);"
@@ -119,7 +122,11 @@ templ Header(user User, currentPath string) {
data-wood={ w.Name }
style="color: var(--text-secondary);"
>
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style="background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);"></span>
if w.Name == "none" {
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style="background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);"></span>
} else {
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style={ "background-image: url(/static/textures/thumb-" + w.Name + ".webp); background-size: cover;" }></span>
}
<span class="flex-1 text-left">{ w.Label }</span>
</button>
}
@@ -127,23 +134,23 @@ templ Header(user User, currentPath string) {
</div>
<!-- Admin section (visible to admins only) -->
if user.Role == "admin" {
<div x-data="{ adminOpen: window.location.pathname.startsWith('/admin') }" class="sidebar-panel">
<div class="sidebar-panel">
<button
type="button"
@click="adminOpen = !adminOpen"
@click="openPanel = openPanel === 'admin' ? null : 'admin'"
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover"
>
@Icon("shield", "h-5 w-5 shrink-0")
<span>Administration</span>
<svg
class="h-4 w-4 ml-auto transition-transform"
:class="{ 'rotate-180': adminOpen }"
:class="{ 'rotate-180': openPanel === 'admin' }"
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div x-show="adminOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
<div x-show="openPanel === 'admin'" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
@Icon("grid", "h-5 w-5 shrink-0")
<span>Dashboard</span>
@@ -196,10 +203,10 @@ templ Header(user User, currentPath string) {
// SidebarUserMenu is the bottom-of-sidebar account control for signed-in users.
templ SidebarUserMenu(user User) {
<div x-data="{ userOpen: false }" class="sidebar-panel">
<div class="sidebar-panel">
<button
type="button"
@click="userOpen = !userOpen"
@click="openPanel = openPanel === 'user' ? null : 'user'"
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover"
style="color: var(--text-primary);"
>
@@ -207,9 +214,11 @@ templ SidebarUserMenu(user User) {
@Icon("user", "h-4 w-4")
</span>
<span class="flex-1 text-left truncate">{ user.Username }</span>
@Icon("chevron-down", "h-4 w-4 shrink-0 transition-transform")
<span class="h-4 w-4 shrink-0 transition-transform" :class="{ 'rotate-180': openPanel === 'user' }">
@Icon("chevron-down", "h-4 w-4")
</span>
</button>
<div x-show="userOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
<div x-show="openPanel === 'user'" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
<a href="/profile" class="flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
@Icon("user", "h-4 w-4")
<span>Profile</span>
@@ -230,10 +239,10 @@ templ SidebarUserMenu(user User) {
// SidebarSignIn is an inline sign-in for signed-out visitors, preserving the
// original header's inline login (htmx POST) so users can auth from any page.
templ SidebarSignIn(currentPath string) {
<div x-data="{ signInOpen: false }" class="sidebar-panel">
<div class="sidebar-panel">
<button
type="button"
@click="signInOpen = !signInOpen"
@click="openPanel = openPanel === 'signin' ? null : 'signin'"
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover"
style="color: var(--text-primary);"
>
@@ -241,8 +250,11 @@ templ SidebarSignIn(currentPath string) {
@Icon("user", "h-4 w-4")
</span>
<span class="flex-1 text-left">Sign in</span>
<span class="h-4 w-4 shrink-0 transition-transform" :class="{ 'rotate-180': openPanel === 'signin' }">
@Icon("chevron-down", "h-4 w-4")
</span>
</button>
<div x-show="signInOpen" x-cloak x-transition class="mt-1 px-1">
<div x-show="openPanel === 'signin'" x-cloak x-transition class="mt-1 px-1">
<form
hx-post="/api/auth/login"
hx-target="#login-result"