Ouverture directe du BookPlayer en lecture, corrections éditoriales

- Suppression des phases intro (livre 3D) et cover (page intermédiaire)
  du BookPlayer : le reader s'ouvre directement depuis la home
- Corrections textuelles : about.md, app.config.ts, app.vue
- Mise à jour de GrateWizard app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-21 03:54:32 +01:00
parent bf4b9e02f1
commit 524c7a0fc2
45 changed files with 335 additions and 237 deletions

View File

@@ -28,40 +28,8 @@
<div class="i-lucide-x h-5 w-5" />
</button>
<!-- PHASE TRANSITIONS -->
<Transition name="phase" mode="out-in">
<!-- INTRO: 3D spinning book -->
<div v-if="phase === 'intro'" key="intro" class="bp-phase bp-intro">
<div class="spin-scene">
<div class="spin-book" @animationend="onSpinEnd">
<div class="spin-face spin-front">
<img src="/images/book-cover-spread.jpg" alt="Couverture" />
</div>
<div class="spin-face spin-back">
<img src="/images/book-cover-spread.jpg" alt="" />
</div>
</div>
</div>
</div>
<!-- COVER: title + CTA -->
<div v-else-if="phase === 'cover'" key="cover" class="bp-phase bp-cover">
<div class="cover-frame">
<img src="/images/book-cover-spread.jpg" :alt="bpContent?.cover.coverAlt ?? 'Couverture'" class="cover-img" />
</div>
<h1 class="cover-title text-gradient">{{ bpContent?.cover.title }}</h1>
<p class="cover-sub">{{ bpContent?.cover.subtitle }}</p>
<p class="cover-desc">
{{ bpContent?.cover.description }}
</p>
<button class="cover-cta" @click="startReading">
{{ bpContent?.cover.cta }}
<div class="i-lucide-arrow-right ml-2 h-5 w-5 inline-block align-middle" />
</button>
</div>
<!-- READING: paginated book reader -->
<div v-else key="reading" class="bp-phase bp-reader">
<!-- READER -->
<div class="bp-phase bp-reader">
<!-- Top bar -->
<div class="reader-bar">
<button
@@ -143,17 +111,11 @@
</button>
</div>
</div>
</Transition>
<!-- Hint -->
<p class="bp-hint">
<template v-if="phase === 'reading'">
<span class="hidden md:inline">{{ bpContent?.reader.hints.desktop }}</span>
<span class="md:hidden">{{ bpContent?.reader.hints.mobile }}</span>
</template>
<template v-else>
<span class="hidden md:inline">{{ bpContent?.reader.hints.default }}</span>
</template>
<span class="hidden md:inline">{{ bpContent?.reader.hints.desktop }}</span>
<span class="md:hidden">{{ bpContent?.reader.hints.mobile }}</span>
</p>
</div>
</Transition>
@@ -177,8 +139,6 @@ const overlayRef = ref<HTMLElement>()
const viewportEl = ref<HTMLElement>()
const contentEl = ref<HTMLElement>()
// ── Phase state ──
const phase = ref<'intro' | 'cover' | 'reading'>('intro')
const chapterIdx = ref(0)
const currentPage = ref(0)
const totalPages = ref(1)
@@ -243,13 +203,12 @@ const chapterHues: [number, number][] = [
]
const sceneVars = computed(() => {
const idx = phase.value === 'reading' ? chapterIdx.value + 1 : 0
const idx = chapterIdx.value + 1
const [h1, h2] = chapterHues[idx] ?? chapterHues[0]
return { '--scene-h1': h1, '--scene-h2': h2 } as Record<string, number>
})
const chapterSong = computed(() => {
if (phase.value !== 'reading') return null
return getPrimarySong(chapters[chapterIdx.value].slug)
})
@@ -278,16 +237,10 @@ watch(activeChapter, async () => {
setTimeout(recalcPages, 100)
})
// ── Phase transitions ──
function onSpinEnd() {
phase.value = 'cover'
}
async function startReading() {
async function initReading() {
await loadContent()
chapterIdx.value = 0
currentPage.value = 0
phase.value = 'reading'
await nextTick()
await nextTick()
// Set up ResizeObserver
@@ -353,16 +306,11 @@ function close() {
}
function handleKeydown(e: KeyboardEvent) {
if (phase.value === 'reading') {
if (e.key === 'ArrowRight') { e.preventDefault(); nextPage() }
else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPage() }
else if (e.key === 'ArrowDown') { e.preventDefault(); if (chapterIdx.value < chapters.length - 1) goToChapter(chapterIdx.value + 1) }
else if (e.key === 'ArrowUp') { e.preventDefault(); if (chapterIdx.value > 0) goToChapter(chapterIdx.value - 1) }
else if (e.key === 'Escape') close()
}
else if (e.key === 'Escape') {
close()
}
if (e.key === 'ArrowRight') { e.preventDefault(); nextPage() }
else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPage() }
else if (e.key === 'ArrowDown') { e.preventDefault(); if (chapterIdx.value < chapters.length - 1) goToChapter(chapterIdx.value + 1) }
else if (e.key === 'ArrowUp') { e.preventDefault(); if (chapterIdx.value > 0) goToChapter(chapterIdx.value - 1) }
else if (e.key === 'Escape') close()
}
// ── Touch / swipe ──
@@ -383,9 +331,6 @@ function onTouchEnd(e: TouchEvent) {
// ── Lifecycle ──
watch(isOpen, async (open) => {
if (open) {
phase.value = 'intro'
chapterIdx.value = 0
currentPage.value = 0
showSommaire.value = false
contentLoaded.value = false
await initBookData()
@@ -398,6 +343,8 @@ watch(isOpen, async (open) => {
if (playlist.length) playerStore.setPlaylist(playlist)
const first = getSongs().find(s => s.id === 'chanson-01')
if (first) audioPlayer.loadAndPlay(first)
// Start reading directly
await initReading()
}
else {
overlayRef.value?.removeEventListener('touchstart', onTouchStart)
@@ -567,135 +514,6 @@ onUnmounted(() => {
flex: 1;
}
/* Phase transitions */
.phase-enter-active { animation: phase-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
.phase-leave-active { animation: phase-out 0.3s cubic-bezier(0.7, 0, 0.84, 0) both; }
@keyframes phase-in {
from { opacity: 0; transform: scale(0.97); filter: blur(4px); }
to { opacity: 1; transform: scale(1); filter: blur(0); }
}
@keyframes phase-out {
from { opacity: 1; transform: scale(1); filter: blur(0); }
to { opacity: 0; transform: scale(0.97); filter: blur(4px); }
}
/* ═══════════════════════════════════════
INTRO: 3D SPINNING BOOK
═══════════════════════════════════════ */
.bp-intro {
justify-content: center;
}
.spin-scene {
perspective: 1200px;
}
.spin-book {
position: relative;
width: min(220px, 45vw);
aspect-ratio: 3 / 4;
transform-style: preserve-3d;
animation: book-spin 2.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.spin-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid hsl(20 8% 18%);
box-shadow: 0 20px 60px hsl(0 0% 0% / 0.5);
}
.spin-front img {
width: 200%;
height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
.spin-back {
transform: rotateY(180deg);
}
.spin-back img {
width: 200%;
height: 100%;
object-fit: cover;
}
@keyframes book-spin {
0% { transform: rotateY(0deg) scale(0.65); opacity: 0; }
8% { opacity: 1; }
45% { transform: rotateY(180deg) scale(0.9); }
75% { transform: rotateY(320deg) scale(1); }
90% { transform: rotateY(352deg) scale(1); }
100% { transform: rotateY(360deg) scale(1); }
}
/* ═══════════════════════════════════════
COVER
═══════════════════════════════════════ */
.bp-cover {
justify-content: center;
text-align: center;
}
.cover-frame {
width: min(200px, 42vw);
aspect-ratio: 3 / 4;
border-radius: 0.625rem;
overflow: hidden;
border: 1px solid hsl(20 8% 18%);
box-shadow:
0 25px 60px hsl(0 0% 0% / 0.5),
0 0 40px hsl(var(--scene-h1) 60% 40% / 0.1);
margin-bottom: 2rem;
animation: cover-float 7s ease-in-out infinite;
}
.cover-img {
width: 200%; height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
@keyframes cover-float {
0%, 100% { transform: translateY(0) rotate(-0.5deg); }
50% { transform: translateY(-10px) rotate(0.5deg); }
}
.cover-title {
font-family: var(--font-display, 'Syne', sans-serif);
font-size: clamp(1.75rem, 5vw, 2.75rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 0.25rem;
}
.cover-sub {
font-family: var(--font-display, 'Syne', sans-serif);
font-size: clamp(1rem, 3vw, 1.4rem);
color: hsl(20 8% 55%);
margin-bottom: 1.5rem;
}
.cover-desc {
font-size: 0.9rem;
color: hsl(20 8% 45%);
max-width: 26rem;
line-height: 1.65;
margin-bottom: 2rem;
}
.cover-cta {
display: inline-flex;
align-items: center;
padding: 0.75rem 2rem;
border-radius: 9999px;
background: hsl(var(--scene-h1) 70% 45%);
color: white;
font-weight: 600;
font-size: 0.95rem;
border: none;
cursor: pointer;
transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 0 24px hsl(var(--scene-h1) 70% 45% / 0.3);
}
.cover-cta:hover {
background: hsl(var(--scene-h1) 70% 52%);
box-shadow: 0 0 36px hsl(var(--scene-h1) 70% 50% / 0.45);
transform: translateY(-2px);
}
/* ═══════════════════════════════════════
READER
═══════════════════════════════════════ */