# Plan : Navigation par slides horizontaux ## Contexte Le site source (world.game) utilise fullpage.js v4 avec un plugin scrollHorizontally propriétaire. Toutes les pages sont dans le DOM simultanément et la navigation fait glisser un conteneur avec `transform: translate3d`. On reproduit ce comportement en Svelte 5 sans dépendance externe. **Approche retenue :** - Charger d'abord la page demandée (chargement initial rapide) - Charger les autres en arrière-plan en parallèle (HTTP/2, payloads légers) - Si l'utilisateur navigue avant qu'une page soit chargée → attendre qu'elle le soit - Hack back/forward inspiré de decarb.one **Pourquoi le chargement progressif plutôt qu'un endpoint unique `/all-pages.json` ?** Un endpoint unique bloquerait le premier rendu jusqu'à ce que Kirby ait tout construit. Le chargement progressif affiche la page initiale immédiatement, les 5 autres chargent en parallèle. Avec HTTP/2 et des payloads JSON légers (6 pages), le coût est négligeable. Chaque page peut aussi être mise en cache indépendamment. --- ## Ordre des slides **Dynamique, tiré de `site->pages->listed` (Kirby).** Ne pas hardcoder l'ordre. Au chargement initial, la réponse JSON d'une page inclut `data.site.navigation` (déjà exposé par le template Kirby et stocké dans `site.svelte.js`). Ce tableau donne l'ordre et les chemins des pages listées. C'est lui qui construit le tableau `SLIDES_CONFIG` au runtime. Exemple de ce que Kirby expose déjà : ```json { "site": { "navigation": [ { "id": "home", "url": "/home", "title": "Home" }, { "id": "expertise", "url": "/expertise", "title": "Expertise" }, ... ] } } ``` Le store `slides` sera initialisé depuis ce tableau dynamiquement après le premier fetch. --- ## Architecture cible ### Conteneur slides (App.svelte) ``` [body: overflow hidden] [Header - fixed] [.slides-wrapper: display flex, width: N*100vw, transition 1000ms] [.slide × N: width 100vw, height 100vh, flex-shrink 0] [] ← rendu seulement si data chargée ``` `transform: translateX(-{activeIndex * 100}vw)` géré via style inline réactif. --- ## Fichiers à créer ### `src/state/slides.svelte.js` (nouveau) Store central de l'état des slides : ```js // Initialisé dynamiquement depuis site.navigation // slidesData: Array<{ id, path, template, data, loaded, loading }> // activeIndex: number // pendingPath: string | null export const slides = { get all(), get activeIndex(), get active(), get pendingPath(), init(siteNavigation), // construit slidesData depuis les pages listées de Kirby setData(path, data), // stocke les données + loaded=true d'une slide setLoading(path, bool), slideTo(path), // anime si chargée, sinon set pendingPath resolvePending(), // appelé quand une slide finit de charger getIndexByPath(path), } ``` ### `src/router/index.js` (réécriture quasi-complète) Remplace navaid par une logique custom : ```js // loadSlide(path) : // fetch(`${path}.json`) → slides.setData() + site/locale init (1er appel seulement) // // loadAllSlidesInBackground(exceptPath) : // slides.all.filter(s => s.path !== exceptPath).forEach(s => loadSlide(s.path)) // // slideTo(path, { skipHistory = false }) : // - slide déjà chargée → slides.slideTo() + history.pushState si !skipHistory // - slide non chargée → slides.setPending(path) (la nav se déclenche quand loaded) // // initRouter() : // 1. path initial = window.location.pathname (ou '/home' si '/') // 2. loadSlide(initialPath) : // → initialise slides.init(site.navigation) après le 1er fetch // → set active slide // 3. loadAllSlidesInBackground(exceptPath: initialPath) // 4. popstate listener : slideTo(window.location.pathname, { skipHistory: true }) // 5. Intercepter clics internes : slideTo() ``` **Hack popstate :** ```js window.addEventListener('popstate', () => { slideTo(window.location.pathname, { skipHistory: true }) }) ``` Le navigateur change déjà l'URL sur ← → on lit simplement `window.location.pathname` et on slide sans re-pousser dans l'historique. **Export : `initRouter`, `slideTo`** --- ## Fichiers à modifier ### `src/App.svelte` ```svelte
{#each slides.all as slide}
{#if slide.loaded} {/if}
{/each}
``` Footer : intégré dans le slide Blog (comme dans le source). Supprimer le Footer global. ### `src/components/layout/Header.svelte` - `import { slideTo } from '@router'` (remplace `navigateTo`) - Appels `navigateTo(path)` → `slideTo(path)` - État actif du lien menu : basé sur `slides.active.id` --- ## Fichiers inchangés - `src/views/*.svelte` — aucune modification - `src/state/site.svelte.js` - `src/state/locale.svelte.js` - `src/state/navigation.svelte.js` - `src/main.js` - `src/state/page.svelte.js` — peut être supprimé après migration (remplacé par slides store) --- ## Accessibilité (RGAA / WCAG 2.1 AA) ### Navigation clavier du système de slides Le système de slides horizontal est le point le plus délicat : par défaut, un utilisateur au clavier tabulerait dans les slides hors-écran. Il faut : - **Slides inactives** : `aria-hidden="true"` + tous les éléments focusables à `tabindex="-1"` → invisibles pour le clavier et les lecteurs d'écran - **Slide active** : `aria-hidden="false"` + tabindex restauré - **Navigation au clavier entre slides** : touches `←` `→` déclenchent `slideTo()` (à l'instar des carousels ARIA) - **Focus management** : à chaque changement de slide, déplacer le focus sur le `

` (ou premier élément focusable) de la nouvelle slide - **Lien "Aller au contenu"** (`