From 7aa6e7e6d9bcab0f0c9c5a2a778506570d895eb6 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 17 May 2026 20:39:03 +0200 Subject: [PATCH] feat: chapter chevrons, sticky app frame, and focus mode (0.21.0) Reader gets chapter-aware chevrons + a persistent app frame + distraction-free focus mode. - Single-mode chevrons (and ArrowLeft/Right + j/k) advance pages within the chapter and fall through to the adjacent chapter at the boundaries. Last page of last chapter / first page of first chapter disables the chevron and silent-no-ops on the keypress. - Continuous-mode gets a fixed bottom bar with prev/next chapter buttons; arrows + j/k jump chapters directly. - `?page=N` and `?page=last` URL query lets the prev-chapter jump land on the previous chapter's last page. - Layout header is fixed at the top; reader nav is sticky just below it; both stay visible while scrolling so reading settings are always reachable. - New "Focus" toggle in the reader nav hides the layout header, reader nav, and bottom chapter bar with smooth 220ms slide animations. Exit via Esc or a small floating Minimize2 button at the top-right (low resting opacity, full on hover). Reset on reader unmount so it doesn't leak to other pages. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- frontend/package.json | 2 +- frontend/src/lib/reader-fullscreen.svelte.ts | 33 ++ frontend/src/lib/styles/tokens.css | 7 + frontend/src/routes/+layout.svelte | 28 ++ .../manga/[id]/chapter/[n]/+page.svelte | 385 +++++++++++++++++- .../routes/manga/[id]/chapter/[n]/+page.ts | 38 +- 8 files changed, 468 insertions(+), 29 deletions(-) create mode 100644 frontend/src/lib/reader-fullscreen.svelte.ts diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 66cda5e..734a320 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1033,7 +1033,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mangalord" -version = "0.20.0" +version = "0.21.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index a6c73b8..46884cd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.20.0" +version = "0.21.0" edition = "2021" [lib] diff --git a/frontend/package.json b/frontend/package.json index b2193f6..d4417b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.20.0", + "version": "0.21.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/reader-fullscreen.svelte.ts b/frontend/src/lib/reader-fullscreen.svelte.ts new file mode 100644 index 0000000..5885c5f --- /dev/null +++ b/frontend/src/lib/reader-fullscreen.svelte.ts @@ -0,0 +1,33 @@ +/** + * Cross-component flag for the reader's "hide all chrome" view. + * + * The reader page toggles this; the root layout reads it to hide the + * top app navbar; the reader itself reads it to hide its own nav bar + * and bottom chapter bar. CSS handles the slide animations via a + * `data-reader-fullscreen` attribute on `` so the entire frame + * (layout chrome included) stays synchronised. + * + * Always reset on reader unmount — letting the flag leak across + * navigation would orphan a hidden app navbar on other pages. + */ +let active = $state(false); + +export const readerFullscreen = { + get value() { + return active; + }, + set value(v: boolean) { + active = v; + if (typeof document !== 'undefined') { + if (v) document.documentElement.dataset.readerFullscreen = 'true'; + else delete document.documentElement.dataset.readerFullscreen; + } + }, + toggle() { + this.value = !active; + }, + /** Force off — call from the reader's onDestroy. */ + reset() { + this.value = false; + } +}; diff --git a/frontend/src/lib/styles/tokens.css b/frontend/src/lib/styles/tokens.css index 41f1819..2408b45 100644 --- a/frontend/src/lib/styles/tokens.css +++ b/frontend/src/lib/styles/tokens.css @@ -60,6 +60,13 @@ --icon-md: 18px; --icon-lg: 22px; + /* App-frame heights (fixed-position bars at the top and bottom of + the viewport). Used by the layout + reader to reserve content + space and animate fullscreen mode. Recomputed once if the + header padding/font-size ever changes — keep in sync. */ + --app-header-h: 60px; + --reader-bar-h: 56px; + --z-dropdown: 10; --z-sticky: 50; --z-modal: 100; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 06966a8..296441a 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -149,6 +149,24 @@ font-size: var(--font-sm); } + /* App frame: header is fixed at the viewport top with a slide + transition so reader fullscreen (set via `data-reader-fullscreen` + on ``) can hide it without jolting the layout. `main` pays + the gap with a matching padding-top that animates in lockstep. */ + header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: var(--z-sticky); + transform: translateY(0); + transition: transform 220ms ease-out; + } + + :global(html[data-reader-fullscreen='true']) header { + transform: translateY(-100%); + } + .session { display: flex; align-items: center; @@ -186,7 +204,17 @@ main { padding: var(--space-4); + /* Reserve room for the fixed header so its presence doesn't + overlap content. The min-height is a fallback that matches + the header at typical viewport sizes (60–72px); resize + observers would be more accurate but the gap is forgiving. */ + padding-top: calc(var(--app-header-h) + var(--space-4)); max-width: 64rem; margin: 0 auto; + transition: padding-top 220ms ease-out; + } + + :global(html[data-reader-fullscreen='true']) main { + padding-top: var(--space-4); } diff --git a/frontend/src/routes/manga/[id]/chapter/[n]/+page.svelte b/frontend/src/routes/manga/[id]/chapter/[n]/+page.svelte index 42a9c18..bde11f0 100644 --- a/frontend/src/routes/manga/[id]/chapter/[n]/+page.svelte +++ b/frontend/src/routes/manga/[id]/chapter/[n]/+page.svelte @@ -1,9 +1,11 @@