2026-07-21 08:00:16 -04:00
|
|
|
<script lang="ts">
|
|
|
|
|
import '$lib/styles/oki-tokens.css';
|
|
|
|
|
import '$lib/styles/base.css';
|
|
|
|
|
|
|
|
|
|
import { afterNavigate, onNavigate } from '$app/navigation';
|
|
|
|
|
import { page } from '$app/state';
|
2026-07-21 12:42:02 -04:00
|
|
|
import { onMount } from 'svelte';
|
2026-07-21 08:00:16 -04:00
|
|
|
import Footer from '$lib/components/Footer.svelte';
|
|
|
|
|
import Nav from '$lib/components/Nav.svelte';
|
2026-07-21 12:42:02 -04:00
|
|
|
import IntroLoader from '$lib/components/motion/IntroLoader.svelte';
|
2026-07-21 08:00:16 -04:00
|
|
|
import ScrollProgressBar from '$lib/components/motion/ScrollProgressBar.svelte';
|
|
|
|
|
import { getBundle, localeFromPath } from '$lib/i18n';
|
2026-07-21 12:42:02 -04:00
|
|
|
import { initSmoothScroll, scrollToAnchor } from '$lib/motion/scroll';
|
2026-07-21 08:00:16 -04:00
|
|
|
import { prefersReducedMotion } from '$lib/motion/tokens';
|
|
|
|
|
|
|
|
|
|
let { children } = $props();
|
|
|
|
|
|
|
|
|
|
const locale = $derived(localeFromPath(page.url.pathname));
|
|
|
|
|
const t = $derived(getBundle(locale));
|
|
|
|
|
|
|
|
|
|
// PageTransition légÚre : View Transitions API, durée lue depuis les tokens.
|
|
|
|
|
// Navigateurs sans support : navigation instantanée (fallback sobre).
|
|
|
|
|
onNavigate((navigation) => {
|
|
|
|
|
if (prefersReducedMotion()) return;
|
|
|
|
|
if (!document.startViewTransition) return;
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
document.startViewTransition(async () => {
|
|
|
|
|
resolve();
|
|
|
|
|
await navigation.complete;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Focus déplacé sur le contenu aprÚs chaque transition de route (playbook §6)
|
|
|
|
|
afterNavigate(({ type, to }) => {
|
|
|
|
|
if (type === 'enter') return;
|
|
|
|
|
if (to?.url.hash) return;
|
|
|
|
|
document.getElementById('contenu')?.focus({ preventScroll: true });
|
|
|
|
|
});
|
2026-07-21 12:42:02 -04:00
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
let cleanupScroll: (() => void) | undefined;
|
|
|
|
|
let destroyed = false;
|
|
|
|
|
initSmoothScroll().then((fn) => {
|
|
|
|
|
if (destroyed) fn();
|
|
|
|
|
else cleanupScroll = fn;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ancres de la page courante : glisse Lenis (playbook P1)
|
|
|
|
|
function onClick(event: MouseEvent) {
|
|
|
|
|
const anchor = (event.target as HTMLElement).closest('a');
|
|
|
|
|
const href = anchor?.getAttribute('href');
|
|
|
|
|
if (href?.startsWith('#') && scrollToAnchor(href)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
history.replaceState(null, '', href);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('click', onClick);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
destroyed = true;
|
|
|
|
|
cleanupScroll?.();
|
|
|
|
|
document.removeEventListener('click', onClick);
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-07-21 08:00:16 -04:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<a href="#contenu" class="skip-link">{locale === 'en' ? 'Skip to content' : 'Aller au contenu'}</a>
|
2026-07-21 12:42:02 -04:00
|
|
|
<IntroLoader />
|
2026-07-21 08:00:16 -04:00
|
|
|
<ScrollProgressBar />
|
|
|
|
|
<Nav {t} {locale} />
|
|
|
|
|
<main id="contenu" tabindex="-1">
|
|
|
|
|
{@render children()}
|
|
|
|
|
</main>
|
|
|
|
|
<Footer {t} {locale} />
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
main {
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::view-transition-old(root),
|
|
|
|
|
::view-transition-new(root) {
|
|
|
|
|
animation-duration: var(--dur-mesure);
|
|
|
|
|
animation-timing-function: var(--ease-ka);
|
|
|
|
|
}
|
|
|
|
|
</style>
|