Skip to content

Commit a6daa8a

Browse files
committed
fix: let the homepage declare its flat header instead of keying it on the pathname
1 parent 947905c commit a6daa8a

2 files changed

Lines changed: 45 additions & 16 deletions

File tree

apps/web/app/(site)/NavbarFrame.tsx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
11
"use client";
22

33
import { classNames } from "@cap/utils/helpers";
4-
import { usePathname } from "next/navigation";
5-
import { type ReactNode, useEffect, useState } from "react";
4+
import {
5+
type ReactNode,
6+
useCallback,
7+
useEffect,
8+
useLayoutEffect,
9+
useRef,
10+
useState,
11+
useSyncExternalStore,
12+
} from "react";
613

7-
const FLAT_HEADER_ROUTES = new Set(["/", "/home"]);
814
const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
915
const SLIDE_MS = 560;
1016
const DOCK_AT = 80;
1117

1218
type Phase = "docked" | "entering" | "floating" | "leaving";
1319

20+
const readSentinel = () => document.querySelector("[data-header-sentinel]");
21+
const noSentinel = () => null;
22+
23+
// The page declares the flat treatment with `data-header-flat` on its root, a
24+
// sibling of this header, and `:has(~ ...)` applies it in the prerendered HTML.
25+
// The island is driven by the page's `data-header-sentinel`, re-read whenever
26+
// the sibling page changes. Neither depends on `usePathname()`: Next prerenders
27+
// `/` with pathname `/index`, so a route-keyed header rendered the fixed bar on
28+
// the server and React kept those attributes after hydration.
1429
export const NavbarFrame = ({ children }: { children: ReactNode }) => {
15-
const pathname = usePathname();
16-
const flat = FLAT_HEADER_ROUTES.has(pathname);
30+
const headerRef = useRef<HTMLElement>(null);
1731
const [phase, setPhase] = useState<Phase>("docked");
1832

19-
useEffect(() => {
20-
if (!FLAT_HEADER_ROUTES.has(pathname)) return;
21-
const sentinel = document.querySelector("[data-header-sentinel]");
22-
if (!sentinel) return;
33+
const subscribeToPage = useCallback((onChange: () => void) => {
34+
const slot = headerRef.current?.parentElement;
35+
if (!slot) return () => {};
36+
const observer = new MutationObserver(onChange);
37+
observer.observe(slot, { childList: true });
38+
return () => observer.disconnect();
39+
}, []);
40+
const sentinel = useSyncExternalStore(
41+
subscribeToPage,
42+
readSentinel,
43+
noSentinel,
44+
);
45+
46+
useLayoutEffect(() => {
47+
if (!sentinel) {
48+
setPhase("docked");
49+
return;
50+
}
2351
const io = new IntersectionObserver(([entry]) => {
2452
const past = entry
2553
? !entry.isIntersecting && entry.boundingClientRect.top < 0
@@ -37,7 +65,7 @@ export const NavbarFrame = ({ children }: { children: ReactNode }) => {
3765
});
3866
io.observe(sentinel);
3967
return () => io.disconnect();
40-
}, [pathname]);
68+
}, [sentinel]);
4169

4270
useEffect(() => {
4371
if (phase === "entering") {
@@ -64,23 +92,23 @@ export const NavbarFrame = ({ children }: { children: ReactNode }) => {
6492
}
6593
}, [phase]);
6694

67-
const island = flat && phase !== "docked";
95+
const island = sentinel !== null && phase !== "docked";
6896
const shown = phase === "floating";
6997

7098
return (
7199
<header
72-
data-flat={flat ? "true" : "false"}
100+
ref={headerRef}
73101
data-island={island ? "true" : "false"}
74102
className={classNames(
75-
"group pointer-events-none inset-x-0 top-0 z-[51]",
76-
flat && !island ? "absolute" : "fixed",
103+
"group pointer-events-none inset-x-0 top-0 z-[51] fixed",
104+
!island && "has-[~[data-header-flat]]:absolute",
77105
)}
78106
>
79107
<div
80108
className={classNames(
81109
"pointer-events-auto mx-auto",
82-
!flat && "max-w-none border-b border-zinc-200/70 bg-white",
83-
flat && !island && "bg-transparent",
110+
!island &&
111+
"border-b border-zinc-200/70 bg-white group-has-[~[data-header-flat]]:border-b-0 group-has-[~[data-header-flat]]:bg-transparent",
84112
island &&
85113
"mt-3 max-w-[calc(100%-24px)] rounded-[18px] bg-white/90 shadow-[0_0_0_1px_rgba(17,17,17,0.06)] backdrop-blur-xl lg:mt-4 lg:max-w-[min(1200px,calc(100%-32px))]",
86114
)}

apps/web/components/pages/HomeTwo/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function HomeTwoPage() {
2323
// over them instead.
2424
return (
2525
<div
26+
data-header-flat
2627
className={`${htSans.className} ${htSans.variable} ${htSerif.variable} ${htMono.variable} text-[#111111]`}
2728
style={grainBg(SHELL)}
2829
>

0 commit comments

Comments
 (0)