fix: corrections lecteur PDF + couverture + navigation chapitres
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
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;
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user