feat(ui): rebuild shared chrome and core browse/read pages
- Header: sticky app-wide, active nav highlighting, data-driven theme picker with active swatch, unified SVG icons, deduped login fragment (was duplicated verbatim for desktop + mobile), removed stray console.log - LibrarySwitcher: cleaner sticky bar, SVG action icons, theme-aware - BookCard: the atomic unit now has a real surface (was floating text when wood-paneling was off due to an undefined --wood-border), with cohesive shadow + hover lift; reused across dashboard/bookshelf/series - Dashboard: calmer section headers, snap carousels with SVG chevrons - Bookshelf: recompose the filter wall into a search toolbar + collapsible Filters panel (all 9 filters + save/load/clear preserved), redesigned empty state and pagination - Book detail, series, collections, browse: cohesive cards, SVG action icons, uppercase muted labels, theme-aware badges/links
This commit is contained in:
+86
-123
@@ -18,13 +18,23 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
<body x-data="dashboard" x-init="initDashboard()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/dashboard")
|
||||
@LibrarySwitcher(libData, currentLibraryID, DashboardActions())
|
||||
<!-- Collections Container -->
|
||||
<main id="collections-container" class="w-full px-4 py-8">
|
||||
for _, section := range sections {
|
||||
@CollectionCarousel(section)
|
||||
<main id="collections-container" class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
if len(sections) == 0 {
|
||||
<div class="card mx-auto max-w-md p-10 text-center">
|
||||
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand">
|
||||
@Icon("book-open", "h-7 w-7")
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-content">Your library is empty</h2>
|
||||
<p class="mt-1 text-sm text-content-muted">Once books are added to your libraries, collections will appear here.</p>
|
||||
</div>
|
||||
} else {
|
||||
<div class="space-y-10">
|
||||
for _, section := range sections {
|
||||
@CollectionCarousel(section)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<!-- Dashboard Settings Modal -->
|
||||
@DashboardSettingsModal(allSections, hiddenCollections, itemsPerSection)
|
||||
@ErrorToast(errorMessage)
|
||||
</body>
|
||||
@@ -32,115 +42,92 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
}
|
||||
|
||||
templ CollectionCarousel(section handlers.SectionData) {
|
||||
<div
|
||||
class="dashboard-collection mb-8"
|
||||
<section
|
||||
class="dashboard-collection"
|
||||
data-collection-id={ section.ID }
|
||||
data-is-system={ section.IsSystem }
|
||||
>
|
||||
<!-- Collection Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl">{ section.Icon }</span>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">{ section.Title }</h2>
|
||||
if section.Description != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ section.Description }</p>
|
||||
<div class="mb-4 flex items-end justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<h2 class="flex items-center gap-2 text-lg font-bold tracking-tight text-content">
|
||||
if section.Icon != "" {
|
||||
<span class="text-base">{ section.Icon }</span>
|
||||
}
|
||||
</div>
|
||||
{ section.Title }
|
||||
</h2>
|
||||
if section.Description != "" {
|
||||
<p class="mt-0.5 text-sm text-content-muted">{ section.Description }</p>
|
||||
}
|
||||
</div>
|
||||
if section.ViewAllURL != "" {
|
||||
<a
|
||||
href={ section.ViewAllURL }
|
||||
class="text-sm font-medium hover:underline transition-colors"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
View All →
|
||||
<a href={ section.ViewAllURL } class="flex items-center gap-1 text-sm font-medium text-content-muted transition-colors hover:text-brand">
|
||||
View All
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<!-- Carousel -->
|
||||
|
||||
<div class="carousel-container relative group">
|
||||
<button
|
||||
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-r from-gray-900 to-transparent
|
||||
flex items-center justify-start opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
class="carousel-nav-left absolute left-0 top-0 z-10 flex h-full w-12 items-center justify-start bg-gradient-to-r from-surface to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-visible:opacity-100"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id={ section.ID }
|
||||
data-direction="-1"
|
||||
aria-label="Scroll left"
|
||||
>
|
||||
<span class="text-3xl pl-2" style="color: var(--text-primary);">‹</span>
|
||||
@Icon("chevron-left", "h-6 w-6 text-content")
|
||||
</button>
|
||||
<div
|
||||
id={ "carousel-track-" + section.ID }
|
||||
class="carousel-track flex gap-4 overflow-x-auto
|
||||
scroll-smooth snap-x snap-mandatory
|
||||
px-12 pb-4"
|
||||
class="carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2"
|
||||
style="scrollbar-width: none; -ms-overflow-style: none;"
|
||||
>
|
||||
for _, item := range section.Items {
|
||||
@BookCard(item)
|
||||
}
|
||||
if len(section.Items) == 0 {
|
||||
<div class="text-center py-8 w-full" style="color: var(--text-secondary);">
|
||||
<p>No items in this collection</p>
|
||||
<div class="w-full py-10 text-center text-sm text-content-muted">
|
||||
No items in this collection
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-l from-gray-900 to-transparent
|
||||
flex items-center justify-end opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
class="carousel-nav-right absolute right-0 top-0 z-10 flex h-full w-12 items-center justify-end bg-gradient-to-l from-surface to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-visible:opacity-100"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id={ section.ID }
|
||||
data-direction="1"
|
||||
aria-label="Scroll right"
|
||||
>
|
||||
<span class="text-3xl pr-2" style="color: var(--text-primary);">›</span>
|
||||
@Icon("chevron-right", "h-6 w-6 text-content")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
// BookCard is the single, canonical cover-forward card used across the
|
||||
// dashboard, bookshelf, series and collections. The metadata block sits on a
|
||||
// real surface so cards always read as cohesive objects (previously the text
|
||||
// floated with no background when wood-paneling was off).
|
||||
templ BookCard(item handlers.BookInfo) {
|
||||
<a href={ "/media/" + item.MediaItemID } title={ item.Title }>
|
||||
<div
|
||||
class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
|
||||
transition-transform duration-200 hover:scale-105"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label={ "View " + item.Title }
|
||||
>
|
||||
<div
|
||||
class="aspect-[2/3] overflow-hidden shadow-lg
|
||||
bg-gradient-to-br from-gray-700 to-gray-900"
|
||||
>
|
||||
<a href={ "/media/" + item.MediaItemID } title={ item.Title } class="block w-36 flex-shrink-0 snap-start">
|
||||
<div class="book-card" tabindex="0" role="button" aria-label={ "View " + item.Title }>
|
||||
<div class="aspect-[2/3] overflow-hidden bg-surface-hover">
|
||||
if item.CoverImagePath != "" {
|
||||
<img
|
||||
src={ item.CoverImagePath }
|
||||
alt={ item.Title }
|
||||
class="w-full h-full object-cover"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
src="/static/placeholder-book.svg"
|
||||
alt={ item.Title }
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<img src="/static/placeholder-book.svg" alt={ item.Title } class="h-full w-full object-cover"/>
|
||||
}
|
||||
</div>
|
||||
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]">
|
||||
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)" title={ item.Title }>
|
||||
{ item.Title }
|
||||
</h3>
|
||||
<div class="book-card-meta">
|
||||
<h3 class="line-clamp-2 text-sm font-semibold leading-snug text-content" title={ item.Title }>{ item.Title }</h3>
|
||||
if item.Author != "" {
|
||||
<p class="text-sm line-clamp-1" style="color: var(--text-secondary)" title={ item.Author }>
|
||||
{ item.Author }
|
||||
</p>
|
||||
<p class="mt-0.5 line-clamp-1 text-xs text-content-muted" title={ item.Author }>{ item.Author }</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,82 +137,69 @@ templ BookCard(item handlers.BookInfo) {
|
||||
templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) {
|
||||
<div
|
||||
id="dashboard-settings-modal"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg p-6 w-full max-w-2xl mx-4 shadow-2xl"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
||||
<button
|
||||
data-action="close-dashboard-settings"
|
||||
class="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||
>
|
||||
✕
|
||||
<div class="card w-full max-w-2xl p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-content">Customize Dashboard</h2>
|
||||
<p class="mt-0.5 text-sm text-content-muted">Drag to reorder, toggle to show or hide.</p>
|
||||
</div>
|
||||
<button data-action="close-dashboard-settings" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary);">
|
||||
Drag to reorder collections, toggle visibility with the switch.
|
||||
</p>
|
||||
<!-- Draggable Collection List -->
|
||||
<div id="collection-list" class="space-y-2 mb-6">
|
||||
|
||||
<div id="collection-list" class="mb-6 space-y-2">
|
||||
for _, section := range sections {
|
||||
<div
|
||||
class="collection-item flex items-center justify-between p-3 rounded border
|
||||
cursor-move select-none"
|
||||
class="flex items-center justify-between gap-3 rounded-xl border border-line bg-surface px-3 py-2.5 cursor-move select-none"
|
||||
data-collection-id={ section.ID }
|
||||
data-is-system={ fmt.Sprintf("%v", section.IsSystem) }
|
||||
draggable="true"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl" style="color: var(--text-secondary);">☰</span>
|
||||
<span class="text-xl">{ section.Icon }</span>
|
||||
<div>
|
||||
<span class="font-medium" style="color: var(--text-primary);">{ section.Title }</span>
|
||||
if section.IsSystem {
|
||||
<span class="text-xs ml-2 px-2 py-1 rounded" style="background-color: var(--accent);">System</span>
|
||||
}
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
@Icon("grip", "h-4 w-4 text-content-muted")
|
||||
if section.Icon != "" {
|
||||
<span class="text-base">{ section.Icon }</span>
|
||||
}
|
||||
<div class="min-w-0">
|
||||
<span class="block truncate text-sm font-medium text-content">{ section.Title }</span>
|
||||
</div>
|
||||
if section.IsSystem {
|
||||
<span class="badge bg-brand/15 text-brand">System</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
if section.IsSystem {
|
||||
<button
|
||||
data-action="restore-system-collection"
|
||||
data-collection-name={ section.ID }
|
||||
class="text-xs px-3 py-1 rounded border hover:opacity-80 transition-opacity"
|
||||
style="border-color: var(--border); color: var(--text-secondary);"
|
||||
title="Restore { section.Title } to defaults"
|
||||
class="btn btn-ghost px-2 py-1 text-xs"
|
||||
title="Restore"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
class="peer sr-only"
|
||||
if !ContainsString(hiddenCollections, section.ID) {
|
||||
checked
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer
|
||||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
|
||||
after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all
|
||||
peer-checked:bg-blue-600"
|
||||
></div>
|
||||
<div class="h-6 w-11 rounded-full bg-surface-hover transition-colors after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-content-muted after:transition-all peer-checked:bg-brand peer-checked:after:translate-x-full peer-checked:after:bg-white peer-focus-visible:ring-2 peer-focus-visible:ring-brand/50"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Items Per Section Slider -->
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Items per Collection: <span id="items-count-display" class="font-bold">{ itemsPerSection }</span>
|
||||
<label class="mb-2 block text-sm font-medium text-content-muted">
|
||||
Items per Collection: <span id="items-count-display" class="font-bold text-content">{ itemsPerSection }</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
@@ -233,26 +207,15 @@ templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections
|
||||
max="50"
|
||||
step="5"
|
||||
value={ itemsPerSection }
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
class="h-2 w-full cursor-pointer appearance-none rounded-full bg-surface-hover"
|
||||
data-input-action="update-items-count"
|
||||
target="items-count-display"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
data-action="close-dashboard-settings"
|
||||
class="px-4 py-2 rounded-lg border hover:bg-gray-700 transition-colors"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
data-action="save-dashboard-settings"
|
||||
class="px-4 py-2 rounded-lg text-white font-medium hover:opacity-90 transition-opacity"
|
||||
style="background-color: var(--accent);"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button data-action="close-dashboard-settings" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="save-dashboard-settings" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user