feat(reader): immersive chrome, slide-over drawers, tri-state PDF pointer mode

Phase 1 of the reader redesign:

- Reading surface is edge-to-edge; top/bottom bars overlay
  translucently (backdrop-blur) instead of reserving insets, killing
  the inset-coordination bug class entirely. Chrome auto-hides after
  2.5s of pointer inactivity (chrome_behavior setting finally wired:
  auto-hide / always-visible; legacy values map to auto-hide). Pointer
  activity inside page iframes keeps it awake; Esc toggles.
- TOC / Settings / Bookmarks become slide-over drawers with a scrim
  (z-50, full-height, safe-area aware), replacing the dockable-panel
  system and its window-shade headers. Only one drawer opens at a
  time; Esc or scrim click closes.
- Bottom bar is contextual: reflowable keeps nav/slider/progress/TOC;
  fixed-layout row adds Fit Page/Width select, zoom cluster,
  magnifier (now shows active state), Double Page Spread toggle, and
  a Smart | Pan | Text segmented control replacing the cryptic
  two-state icon. Smart = text-aware drag; Text = selection-only
  (manual smart-detect off); Pan = force pan. Choice persists via
  pdf_interaction_mode (new setting + foliate 29bc958 'text' mode).
- Settings drawer: Behavior (chrome, progress mode), Appearance with
  18 Kindle-style theme swatches (single source of truth from
  THEME_COLORS), Typography, Layout — each scoped by format.
- Keyboard: t/s/b open TOC/settings/bookmark, Esc closes drawers
  before toggling chrome, shortcuts skip form inputs; both slider
  rows tracked correctly (no duplicate-ID lookups).
- Topbar: Back, title, add-bookmark, bookmarks drawer, Aa settings;
  chrome follows user theme.
