feat(annotations + player): bookmark rows in the banner, stacked volume panel, badge parity for comics/manga

Annotations banner: audio/text bookmarks join notes and highlights —
third row kind (bookmark glyph indicator, H:MM:SS / Page N / Chapter N
friendly labels), edit-note/delete actions branch by kind, and the
banner's export now covers all annotations. The archived ledger
(ListDeletedAnnotationsForBook) already tombstoned bookmarks equally
and Restore/Purge take annotation_type=bookmark — verified live:
delete → restore brings the row back with its note intact.

Volume: the dragger sits BELOW the mute icon in the panel (stacked
flyout shape) with the horizontal slider from the last round.

Badges: the real fix for comics/manga — format_group only knows
'comic_archive' (manga_type is unreliable across sources), so the
comic/manga split is resolved from the LIBRARY type server-side.
handlers.MediaBadgeKind + BookInfo.Badge at every construction site
(dashboard SSR + sections API, collections, series, and the bookshelf
search grid — which was also missing FormatGroup entirely, so no card
ever badged there). Both renderers (templ + dashboard.ts) switch on
the resolved badge. Verified: All-Books grid renders 2 Comic + 3
Manga badges, dashboard the audiobook one.

Infra: header.templ main.js src gains ?v=<stamp> — browsers keyed an
intermediate bundle by URL and served it across rebuilds (the stale
'Add note' the user saw); bump the stamp when JS stale-shadows.
This commit is contained in:
John O'Keefe committed 2026-10-15 19:33:18 -04:00
1 parent 2c714dcb60
commit 89665fb2bc
19 files changed
+273 -92

No files matched your search

+11 -5
View File
@@ -29,7 +29,7 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
aria-label="Annotations"
>
<div class="sticky top-0 px-4 py-3 flex items-center justify-between" style="background: var(--bg-primary); border-bottom: 1px solid var(--border);">
<h3 class="text-lg font-semibold" style="color: var(--text-primary);">Notes &amp; Highlights</h3>
<h3 class="text-lg font-semibold" style="color: var(--text-primary);">Notes, Highlights &amp; Bookmarks</h3>
<button
type="button"
@click="hideAnnotationsBanner()"
@@ -49,12 +49,12 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
class="w-full py-2 mb-3 text-sm font-medium rounded-lg border transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
style="border-color: var(--border); color: var(--text-primary);"
>
Export all notes and highlights
Export all annotations
</button>
<p x-show="annotationsBannerLoading" class="text-sm py-4" style="color: var(--text-secondary);">Loading…</p>
<template x-if="!annotationsBannerLoading && bannerRows.length === 0">
<p class="text-sm py-4" style="color: var(--text-secondary);">
No notes or highlights on this book yet.
No notes, highlights or bookmarks on this book yet.
</p>
</template>
<!-- Notes first, then highlights — stable ledger order -->
@@ -64,6 +64,12 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
<!-- Note indicator: a quiet stroke glyph in the card's
upper-right corner (never left of text) — every
note row, and highlights with an attached note. -->
<svg
x-show="row.kind === 'bookmark'"
class="absolute top-2 right-2 pointer-events-none reader-icon"
style="color: var(--text-secondary);"
width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"
><path d="M5 3h10v14l-5-3.5L5 17z"></path></svg>
<svg
x-show="row.kind === 'note' || (row.kind === 'highlight' && row.hasNote)"
class="absolute top-2 right-2 pointer-events-none reader-icon"
@@ -105,11 +111,11 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
@click="startBannerEdit(row)"
class="text-xs px-2 py-1 rounded-lg hover:bg-surface-hover"
style="color: var(--text-secondary);"
x-text="row.kind === 'highlight' ? (row.hasNote ? 'Edit note' : 'Add note') : 'Edit'"
x-text="row.kind === 'note' ? 'Edit' : (row.hasNote ? 'Edit note' : 'Add note')"
></button>
<button
type="button"
@click="row.kind === 'note' ? deleteBannerNote(row) : deleteBannerHighlight(row)"
@click="row.kind === 'note' ? deleteBannerNote(row) : row.kind === 'bookmark' ? deleteBannerBookmark(row) : deleteBannerHighlight(row)"
class="text-xs px-2 py-1 rounded-lg hover:bg-surface-hover"
style="color: var(--status-danger);"
>✕ Delete</button>