fix: corrections lecteur PDF + couverture + navigation chapitres
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 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:
Yvv
2026-04-05 05:12:37 +02:00
parent f6339400fa
commit dcf64cc924
7 changed files with 56 additions and 109 deletions

View File

@@ -1,13 +1,12 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition name="pdf-overlay"> <div
<div v-if="isOpen"
v-if="isOpen" class="pdf-reader"
class="pdf-reader" @keydown.escape="close"
@keydown.escape="close" tabindex="0"
tabindex="0" ref="overlayRef"
ref="overlayRef" >
>
<!-- Top bar --> <!-- Top bar -->
<div class="pdf-bar"> <div class="pdf-bar">
<div class="pdf-bar-title"> <div class="pdf-bar-title">
@@ -26,10 +25,11 @@
:src="pdfUrl" :src="pdfUrl"
class="pdf-frame" class="pdf-frame"
:title="bpContent?.pdf.iframeTitle" :title="bpContent?.pdf.iframeTitle"
ref="iframeRef"
@load="iframeRef?.focus()"
/> />
</div> </div>
</div> </div>
</Transition>
</Teleport> </Teleport>
</template> </template>
@@ -42,6 +42,7 @@ const bookData = useBookData()
await bookData.init() await bookData.init()
const overlayRef = ref<HTMLElement>() const overlayRef = ref<HTMLElement>()
const iframeRef = ref<HTMLIFrameElement>()
const isOpen = computed({ const isOpen = computed({
get: () => props.modelValue, get: () => props.modelValue,
@@ -138,15 +139,4 @@ onUnmounted(() => {
background: white; 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> </style>

View File

@@ -72,17 +72,25 @@
</div> </div>
<!-- Secondary row --> <!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary"> <div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<component <template v-for="action in secondaryActions(item.actions)" :key="action.label">
:is="action.to ? resolveComponent('NuxtLink') : 'button'" <NuxtLink
v-for="action in secondaryActions(item.actions)" v-if="action.to"
:key="action.label" :to="action.to"
:to="action.to" class="axis-action-btn axis-action-btn--secondary"
class="axis-action-btn axis-action-btn--secondary" @click.stop
@click.stop="!action.to && handleAction(action.id)" >
> <div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" /> {{ action.label }}
{{ action.label }} </NuxtLink>
</component> <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> </div>
</div> </div>

View File

@@ -5,13 +5,12 @@
<!-- Book cover --> <!-- Book cover -->
<UiScrollReveal> <UiScrollReveal>
<div class="book-cover-wrapper relative"> <div class="book-cover-wrapper relative">
<div :class="['book-cover-3d', compact && 'book-cover-3d--compact']"> <div
<img :class="['book-cover-3d', compact && 'book-cover-3d--compact']"
:src="content?.book.coverImage" :style="{ backgroundImage: `url(${content?.book.coverImage})` }"
:alt="content?.book.coverAlt" role="img"
class="book-cover-img" :aria-label="content?.book.coverAlt"
/> />
</div>
</div> </div>
</UiScrollReveal> </UiScrollReveal>
@@ -88,7 +87,12 @@ const titleLine2 = computed(() => titleParts.value[1])
} }
.book-cover-3d { .book-cover-3d {
width: 100%;
max-width: 360px;
aspect-ratio: 3 / 4; aspect-ratio: 3 / 4;
background-size: 200% auto;
background-position: right center;
background-repeat: no-repeat;
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
border: 1px solid hsl(var(--color-text) / 0.1); 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); 0 0 0 1px hsl(var(--color-text) / 0.08);
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1), transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
box-shadow 0.5s ease; box-shadow 0.5s ease;
max-width: 360px;
} }
.book-cover-3d--compact { .book-cover-3d--compact {
@@ -111,12 +114,6 @@ const titleLine2 = computed(() => titleParts.value[1])
0 0 0 1px hsl(var(--color-primary) / 0.2); 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 { .book-heading {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);

View File

@@ -43,6 +43,11 @@ definePageMeta({
const route = useRoute() const route = useRoute()
const slug = route.params.slug as string 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 // Initialize guided mode
useGuidedMode() useGuidedMode()

9
app/router.options.ts Normal file
View 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.

View File

@@ -158,24 +158,6 @@ html, body {
visibility: hidden; 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 */ /* Navigation bar */
.nav-bar { .nav-bar {
display: flex; 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 spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
let pageCanvasCache = new Map(); let pageCanvasCache = new Map();
let outlinePageMap = []; // [{item, pageNum}] for highlighting let outlinePageMap = []; // [{item, pageNum}] for highlighting
let isAnimating = false;
function buildSpreads(numPages) { function buildSpreads(numPages) {
spreads = []; spreads = [];
// Page 1 = couverture seule // Page 1 = couverture seule
@@ -347,33 +327,14 @@ async function renderPageCanvas(pageNum) {
return canvas; return canvas;
} }
async function showSpread(index, animate = true) { async function showSpread(index) {
if (index < 0 || index >= spreads.length) return; 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; currentSpread = index;
const pages = spreads[index]; const pages = spreads[index];
const slotLeft = document.getElementById('slotLeft'); const slotLeft = document.getElementById('slotLeft');
const slotRight = document.getElementById('slotRight'); 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 = ''; slotLeft.innerHTML = '';
slotRight.innerHTML = ''; slotRight.innerHTML = '';
slotLeft.className = 'page-slot'; slotLeft.className = 'page-slot';
@@ -381,40 +342,17 @@ async function showSpread(index, animate = true) {
if (pages.length === 1) { if (pages.length === 1) {
const canvas = await renderPageCanvas(pages[0]); const canvas = await renderPageCanvas(pages[0]);
if (shouldAnimate) {
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
canvas.classList.add('entering');
}
slotLeft.appendChild(canvas); slotLeft.appendChild(canvas);
slotRight.className = 'page-slot empty'; 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 { } else {
const [left, right] = await Promise.all([ const [left, right] = await Promise.all([
renderPageCanvas(pages[0]), renderPageCanvas(pages[0]),
renderPageCanvas(pages[1]), 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); slotLeft.appendChild(left);
slotRight.appendChild(right); 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(); updateNav();
highlightOutline(); highlightOutline();
} }