fix(web): complete the CSP-build migration across remaining surfaces

- audio reader: scrub/volume/speed/genre-tick bindings converted to
  component methods (onScrubInput, setVolumeFromEvent,
  applyDefaultSpeedFromSelect, volumeLabel, railTickStyle,
  isSkipActive/isSpeedActive) — these used Number/Math/String globals
  that the CSP evaluator cannot resolve
- fmtTime restored to the audioReader component: the unexport pass
  had ripped it from the Alpine.data object (time displays were dead)
- book detail: cover-upload click and banner edit validation moved to
  component methods (triggerCoverUpload, bannerEditHasText)
- admin/docs/index components: init() hooks for their x-init chains
- CSP header: 'unsafe-eval' dropped — the CSP build needs no eval
- e2e spec: replace test.info().annotate (unavailable in this
  Playwright version) with console logging
This commit is contained in:
John O'Keefe committed 2026-10-25 17:22:40 -04:00
1 parent eb57b7208b
commit 09545ca5e1
16 files changed
+249 -234

No files matched your search

+5 -5
View File
@@ -194,7 +194,7 @@ func ReaderChrome(metadata ReaderMetadata) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</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 x-show=\"(!isFixedLayout || isPDF) && !treatAsComic\" @click=\"toggleSearch()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Search (/)\">🔍</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Annotations (a)\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button> <button @click=\"toggleHelp()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 text-sm font-bold\" title=\"Shortcuts & gestures (?)\" aria-label=\"Shortcuts and gestures\">?</button> <button @click=\"toggleFullscreen()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Fullscreen (f)\" aria-label=\"Toggle fullscreen\"><svg x-show=\"!isFullscreen\" class=\"reader-icon\" width=\"18\" height=\"18\" aria-hidden=\"true\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"></path></svg> <svg x-show=\"isFullscreen\" x-cloak class=\"reader-icon\" width=\"18\" height=\"18\" aria-hidden=\"true\"><path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\"></path></svg></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 flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5 grow min-w-[190px]\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button> <input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" @pointerup=\"$el.blur()\" 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><div class=\"flex items-center gap-1\"><div class=\"w-px h-6 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\">—</span></div><div class=\"w-px h-6 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\"><!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. --><div class=\"hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button></div><input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" @pointerup=\"$el.blur()\" class=\"grow min-w-[3rem]\"> <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-iLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</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 x-show=\"(!isFixedLayout || isPDF) && !treatAsComic\" @click=\"toggleSearch()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Search (/)\">🔍</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Annotations (a)\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button> <button @click=\"toggleHelp()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 text-sm font-bold\" title=\"Shortcuts & gestures (?)\" aria-label=\"Shortcuts and gestures\">?</button> <button @click=\"toggleFullscreen()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Fullscreen (f)\" aria-label=\"Toggle fullscreen\"><svg x-show=\"!isFullscreen\" class=\"reader-icon\" width=\"18\" height=\"18\" aria-hidden=\"true\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\"></path></svg> <svg x-show=\"isFullscreen\" x-cloak class=\"reader-icon\" width=\"18\" height=\"18\" aria-hidden=\"true\"><path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\"></path></svg></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 flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5 grow min-w-[190px]\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button> <input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" @pointerup=\"$el.blur()\" 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><div class=\"flex items-center gap-1\"><div class=\"w-px h-6 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\">—</span></div><div class=\"w-px h-6 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\"><!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. --><div class=\"hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button></div><input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" @pointerup=\"$el.blur()\" class=\"grow min-w-[3rem]\"> <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-iLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -269,7 +269,7 @@ func ReaderTOCDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<!-- Fixed-layout books get a Pages (thumbnails) tab; outline TOC may be\n\t absent in manga-scan PDFs and CBZs. --><div class=\"reader-tabs\" x-show=\"isFixedLayout\"><button :class=\"tocTab === 'contents' ? 'active' : ''\" @click=\"tocTab = 'contents'\">Contents <span class=\"reader-tab-count\" x-text=\"tocItems.length\">0</span></button> <button :class=\"tocTab === 'pages' ? 'active' : ''\" @click=\"tocTab = 'pages'; initPageThumbs()\">Pages <span class=\"reader-tab-count\" x-text=\"pageThumbList.length || ''\">0</span></button></div><div class=\"reader-drawer-body\"><nav id=\"toc-list\" class=\"space-y-1\" x-show=\"tocTab === 'contents' || !isFixedLayout\"><!-- Key must include the index: PDF outline entries can share the\n\t\t\t same destination href, and Alpine's x-for renders nothing on\n\t\t\t duplicate keys. --><template x-for=\"(item, idx) in tocItems\" :key=\"item.href + '::' + idx\"><a href=\"#\" @click.prevent=\"goToTOCItem(item)\" class=\"block py-1 px-2 rounded hover:bg-gray-700 text-sm\" :style=\"'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'\" x-text=\"item.label\"></a></template><template x-if=\"tocItems.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No table of contents</p></template></nav><div id=\"page-thumb-grid\" class=\"reader-thumb-grid\" x-show=\"isFixedLayout && tocTab === 'pages'\"><template x-for=\"p in pageThumbList\" :key=\"p.index\"><button class=\"reader-thumb\" :class=\"p.index === currentPageIndex ? 'active' : ''\" @click=\"goToPage(p.index)\" :title=\"'Page ' + (p.index + 1)\"><div class=\"reader-thumb-img\" :data-page=\"p.index\"></div><span class=\"reader-thumb-num\" x-text=\"p.index + 1\">1</span></button></template><template x-if=\"pageThumbList.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No pages</p></template></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<!-- Fixed-layout books get a Pages (thumbnails) tab; outline TOC may be\n\t absent in manga-scan PDFs and CBZs. --><div class=\"reader-tabs\" x-show=\"isFixedLayout\"><button :class=\"tocTab === 'contents' ? 'active' : ''\" @click=\"tocTab = 'contents'\">Contents <span class=\"reader-tab-count\" x-text=\"tocItems.length\">0</span></button> <button :class=\"tocTab === 'pages' ? 'active' : ''\" @click=\"openPageThumbs()\">Pages <span class=\"reader-tab-count\" x-text=\"pageThumbList.length || ''\">0</span></button></div><div class=\"reader-drawer-body\"><nav id=\"toc-list\" class=\"space-y-1\" x-show=\"tocTab === 'contents' || !isFixedLayout\"><!-- Key must include the index: PDF outline entries can share the\n\t\t\t same destination href, and Alpine's x-for renders nothing on\n\t\t\t duplicate keys. --><template x-for=\"(item, idx) in tocItems\" :key=\"item.href + '::' + idx\"><a href=\"#\" @click.prevent=\"goToTOCItem(item)\" class=\"block py-1 px-2 rounded hover:bg-gray-700 text-sm\" :style=\"'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'\" x-text=\"item.label\"></a></template><template x-if=\"tocItems.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No table of contents</p></template></nav><div id=\"page-thumb-grid\" class=\"reader-thumb-grid\" x-show=\"isFixedLayout && tocTab === 'pages'\"><template x-for=\"p in pageThumbList\" :key=\"p.index\"><button class=\"reader-thumb\" :class=\"p.index === currentPageIndex ? 'active' : ''\" @click=\"goToPage(p.index)\" :title=\"'Page ' + (p.index + 1)\"><div class=\"reader-thumb-img\" :data-page=\"p.index\"></div><span class=\"reader-thumb-num\" x-text=\"p.index + 1\">1</span></button></template><template x-if=\"pageThumbList.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No pages</p></template></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -302,7 +302,7 @@ func ReaderAnnotationsDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<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=\"note-rich block text-xs mt-0.5 truncate pl-2\" style=\"color: var(--text-secondary)\" x-show=\"hl.note\" x-html=\"'📝 ' + hl.noteHtml\"></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…\" @paste=\"richNotePaste\"></textarea><p class=\"text-xs mt-0.5\" style=\"color: var(--text-secondary)\">Markdown supported — rich-text paste converts to markdown</p><div class=\"text-xs mt-0.5 text-right\" x-show=\"newNoteText.length > 0\" :style=\"noteOverLimit(newNoteText) ? 'color: #f87171;' : 'color: var(--text-secondary);'\" x-text=\"noteCountLabel(newNoteText)\"></div><button @click=\"addNote(newNoteText); newNoteText = ''\" :disabled=\"noteOverLimit(newNoteText)\" class=\"w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">+ Add Note</button><div class=\"space-y-2\"><template x-for=\"note in noteItems\" :key=\"note.id\"><div class=\"reader-hl-row group\"><template x-if=\"!note.editOpen\"><div class=\"flex\"><a href=\"#\" @click.prevent=\"closeDrawers()\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"note-rich block text-sm truncate\" x-html=\"note.contentHtml\"></span> <span class=\"block text-xs mt-0.5 truncate\" style=\"color: var(--text-secondary)\" x-text=\"note.positionLabel\"></span></a> <button @click=\"startNoteEdit(note)\" class=\"p-2 rounded hover:bg-gray-700 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Edit note\" aria-label=\"Edit note\">✎</button> <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><!-- Inline edit: mirrors the bookmark-rename pattern --><div class=\"w-full py-2\" x-show=\"note.editOpen\" @click.stop><textarea x-model=\"note.editText\" rows=\"3\" class=\"reader-note-input\"></textarea><div class=\"flex gap-2 mt-1\"><button @click=\"saveNoteEdit(note)\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\" :disabled=\"!String(note.editText || '').trim()\">Save</button> <button @click=\"cancelNoteEdit(note)\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\">Cancel</button></div></div></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\" x-show=\"!bookmark.renameOpen\"><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><div class=\"flex-1 min-w-0 py-2\" x-show=\"bookmark.renameOpen\" @click.stop><input type=\"text\" class=\"reader-note-input\" x-model=\"bookmark.renameText\" @keydown.enter=\"renameBookmark(bookmark)\" @keydLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<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=\"note-rich block text-xs mt-0.5 truncate pl-2\" style=\"color: var(--text-secondary)\" x-show=\"hl.note\" x-html=\"'📝 ' + hl.noteHtml\"></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…\" @paste=\"richNotePaste\"></textarea><p class=\"text-xs mt-0.5\" style=\"color: var(--text-secondary)\">Markdown supported — rich-text paste converts to markdown</p><div class=\"text-xs mt-0.5 text-right\" x-show=\"newNoteText.length > 0\" :style=\"noteOverLimit(newNoteText) ? 'color: #f87171;' : 'color: var(--text-secondary);'\" x-text=\"noteCountLabel(newNoteText)\"></div><button @click=\"submitNote()\" :disabled=\"noteOverLimit(newNoteText)\" class=\"w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">+ Add Note</button><div class=\"space-y-2\"><template x-for=\"note in noteItems\" :key=\"note.id\"><div class=\"reader-hl-row group\"><template x-if=\"!note.editOpen\"><div class=\"flex\"><a href=\"#\" @click.prevent=\"closeDrawers()\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"note-rich block text-sm truncate\" x-html=\"note.contentHtml\"></span> <span class=\"block text-xs mt-0.5 truncate\" style=\"color: var(--text-secondary)\" x-text=\"note.positionLabel\"></span></a> <button @click=\"startNoteEdit(note)\" class=\"p-2 rounded hover:bg-gray-700 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Edit note\" aria-label=\"Edit note\">✎</button> <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><!-- Inline edit: mirrors the bookmark-rename pattern --><div class=\"w-full py-2\" x-show=\"note.editOpen\" @click.stop><textarea x-model=\"note.editText\" rows=\"3\" class=\"reader-note-input\"></textarea><div class=\"flex gap-2 mt-1\"><button @click=\"saveNoteEdit(note)\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\" :disabled=\"!String(note.editText || '').trim()\">Save</button> <button @click=\"cancelNoteEdit(note)\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\">Cancel</button></div></div></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\" x-show=\"!bookmark.renameOpen\"><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><div class=\"flex-1 min-w-0 py-2\" x-show=\"bookmark.renameOpen\" @click.stop><input type=\"text\" class=\"reader-note-input\" x-model=\"bookmark.renameText\" @keydown.enter=\"renameBookmark(bookmark)\" @keydown.escape=\"bookmark.renaLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -335,7 +335,7 @@ func ReaderSearchDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-search-bar\"><input type=\"search\" x-ref=\"searchInput\" x-model=\"searchQuery\" @keydown.enter.prevent=\"runSearch()\" placeholder=\"Search in book…\" class=\"reader-note-input grow\" aria-label=\"Search query\"> <button x-show=\"searchQuery || searchGroups.length\" @click=\"searchQuery = ''; clearSearchResults()\" class=\"p-2 rounded-lg hover:bg-gray-700 shrink-0\" title=\"Clear results\" aria-label=\"Clear results\">✕</button></div><div class=\"reader-search-status\" x-show=\"searchQuery || searching || searchGroups.length || searchError\"><span x-show=\"searching\" x-text=\"'Searching… ' + Math.round(searchProgress * 100) + '%'\">Searching…</span> <span x-show=\"!searching && searchGroups.length\" x-text=\"searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')\">0 matches</span> <span x-show=\"!searching && searchQuery && !searchGroups.length && !searchError\">No matches</span> <span x-show=\"searchError\" x-text=\"searchError\" style=\"color: #f87171;\">Search failed</span></div><div class=\"reader-drawer-body\"><template x-for=\"(group, gi) in searchGroups\" :key=\"gi\"><div class=\"mb-3\"><div class=\"text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary)\" x-text=\"group.label\"></div><div class=\"space-y-1\"><template x-for=\"(item, si) in group.items\" :key=\"gi + '-' + si\"><a href=\"#\" @click.prevent=\"goToSearchResult(item)\" class=\"search-result\"><span x-text=\"item.pre\"></span><mark x-text=\"item.match\"></mark><span x-text=\"item.post\"></span></a></template></div></div></template></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-search-bar\"><input type=\"search\" x-ref=\"searchInput\" x-model=\"searchQuery\" @keydown.enter.prevent=\"runSearch()\" placeholder=\"Search in book…\" class=\"reader-note-input grow\" aria-label=\"Search query\"> <button x-show=\"searchQuery || searchGroups.length\" @click=\"clearSearch()\" class=\"p-2 rounded-lg hover:bg-gray-700 shrink-0\" title=\"Clear results\" aria-label=\"Clear results\">✕</button></div><div class=\"reader-search-status\" x-show=\"searchQuery || searching || searchGroups.length || searchError\"><span x-show=\"searching\" x-text=\"'Searching… ' + Math.round(searchProgress * 100) + '%'\">Searching…</span> <span x-show=\"!searching && searchGroups.length\" x-text=\"searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')\">0 matches</span> <span x-show=\"!searching && searchQuery && !searchGroups.length && !searchError\">No matches</span> <span x-show=\"searchError\" x-text=\"searchError\" style=\"color: #f87171;\">Search failed</span></div><div class=\"reader-drawer-body\"><template x-for=\"(group, gi) in searchGroups\" :key=\"gi\"><div class=\"mb-3\"><div class=\"text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary)\" x-text=\"group.label\"></div><div class=\"space-y-1\"><template x-for=\"(item, si) in group.items\" :key=\"gi + '-' + si\"><a href=\"#\" @click.prevent=\"goToSearchResult(item)\" class=\"search-result\"><span x-text=\"item.pre\"></span><mark x-text=\"item.match\"></mark><span x-text=\"item.post\"></span></a></template></div></div></template></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -368,7 +368,7 @@ func ReaderSettingsDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"reader-drawer-body\"><!-- Behavior --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Behavior</h3><label class=\"block mb-2\">Chrome <select name=\"chrome_behavior\" x-model=\"chromeBehavior\" @change=\"applyChromeBehavior()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" x-model=\"progressMode\" @change=\"applyProgressMode()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label> <label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"tap_zones_enabled\" x-model=\"tapZonesEnabled\" @change=\"applyTapZoneSettings()\" class=\"mr-2\"> Tap Zones <span class=\"text-xs ml-1\" style=\"color: var(--text-secondary)\">(touch)</span></label> <label class=\"block mb-2\" x-show=\"tapZonesEnabled\">Tap Zone Size <input type=\"range\" name=\"tap_zone_size\" x-model.number=\"tapZoneSize\" @change=\"applyTapZoneSettings()\" min=\"10\" max=\"50\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"tapZoneSize + '%'\" style=\"color: var(--text-secondary)\">30%</span></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><div class=\"grid grid-cols-6 gap-2\"><template x-for=\"sw in themeSwatches\" :key=\"sw.value\"><button type=\"button\" @click=\"readingTheme = sw.value; applyTheme()\" class=\"theme-swatch\" :class=\"readingTheme === sw.value ? 'ring-2 ring-blue-500' : ''\" :style=\"'background-color:' + sw.bg + '; color:' + sw.fg\" :title=\"sw.label\" :aria-label=\"'Theme ' + sw.label\" :aria-pressed=\"readingTheme === sw.value\"><span class=\"text-xs font-semibold\">Aa</span></button></template></div></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Layout & display (fixed-layout only) --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Layout &amp; Display</h3><label class=\"block mb-3\" x-show=\"isComic\">Reading Flow<div class=\"reader-seg w-full mt-1\" role=\"group\" aria-label=\"Reading flow\"><button type=\"button\" class=\"flex-1\" :class=\"comicFlow !== 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('paged')\" title=\"One page or spread at a time\">Paged</button> <button type=\"button\" class=\"flex-1\" :class=\"comicFlow === 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('webtoon')\" title=\"Continuous vertical scroll (long-strip)\">Webtoon</button></div></label> <label class=\"blLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"reader-drawer-body\"><!-- Behavior --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Behavior</h3><label class=\"block mb-2\">Chrome <select name=\"chrome_behavior\" x-model=\"chromeBehavior\" @change=\"applyChromeBehavior()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" x-model=\"progressMode\" @change=\"applyProgressMode()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label> <label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"tap_zones_enabled\" x-model=\"tapZonesEnabled\" @change=\"applyTapZoneSettings()\" class=\"mr-2\"> Tap Zones <span class=\"text-xs ml-1\" style=\"color: var(--text-secondary)\">(touch)</span></label> <label class=\"block mb-2\" x-show=\"tapZonesEnabled\">Tap Zone Size <input type=\"range\" name=\"tap_zone_size\" x-model.number=\"tapZoneSize\" @change=\"applyTapZoneSettings()\" min=\"10\" max=\"50\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"tapZoneSize + '%'\" style=\"color: var(--text-secondary)\">30%</span></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><div class=\"grid grid-cols-6 gap-2\"><template x-for=\"sw in themeSwatches\" :key=\"sw.value\"><button type=\"button\" @click=\"pickReadingTheme(sw.value)\" class=\"theme-swatch\" :class=\"readingTheme === sw.value ? 'ring-2 ring-blue-500' : ''\" :style=\"'background-color:' + sw.bg + '; color:' + sw.fg\" :title=\"sw.label\" :aria-label=\"'Theme ' + sw.label\" :aria-pressed=\"readingTheme === sw.value\"><span class=\"text-xs font-semibold\">Aa</span></button></template></div></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Layout & display (fixed-layout only) --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Layout &amp; Display</h3><label class=\"block mb-3\" x-show=\"isComic\">Reading Flow<div class=\"reader-seg w-full mt-1\" role=\"group\" aria-label=\"Reading flow\"><button type=\"button\" class=\"flex-1\" :class=\"comicFlow !== 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('paged')\" title=\"One page or spread at a time\">Paged</button> <button type=\"button\" class=\"flex-1\" :class=\"comicFlow === 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('webtoon')\" title=\"Continuous vertical scroll (long-strip)\">Webtoon</button></div></label> <label class=\"block mb-2\">Line truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}