diff --git a/PLAN_NAVIGATION_SLIDES.md b/PLAN_NAVIGATION_SLIDES.md new file mode 100644 index 0000000..5a2dbd9 --- /dev/null +++ b/PLAN_NAVIGATION_SLIDES.md @@ -0,0 +1,325 @@ +# 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"** (`