feat(reader): mobile-pattern toolbar — compact row + ⋯ overflow menu under 768px

Wrapping alone isn't how polished mobile readers work. Adopt the
standard pattern (Kindle/Apple Books/Mihon) responsively:

- >= 768px: the full fixed-layout toolbar stays (wrap still absorbs
  mid-size widths) — power users keep one-click zoom/fit/spread.
- < 768px: single-line compact row — page back, back-to-location pin,
  slider, page forward, progress, and a ⋯ overflow button. No
  wrapping, no horizontal scroll.
- ⋯ opens a glass menu anchored above the bar with LABELED rows
  (Zoom −/%/+, Fit, Page position/Recenter, Magnifier, Pointer
  Smart/Pan/Text, Double page, Contents) — labels beat mystery icons
  on touch. Pointer row hides for comics; menu scrolls if tall.
- Dismissal: Esc, outside click (⋯ button exempt so it re-toggles
  cleanly), opening any drawer or TOC closes it; hides with the
  chrome. Compact slider registered in progressSliders() so all
  three stay in sync with relocate events.
This commit is contained in:
2026-08-17 13:35:14 -04:00
parent f283903e2b
commit 6cd0fb226a
5 changed files with 189 additions and 13 deletions
+110 -1
View File
@@ -305,7 +305,9 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
</div>
</div>
<!-- Fixed-layout row (comics, PDFs, manga) -->
<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 x-show="isFixedLayout">
<!-- Full toolbar: desktop/tablet (>= 768px). Wraps gracefully in between. -->
<div class="fx-tools-full 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">
@@ -398,6 +400,113 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
</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>
<!-- Compact row (phone-sized windows): single line + ⋯ overflow.
Standard mobile-reader pattern: paging, slider, progress stay;
power tools move into a labeled menu. -->
<div class="fx-compact items-center gap-1 px-1.5 py-1.5">
<button @click="goLeft()" class="p-1.5 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 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-fx-compact"
type="range"
min="0"
max="1"
step="any"
@input="goToFraction($event.target.value)"
class="grow min-w-[3rem]"
/>
<button @click="goRight()" class="p-1.5 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 @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3rem] text-center cursor-pointer truncate shrink-0">
<span x-text="progressMain">0/0</span>
</div>
<button
data-tools-toggle
@click="toggleTools()"
class="p-1.5 rounded-lg hover:bg-gray-700"
:title="toolsOpen ? 'Close tools' : 'More tools'"
aria-label="More tools"
:aria-expanded="toolsOpen ? 'true' : 'false'"
>
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="5" cy="12" r="1.5" fill="currentColor"></circle>
<circle cx="12" cy="12" r="1.5" fill="currentColor"></circle>
<circle cx="19" cy="12" r="1.5" fill="currentColor"></circle>
</svg>
</button>
</div>
<!-- Tools overflow menu: labeled rows (mobile-reader pattern —
labels beat mystery icons). Anchored above the bottom bar. -->
<div
x-show="toolsOpen"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-2"
class="reader-tools-popover"
role="menu"
aria-label="Reader tools"
@pointerdown.stop
>
<div class="reader-tools-row">
<span>Zoom</span>
<div class="flex items-center gap-1">
<button @click="zoomOut()" class="reader-popover-btn" title="Zoom out"></button>
<button @click="resetZoom()" class="text-xs px-1 min-w-[3rem] text-center" title="Reset zoom"><span x-text="zoomPercent + '%'">100%</span></button>
<button @click="zoomIn()" class="reader-popover-btn" title="Zoom in">+</button>
</div>
</div>
<div class="reader-tools-row">
<span>Fit</span>
<select
@change="applyFitMode($event.target.value)"
class="reader-select text-xs px-1.5 py-1 rounded-lg"
aria-label="Fit mode"
>
<option value="fit-page">Fit Page</option>
<option value="fit-width">Fit Width</option>
</select>
</div>
<div class="reader-tools-row">
<span>Page position</span>
<button @click="recenterView()" class="reader-popover-btn text-xs px-2" title="Reset pan, keep zoom">Recenter</button>
</div>
<div class="reader-tools-row">
<span>Magnifier</span>
<button @click="toggleMagnifier()" class="reader-popover-btn text-xs px-2" :class="magnifierEnabled ? 'text-blue-400' : ''" x-text="magnifierEnabled ? 'On' : 'Off'">Off</button>
</div>
<div class="reader-tools-row" x-show="isPDF">
<span>Pointer</span>
<div class="reader-seg" role="group" aria-label="Pointer mode">
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')">Smart</button>
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')">Pan</button>
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')">Text</button>
</div>
</div>
<div class="reader-tools-row">
<span>Double page</span>
<button @click="toggleSpread()" class="reader-popover-btn text-xs px-2" :class="doublePageSpread ? 'text-blue-400' : ''" x-text="doublePageSpread ? 'On' : 'Off'">Off</button>
</div>
<div class="reader-tools-row">
<span>Contents</span>
<button @click="toggleTOC()" class="reader-popover-btn text-xs px-2">Open</button>
</div>
</div>
</div>
</div>
</div>
File diff suppressed because one or more lines are too long