···66// DynamicLinks stays local — it uses the DID-bound service wrapper.
77export { default as DynamicLinks } from './main/DynamicLinks.svelte';
8899+// Navigation progress bar for page transitions.
1010+export { default as NavigationProgress } from './main/NavigationProgress.svelte';
1111+912// These are shared and prop-only — re-export from the package.
1013export { ThemeToggle, WolfToggle, ScrollToTop } from '@ewanc26/ui';
1114export { LinkCard, ProfileCard } from '@ewanc26/ui';
···11+<script lang="ts">
22+ import { navigating } from '$app/stores';
33+44+ let isLoading = $state(false);
55+ let progress = $state(0);
66+ let showBar = $state(false);
77+ let fadeOut = $state(false);
88+99+ $effect(() => {
1010+ if ($navigating) {
1111+ isLoading = true;
1212+ showBar = true;
1313+ fadeOut = false;
1414+ // Start at a small width, then advance
1515+ progress = 30;
1616+ requestAnimationFrame(() => {
1717+ progress = 80;
1818+ });
1919+ } else if (isLoading) {
2020+ // Navigation complete — fill to 100%
2121+ progress = 100;
2222+ isLoading = false;
2323+ // Fade out after the bar completes
2424+ fadeOut = true;
2525+ setTimeout(() => {
2626+ showBar = false;
2727+ progress = 0;
2828+ fadeOut = false;
2929+ }, 300);
3030+ }
3131+ });
3232+</script>
3333+3434+{#if showBar}
3535+ <div
3636+ class="fixed top-0 left-0 z-[60] h-0.5 bg-primary-500 transition-[width] duration-300 ease-out dark:bg-primary-400"
3737+ class:opacity-0={fadeOut}
3838+ class:transition-opacity={fadeOut}
3939+ style="width: {progress}%"
4040+ role="progressbar"
4141+ aria-label="Loading page"
4242+ ></div>
4343+{/if}
+1
src/lib/components/layout/main/index.ts
···11// DynamicLinks uses the app's DID-bound fetchLinks wrapper — keep it local.
22export { default as DynamicLinks } from './DynamicLinks.svelte';
33export { default as ScrollToTop } from './ScrollToTop.svelte';
44+export { default as NavigationProgress } from './NavigationProgress.svelte';