forked from EHV/librodrome
fix: corrections lecteur PDF + couverture + navigation chapitres
- PDF viewer : suppression animation/lock isAnimating, navigation stable - PDF reader : focus iframe au chargement → flèches actives immédiatement - BookSection : couverture via background-image (right center) — fiable - AxisBlock : boutons secondaires NuxtLink/button explicites (v-if/v-else) - modele-eco/[slug] : scroll top au changement de chapitre (SPA reuse) - router.options.ts : scrollBehavior global top/instant - PDF mis à jour (numéros de pages chapitres 7–11) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="pdf-overlay">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="pdf-reader"
|
||||
@@ -26,10 +25,11 @@
|
||||
:src="pdfUrl"
|
||||
class="pdf-frame"
|
||||
:title="bpContent?.pdf.iframeTitle"
|
||||
ref="iframeRef"
|
||||
@load="iframeRef?.focus()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +42,7 @@ const bookData = useBookData()
|
||||
await bookData.init()
|
||||
|
||||
const overlayRef = ref<HTMLElement>()
|
||||
const iframeRef = ref<HTMLIFrameElement>()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -138,15 +139,4 @@ onUnmounted(() => {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Overlay transitions */
|
||||
.pdf-overlay-enter-active {
|
||||
animation: pdf-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.pdf-overlay-leave-active {
|
||||
animation: pdf-enter 0.3s cubic-bezier(0.7, 0, 0.84, 0) reverse both;
|
||||
}
|
||||
@keyframes pdf-enter {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,17 +72,25 @@
|
||||
</div>
|
||||
<!-- Secondary row -->
|
||||
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
|
||||
<component
|
||||
:is="action.to ? resolveComponent('NuxtLink') : 'button'"
|
||||
v-for="action in secondaryActions(item.actions)"
|
||||
:key="action.label"
|
||||
<template v-for="action in secondaryActions(item.actions)" :key="action.label">
|
||||
<NuxtLink
|
||||
v-if="action.to"
|
||||
:to="action.to"
|
||||
class="axis-action-btn axis-action-btn--secondary"
|
||||
@click.stop="!action.to && handleAction(action.id)"
|
||||
@click.stop
|
||||
>
|
||||
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</component>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
class="axis-action-btn axis-action-btn--secondary"
|
||||
@click.stop="handleAction(action.id)"
|
||||
>
|
||||
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
<!-- Book cover -->
|
||||
<UiScrollReveal>
|
||||
<div class="book-cover-wrapper relative">
|
||||
<div :class="['book-cover-3d', compact && 'book-cover-3d--compact']">
|
||||
<img
|
||||
:src="content?.book.coverImage"
|
||||
:alt="content?.book.coverAlt"
|
||||
class="book-cover-img"
|
||||
<div
|
||||
:class="['book-cover-3d', compact && 'book-cover-3d--compact']"
|
||||
:style="{ backgroundImage: `url(${content?.book.coverImage})` }"
|
||||
role="img"
|
||||
:aria-label="content?.book.coverAlt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UiScrollReveal>
|
||||
|
||||
<!-- Content + CTAs -->
|
||||
@@ -88,7 +87,12 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
}
|
||||
|
||||
.book-cover-3d {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
aspect-ratio: 3 / 4;
|
||||
background-size: 200% auto;
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--color-text) / 0.1);
|
||||
@@ -97,7 +101,6 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
0 0 0 1px hsl(var(--color-text) / 0.08);
|
||||
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||
box-shadow 0.5s ease;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.book-cover-3d--compact {
|
||||
@@ -111,12 +114,6 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
0 0 0 1px hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
|
||||
.book-cover-img {
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.book-heading {
|
||||
font-size: clamp(1.625rem, 4vw, 2.125rem);
|
||||
|
||||
@@ -43,6 +43,11 @@ definePageMeta({
|
||||
const route = useRoute()
|
||||
const slug = route.params.slug as string
|
||||
|
||||
// Scroll to top when navigating between chapters (component reused by Vue Router)
|
||||
watch(() => slug, () => {
|
||||
if (import.meta.client) window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
})
|
||||
|
||||
// Initialize guided mode
|
||||
useGuidedMode()
|
||||
|
||||
|
||||
9
app/router.options.ts
Normal file
9
app/router.options.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
|
||||
export default <RouterConfig>{
|
||||
scrollBehavior(to, _from, savedPosition) {
|
||||
if (savedPosition) return savedPosition
|
||||
if (to.hash) return { el: to.hash, behavior: 'smooth' }
|
||||
return { top: 0, behavior: 'instant' }
|
||||
},
|
||||
}
|
||||
Binary file not shown.
@@ -158,24 +158,6 @@ html, body {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Page turn transition */
|
||||
.page-slot canvas {
|
||||
transition: opacity 1s ease-out, transform 1s ease-out;
|
||||
}
|
||||
|
||||
.page-slot canvas.entering {
|
||||
opacity: 0;
|
||||
transform: translateX(var(--enter-dir, 20px));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.page-slot canvas.leaving {
|
||||
opacity: 0;
|
||||
transform: translateX(var(--leave-dir, -20px));
|
||||
position: absolute;
|
||||
transition: opacity 1.2s ease-in, transform 1.2s ease-in;
|
||||
}
|
||||
|
||||
/* Navigation bar */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
@@ -300,8 +282,6 @@ let currentSpread = 0; // index into spreads[]
|
||||
let spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
|
||||
let pageCanvasCache = new Map();
|
||||
let outlinePageMap = []; // [{item, pageNum}] for highlighting
|
||||
let isAnimating = false;
|
||||
|
||||
function buildSpreads(numPages) {
|
||||
spreads = [];
|
||||
// Page 1 = couverture seule
|
||||
@@ -347,33 +327,14 @@ async function renderPageCanvas(pageNum) {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function showSpread(index, animate = true) {
|
||||
async function showSpread(index) {
|
||||
if (index < 0 || index >= spreads.length) return;
|
||||
if (isAnimating) return;
|
||||
|
||||
const prevIndex = currentSpread;
|
||||
const direction = index > prevIndex ? 1 : -1; // 1 = forward, -1 = back
|
||||
currentSpread = index;
|
||||
|
||||
const pages = spreads[index];
|
||||
const slotLeft = document.getElementById('slotLeft');
|
||||
const slotRight = document.getElementById('slotRight');
|
||||
|
||||
// Collect old canvases for fade-out
|
||||
const oldCanvases = [...slotLeft.querySelectorAll('canvas'), ...slotRight.querySelectorAll('canvas')];
|
||||
const shouldAnimate = animate && oldCanvases.length > 0;
|
||||
|
||||
if (shouldAnimate) {
|
||||
isAnimating = true;
|
||||
// Fade out old canvases with directional slide
|
||||
oldCanvases.forEach(c => {
|
||||
c.style.setProperty('--leave-dir', `${-direction * 20}px`);
|
||||
c.classList.add('leaving');
|
||||
});
|
||||
// Wait for fade-out to mostly complete
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
slotLeft.innerHTML = '';
|
||||
slotRight.innerHTML = '';
|
||||
slotLeft.className = 'page-slot';
|
||||
@@ -381,39 +342,16 @@ async function showSpread(index, animate = true) {
|
||||
|
||||
if (pages.length === 1) {
|
||||
const canvas = await renderPageCanvas(pages[0]);
|
||||
if (shouldAnimate) {
|
||||
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
canvas.classList.add('entering');
|
||||
}
|
||||
slotLeft.appendChild(canvas);
|
||||
slotRight.className = 'page-slot empty';
|
||||
if (shouldAnimate) {
|
||||
// Double rAF ensures the browser has painted the initial state before transitioning
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => canvas.classList.remove('entering')));
|
||||
}
|
||||
} else {
|
||||
const [left, right] = await Promise.all([
|
||||
renderPageCanvas(pages[0]),
|
||||
renderPageCanvas(pages[1]),
|
||||
]);
|
||||
if (shouldAnimate) {
|
||||
left.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
right.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
left.classList.add('entering');
|
||||
right.classList.add('entering');
|
||||
}
|
||||
slotLeft.appendChild(left);
|
||||
slotRight.appendChild(right);
|
||||
if (shouldAnimate) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
left.classList.remove('entering');
|
||||
right.classList.remove('entering');
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAnimate) setTimeout(() => { isAnimating = false; }, 1100);
|
||||
else isAnimating = false;
|
||||
|
||||
updateNav();
|
||||
highlightOutline();
|
||||
|
||||
Reference in New Issue
Block a user