From f402a3ee03a44dc9503e19d282709dc2907726ea Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 5 Aug 2026 16:44:42 -0400 Subject: [PATCH] feat(ui): sidebar app shell and bold design system Replace the top-nav with a fixed left sidebar + slim content topbar (the Komga/Audiobookshelf layout), the signature change versus the conservative tighten-ui branch. - App shell: .app-sidebar (off-canvas on mobile via Alpine mobileMenuOpen, pinned at 16rem on lg), .app-topbar (fixed, blurred, 4rem), and .app-subbar for in-page sticky bars (parks under the topbar). Content is auto-offset via body:has(.app-sidebar) so reader.templ/error.templ (which have no sidebar) are untouched. - Header rebuilt as the sidebar: logo + vertical nav (activeClass), an inline Appearance picker driven by ThemeOptions/WoodOptions, and an inline account menu / sign-in (preserving the inline htmx login). The search lives in the topbar so its dropdown still anchors correctly. - Bolder primitives: .card -> rounded-2xl, cinematic .book-card-cover hover overlay with a quick-action affordance, .brand-gradient hero surface, .hero-backdrop (blurred cover) and .stat-card utilities. --- templates/header.templ | 721 +++++++++---------------------- templates/library_switcher.templ | 38 +- web/static/input.css | 195 ++++++++- 3 files changed, 414 insertions(+), 540 deletions(-) diff --git a/templates/header.templ b/templates/header.templ index a52d6bc..43f8e8d 100644 --- a/templates/header.templ +++ b/templates/header.templ @@ -1,531 +1,228 @@ package templates templ Header(user User, currentPath string) { - - + +} + +// 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) { +
+ +
+
+ + + + +
+
+ + Create an account + +
+
} diff --git a/templates/library_switcher.templ b/templates/library_switcher.templ index 6a22181..711f9d0 100644 --- a/templates/library_switcher.templ +++ b/templates/library_switcher.templ @@ -1,24 +1,26 @@ package templates templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...templ.Component) { -
-
-
- +
+
+
+
if len(actions) > 0 { @@ -42,18 +44,16 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions .. templ DashboardActions() { } diff --git a/web/static/input.css b/web/static/input.css index 409932e..b075563 100644 --- a/web/static/input.css +++ b/web/static/input.css @@ -169,11 +169,107 @@ display: none !important; } - /* Header is sticky on every authenticated page, plus docs. */ - .header-nav { - position: sticky; + /* + * App shell — a fixed left sidebar with a content area to its right. + * The sidebar is off-canvas on mobile (toggled via the `is-open` class, + * which Alpine flips through `mobileMenuOpen`) and pinned on >= lg. + */ + :root { + --sidebar-w: 16rem; + --topbar-h: 4rem; + } + + .app-sidebar { + position: fixed; top: 0; - z-index: 50; + left: 0; + bottom: 0; + width: var(--sidebar-w); + z-index: 60; + background-color: var(--bg-secondary); + border-right: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent); + display: flex; + flex-direction: column; + overflow-y: auto; + transform: translateX(-100%); + transition: transform 0.25s ease; + } + .app-sidebar.is-open { + transform: translateX(0); + } + + .app-sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 55; + background: var(--surface-overlay); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; + } + .app-sidebar-backdrop.is-open { + opacity: 1; + pointer-events: auto; + } + + /* Desktop: pin the sidebar, offset body content, drop the backdrop. */ + @media (min-width: 1024px) { + body { + padding-left: var(--sidebar-w); + } + .app-sidebar { + transform: none; + } + .app-sidebar-backdrop { + display: none !important; + } + .app-mobile-only { + display: none !important; + } + } + @media (max-width: 1023.98px) { + .app-desktop-only { + display: none !important; + } + } + + /* Slim fixed topbar that spans the content area (right of the sidebar). */ + .app-topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 40; + height: var(--topbar-h); + background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent); + } + + /* + * Content offsets — applied only on pages whose Header renders the + * fixed sidebar (reader.templ / error.templ have no sidebar, so they are + * left untouched). + */ + body:has(.app-sidebar) { + padding-top: var(--topbar-h); + } + @media (min-width: 1024px) { + body:has(.app-sidebar) { + padding-left: var(--sidebar-w); + } + /* On desktop the topbar starts right of the pinned sidebar. */ + .app-topbar { + left: var(--sidebar-w); + } + } + + /* A secondary sticky bar (e.g. library switcher) parks under the topbar. */ + .app-subbar { + position: sticky; + top: var(--topbar-h); + z-index: 30; } /* Smooth, theme-aware scrollbars */ @@ -187,8 +283,8 @@ .card { background-color: var(--bg-secondary); - border: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent); - border-radius: 0.75rem; + border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent); + border-radius: 1rem; box-shadow: var(--shadow-card); } @@ -318,21 +414,102 @@ .book-card { background-color: var(--bg-secondary); border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent); - border-radius: 0.625rem; + border-radius: 0.75rem; overflow: hidden; box-shadow: var(--shadow-card); transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } .book-card:hover { - transform: translateY(-3px); + transform: translateY(-4px); box-shadow: var(--shadow-card-hover); - border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); + } + /* Cinematic cover overlay: a centered quick-action that fades in on hover. */ + .book-card-cover { + position: relative; + } + .book-card-cover::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent 55%); + opacity: 0; + transition: opacity 0.18s ease; + pointer-events: none; + } + .book-card:hover .book-card-cover::after { + opacity: 1; + } + .book-card-action { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.18s ease; + } + .book-card:hover .book-card-action { + opacity: 1; + } + .book-card-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + border-radius: 9999px; + background-color: color-mix(in srgb, var(--accent) 92%, white); + color: #fff; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); } .book-card-meta { background-color: var(--bg-secondary); padding: 0.5rem 0.625rem; } + /* ---------- Bold accents ---------- */ + + /* Signature gradient surface for heroes / primary panels. */ + .brand-gradient { + background-image: linear-gradient( + 135deg, + color-mix(in srgb, var(--accent) 28%, var(--bg-secondary)), + var(--bg-secondary) 62% + ); + } + + /* Blurred cover backdrop used behind the book-detail hero. */ + .hero-backdrop { + position: absolute; + inset: 0; + z-index: 0; + background-size: cover; + background-position: center; + filter: blur(28px) saturate(1.4); + transform: scale(1.15); + opacity: 0.5; + } + .hero-backdrop::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--bg-primary) 35%, transparent), + var(--bg-primary) 92% + ); + } + + /* Stat tile for analytics / dashboard summaries. */ + .stat-card { + background-color: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent); + border-radius: 1rem; + padding: 1.25rem; + box-shadow: var(--shadow-card); + } + /* ---------- TriState Button (has_cover filter) ---------- */ .tristate-btn { background-color: var(--bg-primary);