This commit is contained in:
2026-08-14 14:59:51 -04:00
parent ba95cc3e8b
commit 612f888683
8 changed files with 678 additions and 445 deletions
+309 -271
View File
@@ -67,59 +67,102 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
x-init={ readerInitExpr(metadata, progress, bookmarks) }
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
>
@ReaderChrome(user, metadata, progress)
<!-- Dockable Panels Container -->
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
<!-- Left Sidebar (TOC) -->
<div id="left-sidebar" class="absolute left-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
<div id="toc-panel" x-show="tocOpen" x-transition class="panel-container pointer-events-auto" data-panel="toc">
@ReaderTOCPanel(metadata)
</div>
<!-- Reading surface: edge-to-edge. Chrome overlays translucently;
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>
<!-- Right Sidebar (Settings, Bookmarks) -->
<div id="right-sidebar" class="absolute right-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
<div id="settings-panel" x-show="settingsOpen" x-transition class="panel-container pointer-events-auto" data-panel="settings">
@ReaderSettingsPanel()
</div>
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
@ReaderBookmarksPanel()
</div>
@ReaderChrome(metadata, progress)
<!-- Drawer scrim -->
<div
x-show="tocOpen || settingsOpen || bookmarksOpen"
x-transition.opacity.duration.200ms
@click="closeDrawers()"
class="drawer-scrim"
aria-hidden="true"
></div>
<!-- TOC drawer (left) -->
<div
id="toc-drawer"
x-show="tocOpen"
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 left"
role="dialog"
aria-label="Table of contents"
>
@ReaderTOCDrawer()
</div>
</div>
<div id="reader-viewport" class="absolute inset-x-0" style="top: 56px; bottom: 56px;">
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
</div>
</body>
<!-- Settings drawer (right) -->
<div
id="settings-drawer"
x-show="settingsOpen"
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="Reader settings"
>
@ReaderSettingsDrawer()
</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"
>
@ReaderBookmarksDrawer()
</div>
</body>
</html>
}
templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) {
<div id="reader-chrome" class="transition-opacity duration-300">
templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
<div id="reader-chrome" class="transition-opacity duration-300" :class="chromeVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'">
<!-- Top bar -->
<div id="reader-topbar" class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]" style="background-color: var(--bg-primary);">
<div class="flex items-center justify-between px-3 py-2 sm:px-4 sm:py-3">
<div id="reader-topbar" class="fixed top-0 left-0 right-0 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]" style="background-color: var(--bg-primary);">
<div class="flex items-center justify-between px-3 py-2 sm:px-4">
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
Back
</a>
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
<button
@click="toggleSettings()"
class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700"
title="Settings"
>
⚙️
</button>
<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 bg-opacity-95 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]" style="background-color: var(--bg-primary);">
<div class="flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1">
<!-- Left navigation -->
</div>
<!-- Bottom bar -->
<div id="reader-bottombar" class="fixed bottom-0 left-0 right-0 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]" style="background-color: var(--bg-primary);">
<!-- 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>
<!-- Progress slider -->
<input
id="progress-slider"
type="range"
@@ -131,56 +174,12 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
class="grow"
/>
<datalist id="tick-marks"></datalist>
<!-- Right navigation -->
<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>
<!-- Separator -->
<div class="w-px h-6 bg-gray-600 mx-1"></div>
<!-- Zoom controls (fixed-layout only) -->
<template x-if="isFixedLayout">
<div class="flex items-center gap-1">
<!-- Zoom out -->
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out" aria-label="Zoom out">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 5 10 L 15 10"></path>
</svg>
</button>
<!-- Zoom percentage -->
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom" aria-label="Reset zoom">
<span x-text="zoomPercent + '%'">100%</span>
</button>
<!-- Zoom in -->
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In" aria-label="Zoom in">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
</svg>
</button>
<!-- Magnifier -->
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="9" cy="9" r="5"></circle>
<path d="M 13 13 L 18 18"></path>
</svg>
</button>
<!-- Pan/Select mode (PDF only) -->
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="interactionMode === 'pan' ? 'bg-blue-600 hover:bg-blue-700' : ''" :title="interactionMode === 'pan' ? 'Pan mode: drag anywhere to move. Click for Smart select.' : 'Smart select: drag text to select, drag blank to pan (Shift = force pan). Click for Pan mode.'" :aria-label="interactionMode === 'pan' ? 'Pan mode' : 'Smart select mode'">
<!-- Smart select (default): text cursor -->
<svg x-show="interactionMode !== 'pan'" class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 3 L 10 17 M 7 5 L 10 4 L 13 5 M 7 15 L 10 16 L 13 15"></path>
</svg>
<!-- Force pan: move arrow -->
<svg x-show="interactionMode === 'pan'" class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3"></path>
</svg>
</button>
</div>
</template>
<!-- Separator -->
<div class="w-px h-6 bg-gray-600 mx-1"></div>
<!-- Progress display -->
<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 progress.FormatGroup == "reflowable" {
@@ -194,214 +193,253 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
}
</span>
</div>
<!-- Separator -->
<div class="w-px h-6 bg-gray-600 mx-1"></div>
<!-- Action buttons -->
<div class="flex items-center gap-1">
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position">🏷️</button>
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmarks">📝</button>
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
</div>
<!-- Fixed-layout row (comics, PDFs, manga) -->
<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 overflow-x-auto">
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" 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-fx"
type="range"
min="0"
max="1"
step="any"
@input="goToFraction($event.target.value)"
class="grow min-w-[4rem]"
/>
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" 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 bg-gray-600 mx-1 shrink-0"></div>
<!-- Fit mode -->
<select
@change="applyFitMode($event.target.value)"
class="text-xs px-1.5 py-1 rounded-lg bg-transparent border shrink-0"
style="border-color: var(--border);"
title="Fit mode"
aria-label="Fit mode"
>
<option value="fit-page">Fit Page</option>
<option value="fit-width">Fit Width</option>
</select>
<!-- Zoom -->
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Zoom Out (-)" aria-label="Zoom out">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 5 10 L 15 10"></path>
</svg>
</button>
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem] shrink-0" title="Reset Zoom (0)" aria-label="Reset zoom">
<span x-text="zoomPercent + '%'">100%</span>
</button>
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Zoom In (+)" aria-label="Zoom in">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
</svg>
</button>
<!-- Magnifier -->
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<circle cx="9" cy="9" r="5"></circle>
<path d="M 13 13 L 18 18"></path>
</svg>
</button>
<!-- Pointer mode (PDF only): Smart / Pan / Text -->
<div class="reader-seg shrink-0" x-show="isPDF" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
</div>
<div class="w-px h-6 bg-gray-600 mx-1 shrink-0"></div>
<!-- Double page spread -->
<button @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
</svg>
</button>
<div class="w-px h-6 bg-gray-600 mx-1 shrink-0"></div>
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden shrink-0">
<span x-text="progressMain">
if progress.FormatGroup == "reflowable" {
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
} else {
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
}
</span>
</div>
<div class="w-px h-6 bg-gray-600 mx-1 shrink-0"></div>
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Table of Contents (t)">📖</button>
</div>
</div>
</div>
}
templ ReaderSettingsPanel() {
<div
class="dockable-panel"
data-panel="settings"
data-side="left"
x-ref="settingsPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.settingsPanel)">
<h3 class="panel-title font-semibold">⚙️ Settings</h3>
<div class="panel-controls flex items-center gap-2">
<button class="window-shade-toggle"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
<!-- Display settings -->
<div class="mb-6">
<h3 class="font-semibold mb-2">Display</h3>
<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">
<option value="pages">Pages</option>
<option value="chapter">Chapter</option>
<option value="percentage">Percentage</option>
<option value="time-left">Time Left</option>
</select>
</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>
<label class="block mb-2">
<select name="reading_theme" x-model="readingTheme" @change="applyTheme()" class="w-full mt-1 px-3 py-2 rounded border">
<optgroup label="📖 Classic Reading">
<option value="light">Light</option>
<option value="paper">Paper</option>
<option value="sepia">Sepia</option>
<option value="parchment">Parchment</option>
<option value="warm">Warm</option>
<option value="candlelight">Candlelight</option>
</optgroup>
<optgroup label="🌤️ Sky & Atmosphere">
<option value="azure">Azure</option>
<option value="sky">Sky</option>
<option value="arctic">Arctic</option>
<option value="frost">Frost</option>
</optgroup>
<optgroup label="🌅 Sunset & Warmth">
<option value="dusk">Dusk</option>
<option value="sunset">Sunset</option>
<option value="twilight">Twilight</option>
</optgroup>
<optgroup label="🌲 Nature & Earth">
<option value="forest">Forest</option>
<option value="moss">Moss</option>
<option value="slate">Slate</option>
</optgroup>
<optgroup label="⚡ High Performance">
<option value="oled">OLED</option>
<option value="solarized">Solarized</option>
</optgroup>
</select>
</label>
</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">
<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>
<!-- Navigation -->
<div class="mb-6" x-show="isFixedLayout">
<h3 class="font-semibold mb-2">Navigation</h3>
<label class="flex items-center mb-2">
<input type="checkbox" name="double_page_spread" x-model="doublePageSpread" @change="applyDoublePageSpread()" class="mr-2"/>
Double Page Spread
</label>
</div>
<div class="flex gap-2 mt-6">
<button @click="restoreDefaults()" class="flex-1 py-2 border rounded hover:opacity-80" style="border-color: var(--border);">
Restore Defaults
</button>
<button @click="toggleSettings()" class="flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Done
</button>
</div>
</div>
templ drawerHeader(title string) {
<div class="reader-drawer-header">
<h3 class="font-semibold">{ title }</h3>
<button @click="closeDrawers()" class="p-1.5 rounded-lg hover:bg-gray-700" title="Close (Esc)" aria-label="Close"></button>
</div>
}
templ ReaderTOCPanel(_ ReaderMetadata) {
<div
class="dockable-panel"
data-panel="toc"
data-side="left"
x-ref="tocPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.tocPanel)">
<h3 class="panel-title font-semibold">📖 Table of Contents</h3>
<div class="panel-controls flex items-center gap-2">
<button class="window-shade-toggle"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
<nav id="toc-list" class="space-y-1">
<template x-for="item in tocItems" :key="item.href">
templ ReaderTOCDrawer() {
@drawerHeader("Contents")
<div class="reader-drawer-body">
<nav id="toc-list" class="space-y-1">
<template x-for="item in tocItems" :key="item.href">
<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>
}
templ ReaderBookmarksDrawer() {
@drawerHeader("Bookmarks")
<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="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>
</nav>
@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 ReaderBookmarksPanel() {
<div
class="dockable-panel"
data-panel="bookmarks"
data-side="right"
x-ref="bookmarksPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.bookmarksPanel)">
<h3 class="panel-title font-semibold">🔖 Bookmarks</h3>
<div class="panel-controls flex items-center gap-2">
<button class="window-shade-toggle"></button>
templ ReaderSettingsDrawer() {
@drawerHeader("Settings")
<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>
</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>
<div class="panel-content p-4 overflow-y-auto">
<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
<!-- 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 (fixed-layout only) -->
<div class="mb-6" x-show="isFixedLayout">
<h3 class="font-semibold mb-2">Layout</h3>
<label class="flex items-center mb-2">
<input type="checkbox" name="double_page_spread" x-model="doublePageSpread" @change="applyDoublePageSpread()" class="mr-2"/>
Double Page Spread
</label>
</div>
<div class="flex gap-2 mt-6">
<button @click="restoreDefaults()" class="flex-1 py-2 border rounded hover:opacity-80" style="border-color: var(--border);">
Restore Defaults
</button>
<button @click="closeDrawers()" class="flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Done
</button>
</div>
</div>