feat(reader): EPUB highlights & notes — selection popover, overlayer rendering, annotations drawer

Phase 3 (EPUB half) of the reader redesign:

- Select text in a reflowable book → floating glass popover at the
  selection (5 colors, note, copy). Clicking a color creates the
  highlight via POST /api/media-items/:id/highlights, anchored by the
  foliate range CFI (epubcfi_start) with percentage position.
- Highlights render through foliate's overlayer pipeline: draw-
  annotation draws Overlayer.highlight with the stored color,
  create-overlay re-adds persisted highlights as sections load,
  show-annotation opens the edit popover when a highlight is clicked
  (recolor, edit note, copy, delete).
- Backend: highlight create/update accept epubcfi_start/end,
  note_text, and percentage fields; position validation relaxed
  (CFIs exceed the old 100-char cap); PUT routes through
  AnnotationService.SaveHighlight so edits get dedup/LWW treatment
  and actually persist note_text (the plain query can't).
- Bookmarks drawer becomes the Annotations drawer with tabs:
  Highlights (color-bar list, note previews, jump/edit/delete),
  Notes (add note at current position, list, delete — backed by the
  existing notes API), and Bookmarks (unchanged behavior).
- Popover dismissed on outside click, collapsed selection, page
  navigation, or Esc (new top-priority Esc branch).
This commit is contained in:
2026-08-16 12:33:55 -04:00
parent bd7d71a284
commit 40d70513da
6 changed files with 682 additions and 46 deletions
+162 -11
View File
@@ -118,9 +118,89 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
@ReaderSettingsDrawer()
</div>
<!-- Bookmarks drawer (right) -->
<!-- Selection popover (highlights) -->
<div
id="bookmarks-drawer"
id="selection-popover"
x-show="selectionPopover.open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
:style="'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'"
class="reader-popover"
role="dialog"
aria-label="Highlight selection"
@pointerdown.stop
>
<div class="flex items-center gap-2">
<template x-for="c in highlightColors" :key="c">
<button
type="button"
class="color-dot"
:class="selectionPopover.color === c ? 'selected' : ''"
:style="'background-color:' + c"
:title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'"
:aria-label="'Highlight color ' + c"
@click="selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())"
></button>
</template>
<div class="w-px h-5 reader-sep"></div>
<button
type="button"
class="reader-popover-btn"
title="Note"
@click="selectionPopover.noteOpen = true"
>
<svg class="reader-icon" width="16" height="16" aria-hidden="true"><path d="M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5"></path></svg>
</button>
<button
type="button"
class="reader-popover-btn"
title="Copy text"
@click="copySelectionText()"
>
<svg class="reader-icon" width="16" height="16" aria-hidden="true"><path d="M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z"></path></svg>
</button>
<template x-if="selectionPopover.mode === 'edit'">
<button
type="button"
class="reader-popover-btn danger"
title="Delete highlight"
@click="deleteHighlightById(selectionPopover.id)"
>
<svg class="reader-icon" width="16" height="16" aria-hidden="true"><path d="M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16"></path></svg>
</button>
</template>
</div>
<div x-show="selectionPopover.noteOpen" class="mt-2">
<textarea
x-model="selectionPopover.note"
rows="3"
class="reader-note-input"
placeholder="Note…"
></textarea>
<div class="flex gap-2 mt-1">
<button
type="button"
class="flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
@click="selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()"
x-text="selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'"
>Save</button>
<button
type="button"
class="flex-1 py-1 text-xs border rounded hover:opacity-80"
style="border-color: var(--border);"
@click="selectionPopover.noteOpen = false"
>Cancel</button>
</div>
</div>
</div>
<!-- Annotations drawer (right): highlights / notes / bookmarks -->
<div
id="annotations-drawer"
x-show="bookmarksOpen"
x-transition:enter="transition-transform duration-200 ease-out"
x-transition:enter-start="translate-x-full"
@@ -130,9 +210,9 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
x-transition:leave-end="translate-x-full"
class="reader-drawer right"
role="dialog"
aria-label="Bookmarks"
aria-label="Annotations"
>
@ReaderBookmarksDrawer()
@ReaderAnnotationsDrawer()
</div>
</body>
</html>
@@ -149,7 +229,7 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
<div class="flex items-center gap-1">
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmarks">📝</button>
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Annotations">📝</button>
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
</div>
</div>
@@ -306,10 +386,81 @@ templ ReaderTOCDrawer() {
</div>
}
templ ReaderBookmarksDrawer() {
@drawerHeader("Bookmarks")
templ ReaderAnnotationsDrawer() {
@drawerHeader("Annotations")
<div class="reader-tabs">
<button :class="annotationsTab === 'highlights' ? 'active' : ''" @click="annotationsTab = 'highlights'">
Highlights <span class="reader-tab-count" x-text="highlightItems.length">0</span>
</button>
<button :class="annotationsTab === 'notes' ? 'active' : ''" @click="annotationsTab = 'notes'">
Notes <span class="reader-tab-count" x-text="noteItems.length">0</span>
</button>
<button :class="annotationsTab === 'bookmarks' ? 'active' : ''" @click="annotationsTab = 'bookmarks'">
Bookmarks <span class="reader-tab-count" x-text="bookmarkItems.length">0</span>
</button>
</div>
<div class="reader-drawer-body">
<div id="bookmarks-list" class="space-y-2">
<!-- Highlights -->
<div x-show="annotationsTab === 'highlights'" class="space-y-2">
<template x-for="hl in highlightItems" :key="hl.id">
<div class="reader-hl-row group">
<a
href="#"
@click.prevent="goToHighlight(hl)"
class="flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700"
>
<span class="block text-sm truncate" :style="'border-left: 3px solid ' + hl.color + '; padding-left: 0.5rem;'" x-text="hl.text"></span>
<span class="block text-xs mt-0.5 truncate pl-2" style="color: var(--text-secondary)" x-show="hl.note" x-text="'📝 ' + hl.note"></span>
</a>
<button
@click="deleteHighlightById(hl.id)"
class="p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity"
title="Delete highlight"
aria-label="Delete highlight"
>
</button>
</div>
</template>
<template x-if="highlightItems.length === 0">
<p class="text-sm" style="color: var(--text-secondary)">Select text in the book to highlight it</p>
</template>
</div>
<!-- Notes -->
<div x-show="annotationsTab === 'notes'">
<textarea
x-model="newNoteText"
rows="2"
class="reader-note-input"
placeholder="Add a note at the current position…"
></textarea>
<button @click="addNote(newNoteText); newNoteText = ''" class="w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
+ Add Note
</button>
<div class="space-y-2">
<template x-for="note in noteItems" :key="note.id">
<div class="reader-hl-row group">
<a href="#" @click.prevent="closeDrawers()" class="flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700">
<span class="block text-sm" x-text="note.content"></span>
<span class="block text-xs mt-0.5 truncate" style="color: var(--text-secondary)" x-text="note.positionLabel"></span>
</a>
<button
@click="deleteNoteById(note.id)"
class="p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity"
title="Delete note"
aria-label="Delete note"
>
</button>
</div>
</template>
<template x-if="noteItems.length === 0">
<p class="text-sm" style="color: var(--text-secondary)">No notes yet</p>
</template>
</div>
</div>
<!-- Bookmarks -->
<div x-show="annotationsTab === 'bookmarks'" class="space-y-2">
<template x-for="bookmark in bookmarkItems" :key="bookmark.id">
<div class="flex items-center gap-1 group">
<a
@@ -333,10 +484,10 @@ templ ReaderBookmarksDrawer() {
<template x-if="bookmarkItems.length === 0">
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
</template>
<button @click="addBookmark()" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
+ Add Bookmark
</button>
</div>
<button @click="addBookmark()" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
+ Add Bookmark
</button>
</div>
}
+14 -14
View File
@@ -149,11 +149,11 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Bookmarks drawer (right) --><div id=\"bookmarks-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Bookmarks\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Selection popover (highlights) --><div id=\"selection-popover\" x-show=\"selectionPopover.open\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" :style=\"'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'\" class=\"reader-popover\" role=\"dialog\" aria-label=\"Highlight selection\" @pointerdown.stop><div class=\"flex items-center gap-2\"><template x-for=\"c in highlightColors\" :key=\"c\"><button type=\"button\" class=\"color-dot\" :class=\"selectionPopover.color === c ? 'selected' : ''\" :style=\"'background-color:' + c\" :title=\"selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'\" :aria-label=\"'Highlight color ' + c\" @click=\"selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())\"></button></template><div class=\"w-px h-5 reader-sep\"></div><button type=\"button\" class=\"reader-popover-btn\" title=\"Note\" @click=\"selectionPopover.noteOpen = true\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg></button> <button type=\"button\" class=\"reader-popover-btn\" title=\"Copy text\" @click=\"copySelectionText()\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z\"></path></svg></button><template x-if=\"selectionPopover.mode === 'edit'\"><button type=\"button\" class=\"reader-popover-btn danger\" title=\"Delete highlight\" @click=\"deleteHighlightById(selectionPopover.id)\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16\"></path></svg></button></template></div><div x-show=\"selectionPopover.noteOpen\" class=\"mt-2\"><textarea x-model=\"selectionPopover.note\" rows=\"3\" class=\"reader-note-input\" placeholder=\"Note…\"></textarea><div class=\"flex gap-2 mt-1\"><button type=\"button\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\" @click=\"selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()\" x-text=\"selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'\">Save</button> <button type=\"button\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\" @click=\"selectionPopover.noteOpen = false\">Cancel</button></div></div></div><!-- Annotations drawer (right): highlights / notes / bookmarks --><div id=\"annotations-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Annotations\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ReaderBookmarksDrawer().Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = ReaderAnnotationsDrawer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -193,7 +193,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 146, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 226, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -206,13 +206,13 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 149, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 229, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1><div class=\"flex items-center gap-1\"><button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark this position (b)\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmarks\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button></div></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 border-t z-40 pb-[env(safe-area-inset-bottom)] reader-glass\"><!-- Reflowable row --><div x-show=\"!isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><button @click=\"goLeft()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Left\" aria-label=\"Go left\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 15 6 L 9 12 L 15 18\"></path></svg></button> <input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"grow\"> <datalist id=\"tick-marks\"></datalist> <button @click=\"goRight()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Right\" aria-label=\"Go right\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 9 6 L 15 12 L 9 18\"></path></svg></button><div class=\"w-px h-6 mx-1 reader-sep\"></div><div id=\"progress-display\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden\"><span class=\"hidden sm:inline\" x-text=\"progressLabel\"></span><span x-text=\"progressMain\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1><div class=\"flex items-center gap-1\"><button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark this position (b)\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Annotations\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button></div></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 border-t z-40 pb-[env(safe-area-inset-bottom)] reader-glass\"><!-- Reflowable row --><div x-show=\"!isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><button @click=\"goLeft()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Left\" aria-label=\"Go left\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 15 6 L 9 12 L 15 18\"></path></svg></button> <input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"grow\"> <datalist id=\"tick-marks\"></datalist> <button @click=\"goRight()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Right\" aria-label=\"Go right\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 9 6 L 15 12 L 9 18\"></path></svg></button><div class=\"w-px h-6 mx-1 reader-sep\"></div><div id=\"progress-display\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden\"><span class=\"hidden sm:inline\" x-text=\"progressLabel\"></span><span x-text=\"progressMain\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -221,7 +221,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 187, Col: 113}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 267, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -231,7 +231,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 189, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 269, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -242,7 +242,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 192, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 272, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -257,7 +257,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 269, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 349, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -267,7 +267,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 271, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 351, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -310,7 +310,7 @@ func drawerHeader(title string) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 284, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 364, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -357,7 +357,7 @@ func ReaderTOCDrawer() templ.Component {
})
}
func ReaderBookmarksDrawer() templ.Component {
func ReaderAnnotationsDrawer() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -378,11 +378,11 @@ func ReaderBookmarksDrawer() templ.Component {
templ_7745c5c3_Var17 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = drawerHeader("Bookmarks").Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = drawerHeader("Annotations").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-drawer-body\"><div id=\"bookmarks-list\" class=\"space-y-2\"><template x-for=\"bookmark in bookmarkItems\" :key=\"bookmark.id\"><div class=\"flex items-center gap-1 group\"><a href=\"#\" @click.prevent=\"goToBookmark(bookmark)\" class=\"flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium block truncate\" x-text=\"bookmark.title\"></span> <span class=\"text-xs block truncate\" style=\"color: var(--text-secondary)\" x-text=\"bookmark.positionLabel\"></span></a> <button @click=\"deleteBookmark(bookmark.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete bookmark\" aria-label=\"Delete bookmark\">✕</button></div></template><template x-if=\"bookmarkItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p></template></div><button @click=\"addBookmark()\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-tabs\"><button :class=\"annotationsTab === 'highlights' ? 'active' : ''\" @click=\"annotationsTab = 'highlights'\">Highlights <span class=\"reader-tab-count\" x-text=\"highlightItems.length\">0</span></button> <button :class=\"annotationsTab === 'notes' ? 'active' : ''\" @click=\"annotationsTab = 'notes'\">Notes <span class=\"reader-tab-count\" x-text=\"noteItems.length\">0</span></button> <button :class=\"annotationsTab === 'bookmarks' ? 'active' : ''\" @click=\"annotationsTab = 'bookmarks'\">Bookmarks <span class=\"reader-tab-count\" x-text=\"bookmarkItems.length\">0</span></button></div><div class=\"reader-drawer-body\"><!-- Highlights --><div x-show=\"annotationsTab === 'highlights'\" class=\"space-y-2\"><template x-for=\"hl in highlightItems\" :key=\"hl.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"goToHighlight(hl)\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm truncate\" :style=\"'border-left: 3px solid ' + hl.color + '; padding-left: 0.5rem;'\" x-text=\"hl.text\"></span> <span class=\"block text-xs mt-0.5 truncate pl-2\" style=\"color: var(--text-secondary)\" x-show=\"hl.note\" x-text=\"'📝 ' + hl.note\"></span></a> <button @click=\"deleteHighlightById(hl.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete highlight\" aria-label=\"Delete highlight\">✕</button></div></template><template x-if=\"highlightItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Select text in the book to highlight it</p></template></div><!-- Notes --><div x-show=\"annotationsTab === 'notes'\"><textarea x-model=\"newNoteText\" rows=\"2\" class=\"reader-note-input\" placeholder=\"Add a note at the current position…\"></textarea> <button @click=\"addNote(newNoteText); newNoteText = ''\" class=\"w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Note</button><div class=\"space-y-2\"><template x-for=\"note in noteItems\" :key=\"note.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"closeDrawers()\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm\" x-text=\"note.content\"></span> <span class=\"block text-xs mt-0.5 truncate\" style=\"color: var(--text-secondary)\" x-text=\"note.positionLabel\"></span></a> <button @click=\"deleteNoteById(note.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete note\" aria-label=\"Delete note\">✕</button></div></template><template x-if=\"noteItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No notes yet</p></template></div></div><!-- Bookmarks --><div x-show=\"annotationsTab === 'bookmarks'\" class=\"space-y-2\"><template x-for=\"bookmark in bookmarkItems\" :key=\"bookmark.id\"><div class=\"flex items-center gap-1 group\"><a href=\"#\" @click.prevent=\"goToBookmark(bookmark)\" class=\"flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium block truncate\" x-text=\"bookmark.title\"></span> <span class=\"text-xs block truncate\" style=\"color: var(--text-secondary)\" x-text=\"bookmark.positionLabel\"></span></a> <button @click=\"deleteBookmark(bookmark.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete bookmark\" aria-label=\"Delete bookmark\">✕</button></div></template><template x-if=\"bookmarkItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p></template><button @click=\"addBookmark()\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}