bugfix: reader-nav is fully fixed; no settle-on-scroll (0.21.3)
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1033,7 +1033,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.21.2"
|
version = "0.21.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.21.2"
|
version = "0.21.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.21.2",
|
"version": "0.21.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -61,10 +61,12 @@
|
|||||||
--icon-lg: 22px;
|
--icon-lg: 22px;
|
||||||
|
|
||||||
/* App-frame heights (fixed-position bars at the top and bottom of
|
/* App-frame heights (fixed-position bars at the top and bottom of
|
||||||
the viewport). Used by the layout + reader to reserve content
|
the viewport). These are first-paint fallbacks — the real
|
||||||
space and animate fullscreen mode. Recomputed once if the
|
values are written by ResizeObservers on the actual elements
|
||||||
header padding/font-size ever changes — keep in sync. */
|
in +layout.svelte and the reader, so they reflect rendered
|
||||||
|
size and survive font / zoom / wrap changes. */
|
||||||
--app-header-h: 60px;
|
--app-header-h: 60px;
|
||||||
|
--reader-nav-h: 56px;
|
||||||
--reader-bar-h: 56px;
|
--reader-bar-h: 56px;
|
||||||
|
|
||||||
--z-dropdown: 10;
|
--z-dropdown: 10;
|
||||||
|
|||||||
@@ -226,9 +226,9 @@
|
|||||||
main {
|
main {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
/* Reserve room for the fixed header so its presence doesn't
|
/* Reserve room for the fixed header so its presence doesn't
|
||||||
overlap content. The min-height is a fallback that matches
|
overlap content. The header height comes from a runtime
|
||||||
the header at typical viewport sizes (60–72px); resize
|
ResizeObserver (see onMount above) so this always tracks
|
||||||
observers would be more accurate but the gap is forgiving. */
|
the rendered size. */
|
||||||
padding-top: calc(var(--app-header-h) + var(--space-4));
|
padding-top: calc(var(--app-header-h) + var(--space-4));
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -236,6 +236,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(html[data-reader-fullscreen='true']) main {
|
:global(html[data-reader-fullscreen='true']) main {
|
||||||
padding-top: var(--space-4);
|
/* No top reservation in focus mode — the chapter image runs
|
||||||
|
edge-to-edge once the header has slid off. */
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -69,6 +69,31 @@
|
|||||||
let index = $state(initialIndex);
|
let index = $state(initialIndex);
|
||||||
let continuousPageEls: HTMLImageElement[] = $state([]);
|
let continuousPageEls: HTMLImageElement[] = $state([]);
|
||||||
let chapterBarEl: HTMLElement | undefined = $state();
|
let chapterBarEl: HTMLElement | undefined = $state();
|
||||||
|
let readerNavEl: HTMLElement | undefined = $state();
|
||||||
|
|
||||||
|
// Publish the reader nav's actual measured height. Sticky
|
||||||
|
// positioning had a "settle on scroll" effect: the bar's natural
|
||||||
|
// position sat 16px below the app header (main's space-4 padding),
|
||||||
|
// and only docked against the header once the user scrolled
|
||||||
|
// enough to consume that gap. The fix is to lift the bar out of
|
||||||
|
// document flow entirely (position: fixed) and reserve space in
|
||||||
|
// the chapter content via `--reader-nav-h`.
|
||||||
|
$effect(() => {
|
||||||
|
if (!readerNavEl) return;
|
||||||
|
const publish = () => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--reader-nav-h',
|
||||||
|
`${readerNavEl!.offsetHeight}px`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
publish();
|
||||||
|
const ro = new ResizeObserver(publish);
|
||||||
|
ro.observe(readerNavEl);
|
||||||
|
return () => {
|
||||||
|
ro.disconnect();
|
||||||
|
document.documentElement.style.removeProperty('--reader-nav-h');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Publish the bottom chapter-bar's actual measured height so the
|
// Publish the bottom chapter-bar's actual measured height so the
|
||||||
// continuous container's `padding-bottom` exactly matches it. Same
|
// continuous container's `padding-bottom` exactly matches it. Same
|
||||||
@@ -389,7 +414,7 @@
|
|||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<nav class="reader-nav" aria-label="reader">
|
<nav class="reader-nav" aria-label="reader" bind:this={readerNavEl}>
|
||||||
<a href="/manga/{manga.id}" class="back" data-testid="back-to-manga">
|
<a href="/manga/{manga.id}" class="back" data-testid="back-to-manga">
|
||||||
<ArrowLeft size={18} aria-hidden="true" />
|
<ArrowLeft size={18} aria-hidden="true" />
|
||||||
{#if manga.cover_image_path}
|
{#if manga.cover_image_path}
|
||||||
@@ -605,45 +630,36 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Sticky directly under the fixed layout header. `top: var(--app-header-h)`
|
/* Pinned to the viewport directly below the (also fixed) layout
|
||||||
pins the bar's top edge to the bottom edge of the layout
|
header. `position: fixed` rather than `sticky` because the
|
||||||
header so it never slides under it. Focus mode slides it up
|
latter would have a "settle on scroll" period until its natural
|
||||||
off-screen via a transform AND clips its height to 0 so the
|
position consumes main's padding-top. Chapter content reserves
|
||||||
chapter pages get the full top of the viewport. */
|
room for this bar via `--reader-nav-h` (measured at runtime by
|
||||||
|
a ResizeObserver in onMount). Focus mode slides it up
|
||||||
|
off-screen via transform — the fixed origin keeps the slide
|
||||||
|
distance equal to the bar's height regardless of scroll. */
|
||||||
.reader-nav {
|
.reader-nav {
|
||||||
position: sticky;
|
position: fixed;
|
||||||
top: var(--app-header-h);
|
top: var(--app-header-h);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: var(--space-2);
|
padding: var(--space-2) var(--space-4);
|
||||||
padding-bottom: var(--space-2);
|
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
margin: 0 calc(-1 * var(--space-4)) var(--space-3);
|
|
||||||
padding-left: var(--space-4);
|
|
||||||
padding-right: var(--space-4);
|
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
transition:
|
transition:
|
||||||
transform 220ms ease-out,
|
transform 220ms ease-out,
|
||||||
max-height 220ms ease-out,
|
opacity 220ms ease-out;
|
||||||
opacity 220ms ease-out,
|
|
||||||
padding 220ms ease-out,
|
|
||||||
margin 220ms ease-out;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html[data-reader-fullscreen='true']) .reader-nav {
|
:global(html[data-reader-fullscreen='true']) .reader-nav {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
max-height: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
border-bottom-color: transparent;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,17 +779,37 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reserve room for the fixed reader-nav above so the first page
|
||||||
|
image (single mode) and the top of the chapter stack
|
||||||
|
(continuous mode) aren't hidden behind it. The variable is
|
||||||
|
written by the ResizeObserver in onMount so the reservation
|
||||||
|
always matches actual rendered height. Focus mode collapses
|
||||||
|
the reservation in lockstep with the bar's slide-out. */
|
||||||
.page-wrap {
|
.page-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-top: var(--reader-nav-h);
|
||||||
|
transition: padding-top 220ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.continuous {
|
.continuous {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-top: var(--reader-nav-h);
|
||||||
|
transition:
|
||||||
|
padding-top 220ms ease-out,
|
||||||
|
padding-bottom 220ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-reader-fullscreen='true']) .page-wrap,
|
||||||
|
:global(html[data-reader-fullscreen='true']) .continuous {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
:global(html[data-reader-fullscreen='true']) .continuous {
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-image {
|
.page-image {
|
||||||
|
|||||||
Reference in New Issue
Block a user