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:
+162
-11
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user