From 78f721d375bc19863918000212b7a1639b202984 Mon Sep 17 00:00:00 2001 From: Niek Nijland Date: Wed, 30 Sep 2026 18:19:12 +0200 Subject: [PATCH] Homepage: size chapter titles to the intro beside them Where a chapter's title and intro sit side by side, the title now takes the font size that sets it closest to the intro's height, so the two start and end level. The size stays between 32px and title-1's 64px, and may overshoot slightly when that lands closer than dropping a line. Stacked on phones, the title keeps the type scale's size. Co-Authored-By: Claude Opus 5.5 --- docs/src/components/home/Chapter.astro | 57 +++++++++++++++++++++++++- docs/src/lib/utils/fitFontSize.ts | 34 +++++++++++++++ docs/tests/fit-font-size.test.js | 34 +++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 docs/src/lib/utils/fitFontSize.ts create mode 100644 docs/tests/fit-font-size.test.js diff --git a/docs/src/components/home/Chapter.astro b/docs/src/components/home/Chapter.astro index eaa1bb5f..878c9708 100644 --- a/docs/src/components/home/Chapter.astro +++ b/docs/src/components/home/Chapter.astro @@ -33,18 +33,21 @@ if (aside && presentation !== "tabs") { style={`--chapter: var(--color-chapter-${color})`} > + title (src/styles/animations.css). Beside the intro, the title sizes + to the intro's height (the script below). -->

{name}

-

+

{title}

@@ -93,3 +96,53 @@ if (aside && presentation !== "tabs") { ) } + + diff --git a/docs/src/lib/utils/fitFontSize.ts b/docs/src/lib/utils/fitFontSize.ts new file mode 100644 index 00000000..2cd396f3 --- /dev/null +++ b/docs/src/lib/utils/fitFontSize.ts @@ -0,0 +1,34 @@ +// Sizes a homepage chapter title to the height of the intro beside it +// (src/components/home/Chapter.astro). Kept free of the DOM so it can be +// unit-tested with `node --test`. + +interface Bounds { + min: number; + max: number; + // How close the search gets before it stops, in px. + precision?: number; +} + +// The font size, within the bounds, whose height lands closest to `target`. +// `heightAt` lays the text out at a size and returns its height. Height grows +// with the size, jumping by a line wherever the text rewraps, so the closest +// fit sits either just below or just above the size where it first overshoots. +export function fitFontSize( + heightAt: (size: number) => number, + target: number, + { min, max, precision = 0.5 }: Bounds, +): number { + if (heightAt(max) <= target) return max; + if (heightAt(min) >= target) return min; + + let fits = min; + let overshoots = max; + while (overshoots - fits > precision) { + const size = (fits + overshoots) / 2; + if (heightAt(size) <= target) fits = size; + else overshoots = size; + } + const under = target - heightAt(fits); + const over = heightAt(overshoots) - target; + return under <= over ? fits : overshoots; +} diff --git a/docs/tests/fit-font-size.test.js b/docs/tests/fit-font-size.test.js new file mode 100644 index 00000000..b87115dc --- /dev/null +++ b/docs/tests/fit-font-size.test.js @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { fitFontSize } from "../src/lib/utils/fitFontSize.ts"; + +// A title that sets on one line up to `wrapsAt`, and on two lines above it. +const title = + (wrapsAt, leading = 1) => + (size) => + (size <= wrapsAt ? 1 : 2) * size * leading; + +const bounds = { min: 32, max: 64, precision: 0.25 }; + +test("shrinks the title until it matches the target height", () => { + const size = fitFontSize(title(40), 110, bounds); + assert.ok(Math.abs(size - 55) <= 0.25, `got ${size}`); +}); + +test("wraps onto another line when that lands closer to the target", () => { + // One line tops out at 50px tall; two lines at 51px are 102px tall. + const size = fitFontSize(title(50), 100, bounds); + assert.ok(size > 50 && size <= 50.25, `got ${size}`); +}); + +test("stays on fewer lines when rewrapping overshoots by more", () => { + // One line at 60px is 60px tall; two lines at 61px are 122px tall. + const size = fitFontSize(title(60), 70, bounds); + assert.ok(size >= 59.75 && size <= 60, `got ${size}`); +}); + +test("keeps within the bounds", () => { + assert.equal(fitFontSize(title(40), 400, bounds), 64); + assert.equal(fitFontSize(title(64), 10, bounds), 32); +});