nandi/gleanpublic Fork 0
5bd3cd5
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

refactor(web): improve navbarUnverified

Julien Robert committed 2026-08-03T11:24:49+02:00 Browse files
5bd3cd5 parent: ff72525
modified web/src/lib/components/NavigationProgress.svelte +58 -29
@@ -1,28 +1,68 @@
11 <script lang="ts">
22 import { navigating } from "$app/state";
33
4- // Avoid flashing on instant navigations: only show the bar after a short
5- // delay if the navigation is still in progress.
4+ // Monotonic progress bar: the fill only ever moves forward while a
5+ // navigation is active, then completes to 100% and fades out. This avoids
6+ // the back-and-forth jitter caused by restarting a looping keyframe
7+ // animation on every navigation.
8+ let progress = $state(0);
69 let visible = $state(false);
7- let timer: ReturnType<typeof setTimeout> | undefined;
10+ let fading = $state(false);
811
9- $effect(() => {
10- const active = navigating.to !== null;
11- if (active) {
12- timer = setTimeout(() => (visible = true), 100);
13- } else {
14- if (timer) clearTimeout(timer);
12+ let showTimer: ReturnType<typeof setTimeout> | undefined;
13+ let trickleTimer: ReturnType<typeof setInterval> | undefined;
14+ let completeTimer: ReturnType<typeof setTimeout> | undefined;
15+
16+ function clearTimers() {
17+ if (showTimer) clearTimeout(showTimer);
18+ if (trickleTimer) clearInterval(trickleTimer);
19+ if (completeTimer) clearTimeout(completeTimer);
20+ showTimer = trickleTimer = completeTimer = undefined;
21+ }
22+
23+ function start() {
24+ clearTimers();
25+ progress = 0;
26+ fading = false;
27+ // Avoid flashing on instant navigations: only show after a delay.
28+ showTimer = setTimeout(() => {
29+ visible = true;
30+ progress = 0.2;
31+ // Asymptotic trickle toward 0.9 so the bar never stalls at 100%
32+ // before the navigation actually completes.
33+ trickleTimer = setInterval(() => {
34+ progress = Math.min(0.9, progress + (0.9 - progress) * 0.1);
35+ }, 200);
36+ }, 100);
37+ }
38+
39+ function done() {
40+ clearTimers();
41+ if (!visible) return;
42+ progress = 1;
43+ fading = true;
44+ completeTimer = setTimeout(() => {
1545 visible = false;
16- }
46+ fading = false;
47+ }, 300);
48+ }
49+
50+ $effect(() => {
51+ if (navigating.to !== null) start();
52+ else done();
1753 });
54+
55+ $effect(() => () => clearTimers());
1856 </script>
1957
2058 {#if visible}
2159 <div
2260 class="nav-progress"
61+ class:fading
2362 role="progressbar"
2463 aria-label="Loading"
2564 aria-busy="true"
65+ style="transform: scaleX({progress})"
2666 ></div>
2767 {/if}
2868
@@ -34,32 +74,21 @@
3474 height: 3px;
3575 width: 100%;
3676 background: var(--accent);
37- z-index: 60;
38- /* Indeterminate sweep: shrink-grow across the viewport. */
3977 transform-origin: left center;
40- animation: nav-progress-indeterminate 1s ease-in-out infinite;
78+ opacity: 1;
79+ z-index: 60;
80+ transition:
81+ transform 0.2s ease,
82+ opacity 0.3s ease;
4183 }
4284
43- @keyframes nav-progress-indeterminate {
44- 0% {
45- transform: scaleX(0);
46- opacity: 0.85;
47- }
48- 50% {
49- transform: scaleX(0.7);
50- opacity: 1;
51- }
52- 100% {
53- transform: scaleX(0);
54- opacity: 0.85;
55- }
85+ .nav-progress.fading {
86+ opacity: 0;
5687 }
5788
5889 @media (prefers-reduced-motion: reduce) {
5990 .nav-progress {
60- animation: none;
61- transform: scaleX(1);
62- opacity: 1;
91+ transition: none;
6392 }
6493 }
6594 </style>
@@ -1,28 +1,68 @@
1 <script lang="ts">1 <script lang="ts">
2 import { navigating } from "$app/state";2 import { navigating } from "$app/state";
3 3
4- // Avoid flashing on instant navigations: only show the bar after a short4+ // Monotonic progress bar: the fill only ever moves forward while a
5- // delay if the navigation is still in progress.5+ // navigation is active, then completes to 100% and fades out. This avoids
6+ // the back-and-forth jitter caused by restarting a looping keyframe
7+ // animation on every navigation.
8+ let progress = $state(0);
6 let visible = $state(false);9 let visible = $state(false);
7- let timer: ReturnType<typeof setTimeout> | undefined;10+ let fading = $state(false);
8 11
9- $effect(() => {12+ let showTimer: ReturnType<typeof setTimeout> | undefined;
10- const active = navigating.to !== null;13+ let trickleTimer: ReturnType<typeof setInterval> | undefined;
11- if (active) {14+ let completeTimer: ReturnType<typeof setTimeout> | undefined;
12- timer = setTimeout(() => (visible = true), 100);15+
13- } else {16+ function clearTimers() {
14- if (timer) clearTimeout(timer);17+ if (showTimer) clearTimeout(showTimer);
18+ if (trickleTimer) clearInterval(trickleTimer);
19+ if (completeTimer) clearTimeout(completeTimer);
20+ showTimer = trickleTimer = completeTimer = undefined;
21+ }
22+
23+ function start() {
24+ clearTimers();
25+ progress = 0;
26+ fading = false;
27+ // Avoid flashing on instant navigations: only show after a delay.
28+ showTimer = setTimeout(() => {
29+ visible = true;
30+ progress = 0.2;
31+ // Asymptotic trickle toward 0.9 so the bar never stalls at 100%
32+ // before the navigation actually completes.
33+ trickleTimer = setInterval(() => {
34+ progress = Math.min(0.9, progress + (0.9 - progress) * 0.1);
35+ }, 200);
36+ }, 100);
37+ }
38+
39+ function done() {
40+ clearTimers();
41+ if (!visible) return;
42+ progress = 1;
43+ fading = true;
44+ completeTimer = setTimeout(() => {
15 visible = false;45 visible = false;
16- }46+ fading = false;
47+ }, 300);
48+ }
49+
50+ $effect(() => {
51+ if (navigating.to !== null) start();
52+ else done();
17 });53 });
54+
55+ $effect(() => () => clearTimers());
18 </script>56 </script>
19 57
20 {#if visible}58 {#if visible}
21 <div59 <div
22 class="nav-progress"60 class="nav-progress"
61+ class:fading
23 role="progressbar"62 role="progressbar"
24 aria-label="Loading"63 aria-label="Loading"
25 aria-busy="true"64 aria-busy="true"
65+ style="transform: scaleX({progress})"
26 ></div>66 ></div>
27 {/if}67 {/if}
28 68
@@ -34,32 +74,21 @@
34 height: 3px;74 height: 3px;
35 width: 100%;75 width: 100%;
36 background: var(--accent);76 background: var(--accent);
37- z-index: 60;
38- /* Indeterminate sweep: shrink-grow across the viewport. */
39 transform-origin: left center;77 transform-origin: left center;
40- animation: nav-progress-indeterminate 1s ease-in-out infinite;78+ opacity: 1;
79+ z-index: 60;
80+ transition:
81+ transform 0.2s ease,
82+ opacity 0.3s ease;
41 }83 }
42 84
43- @keyframes nav-progress-indeterminate {85+ .nav-progress.fading {
44- 0% {86+ opacity: 0;
45- transform: scaleX(0);
46- opacity: 0.85;
47- }
48- 50% {
49- transform: scaleX(0.7);
50- opacity: 1;
51- }
52- 100% {
53- transform: scaleX(0);
54- opacity: 0.85;
55- }
56 }87 }
57 88
58 @media (prefers-reduced-motion: reduce) {89 @media (prefers-reduced-motion: reduce) {
59 .nav-progress {90 .nav-progress {
60- animation: none;91+ transition: none;
61- transform: scaleX(1);
62- opacity: 1;
63 }92 }
64 }93 }
65 </style>94 </style>