65 lines
1.4 KiB
Svelte
65 lines
1.4 KiB
Svelte
<script lang="ts">
|
|||
|
|
import { onMount } from 'svelte';
|
||
|
|
|
||
|
|
let bar: HTMLDivElement;
|
||
|
|
let progress = $state(0);
|
||
|
|
|
||
|
|
onMount(() => {
|
||
|
|
let ticking = false;
|
||
|
|
|
||
|
|
function update() {
|
||
|
|
ticking = false;
|
||
|
|
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
||
|
|
progress = scrollable > 0 ? Math.min(window.scrollY / scrollable, 1) : 0;
|
||
|
|
// Écriture DOM directe : jamais d'état réactif par frame (playbook §3)
|
||
|
|
bar.style.transform = `scaleX(${progress})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function onScroll() {
|
||
|
|
if (!ticking) {
|
||
|
|
ticking = true;
|
||
|
|
requestAnimationFrame(update);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
update();
|
||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||
|
|
window.addEventListener('resize', onScroll, { passive: true });
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener('scroll', onScroll);
|
||
|
|
window.removeEventListener('resize', onScroll);
|
||
|
|
};
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div
|
||
|
|
class="scroll-progress"
|
||
|
|
role="progressbar"
|
||
|
|
aria-label="Progression de lecture"
|
||
|
|
aria-valuemin="0"
|
||
|
|
aria-valuemax="100"
|
||
|
|
aria-valuenow={Math.round(progress * 100)}
|
||
|
|
bind:this={bar}
|
||
|
|
></div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.scroll-progress {
|
||
|
|
position: fixed;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 3px;
|
||
|
|
z-index: 1001;
|
||
|
|
background: linear-gradient(to right, var(--or-oki), var(--vert-oki));
|
||
|
|
transform: scaleX(0);
|
||
|
|
transform-origin: left;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
.scroll-progress {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|