Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions packages/website/src/components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,6 @@ function LogoWordmark({
const lOpacity = useTransform(scrollY, [40, 120], [1, 0]);
const lWidth = useTransform(scrollY, [40, 120], [14, 0]);

/* The "." in the icon fades out as it merges with the wordmark. Width
collapses over the same range as opacity, since the dot is an inline
sibling of "L" inside a `justify-content: center` box, so fading it to
`opacity: 0` alone left its layout width in place, which kept "L" off
centre (reading as if the square clipped its left side once rotated). */
const dotOpacity = useTransform(scrollY, [60, 130], [1, 0]);
const dotWidth = useTransform(scrollY, [60, 130], ["0.4em", "0em"]);

return (
/* No clip on this box. The square's rotated bounding box and its hard
offset shadow both grow past the flex item's edges, so an `overflow-hidden`
Expand All @@ -254,9 +246,6 @@ function LogoWordmark({
backgroundColor: LOGO_ORIGINAL_ORANGE,
}}
>
{/* The dot stays in flow, as it does on the business site. Taking it
out with `position: absolute` left the mark centring the bare "L",
pushing the visible "L." off-centre inside the square. */}
<m.span
className="leading-none text-background"
style={{
Expand All @@ -266,21 +255,6 @@ function LogoWordmark({
}}
>
L
{/* `inline-block` defaults to `vertical-align: baseline`, which
aligns the dot's OWN baseline (its bottom edge, per spec, once
`overflow-hidden` is set) to L's text baseline - not to L's own
visible bottom. Measured at every font size this icon uses
(13-20px): that left the dot 4-6px above L's bottom edge.
`text-bottom` aligns to the bottom of the parent's font instead,
which lands exactly on L's bottom at every size (0px gap,
measured), with no magic-number offset to keep in sync with the
font. */}
<m.span
className="inline-block overflow-hidden align-text-bottom"
style={{ opacity: dotOpacity, width: dotWidth }}
>
.
</m.span>
</m.span>
</m.div>

Expand Down
76 changes: 19 additions & 57 deletions packages/website/tests/e2e/site-header-logo-mark.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
import { test, expect, type Page } from "@playwright/test";

/**
* Header logo mark contract: the scroll-driven "L." icon must keep "." sitting
* on L's own bottom edge (not merely overlapping its line), and "L" centred in
* the square, at every point of its scroll transition (0-160px), not just at
* rest and at the end.
* Header logo mark contract: the scroll-driven icon must keep "L" centred in
* the square at every point of its scroll transition (0-160px), not just at
* rest and at the end, and the square must actually rotate.
*
* Two regressions this guards, both about the "." span, both invisible to a
* plain screenshot at a glance:
*
* 1. (#37) The dot faded via `opacity` alone. Opacity does not remove layout
* width, so the invisible dot kept reserving space inside the square's
* `justify-content: center` box for the whole 60-130px fade range, pushing
* "L" left of true centre the entire time - on a rotated square this reads
* as a clipped corner rather than off-centre text.
* 2. The dot's `inline-block` defaulted to `vertical-align: baseline`, which
* aligns the dot's OWN baseline - its bottom edge, per spec, once
* `overflow-hidden` is set - to L's TEXT baseline, not to L's own visible
* bottom. Measured 4-6px of gap depending on the icon's current font size
* (13-20px across the scroll range): the dot visibly floated above L's
* bottom rather than resting on it. `align-text-bottom` closes this to 0px
* at every size, measured directly, not eyeballed.
*
* A same-line check that only asks "do these two elements' vertical bands
* overlap at all" passes even with the 4-6px gap from #2 - overlap is a much
* weaker property than "sits on the same baseline" and is why that bug shipped
* once already. `sitsOnBaseline` below asserts the tight version instead.
* The mark previously paired "L" with a "." that faded and collapsed on
* scroll. That pairing went through two rounds of alignment bugs - the dot's
* `opacity`-only fade left stale layout width that pushed "L" off centre
* (#37), and even after that fix the dot's `vertical-align: baseline`
* default put it 4-6px above L's own bottom edge instead of resting on it.
* The dot was dropped entirely rather than chase a third alignment fix: "L"
* alone, centred, has no second element to misalign against.
*/

const HOME_LINK = 'nav a[href="/"]';
Expand All @@ -36,37 +22,21 @@ async function markGeometry(page: Page) {
const square = document.querySelector(sel);
if (!square) throw new Error(`icon mark not found: ${sel}`);
const inner = square.querySelector("span");
const dot = inner?.querySelector("span");
if (!inner || !dot) throw new Error("L/dot spans not found inside mark");
if (!inner) throw new Error("L span not found inside mark");

const range = document.createRange();
range.selectNodeContents(inner.childNodes[0]);
const lRect = range.getBoundingClientRect();
const dotRect = dot.getBoundingClientRect();
const squareRect = square.getBoundingClientRect();
const dotStyle = getComputedStyle(dot);

return {
squareCenterX: squareRect.x + squareRect.width / 2,
squareTransform: getComputedStyle(square).transform,
lTop: lRect.top,
lBottom: lRect.bottom,
lCenterX: lRect.x + lRect.width / 2,
dotTop: dotRect.top,
dotBottom: dotRect.bottom,
dotOpacity: Number(dotStyle.opacity),
};
}, ICON_MARK);
}

// Deliberately NOT "do the two vertical bands overlap at all" - a 4-6px gap
// between the dot's bottom and L's bottom (the #2 regression above) still
// overlaps, since the dot's own height is well over 6px. This checks the
// specific thing that matters: the dot's bottom edge sits where L's does.
function sitsOnBaseline(g: Awaited<ReturnType<typeof markGeometry>>) {
return Math.abs(g.dotBottom - g.lBottom) < 1.5;
}

// The document sets `scroll-behavior: smooth`, so the two-argument
// `window.scrollTo(x, y)` form (which always defers to that CSS property)
// animates over several hundred ms instead of jumping. `behavior: "instant"`
Expand Down Expand Up @@ -94,8 +64,8 @@ test.describe("primary navigation logo mark", () => {
// project's WebKit run: neither a programmatic `scrollTo` + a manually
// dispatched `scroll` event, nor a real `mouse.wheel` gesture on a desktop
// (non-mobile) WebKit context, ever produced a change in this mark's
// rendered `transform`/`opacity` in Playwright's bundled WebKit, headless or
// headed - while `window.scrollY` itself updated correctly every time. That
// rendered `transform` in Playwright's bundled WebKit, headless or headed -
// while `window.scrollY` itself updated correctly every time. That
// implicates framer-motion's `useScroll()` reporting to Playwright's WebKit
// specifically, not this component: every other scroll-driven nav property
// (icon size, border, wordmark tracking) rides the same MotionValue and
Expand All @@ -109,7 +79,7 @@ test.describe("primary navigation logo mark", () => {
);
});

test("L and the dot stay on the same line and centred through the whole scroll transition", async ({
test("L stays centred in the square through the whole scroll transition", async ({
page,
}) => {
await page.goto("/");
Expand All @@ -120,21 +90,14 @@ test.describe("primary navigation logo mark", () => {
for (const scrollY of [0, 60, 95, 130, 200]) {
settled = await scrollToAndSettle(page, scrollY);

// The dot's bottom edge must sit on L's bottom edge (within 1.5px), not
// float above it - a looser "these two elements' lines merely overlap"
// check would miss the 4-6px baseline gap this guards.
expect(
sitsOnBaseline(settled),
`at scrollY=${scrollY}, dot bottom ${settled.dotBottom.toFixed(1)} should sit on L's bottom ${settled.lBottom.toFixed(1)} (within 1.5px)`,
).toBe(true);
Math.abs(settled.lCenterX - settled.squareCenterX),
`at scrollY=${scrollY}, L (centre ${settled.lCenterX.toFixed(1)}) should sit on the square's centre (${settled.squareCenterX.toFixed(1)})`,
).toBeLessThan(3);
}
if (settled === null) throw new Error("unreachable: loop always assigns");

// At full scroll, the dot has faded and collapsed; "L" must sit on the
// square's true horizontal centre, not left of it, and the square must
// actually be rotating rather than stuck at rest.
expect(settled.dotOpacity).toBeLessThan(0.05);
expect(Math.abs(settled.lCenterX - settled.squareCenterX)).toBeLessThan(3);
// The square must actually be rotating at full scroll, not stuck at rest.
expect(settled.squareTransform).not.toBe("none");
});

Expand All @@ -147,8 +110,7 @@ test.describe("primary navigation logo mark", () => {
await scrollToAndSettle(page, 200);
const g = await scrollToAndSettle(page, 0);

expect(g.dotOpacity).toBeCloseTo(1, 1);
expect(g.squareTransform).toBe("none");
expect(sitsOnBaseline(g)).toBe(true);
expect(Math.abs(g.lCenterX - g.squareCenterX)).toBeLessThan(3);
});
});
Loading