The account and appearance menus sat in one container with no separation, so expanding one made the other hard to find. Each collapsible menu now lives in its own subtly lifted, bordered panel (.sidebar-panel) so the expanded items stay visually contained and the menus never blend together. Applied to the account, appearance, and sign-in menus.
229 lines
8.6 KiB
Templ
229 lines
8.6 KiB
Templ
package templates
|
|
|
|
templ Header(user User, currentPath string) {
|
|
<div
|
|
x-data="header"
|
|
x-init="initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators(); restoreLibrarySelection(); initializeScanListener()"
|
|
>
|
|
<!-- Mobile backdrop -->
|
|
<div
|
|
class="app-sidebar-backdrop lg:hidden"
|
|
:class="{ 'is-open': mobileMenuOpen }"
|
|
@click="mobileMenuOpen = false"
|
|
x-cloak
|
|
></div>
|
|
<!-- Sidebar -->
|
|
<aside class="app-sidebar" :class="{ 'is-open': mobileMenuOpen }">
|
|
<!-- Logo -->
|
|
<a
|
|
href="/dashboard"
|
|
class="flex items-center gap-2.5 px-5 h-16 shrink-0 border-b"
|
|
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
|
|
>
|
|
<span class="text-xl">📚</span>
|
|
<span class="font-bold text-lg tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
|
<svg
|
|
x-show="scanning"
|
|
x-cloak
|
|
x-transition
|
|
class="h-4 w-4 animate-spin ml-auto"
|
|
style="color: var(--accent);"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</a>
|
|
<span
|
|
x-show="scanning && scanProgress > 0"
|
|
x-cloak
|
|
x-transition
|
|
class="block px-5 py-1.5 text-xs font-medium -mt-1"
|
|
style="color: var(--text-secondary);"
|
|
x-text="'Scanning… ' + scanProgress + '%'"
|
|
></span>
|
|
<!-- Primary navigation -->
|
|
<nav class="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
|
<a href="/dashboard" class={ activeClass(currentPath, "/dashboard") }>
|
|
@Icon("library", "h-5 w-5 shrink-0")
|
|
<span>Library</span>
|
|
</a>
|
|
<a href="/bookshelf" class={ activeClass(currentPath, "/bookshelf") }>
|
|
@Icon("book", "h-5 w-5 shrink-0")
|
|
<span>All Books</span>
|
|
</a>
|
|
<a href="/series" class={ activeClass(currentPath, "/series") }>
|
|
@Icon("layers", "h-5 w-5 shrink-0")
|
|
<span>Series</span>
|
|
</a>
|
|
<a href="/collections" class={ activeClass(currentPath, "/collections") }>
|
|
@Icon("folder", "h-5 w-5 shrink-0")
|
|
<span>Collections</span>
|
|
</a>
|
|
<a href="/progress" class={ activeClass(currentPath, "/progress") }>
|
|
@Icon("clock", "h-5 w-5 shrink-0")
|
|
<span>Progress</span>
|
|
</a>
|
|
<a href="/devices" class={ activeClass(currentPath, "/devices") }>
|
|
@Icon("device", "h-5 w-5 shrink-0")
|
|
<span>Devices</span>
|
|
</a>
|
|
</nav>
|
|
<!-- Bottom: theme picker + user -->
|
|
<div
|
|
class="shrink-0 px-3 py-3 space-y-2 border-t"
|
|
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
|
|
>
|
|
if user.ID != "" {
|
|
@SidebarUserMenu(user)
|
|
} else {
|
|
@SidebarSignIn(currentPath)
|
|
}
|
|
<!-- Theme picker -->
|
|
<div x-data="{ themeOpen: false }" class="sidebar-panel">
|
|
<button
|
|
type="button"
|
|
@click="themeOpen = !themeOpen"
|
|
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")
|
|
</button>
|
|
<div x-show="themeOpen" 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" }
|
|
class="w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
|
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: " + opt.Color }></span>
|
|
<span class="flex-1 text-left">{ opt.Label }</span>
|
|
if user.Theme == opt.Name {
|
|
@Icon("check", "h-4 w-4 shrink-0")
|
|
}
|
|
</button>
|
|
}
|
|
<div class="my-1.5 mx-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"></div>
|
|
<p class="px-3 pb-1 text-xs font-medium uppercase tracking-wide" style="color: var(--text-secondary);">Bookshelf</p>
|
|
for _, w := range WoodOptions {
|
|
<button
|
|
type="button"
|
|
@click={ "changeWoodPaneling('" + w.Name + "')" }
|
|
class="wood-paneling-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors"
|
|
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>
|
|
<span class="flex-1 text-left">{ w.Label }</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
<!-- Topbar -->
|
|
<header class="app-topbar">
|
|
<div class="flex items-center gap-3 px-4 sm:px-6 h-full">
|
|
<button
|
|
type="button"
|
|
class="app-mobile-only icon-btn"
|
|
@click="mobileMenuOpen = true"
|
|
aria-label="Open menu"
|
|
>
|
|
@Icon("menu", "h-5 w-5")
|
|
</button>
|
|
<div class="relative flex-1 max-w-xl">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" style="color: var(--text-secondary);">
|
|
@Icon("search", "h-4 w-4")
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="header-search"
|
|
placeholder="Search your library…"
|
|
class="input pl-10"
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<script type="module" src="/static/main.js"></script>
|
|
</div>
|
|
}
|
|
|
|
// SidebarUserMenu is the bottom-of-sidebar account control for signed-in users.
|
|
templ SidebarUserMenu(user User) {
|
|
<div x-data="{ userOpen: false }" class="sidebar-panel">
|
|
<button
|
|
type="button"
|
|
@click="userOpen = !userOpen"
|
|
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);"
|
|
>
|
|
<span class="grid place-items-center h-7 w-7 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@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")
|
|
</button>
|
|
<div x-show="userOpen" 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>
|
|
</a>
|
|
if user.Role == "admin" {
|
|
<a href="/admin" 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("settings", "h-4 w-4")
|
|
<span>Admin Panel</span>
|
|
</a>
|
|
}
|
|
<button
|
|
type="button"
|
|
@click="logout()"
|
|
class="w-full 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("logout", "h-4 w-4")
|
|
<span>Logout</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// 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">
|
|
<button
|
|
type="button"
|
|
@click="signInOpen = !signInOpen"
|
|
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);"
|
|
>
|
|
<span class="grid place-items-center h-7 w-7 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
|
@Icon("user", "h-4 w-4")
|
|
</span>
|
|
<span class="flex-1 text-left">Sign in</span>
|
|
</button>
|
|
<div x-show="signInOpen" x-cloak x-transition class="mt-1 px-1">
|
|
<form
|
|
hx-post="/api/auth/login"
|
|
hx-target="#login-result"
|
|
hx-swap="innerHTML"
|
|
class="space-y-2"
|
|
>
|
|
<input type="hidden" name="redirect" value={ currentPath }/>
|
|
<input type="text" name="login" class="input py-1.5 text-sm" placeholder="Email or username" required/>
|
|
<input type="password" name="password" class="input py-1.5 text-sm" placeholder="Password" required/>
|
|
<button type="submit" class="btn btn-primary w-full py-1.5 text-sm">Sign In</button>
|
|
</form>
|
|
<div id="login-result"></div>
|
|
<a href="/register" class="block text-center text-xs mt-2 hover:underline" style="color: var(--text-secondary);">
|
|
Create an account
|
|
</a>
|
|
</div>
|
|
</div>
|
|
}
|