feat(ui): split book-card play action into reader/detail routing

The play button on book cards now opens the reader directly, instead of
always going to the detail page. Cards with an active progress sync
conflict route the play button to the detail page (which hosts the
conflict dialogue and resolves before writing progress), so the user is
never silently dropped into the reader with an unresolved conflict.

Backend:
- Add HasConflict to BookInfo and stamp it via ListSyncConflictsByUser
  (MarkActiveConflicts / MarkActiveConflictsSections) on the dashboard,
  bookshelf, series, tag, and search result card builders.
- Each page issues a single conflict query regardless of card count.

BookCard:
- Restructure into a detail link (cover + meta) with the play action as a
  sibling overlay using a pointer-events split: the container passes
  clicks through to detail while only the circular button routes to the
  reader. No nested anchors.
- On touch devices (hover: none) the play button stays visible.

Fix: carousel nav buttons had opacity-0 without pointer-events-none, so
they swallowed hover/clicks over book cards on the dashboard. They are
now click-through until the carousel is hovered.
This commit is contained in:
2026-08-06 07:52:20 -04:00
parent 987ece38f0
commit 9ef6c5b6ed
8 changed files with 309 additions and 165 deletions
+26 -13
View File
@@ -62,7 +62,7 @@ templ CollectionCarousel(section handlers.SectionData) {
</div>
<div class="carousel-container relative group">
<button
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200"
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200"
data-action="scroll-carousel"
data-collection-id={ section.ID }
data-direction="-1"
@@ -89,7 +89,7 @@ templ CollectionCarousel(section handlers.SectionData) {
}
</div>
<button
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200"
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200"
data-action="scroll-carousel"
data-collection-id={ section.ID }
data-direction="1"
@@ -103,13 +103,8 @@ templ CollectionCarousel(section handlers.SectionData) {
}
templ BookCard(item handlers.BookInfo) {
<a href={ "/media/" + item.MediaItemID } title={ item.Title } class="block h-full">
<div
class="book-card w-full h-full rounded-xl overflow-hidden cursor-pointer"
tabindex="0"
role="button"
aria-label={ "View " + item.Title }
>
<div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
<a href={ "/media/" + item.MediaItemID } class="block h-full">
<div
class="book-card-cover aspect-[2/3] overflow-hidden"
style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);"
@@ -129,9 +124,6 @@ templ BookCard(item handlers.BookInfo) {
class="w-full h-full object-cover"
/>
}
<div class="book-card-action">
<span class="book-card-action-btn">@Icon("play", "h-5 w-5 translate-x-0.5")</span>
</div>
</div>
<div class="book-card-meta">
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title={ item.Title }>
@@ -143,8 +135,29 @@ templ BookCard(item handlers.BookInfo) {
</p>
}
</div>
</a>
<div class="book-card-action">
if item.HasConflict {
<a
href={ "/media/" + item.MediaItemID }
class="book-card-action-btn"
aria-label={ "Resolve progress conflict for " + item.Title }
title="Resolve progress conflict"
>
@Icon("play", "h-5 w-5 translate-x-0.5")
</a>
} else {
<a
href={ "/readers/" + item.MediaItemID }
class="book-card-action-btn"
aria-label={ "Read " + item.Title }
title="Read"
>
@Icon("play", "h-5 w-5 translate-x-0.5")
</a>
}
</div>
</a>
</div>
}
templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) {