Feat: navigation clavier + routing ancre sur Play

- ArrowRight/Left : navigue entre les jeux
  aux limites (premier/dernier), passe à la slide prev/next
- Ancres URL (#slug) : set à chaque changement de jeu,
  restaurées au chargement, effacées quand on quitte la slide

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-10 09:03:10 +01:00
parent 90f155b679
commit fdab621b48

View file

@ -1,34 +1,57 @@
<script>
import { onMount } from 'svelte'
import { slides } from '@state/slides.svelte'
import { slideTo } from '@router'
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
let { data } = $props()
// --- Derived ---
const isActive = $derived(slides.active?.id === 'jouer')
const games = $derived(data?.games ?? [])
const isActive = $derived(slides.active?.id === 'jouer')
const games = $derived(data?.games ?? [])
const prevSlidePath = $derived(slides.all[slides.activeIndex - 1]?.path ?? null)
const nextSlidePath = $derived(slides.all[slides.activeIndex + 1]?.path ?? null)
// --- State ---
let currentIndex = $state(0) // index actif (carousel + bg)
let displayedIndex = $state(0) // index du contenu affiché
let currentIndex = $state(0)
let displayedIndex = $state(0)
let phase = $state('idle') // 'idle' | 'exiting' | 'entering'
let slideDir = $state(1) // 1 = click droite (exit gauche), -1 = click gauche (exit droite)
let slideDir = $state(1) // 1 = exit gauche, -1 = exit droite
// Bg crossfade
let prevBgColor = $state(null)
let bgFading = $state(false)
let prevBgColor = $state(null)
let bgFading = $state(false)
const displayedGame = $derived(games[displayedIndex] ?? null)
// Capture du hash synchrone avant que tout effect puisse le modifier
const initialHash = window.location.hash.slice(1)
let t1 = null
let t2 = null
let t3 = null
// --- Ancres ---
function setAnchor(index) {
const slug = games[index]?.slug
if (!slug) return
history.replaceState(null, '', '#' + slug)
}
function clearAnchor() {
history.replaceState(null, '', window.location.pathname + window.location.search)
}
// Restauration depuis l'ancre URL — une seule fois quand games est prêt
$effect(() => {
if (games.length === 0 || !initialHash) return
const idx = games.findIndex(g => g.slug === initialHash)
if (idx > 0) currentIndex = idx
})
function selectGame(i) {
if (i === currentIndex || phase !== 'idle' || !games.length) return
// slideDir = 1 → clic à droite → content sort par la gauche, entre par la droite
slideDir = i > currentIndex ? 1 : -1
phase = 'exiting'
@ -36,22 +59,39 @@
clearTimeout(t2)
clearTimeout(t3)
// Phase 2 (300ms) : swap contenu + carousel + bg simultanés
t1 = setTimeout(() => {
prevBgColor = displayedGame?.backgroundColor ?? null
displayedIndex = i
currentIndex = i
phase = 'entering'
bgFading = true
setAnchor(i)
// Fin du bg fade
t3 = setTimeout(() => { bgFading = false }, 500)
// Fin de l'entrée
t2 = setTimeout(() => { phase = 'idle' }, 350)
}, 300)
}
// --- Clavier ---
function onKeyDown(e) {
if (!isActive) return
if (e.key === 'ArrowRight') {
e.preventDefault()
if (currentIndex < games.length - 1) {
selectGame(currentIndex + 1)
} else if (nextSlidePath) {
slideTo(nextSlidePath)
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault()
if (currentIndex > 0) {
selectGame(currentIndex - 1)
} else if (prevSlidePath) {
slideTo(prevSlidePath)
}
}
}
// Réinitialisation quand on quitte la slide
$effect(() => {
if (!isActive) {
@ -62,13 +102,18 @@
displayedIndex = 0
phase = 'idle'
bgFading = false
clearAnchor()
}
})
onMount(() => () => {
clearTimeout(t1)
clearTimeout(t2)
clearTimeout(t3)
onMount(() => {
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
clearTimeout(t1)
clearTimeout(t2)
clearTimeout(t3)
}
})
</script>