feat(search): viewer progress + completed badges; dropdown to templ
Search results now show where you are in a book: a completed check at the 0.999 threshold (same cut as the filter status arms), a partial progress bar along the cover while mid-read, nothing when you never opened it. Rendered in BookCard (any grid whose adapter loads viewer progress) and in the header-search dropdown rows. The progress rides a LEFT JOIN reading_progress on the SAME user_id param SearchMediaItemsUnified already binds for library visibility — one requester param, no new wire inputs, row-neutral join (the table is UNIQUE per media_item+user). SearchMediaItemsUnifiedRow and BookInfo gain the keys additively (viewer_percentage null when unknown, library_name empty on feeds that don't load it); contract_pins.md carries the changelog entry and the pin tests widen in lockstep. The header dropdown itself migrates off the innerHTML string renderer in search.ts (a rendering-ownership violation: a templ twin existed via the HX-Request branch). The input now hx-gets layout=dropdown and swaps the new templ SearchResults partial — titles/authors highlighted server-side (escape-first, safe by construction), rows with the same progress overlays — while a slim Alpine component owns only client-local state (min-length gating, open/close, keyboard selection, htmx lifecycle). web/src/rendering-allowlist.txt drops the search.ts baseline to zero accordingly. Integration test pins the per-user join (fresh user sees 1.0/0.42/null on the fixture; the shared admin sees null everywhere — read-only) and the dropdown partial's overlays/count/row count.
This commit is contained in:
1 parent
d1f9ad3b25
commit
61e5f47ec0
18 files changed
+1140
-616
No files matched your search
@@ -144,7 +144,7 @@ templ BookCard(item handlers.BookInfo) {
|
||||
/>
|
||||
}
|
||||
<!-- Media-type identifier (A2c): books stay unbadged — the
|
||||
shelf default already reads as a book -->
|
||||
shelf default already reads as a book -->
|
||||
if MediaBadgeIcon(item.Badge) != "" {
|
||||
<span
|
||||
class="absolute top-2 right-2 z-10 flex items-center justify-center h-7 w-7 rounded-full shadow-sm"
|
||||
@@ -154,6 +154,25 @@ templ BookCard(item handlers.BookInfo) {
|
||||
@Icon(MediaBadgeIcon(item.Badge), "h-4 w-4")
|
||||
</span>
|
||||
}
|
||||
<!-- Viewer progress (2026-10-27 search-progress batch):
|
||||
completed check top-left, partial bar along the cover
|
||||
bottom. Renders only when the feeding adapter loaded
|
||||
the requester's progress (search today). -->
|
||||
if item.ViewerPercentage != nil {
|
||||
if *item.ViewerPercentage >= CompletedProgressThreshold {
|
||||
<span
|
||||
class="absolute top-2 left-2 z-10 flex items-center justify-center h-7 w-7 rounded-full shadow-sm"
|
||||
style="background-color: color-mix(in srgb, var(--bg-primary) 78%, transparent); color: var(--accent); backdrop-filter: blur(3px);"
|
||||
title="Completed"
|
||||
>
|
||||
@Icon("check", "h-4 w-4")
|
||||
</span>
|
||||
} else if *item.ViewerPercentage > 0 {
|
||||
<div class="absolute bottom-0 left-0 right-0 z-10 h-1" style="background-color: color-mix(in srgb, var(--bg-primary) 55%, transparent);">
|
||||
<div class="h-full" style={ progressFillCSS(*item.ViewerPercentage) }></div>
|
||||
</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 }>
|
||||
|
||||
Reference in new issue
Block a user