feat(reader): shortcuts help modal + desktop edge page-turn zones
Help menu (the reader had a growing shortcut/gesture vocabulary with no discoverability): a ? topbar button, the '?' key, and F1 open a glass modal listing navigation, zoom/pan, highlight, and touch gesture reference — format-aware (fixed-layout/PDF rows appear only where they apply), Esc closes it first in the dismiss chain. Desktop edge zones: clickable page-turn strips on the left/right viewport edges (8% width, 44-72px), desktop only (hover+fine-pointer media query — touch devices use tap zones, avoiding double paging). Hovering reveals a chevron arrow and a subtle edge gradient. Zones disable (pointer-events pass-through) while a fixed-layout page is zoomed so edge clicks belong to content: panning, selection, highlight editing. fxZoomed tracks zoom state via the renderer zoom event, reset/fit actions, and init.
This commit is contained in:
@@ -71,6 +71,29 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
drawers and scrim float above everything. -->
|
||||
<div id="reader-viewport" class="fixed inset-0">
|
||||
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
||||
<!-- Desktop edge page-turn zones: hover to reveal the arrow.
|
||||
Hidden on touch (tap zones cover that) and while a
|
||||
fixed-layout page is zoomed (edges belong to content then). -->
|
||||
<div
|
||||
class="reader-edge-zone left"
|
||||
:class="edgeZonesActive ? '' : 'pointer-events-none'"
|
||||
@click="goLeft()"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="reader-icon" width="28" height="28" aria-hidden="true">
|
||||
<path d="M 18 6 L 10 14 L 18 22"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="reader-edge-zone right"
|
||||
:class="edgeZonesActive ? '' : 'pointer-events-none'"
|
||||
@click="goRight()"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="reader-icon" width="28" height="28" aria-hidden="true">
|
||||
<path d="M 10 6 L 18 14 L 10 22"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ReaderChrome(metadata, progress)
|
||||
@@ -231,6 +254,59 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
>
|
||||
@ReaderSearchDrawer()
|
||||
</div>
|
||||
|
||||
<!-- Shortcuts & gestures help modal -->
|
||||
<div
|
||||
x-show="helpOpen"
|
||||
x-transition.opacity.duration.200ms
|
||||
class="reader-help-backdrop"
|
||||
@click="helpOpen = false"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div
|
||||
x-show="helpOpen"
|
||||
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"
|
||||
class="reader-help"
|
||||
role="dialog"
|
||||
aria-label="Shortcuts and gestures"
|
||||
>
|
||||
<div class="reader-help-header">
|
||||
<h3 class="font-semibold text-base">Shortcuts & Gestures</h3>
|
||||
<button @click="helpOpen = false" class="p-1.5 rounded-lg hover:bg-gray-700" title="Close (Esc)" aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="reader-help-body">
|
||||
<h4 class="reader-help-section">Navigation</h4>
|
||||
<div class="help-row"><span>Previous / next page</span><span class="help-keys"><kbd class="kbd">←</kbd><kbd class="kbd">→</kbd> or <kbd class="kbd">h</kbd><kbd class="kbd">l</kbd></span></div>
|
||||
<div class="help-row"><span>Return to previous location</span><span class="help-keys"><kbd class="kbd">Alt</kbd>+<kbd class="kbd">←</kbd></span></div>
|
||||
<div class="help-row"><span>Page edge zones <span class="help-note">(desktop)</span></span><span class="help-keys">click page edges</span></div>
|
||||
<div class="help-row" x-show="!isFixedLayout || isPDF"><span>Search in book</span><span class="help-keys"><kbd class="kbd">/</kbd></span></div>
|
||||
<div class="help-row"><span>Contents / thumbnails</span><span class="help-keys"><kbd class="kbd">t</kbd></span></div>
|
||||
<div class="help-row"><span>Annotations drawer</span><span class="help-keys">📝 button</span></div>
|
||||
<div class="help-row"><span>Settings</span><span class="help-keys"><kbd class="kbd">s</kbd></span></div>
|
||||
<div class="help-row"><span>Add bookmark</span><span class="help-keys"><kbd class="kbd">b</kbd></span></div>
|
||||
<div class="help-row"><span>Close menus / toggle chrome</span><span class="help-keys"><kbd class="kbd">Esc</kbd></span></div>
|
||||
|
||||
<h4 class="reader-help-section" x-show="isFixedLayout">Zoom & pan <span class="help-note">(comics, PDFs)</span></h4>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Zoom in / out / reset</span><span class="help-keys"><kbd class="kbd">+</kbd><kbd class="kbd">−</kbd><kbd class="kbd">0</kbd> or wheel</span></div>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Move the page</span><span class="help-keys">drag with the mouse</span></div>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Recenter <span class="help-note">(keeps zoom)</span></span><span class="help-keys">⊙ button</span></div>
|
||||
<div class="help-row" x-show="isPDF"><span>Select text in a PDF</span><span class="help-keys">Smart mode: drag over text · <kbd class="kbd">Shift</kbd>+drag anywhere · Text mode</span></div>
|
||||
|
||||
<h4 class="reader-help-section">Highlights</h4>
|
||||
<div class="help-row"><span>Create</span><span class="help-keys">select text → pick a color</span></div>
|
||||
<div class="help-row"><span>Edit / delete</span><span class="help-keys">click the highlight</span></div>
|
||||
|
||||
<h4 class="reader-help-section">Touch</h4>
|
||||
<div class="help-row"><span>Page turn</span><span class="help-keys">swipe, or tap outer edges</span></div>
|
||||
<div class="help-row"><span>Toggle chrome</span><span class="help-keys">tap center</span></div>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Zoom / move</span><span class="help-keys">pinch · two-finger drag · double-tap</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
@@ -249,6 +325,7 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<button x-show="!isFixedLayout || isPDF" @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">📝</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-11
@@ -125,7 +125,7 @@ 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, 5, "\"><!-- Reading surface: edge-to-edge. Chrome overlays translucently;\n\t\t\t drawers and scrim float above everything. --><div id=\"reader-viewport\" class=\"fixed inset-0\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><!-- Reading surface: edge-to-edge. Chrome overlays translucently;\n\t\t\t drawers and scrim float above everything. --><div id=\"reader-viewport\" class=\"fixed inset-0\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view><!-- Desktop edge page-turn zones: hover to reveal the arrow.\n\t\t\t\t Hidden on touch (tap zones cover that) and while a\n\t\t\t\t fixed-layout page is zoomed (edges belong to content then). --><div class=\"reader-edge-zone left\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goLeft()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 18 6 L 10 14 L 18 22\"></path></svg></div><div class=\"reader-edge-zone right\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goRight()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 10 6 L 18 14 L 10 22\"></path></svg></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -165,7 +165,7 @@ 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, 10, "</div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><!-- Shortcuts & gestures help modal --><div x-show=\"helpOpen\" x-transition.opacity.duration.200ms class=\"reader-help-backdrop\" @click=\"helpOpen = false\" aria-hidden=\"true\"></div><div x-show=\"helpOpen\" 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\" class=\"reader-help\" role=\"dialog\" aria-label=\"Shortcuts and gestures\"><div class=\"reader-help-header\"><h3 class=\"font-semibold text-base\">Shortcuts & Gestures</h3><button @click=\"helpOpen = false\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" title=\"Close (Esc)\" aria-label=\"Close\">✕</button></div><div class=\"reader-help-body\"><h4 class=\"reader-help-section\">Navigation</h4><div class=\"help-row\"><span>Previous / next page</span><span class=\"help-keys\"><kbd class=\"kbd\">←</kbd><kbd class=\"kbd\">→</kbd> or <kbd class=\"kbd\">h</kbd><kbd class=\"kbd\">l</kbd></span></div><div class=\"help-row\"><span>Return to previous location</span><span class=\"help-keys\"><kbd class=\"kbd\">Alt</kbd>+<kbd class=\"kbd\">←</kbd></span></div><div class=\"help-row\"><span>Page edge zones <span class=\"help-note\">(desktop)</span></span><span class=\"help-keys\">click page edges</span></div><div class=\"help-row\" x-show=\"!isFixedLayout || isPDF\"><span>Search in book</span><span class=\"help-keys\"><kbd class=\"kbd\">/</kbd></span></div><div class=\"help-row\"><span>Contents / thumbnails</span><span class=\"help-keys\"><kbd class=\"kbd\">t</kbd></span></div><div class=\"help-row\"><span>Annotations drawer</span><span class=\"help-keys\">📝 button</span></div><div class=\"help-row\"><span>Settings</span><span class=\"help-keys\"><kbd class=\"kbd\">s</kbd></span></div><div class=\"help-row\"><span>Add bookmark</span><span class=\"help-keys\"><kbd class=\"kbd\">b</kbd></span></div><div class=\"help-row\"><span>Close menus / toggle chrome</span><span class=\"help-keys\"><kbd class=\"kbd\">Esc</kbd></span></div><h4 class=\"reader-help-section\" x-show=\"isFixedLayout\">Zoom & pan <span class=\"help-note\">(comics, PDFs)</span></h4><div class=\"help-row\" x-show=\"isFixedLayout\"><span>Zoom in / out / reset</span><span class=\"help-keys\"><kbd class=\"kbd\">+</kbd><kbd class=\"kbd\">−</kbd><kbd class=\"kbd\">0</kbd> or wheel</span></div><div class=\"help-row\" x-show=\"isFixedLayout\"><span>Move the page</span><span class=\"help-keys\">drag with the mouse</span></div><div class=\"help-row\" x-show=\"isFixedLayout\"><span>Recenter <span class=\"help-note\">(keeps zoom)</span></span><span class=\"help-keys\">⊙ button</span></div><div class=\"help-row\" x-show=\"isPDF\"><span>Select text in a PDF</span><span class=\"help-keys\">Smart mode: drag over text · <kbd class=\"kbd\">Shift</kbd>+drag anywhere · Text mode</span></div><h4 class=\"reader-help-section\">Highlights</h4><div class=\"help-row\"><span>Create</span><span class=\"help-keys\">select text → pick a color</span></div><div class=\"help-row\"><span>Edit / delete</span><span class=\"help-keys\">click the highlight</span></div><h4 class=\"reader-help-section\">Touch</h4><div class=\"help-row\"><span>Page turn</span><span class=\"help-keys\">swipe, or tap outer edges</span></div><div class=\"help-row\"><span>Toggle chrome</span><span class=\"help-keys\">tap center</span></div><div class=\"help-row\" x-show=\"isFixedLayout\"><span>Zoom / move</span><span class=\"help-keys\">pinch · two-finger drag · double-tap</span></div></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -201,7 +201,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: 243, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 319, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -214,13 +214,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: 246, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 322, 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, 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\" @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\">📝</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 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)\" 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\">")
|
||||
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\" @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\">📝</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></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)\" 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -229,7 +229,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: 294, Col: 114}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 371, Col: 114}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -239,7 +239,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: 296, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 373, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -250,7 +250,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: 299, Col: 73}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 376, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -265,7 +265,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: 395, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 472, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -275,7 +275,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: 397, Col: 73}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 474, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -318,7 +318,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: 517, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 594, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -395,6 +395,9 @@ document.addEventListener("alpine:init", () => {
|
||||
chromeBehavior: "auto-hide" as string,
|
||||
hideTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
toolsOpen: false,
|
||||
helpOpen: false,
|
||||
pointerFine: true as boolean,
|
||||
fxZoomed: false as boolean,
|
||||
tapZonesEnabled: true as boolean,
|
||||
tapZoneSize: 30 as number,
|
||||
tapZoneTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
@@ -511,6 +514,9 @@ document.addEventListener("alpine:init", () => {
|
||||
);
|
||||
},
|
||||
init() {
|
||||
this.pointerFine = window
|
||||
.matchMedia("(hover: hover) and (pointer: fine)")
|
||||
.matches;
|
||||
const link = document.getElementById("reader-back");
|
||||
if (!link) return;
|
||||
const storageKey = "reader_back";
|
||||
@@ -637,6 +643,7 @@ document.addEventListener("alpine:init", () => {
|
||||
);
|
||||
this.renderer.addEventListener("zoom", () => {
|
||||
this.zoomPercent = this.renderer.zoomPercent;
|
||||
this.fxZoomed = this.renderer.zoom != null;
|
||||
});
|
||||
this.computeFixedLayoutChapterBoundaries();
|
||||
this.applyDoublePageSpread();
|
||||
@@ -880,6 +887,7 @@ document.addEventListener("alpine:init", () => {
|
||||
if (this.isPDF) {
|
||||
this.renderer.setAttribute("interaction-mode", this.interactionMode);
|
||||
}
|
||||
this.fxZoomed = this.isFixedLayout && this.renderer?.zoom != null;
|
||||
this.initTime = Date.now();
|
||||
this.fetchReadingSpeed();
|
||||
this.refreshAnnotations();
|
||||
@@ -1425,6 +1433,7 @@ document.addEventListener("alpine:init", () => {
|
||||
this.renderer.resetZoom();
|
||||
this.renderer.dragOffset = { x: 0, y: 0 };
|
||||
this.zoomPercent = 100;
|
||||
this.fxZoomed = false;
|
||||
},
|
||||
toggleMagnifier() {
|
||||
if (!this.isFixedLayout) return;
|
||||
@@ -1449,6 +1458,7 @@ document.addEventListener("alpine:init", () => {
|
||||
if (mode === "fit-width" || mode === "fit-page") {
|
||||
this.renderer.setAttribute("zoom", mode);
|
||||
this.renderer.dragOffset = { x: 0, y: 0 };
|
||||
this.fxZoomed = false;
|
||||
}
|
||||
},
|
||||
toggleSpread() {
|
||||
@@ -1512,6 +1522,16 @@ document.addEventListener("alpine:init", () => {
|
||||
toggleTools() {
|
||||
this.toolsOpen = !this.toolsOpen;
|
||||
},
|
||||
toggleHelp() {
|
||||
this.helpOpen = !this.helpOpen;
|
||||
},
|
||||
// Desktop edge page-turn zones: fine-pointer devices only, and disabled
|
||||
// while a fixed-layout page is zoomed (edge clicks then belong to the
|
||||
// content: panning, text selection, highlight editing).
|
||||
get edgeZonesActive(): boolean {
|
||||
if (!this.pointerFine) return false;
|
||||
return !(this.isFixedLayout && this.fxZoomed);
|
||||
},
|
||||
anyDrawerOpen(): boolean {
|
||||
return this.tocOpen || this.settingsOpen || this.bookmarksOpen || this.searchOpen;
|
||||
},
|
||||
@@ -2297,7 +2317,9 @@ document.addEventListener("alpine:init", () => {
|
||||
else if (k === "-" || k === "_") this.zoomOut();
|
||||
else if (k === "0") this.resetZoom();
|
||||
else if (k === "Escape") {
|
||||
if (this.selectionPopover.open) {
|
||||
if (this.helpOpen) {
|
||||
this.helpOpen = false;
|
||||
} else if (this.selectionPopover.open) {
|
||||
this.hideSelectionPopover();
|
||||
} else if (this.toolsOpen) {
|
||||
this.toolsOpen = false;
|
||||
@@ -2310,10 +2332,14 @@ document.addEventListener("alpine:init", () => {
|
||||
this.chromeVisible = !this.chromeVisible;
|
||||
if (this.hideTimer) clearTimeout(this.hideTimer);
|
||||
}
|
||||
} else if (k === "F1") {
|
||||
event.preventDefault();
|
||||
this.toggleHelp();
|
||||
} else if (!typing) {
|
||||
if (k === "t") this.toggleTOC();
|
||||
else if (k === "s") this.toggleSettings();
|
||||
else if (k === "b") this.addBookmark();
|
||||
else if (k === "?") this.toggleHelp();
|
||||
else if (k === "/") {
|
||||
event.preventDefault();
|
||||
this.toggleSearch();
|
||||
|
||||
@@ -1074,6 +1074,144 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Desktop edge page-turn zones: only on hover-capable fine-pointer
|
||||
devices (touch uses tap zones instead). Arrow + subtle edge gradient
|
||||
appear on hover. */
|
||||
.reader-edge-zone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8%;
|
||||
max-width: 72px;
|
||||
min-width: 44px;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.reader-edge-zone.left {
|
||||
left: 0;
|
||||
justify-content: flex-start;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
.reader-edge-zone.right {
|
||||
right: 0;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.reader-edge-zone::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.reader-edge-zone.left::before {
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0.08), transparent);
|
||||
}
|
||||
.reader-edge-zone.right::before {
|
||||
background: linear-gradient(to left, rgba(255, 255, 255, 0.08), transparent);
|
||||
}
|
||||
.reader-edge-zone .reader-icon {
|
||||
color: #ffffff;
|
||||
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.reader-edge-zone:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.reader-edge-zone:hover .reader-icon {
|
||||
opacity: 0.85;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.reader-edge-zone {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shortcuts help modal */
|
||||
.reader-help-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 70;
|
||||
}
|
||||
.reader-help {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(34rem, calc(100vw - 2rem));
|
||||
max-height: min(38rem, calc(100vh - 4rem));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: color-mix(in srgb, var(--bg-primary) 90%, transparent);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.3);
|
||||
backdrop-filter: blur(20px) saturate(1.3);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||
z-index: 71;
|
||||
}
|
||||
.reader-help-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.reader-help-body {
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.reader-help-section {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-secondary);
|
||||
margin: 1.1rem 0 0.4rem;
|
||||
}
|
||||
.reader-help-body .reader-help-section:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.help-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.3rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.help-row > span:first-child {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.help-keys {
|
||||
text-align: right;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.help-note {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
min-width: 1.4rem;
|
||||
text-align: center;
|
||||
padding: 0.05rem 0.4rem;
|
||||
margin: 0 0.1rem;
|
||||
border-radius: 0.3rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
border-bottom-width: 2px;
|
||||
background-color: color-mix(in srgb, currentColor 8%, transparent);
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
/* In-book search */
|
||||
.reader-search-bar {
|
||||
display: flex;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user