Center and scale the catalog to fill mobile fullscreen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-19 18:54:34 +03:30
co-authored by Cursor
parent 06bcba125f
commit 16cd3197ef
4 changed files with 98 additions and 5 deletions
+2
View File
@@ -255,6 +255,7 @@ watch(
:index="index"
:single-page="singlePage"
:flip-ms="flipMs"
:immersive="immersive"
@update:index="index = $event"
@fallback="fallbackSlide = true"
/>
@@ -265,6 +266,7 @@ watch(
:mode="visualEffect === 'fade' ? 'fade' : visualEffect === 'card' ? 'card' : 'slide'"
:zoom="zoom"
:flip-ms="flipMs"
:immersive="immersive"
@next="onUserNav(next)"
@prev="onUserNav(prev)"
/>
@@ -7,6 +7,7 @@ const props = defineProps<{
mode: "slide" | "fade" | "card";
zoom: boolean;
flipMs: number;
immersive?: boolean;
}>();
const emit = defineEmits<{
@@ -54,7 +55,7 @@ function onPointerUp(ev: PointerEvent) {
<template>
<div
class="fx-stage"
:class="[mode, { 'zoom-in': zoom }]"
:class="[mode, { 'zoom-in': zoom, fill: immersive }]"
:style="{ '--flip-ms': `${flipMs}ms` }"
@pointerdown.prevent="onPointerDown"
@pointerup.prevent="onPointerUp"
@@ -81,6 +82,17 @@ function onPointerUp(ev: PointerEvent) {
overflow: visible;
}
.fx-stage.fill {
width: 100%;
height: 100%;
max-height: none;
}
.fx-stage.fill .page-img {
max-width: min(100vw, 100%);
max-height: 100dvh;
}
.fx-stage.card {
perspective: 2600px;
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, watch } from "vue";
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { PageFlip } from "page-flip";
const props = defineProps<{
@@ -7,6 +7,7 @@ const props = defineProps<{
index: number;
singlePage: boolean;
flipMs: number;
immersive?: boolean;
}>();
const emit = defineEmits<{
@@ -17,10 +18,43 @@ const emit = defineEmits<{
const host = ref<HTMLElement | null>(null);
const outer = ref<HTMLElement | null>(null);
const mountKey = ref(0);
const fitScale = ref(1);
let book: PageFlip | null = null;
let ignore = false;
let prepared: string[] = [];
let makeGen = 0;
let fitTimer = 0;
function updateFitScale() {
if (!props.immersive || !host.value || !outer.value) {
fitScale.value = 1;
return;
}
const w = host.value.offsetWidth;
const h = host.value.offsetHeight;
const maxW = outer.value.clientWidth;
const maxH = outer.value.clientHeight;
if (w < 8 || h < 8 || maxW < 8 || maxH < 8) {
fitScale.value = 1;
return;
}
const pad = 16;
const s = Math.min((maxW - pad) / w, (maxH - pad) / h);
if (!Number.isFinite(s) || s <= 0) {
fitScale.value = 1;
return;
}
fitScale.value = Math.min(s, 8);
}
function scheduleFit() {
window.clearTimeout(fitTimer);
fitTimer = window.setTimeout(() => {
void nextTick().then(() => {
requestAnimationFrame(updateFitScale);
});
}, 40);
}
function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
@@ -153,6 +187,7 @@ async function makeBook() {
});
});
for (const url of previous) URL.revokeObjectURL(url);
scheduleFit();
} catch {
emit("fallback");
}
@@ -178,7 +213,20 @@ watch(
},
);
watch(
() => props.immersive,
() => {
scheduleFit();
},
);
onMounted(() => {
window.addEventListener("resize", scheduleFit);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", scheduleFit);
window.clearTimeout(fitTimer);
try {
book?.destroy();
} catch {
@@ -220,7 +268,7 @@ function onPointerUp(ev: PointerEvent) {
</script>
<template>
<div class="magazine-frame" dir="ltr">
<div class="magazine-frame" dir="ltr" :class="{ fill: immersive }">
<div class="magazine-glow"></div>
<div
ref="outer"
@@ -228,7 +276,12 @@ function onPointerUp(ev: PointerEvent) {
@pointerdown.prevent="onPointerDown"
@pointerup.prevent="onPointerUp"
>
<div :key="mountKey" ref="host" class="magazine-host"></div>
<div
:key="mountKey"
ref="host"
class="magazine-host"
:style="{ transform: `scaleX(-1) scale(${fitScale})` }"
></div>
</div>
</div>
</template>
@@ -256,11 +309,21 @@ function onPointerUp(ev: PointerEvent) {
justify-content: center;
direction: ltr;
}
.magazine-frame.fill {
width: 100%;
max-width: none;
height: 100%;
}
.magazine-frame.fill .magazine-outer {
width: 100%;
height: 100%;
}
.magazine-host {
width: max-content;
height: max-content;
max-width: 100%;
max-width: none;
transform: scaleX(-1);
transform-origin: center center;
filter: drop-shadow(0 28px 50px rgba(0, 0, 0, 0.55));
}
</style>
+16
View File
@@ -415,9 +415,25 @@ a {
display: none !important;
}
.stage-page.immersive {
height: 100dvh;
overflow: hidden;
}
.stage-page.immersive .viewer-wrap {
padding: 0 !important;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
height: 100dvh;
}
.stage-page.immersive .magazine-frame,
.stage-page.immersive .fx-stage {
width: 100%;
height: 100%;
}
.tap-exit {