diff --git a/app/assets/css/main.css b/app/assets/css/main.css
index 8d30d5e..86f81c8 100644
--- a/app/assets/css/main.css
+++ b/app/assets/css/main.css
@@ -12,7 +12,7 @@
--color-text-muted: 0 0% 100% / 0.6;
--header-height: 4rem;
- --player-height: 5rem;
+ --player-height: 0rem;
--sidebar-width: 280px;
--font-display: 'Syne', sans-serif;
diff --git a/app/components/book/BookPlayer.vue b/app/components/book/BookPlayer.vue
index be29123..931a254 100644
--- a/app/components/book/BookPlayer.vue
+++ b/app/components/book/BookPlayer.vue
@@ -39,12 +39,21 @@
>
+
@@ -103,9 +118,9 @@
@@ -114,8 +129,14 @@
- {{ bpContent?.reader.hints.desktop }}
- {{ bpContent?.reader.hints.mobile }}
+
+ {{ bpContent?.reader.hints.desktopScroll ?? '← → chapitres · Défilement libre · Esc fermer' }}
+ {{ bpContent?.reader.hints.mobileScroll ?? 'Défilez pour lire' }}
+
+
+ {{ bpContent?.reader.hints.desktop }}
+ {{ bpContent?.reader.hints.mobile }}
+
@@ -146,6 +167,31 @@ const colWidth = ref(500)
const showSommaire = ref(false)
const isTurning = ref(false)
+// ── Reading mode ──
+const readingMode = ref<'paginated' | 'scroll'>('paginated')
+const isScrollMode = computed(() => readingMode.value === 'scroll')
+const scrollPercent = ref(0)
+
+function toggleReadingMode() {
+ readingMode.value = readingMode.value === 'paginated' ? 'scroll' : 'paginated'
+}
+
+// When switching back to paginated, recalc pages
+watch(readingMode, async (mode) => {
+ if (mode === 'paginated') {
+ await nextTick()
+ await nextTick()
+ setTimeout(recalcPages, 100)
+ }
+})
+
+function onViewportScroll() {
+ if (!isScrollMode.value || !viewportEl.value) return
+ const el = viewportEl.value
+ const max = el.scrollHeight - el.clientHeight
+ scrollPercent.value = max > 0 ? Math.round((el.scrollTop / max) * 100) : 0
+}
+
const { init: initBookData, getSongs, getPrimarySong, getPlaylistOrder } = useBookData()
const audioPlayer = useAudioPlayer()
const playerStore = usePlayerStore()
@@ -213,13 +259,17 @@ const chapterSong = computed(() => {
})
// ── CSS columns pagination ──
-const contentStyle = computed(() => ({
- columnWidth: colWidth.value + 'px',
- columnGap: COL_GAP + 'px',
- transform: `translateX(-${currentPage.value * (colWidth.value + COL_GAP)}px)`,
-}))
+const contentStyle = computed(() => {
+ if (isScrollMode.value) return {}
+ return {
+ columnWidth: colWidth.value + 'px',
+ columnGap: COL_GAP + 'px',
+ transform: `translateX(-${currentPage.value * (colWidth.value + COL_GAP)}px)`,
+ }
+})
function recalcPages() {
+ if (isScrollMode.value) return
if (!contentEl.value || !viewportEl.value) return
colWidth.value = viewportEl.value.offsetWidth
const sw = contentEl.value.scrollWidth
@@ -256,11 +306,27 @@ function goToChapter(idx: number) {
chapterIdx.value = idx
currentPage.value = 0
showSommaire.value = false
+ // Scroll to top in scroll mode
+ if (isScrollMode.value && viewportEl.value) {
+ viewportEl.value.scrollTop = 0
+ }
// Play chapter song
const song = getPrimarySong(chapters[idx].slug)
if (song) audioPlayer.loadAndPlay(song)
}
+function nextChapter() {
+ if (chapterIdx.value < chapters.length - 1) {
+ goToChapter(chapterIdx.value + 1)
+ }
+}
+
+function prevChapter() {
+ if (chapterIdx.value > 0) {
+ goToChapter(chapterIdx.value - 1)
+ }
+}
+
function nextPage() {
if (currentPage.value < totalPages.value - 1) {
triggerTurn()
@@ -306,11 +372,20 @@ function close() {
}
function handleKeydown(e: KeyboardEvent) {
- 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()
+ if (isScrollMode.value) {
+ // Scroll mode: left/right = chapters, up/down = natural scroll (no preventDefault)
+ if (e.key === 'ArrowRight') { e.preventDefault(); nextChapter() }
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); prevChapter() }
+ else if (e.key === 'Escape') close()
+ }
+ else {
+ // Paginated mode: left/right = pages, up/down = chapters
+ if (e.key === 'ArrowRight') { e.preventDefault(); nextPage() }
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPage() }
+ else if (e.key === 'ArrowDown') { e.preventDefault(); nextChapter() }
+ else if (e.key === 'ArrowUp') { e.preventDefault(); prevChapter() }
+ else if (e.key === 'Escape') close()
+ }
}
// ── Touch / swipe ──
@@ -321,6 +396,8 @@ function onTouchStart(e: TouchEvent) {
}
function onTouchEnd(e: TouchEvent) {
+ // Disable page-swipe in scroll mode (vertical scroll is native)
+ if (isScrollMode.value) return
const diff = touchStartX - (e.changedTouches[0]?.screenX ?? 0)
if (Math.abs(diff) > 50) {
if (diff > 0) nextPage()
@@ -389,7 +466,7 @@ onUnmounted(() => {
align-items: center;
outline: none;
overflow: hidden;
- padding-bottom: 4.5rem;
+ padding-bottom: 1rem;
transition: --scene-h1 1.6s ease, --scene-h2 1.6s ease;
}
@@ -492,7 +569,7 @@ onUnmounted(() => {
/* ─── HINT ─── */
.bp-hint {
position: absolute;
- bottom: 5rem;
+ bottom: 0.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 10;
@@ -678,6 +755,16 @@ onUnmounted(() => {
max-width: none;
}
+/* ─── Scroll mode overrides ─── */
+.reader-viewport--scroll {
+ overflow-y: auto;
+}
+.reader-columns--scroll {
+ height: auto;
+ column-fill: unset;
+ transition: none;
+}
+
/* Tighten prose for column context */
.reader-columns :deep(h1) {
font-size: clamp(1.5rem, 3.5vw, 2rem);
diff --git a/app/components/layout/TheFooter.vue b/app/components/layout/TheFooter.vue
index a7a54c3..74fb45f 100644
--- a/app/components/layout/TheFooter.vue
+++ b/app/components/layout/TheFooter.vue
@@ -1,5 +1,5 @@
-