Skip to content

Commit b8d612c

Browse files
committed
simple navigation indicator
1 parent 872d36b commit b8d612c

File tree

4 files changed

+100
-5
lines changed

4 files changed

+100
-5
lines changed

packages/gitbook/src/components/Header/Header.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getSpaceLanguage, t } from '@/intl/server';
55
import { tcls } from '@/lib/tailwind';
66
import { SearchContainer } from '../Search';
77
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
8+
import { NavigationLoader } from '../SpaceLayout/NavigationLoader';
89
import { HeaderLink } from './HeaderLink';
910
import { HeaderLinkMore } from './HeaderLinkMore';
1011
import { HeaderLinks } from './HeaderLinks';
@@ -201,6 +202,7 @@ export function Header(props: {
201202
</div>
202203
</div>
203204
) : null}
205+
<NavigationLoader />
204206
</header>
205207
);
206208
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
3+
import { tcls } from '@/lib/tailwind';
4+
import { useIsNavigating } from '../hooks';
5+
6+
export function NavigationLoader() {
7+
const isNavigating = useIsNavigating();
8+
if (!isNavigating) {
9+
return null;
10+
}
11+
return (
12+
<div
13+
className={tcls(
14+
'w-full',
15+
'h-1',
16+
'relative',
17+
'inline-block',
18+
'overflow-hidden',
19+
'bg-info'
20+
)}
21+
>
22+
<span
23+
className={tcls(
24+
'w-24',
25+
'h-1',
26+
'absolute',
27+
'bg-primary-original',
28+
'top-0',
29+
'left-0',
30+
'box-border',
31+
'rounded-md',
32+
'animate-progress-loader'
33+
)}
34+
/>
35+
</div>
36+
);
37+
}
Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use client';
2+
import { usePathname } from 'next/navigation';
23
import React from 'react';
34

45
export const HashContext = React.createContext<{
@@ -9,9 +10,17 @@ export const HashContext = React.createContext<{
910
* URL can be relative or absolute.
1011
*/
1112
updateHashFromUrl: (href: string) => void;
13+
/**
14+
* Indicates if a link has been clicked recently.
15+
* Becomes true after a click and resets to false when pathname changes.
16+
* It is debounced to avoid flickering on fast navigations.
17+
* Debounce time is 400ms (= doherty threshold for responsiveness).
18+
*/
19+
isNavigating: boolean;
1220
}>({
1321
hash: null,
1422
updateHashFromUrl: () => {},
23+
isNavigating: false,
1524
});
1625

1726
function getHash(): string | null {
@@ -21,32 +30,74 @@ function getHash(): string | null {
2130
return window.location.hash.slice(1);
2231
}
2332

24-
export const HashProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
33+
export const HashProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
2534
const [hash, setHash] = React.useState<string | null>(getHash);
35+
const [isNavigating, setIsNavigating] = React.useState(false);
36+
const timeoutRef = React.useRef<number | null>(null);
37+
const pathname = usePathname();
38+
const pathnameRef = React.useRef(pathname);
39+
40+
// Reset isNavigating when pathname changes
41+
React.useEffect(() => {
42+
if (pathnameRef.current !== pathname) {
43+
setIsNavigating(false);
44+
if (timeoutRef.current) {
45+
clearTimeout(timeoutRef.current);
46+
timeoutRef.current = null;
47+
}
48+
pathnameRef.current = pathname;
49+
}
50+
}, [pathname]);
51+
52+
// Cleanup timeout on unmount
53+
React.useEffect(() => {
54+
return () => {
55+
if (timeoutRef.current) {
56+
clearTimeout(timeoutRef.current);
57+
}
58+
};
59+
}, []);
60+
2661
const updateHashFromUrl = React.useCallback((href: string) => {
2762
const url = new URL(
2863
href,
2964
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
3065
);
3166
setHash(url.hash.slice(1));
67+
68+
if (timeoutRef.current) {
69+
clearTimeout(timeoutRef.current);
70+
}
71+
if (pathnameRef.current !== url.pathname) {
72+
timeoutRef.current = window.setTimeout(() => {
73+
setIsNavigating(true);
74+
timeoutRef.current = null;
75+
return;
76+
}, 400); // 400ms timeout - doherty threshold for responsiveness
77+
}
3278
}, []);
79+
3380
const memoizedValue = React.useMemo(
34-
() => ({ hash, updateHashFromUrl }),
35-
[hash, updateHashFromUrl]
81+
() => ({ hash, updateHashFromUrl, isNavigating }),
82+
[hash, updateHashFromUrl, isNavigating]
3683
);
3784
return <HashContext.Provider value={memoizedValue}>{children}</HashContext.Provider>;
3885
};
3986

4087
/**
41-
* Hook to get the current hash from the URL.
88+
* Hook to get the current hash from the URL and click state.
4289
* @see https://github.com/vercel/next.js/discussions/49465
4390
* We use a different hack than this one, because for same page link it don't work
4491
* We can't use the `hashChange` event because it doesn't fire for `replaceState` and `pushState` which are used by Next.js.
4592
* Since we have a single Link component that handles all links, we can use a context to share the hash.
4693
*/
4794
export function useHash() {
48-
// const params = useParams();
4995
const { hash } = React.useContext(HashContext);
5096

5197
return hash;
5298
}
99+
100+
export function useIsNavigating() {
101+
const { isNavigating: hasBeenClicked } = React.useContext(HashContext);
102+
return hasBeenClicked;
103+
}

packages/gitbook/tailwind.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ const config: Config = {
326326
exitToRight: 'exitToRight 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
327327

328328
heightIn: 'heightIn 200ms ease both',
329+
progressLoader: 'progressLoader 1.5s ease-in-out infinite alternate',
329330
},
330331
keyframes: {
331332
bounceSmall: {
@@ -495,6 +496,10 @@ const config: Config = {
495496
from: { height: '0' },
496497
to: { height: 'max-content' },
497498
},
499+
progressLoader: {
500+
from: { transform: 'translateX(-1%)', left: 0 },
501+
to: { transform: 'translateX(-99%)', left: '100%' },
502+
},
498503
},
499504
boxShadow: {
500505
thinbottom: '0px 1px 0px rgba(0, 0, 0, 0.05)',

0 commit comments

Comments
 (0)