feat(frontend): mobile reader with tap zones + settings sheet (0.59.0)
Phase 4 of the mobile redesign: the reader gets a mobile-native interaction model — invisible tap zones for prev / next / toggle, a chapter-jump bottom sheet, a reader settings sheet (mode / gap / brightness), a fixed bottom page scrubber, a brightness overlay driven by a CSS variable, and an idle-timer auto-hide for the chrome after 3s of inactivity. Desktop keyboard shortcuts and chevrons are preserved above 640px. - New TapZone primitive: invisible 3-column grid that splits the viewport into prev / toggle / next thirds (Mihon convention). Wired to the existing `prev()` / `next()` / new `toggleChrome()` so chapter-edge behavior, page preload, and read-progress tracking all keep working. Has its own vitest coverage. - matchMedia gates every mobile addition so the same DOM never carries two copies of the chapter selector or two sets of nav controls — the desktop <select>, mode toggle, and gap field stay inline above 640px; below 640px they swap to a chapter-jump button and a settings ⋯ button hosting the Sheets. - Chapter jump Sheet lists every chapter with the current one highlighted; tapping a row navigates and dismisses. - Reader settings Sheet uses the SegmentedControl primitive for mode and (continuous-only) page gap. Brightness lives next to them as a range slider 0.3..1; its value publishes `--reader-dim` as a CSS variable that drives the always-rendered fixed dim overlay (pointer-events: none so taps fall through). Brightness is client-side only in localStorage — the Preferences table doesn't carry it and Phase 4 deferred a backend migration. - Idle auto-hide: 3s timer scoped to mobile + single mode, reuses the existing focus-mode CSS for the actual slide-off. The timer resets on every index/mode/viewport/fullscreen change via a tracked $effect. - Bottom page scrubber: a styled <input type="range"> at the viewport foot, single + multi-page only, sliding off in focus mode alongside the chrome. Honors env(safe-area-inset-bottom). - Reader joins the layout's `data-mobile-full-bleed` attribute so the hero/top reader-nav can sit at viewport top under main's cleared padding-top on mobile. - Playwright spec covers tap left/right/center, the 3s auto-hide, the chapter-jump sheet, the settings sheet swapping to continuous, the brightness slider driving `--reader-dim`, and a desktop regression for the unchanged inline controls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
frontend/src/lib/components/TapZone.svelte
Normal file
70
frontend/src/lib/components/TapZone.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
onPrev,
|
||||
onNext,
|
||||
onToggle,
|
||||
testid = 'tap-zone'
|
||||
}: {
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onToggle: () => void;
|
||||
testid?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="tap-zones" data-testid={testid}>
|
||||
<button
|
||||
type="button"
|
||||
class="zone left"
|
||||
onclick={onPrev}
|
||||
aria-label="Previous page"
|
||||
data-testid="{testid}-left"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="zone center"
|
||||
onclick={onToggle}
|
||||
aria-label="Toggle controls"
|
||||
data-testid="{testid}-center"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="zone right"
|
||||
onclick={onNext}
|
||||
aria-label="Next page"
|
||||
data-testid="{testid}-right"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Invisible viewport overlay split into thirds. Mirrors the Mihon
|
||||
/ Tachiyomi convention: left-third = prev, center-third = toggle
|
||||
chrome, right-third = next. `pointer-events: none` on the wrap
|
||||
so unrelated clicks (chrome buttons, sheets) aren't trapped;
|
||||
each zone re-enables pointer events locally. */
|
||||
.tap-zones {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zone {
|
||||
pointer-events: auto;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* No visible style — these are interaction surfaces, not
|
||||
controls. Focus-visible is honored so keyboard users get
|
||||
a ring if they tab into one. */
|
||||
}
|
||||
|
||||
.zone:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user