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
+1
View File
@@ -73,6 +73,7 @@ type BookInfo struct {
Title string `json:"title"` Title string `json:"title"`
Author string `json:"author"` Author string `json:"author"`
CoverImagePath string `json:"cover_image_path"` CoverImagePath string `json:"cover_image_path"`
HasConflict bool `json:"has_conflict"`
} }
type SectionData struct { type SectionData struct {
+54
View File
@@ -4,6 +4,7 @@ import (
"bookhoard/internal/database" "bookhoard/internal/database"
"bookhoard/internal/services" "bookhoard/internal/services"
"bookhoard/internal/utils" "bookhoard/internal/utils"
"context"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -188,6 +189,59 @@ func BuildSections(sections []services.DashboardSection, currentLibraryID string
return result return result
} }
// activeConflictSet returns the set of media item IDs (as strings) that have an
// active (unresolved) progress sync conflict for the given user. A single query
// is issued; resolved conflicts are filtered out in memory.
func activeConflictSet(ctx context.Context, db *database.Queries, userID pgtype.UUID) map[string]bool {
conflicts, err := db.ListSyncConflictsByUser(ctx, userID)
if err != nil {
return nil
}
set := make(map[string]bool, len(conflicts))
for _, c := range conflicts {
if c.ResolutionStatus.String == "unresolved" {
set[uuid.UUID(c.MediaItemID.Bytes).String()] = true
}
}
return set
}
// MarkActiveConflicts stamps HasConflict on each book whose media item has an
// active progress sync conflict for the user. It performs a single query
// regardless of how many books are passed.
func MarkActiveConflicts(ctx context.Context, db *database.Queries, userID pgtype.UUID, books []BookInfo) []BookInfo {
if len(books) == 0 {
return books
}
set := activeConflictSet(ctx, db, userID)
for i := range books {
if set[books[i].MediaItemID] {
books[i].HasConflict = true
}
}
return books
}
// MarkActiveConflictsSections is the section-aware variant of MarkActiveConflicts,
// used by the dashboard which renders books grouped into sections.
func MarkActiveConflictsSections(ctx context.Context, db *database.Queries, userID pgtype.UUID, sections []SectionData) []SectionData {
if len(sections) == 0 {
return sections
}
set := activeConflictSet(ctx, db, userID)
if len(set) == 0 {
return sections
}
for s := range sections {
for i := range sections[s].Items {
if set[sections[s].Items[i].MediaItemID] {
sections[s].Items[i].HasConflict = true
}
}
}
return sections
}
func getViewAllURL(collectionID string, libraryID string) string { func getViewAllURL(collectionID string, libraryID string) string {
if collectionID != "" { if collectionID != "" {
if libraryID != "" { if libraryID != "" {
+9 -1
View File
@@ -215,6 +215,9 @@ func registerFrontendRoutes(cfg *Config) {
bookInfoList = []handlers.BookInfo{} bookInfoList = []handlers.BookInfo{}
} }
seriesUserUUID, _ := uuid.Parse(user.ID)
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: seriesUserUUID, Valid: true}, bookInfoList)
var buf bytes.Buffer var buf bytes.Buffer
err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf) err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -267,6 +270,9 @@ func registerFrontendRoutes(cfg *Config) {
bookInfoList = []handlers.BookInfo{} bookInfoList = []handlers.BookInfo{}
} }
tagUserUUID, _ := uuid.Parse(user.ID)
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: tagUserUUID, Valid: true}, bookInfoList)
var buf bytes.Buffer var buf bytes.Buffer
err = templates.BrowseDetail(user, "🏷️", "Tag", tagName, tagName, "/bookshelf", "Bookshelf", "🏷️", "No books found with this tag", bookInfoList, errorMsg).Render(c.Request().Context(), &buf) err = templates.BrowseDetail(user, "🏷️", "Tag", tagName, tagName, "/bookshelf", "Bookshelf", "🏷️", "No books found with this tag", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -354,6 +360,7 @@ func registerFrontendRoutes(cfg *Config) {
} }
var buf bytes.Buffer var buf bytes.Buffer
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: userUUID, Valid: true}, bookInfoList)
err = templates.BookShelf(user, libData, libraryID, errorMsg, savedFilters, bookInfoList, limit, offset, totalCount).Render(c.Request().Context(), &buf) err = templates.BookShelf(user, libData, libraryID, errorMsg, savedFilters, bookInfoList, limit, offset, totalCount).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
return err return err
@@ -408,7 +415,8 @@ func registerFrontendRoutes(cfg *Config) {
// Get only visible sections for the dashboard display // Get only visible sections for the dashboard display
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections) visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
sectionData := handlers.BuildSections(visibleSections, libraryID) userPgID := pgtype.UUID{Bytes: userUUID, Valid: true}
sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID))
allSectionsData := handlers.BuildSections(allSections, libraryID) allSectionsData := handlers.BuildSections(allSections, libraryID)
var buf bytes.Buffer var buf bytes.Buffer
+2
View File
@@ -112,6 +112,8 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error {
CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath), CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath),
} }
} }
// Stamp active conflict flags so cards route the play action correctly
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, user.ID, bookInfoList)
// Render using BooksGrid template // Render using BooksGrid template
var buf bytes.Buffer var buf bytes.Buffer
err = templates.BooksGrid(bookInfoList, limit, offset, totalCount, libraryID).Render(c.Request().Context(), &buf) err = templates.BooksGrid(bookInfoList, limit, offset, totalCount, libraryID).Render(c.Request().Context(), &buf)
+26 -13
View File
@@ -62,7 +62,7 @@ templ CollectionCarousel(section handlers.SectionData) {
</div> </div>
<div class="carousel-container relative group"> <div class="carousel-container relative group">
<button <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-action="scroll-carousel"
data-collection-id={ section.ID } data-collection-id={ section.ID }
data-direction="-1" data-direction="-1"
@@ -89,7 +89,7 @@ templ CollectionCarousel(section handlers.SectionData) {
} }
</div> </div>
<button <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-action="scroll-carousel"
data-collection-id={ section.ID } data-collection-id={ section.ID }
data-direction="1" data-direction="1"
@@ -103,13 +103,8 @@ templ CollectionCarousel(section handlers.SectionData) {
} }
templ BookCard(item handlers.BookInfo) { templ BookCard(item handlers.BookInfo) {
<a href={ "/media/" + item.MediaItemID } title={ item.Title } class="block h-full"> <div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
<div <a href={ "/media/" + item.MediaItemID } class="block h-full">
class="book-card w-full h-full rounded-xl overflow-hidden cursor-pointer"
tabindex="0"
role="button"
aria-label={ "View " + item.Title }
>
<div <div
class="book-card-cover aspect-[2/3] overflow-hidden" class="book-card-cover aspect-[2/3] overflow-hidden"
style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);" 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" 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>
<div class="book-card-meta"> <div class="book-card-meta">
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title={ item.Title }> <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> </p>
} }
</div> </div>
</div>
</a> </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>
</div>
} }
templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) { templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) {
+193 -144
View File
@@ -203,7 +203,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</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\" data-action=\"scroll-carousel\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</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 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -269,7 +269,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</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\" data-action=\"scroll-carousel\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</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 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -319,115 +319,112 @@ func BookCard(item handlers.BookInfo) templ.Component {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var12 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 templ.SafeURL var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 106, Col: 39} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 107, Col: 40}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" title=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" 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);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.CoverImagePath != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<img src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 106, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 31}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"block h-full\"><div class=\"book-card w-full h-full rounded-xl overflow-hidden cursor-pointer\" tabindex=\"0\" role=\"button\" aria-label=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("View " + item.Title) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 111, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\"><div class=\"book-card-cover aspect-[2/3] overflow-hidden\" style=\"background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.CoverImagePath != "" { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<img src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<img src=\"/static/placeholder-book.svg\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 119, Col: 31} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 123, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" alt=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"w-full h-full object-cover\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div><div class=\"book-card-meta\"><h3 class=\"font-semibold text-sm leading-snug line-clamp-2\" style=\"color: var(--text-primary)\" title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 120, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 129, Col: 117}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<img src=\"/static/placeholder-book.svg\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 128, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-full h-full object-cover\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</h3>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} if item.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"book-card-action\"><span class=\"book-card-action-btn\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p class=\"text-xs mt-0.5 line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-5 w-5 translate-x-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</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=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 137, Col: 117} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 133, Col: 100}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -438,51 +435,103 @@ func BookCard(item handlers.BookInfo) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var20 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 138, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 19}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h3>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.Author != "" { }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<p class=\"text-xs mt-0.5 line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></a><div class=\"book-card-action\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string if item.HasConflict {
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 141, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">") var templ_7745c5c3_Var21 templ.SafeURL
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 142, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" class=\"book-card-action-btn\" aria-label=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("Resolve progress conflict for " + item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 142, Col: 19} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 144, Col: 63}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" title=\"Resolve progress conflict\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-5 w-5 translate-x-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 templ.SafeURL
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs("/readers/" + item.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 151, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"book-card-action-btn\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue("Read " + item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 153, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" title=\"Read\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-5 w-5 translate-x-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div></a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -506,12 +555,12 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var23 := templ.GetChildren(ctx) templ_7745c5c3_Var25 := templ.GetChildren(ctx)
if templ_7745c5c3_Var23 == nil { if templ_7745c5c3_Var25 == nil {
templ_7745c5c3_Var23 = templ.NopComponent templ_7745c5c3_Var25 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -519,38 +568,38 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></div><div id=\"collection-list\" class=\"space-y-2 mb-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</button></div><div id=\"collection-list\" class=\"space-y-2 mb-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, section := range sections { for _, section := range sections {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<div class=\"collection-item flex items-center justify-between p-3 rounded-xl cursor-move select-none card\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div class=\"collection-item flex items-center justify-between p-3 rounded-xl cursor-move select-none card\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 170, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 183, Col: 37}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" data-is-system=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-is-system=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem)) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 171, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 58}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" draggable=\"true\"><div class=\"flex items-center gap-3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" draggable=\"true\"><div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -558,107 +607,107 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"text-xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"text-xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 176, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span><div class=\"flex items-center gap-2\"><span class=\"font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 178, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">System</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div></div><div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<button data-action=\"restore-system-collection\" data-collection-name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 188, Col: 42} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 189, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" class=\"btn btn-secondary py-1 px-2.5 text-xs\" title=\"Restore { section.Title } to defaults\">Restore</button> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span><div class=\"flex items-center gap-2\"><span class=\"font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !ContainsString(hiddenCollections, section.ID) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "><div class=\"w-11 h-6 rounded-full peer peer-checked:after:translate-x-full 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\" style=\"background-color: var(--border-strong);\"></div></label></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div><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\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection) templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 214, Col: 128} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 191, Col: 84}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">System</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div></div><div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button data-action=\"restore-system-collection\" data-collection-name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 221, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 201, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"w-full h-2 rounded-lg appearance-none cursor-pointer\" style=\"background-color: var(--border-strong);\" 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=\"btn btn-secondary\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"btn btn-primary\">Save Changes</button></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"btn btn-secondary py-1 px-2.5 text-xs\" title=\"Restore { section.Title } to defaults\">Restore</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !ContainsString(hiddenCollections, section.ID) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "><div class=\"w-11 h-6 rounded-full peer peer-checked:after:translate-x-full 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\" style=\"background-color: var(--border-strong);\"></div></label></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</div><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\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 227, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 234, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" class=\"w-full h-2 rounded-lg appearance-none cursor-pointer\" style=\"background-color: var(--border-strong);\" 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=\"btn btn-secondary\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"btn btn-primary\">Save Changes</button></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+19 -2
View File
@@ -412,6 +412,7 @@
/* ---------- Book card (the atomic unit) ---------- */ /* ---------- Book card (the atomic unit) ---------- */
.book-card { .book-card {
position: relative;
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent); border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent);
border-radius: 0.75rem; border-radius: 0.75rem;
@@ -442,14 +443,20 @@
} }
.book-card-action { .book-card-action {
position: absolute; position: absolute;
inset: 0; top: 0;
left: 0;
right: 0;
aspect-ratio: 2 / 3;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
opacity: 0; opacity: 0;
pointer-events: none;
z-index: 2;
transition: opacity 0.18s ease; transition: opacity 0.18s ease;
} }
.book-card:hover .book-card-action { .book-card:hover .book-card-action,
.book-card:focus-within .book-card-action {
opacity: 1; opacity: 1;
} }
.book-card-action-btn { .book-card-action-btn {
@@ -462,11 +469,21 @@
background-color: color-mix(in srgb, var(--accent) 92%, white); background-color: color-mix(in srgb, var(--accent) 92%, white);
color: #fff; color: #fff;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
pointer-events: auto;
text-decoration: none;
} }
.book-card-meta { .book-card-meta {
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
padding: 0.5rem 0.625rem; padding: 0.5rem 0.625rem;
} }
/* On touch devices there is no hover, so the quick-action stays visible and
only the circular button itself is clickable (the container passes clicks
through to the detail link). */
@media (hover: none) {
.book-card-action {
opacity: 1;
}
}
/* ---------- Bold accents ---------- */ /* ---------- Bold accents ---------- */
+1 -1
View File
File diff suppressed because one or more lines are too long