commit 1b203b6b3ee04b0961b544581fd7d10fb99d25b1
parent aaf834d18bd67f3d47715dd49a8b0e66dd31561d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 29 Sep 2026 00:29:45 -0400
export, common: the wordmark's reservation is a minimum width, so text that renders wider than the display face's metrics widens its box instead of clipping (re-review N2)
The header sets wordmarkWidthEm as `min-width`, not `width`. The fit is
decided by the wider of the reservation and the rendered text: the same in
the fallback face and in Archivo wherever Archivo renders within the
reservation, and never a clipped last letter where it renders wider.
e2e: header.spec forces the text 8 % wider (letter-spacing) and sweeps 519 to
280 px: wherever the name shows, it does not overflow its box or the clip box.
With a fixed width it is clipped from 519 to 344 px.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 49 insertions(+), 11 deletions(-)
diff --git a/common/lib/wordmarkWidth.ts b/common/lib/wordmarkWidth.ts
@@ -11,11 +11,12 @@ import { WORDMARK_METRICS } from "./wordmarkMetrics";
// The lead is weight 720 and the suffix 380 (common/components/Wordmark.tsx),
// both at wdth 118; each character is its advance from the vendored font
// (lib/wordmarkMetrics.ts, generated) plus `letterSpacingEm`, kerning left
-// out, and MARGIN more: the web font Google serves renders 0.2–1.7 % wider
-// than the vendored file's advances sum to (measured in Chromium on six
-// titles), and the reserved box must never be narrower than the text. A
-// character the table does not hold counts as UNMAPPED_EM, wider than any it
-// does.
+// out, and MARGIN more: the web font Google serves renders 0.2–2.4 % wider
+// than the vendored file's advances sum to (measured in Chromium). The header
+// sets this as the wordmark's MIN-width, not its width: where a platform
+// renders the text wider still, the box grows with it (and the text drops a
+// little sooner) rather than clipping its last letter. A character the table
+// does not hold counts as UNMAPPED_EM, wider than any it does.
const UNMAPPED_EM = 1.25;
const MARGIN = 1.03;
diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx
@@ -55,11 +55,13 @@ import MobileMenu from "./MobileMenu";
// threshold is written down: the text sits in a one-line box (`h-7`,
// `overflow-hidden`, `flex-wrap`) behind a zero-width strut, and a flex item
// that does not fit beside the strut wraps to the box's second line, which is
-// clipped. The text keeps a WIDTH RESERVED from the display face's own
-// metrics (lib/wordmarkWidth.ts, in em, so it scales with the reader's text
-// size): the decision is the same in the fallback face, which is narrower,
-// and after Archivo loads — a title never shows and then vanishes. The text
-// stays in the DOM, so the link keeps its name.
+// clipped. The text keeps a MINIMUM WIDTH reserved from the display face's
+// own metrics (lib/wordmarkWidth.ts, in em, so it scales with the reader's
+// text size): the decision is the same in the fallback face, which is
+// narrower, and after Archivo loads — a title never shows and then vanishes.
+// A minimum, not a width: where the text renders wider than the reservation,
+// its box grows with it instead of clipping the last letter. The text stays
+// in the DOM, so the link keeps its name.
// Below `lg` the brand link takes the bar's free space (basis 0, grow 1) and
// can shrink to the mark alone; only then does the group give way, and its
// social row scrolls in SocialScroll's box, the last resort, its END shown
@@ -111,7 +113,7 @@ export default function Header() {
title={site.headerTitle}
lead={site.wordmarkLead}
className="ml-2.5 shrink-0 whitespace-nowrap text-[1.35rem] leading-none tracking-[-0.01em]"
- style={{ width: `${wordmarkWidthEm(site.headerTitle, site.wordmarkLead, -0.01)}em` }}
+ style={{ minWidth: `${wordmarkWidthEm(site.headerTitle, site.wordmarkLead, -0.01)}em` }}
/>
</span>
</Link>
diff --git a/export/e2e/header.spec.ts b/export/e2e/header.spec.ts
@@ -245,6 +245,41 @@ test("the title shows at the same widths in the fallback face and in Archivo", a
}
});
+// The reservation is a MINIMUM width: where the text renders wider than the
+// display face's metrics say (here forced wider by letter-spacing), its box
+// grows with it and the text drops sooner; its last letter is never clipped.
+test("a wordmark that renders wider than its reservation widens its box and is never clipped", async ({ page }) => {
+ await installRoutes(page);
+ writeSite({ headerTitle: "Rekietalyzer", wordmarkLead: "Rekieta", socialLinks: lastMarked(SIX.slice(0, 4)) });
+ await open(page, 519);
+ await page.addStyleTag({ content: "header [data-wordmark] { letter-spacing: 0.08em !important; }" });
+ await page.evaluate(() => document.fonts.ready);
+ let shown = 0;
+ let hidden = 0;
+ for (let w = 519; w >= 280; w--) {
+ await page.setViewportSize({ width: w, height: 800 });
+ const r = await page.evaluate(() => {
+ const box = document.querySelector("header [data-header-brand] span.flex-wrap") as HTMLElement;
+ const wm = box.lastElementChild as HTMLElement;
+ return {
+ shown: wm.offsetTop < box.clientHeight - 1,
+ textRight: (wm.lastElementChild as HTMLElement).getBoundingClientRect().right,
+ boxRight: box.getBoundingClientRect().right,
+ over: wm.scrollWidth - wm.clientWidth,
+ };
+ });
+ if (!r.shown) {
+ hidden++;
+ continue;
+ }
+ shown++;
+ expect(r.over, `${w} px: the text overflows its box`).toBeLessThanOrEqual(1);
+ expect(r.textRight, `${w} px: the last letter is clipped`).toBeLessThanOrEqual(r.boxRight + 0.5);
+ }
+ expect(shown, "widths with the name").toBeGreaterThan(0);
+ expect(hidden, "widths without it").toBeGreaterThan(0);
+});
+
// THE RULING: on a narrow screen the header keeps the NAME and shows only the
// marked link(s), in the accessibility tree too; focus goes brand → the marked
// link → the toggle → the menu. Every real site title shows in full at 360 px