diff --git a/plan/daily-challenge.md b/plan/daily-challenge.md index 567dfac..168d70a 100644 --- a/plan/daily-challenge.md +++ b/plan/daily-challenge.md @@ -245,6 +245,16 @@ of those is the real design question rather than the plumbing: match. They are numbers now, `UnitV2::facing_applied` turns one back into the letter the card wants, and a letter reads as no facing at all — which the existing "the two sides face each other" test then fails on. +- [x] **The card leaves room for the ground beside the text.** The tile's + track was a fixed 8rem and the readable column got what was left - about + 200px on the front page, narrower than the launch control, which does + not wrap - so the text ran on under the tile. The media query meant to + catch it asked about the window, which was 1440px while the column that + had run out was 344px. The text side of the card has a floor under it + now, at the column plus the gap plus the tile, and the board gives up + the difference; the battle values, the launch control and the eyebrow + came down to what they need, and the type sizes off its own column + rather than the window, so the same card reads at three widths. - [x] **A scheduled check**, `scripts/daily.sh`. Not a rollover: which fight is today's is computed from the schedule by both readers, so nothing has to run at midnight. What a job is for is noticing the schedule is running diff --git a/web/scripts/daily-terrain.test.mjs b/web/scripts/daily-terrain.test.mjs new file mode 100644 index 0000000..1e2aff1 --- /dev/null +++ b/web/scripts/daily-terrain.test.mjs @@ -0,0 +1,149 @@ +/** + * The daily card's ground belongs to the right of its text, and the card has + * to leave room for it there. + * + * `.daily-text` holds the readable column and the terrain tile side by side, + * and the tile is a fixed 8rem. The readable column got whatever the card had + * left - about 200px on the front page, narrower than the launch control, + * which does not wrap - so the blurb and the battle values ran on under the + * tile and were read through it. + * + * A media query was meant to catch that, but it asked about the window: the + * window was 1440px wide while the column that had run out was 344px. The fix + * is not to move the tile. It is for the card's own split to have a floor + * under the side that holds both of them, and for the board - the one thing + * here that scales - to give up the difference. + * + * The rule this file keeps: that floor is at least the readable column, the + * gap and the tile added up. The arithmetic is here rather than a remembered + * number, because it moves the moment anyone touches the tile's width, the + * gap, or what the launch control needs. + * + * Run with `npm test`. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const src = fileURLToPath(new URL("../src/", import.meta.url)); +const css = await readFile(`${src}styles.css`, "utf8"); + +const ROOT_FONT = 16; + +/** + * What PLAY and Download need side by side, measured in a browser. Neither + * half wraps, so this is a floor rather than a preference: below it the + * control overflows its column, which is the bug this file is about. Node + * cannot measure text, so a change to either half's type or padding means + * re-measuring this. + */ +const LAUNCH_CONTROL = 229; + +/** The body of a rule, by its selector. */ +function rule(selector) { + const start = css.indexOf(`\n${selector} {`); + assert.notEqual(start, -1, `${selector} moved or was renamed`); + const end = css.indexOf("\n}", start); + return css.slice(start, end); +} + +function declaration(body, property, what) { + const found = new RegExp(`\\n\\s*${property}:\\s*([^;]+);`).exec(body); + assert.ok(found, `${what} moved or was renamed`); + return found[1].trim(); +} + +/** A CSS length in px. Only px and rem appear in these rules. */ +function length(raw, what) { + const found = /^([\d.]+)(px|rem)$/.exec(raw.trim()); + assert.ok(found, `${what} is ${raw}, which this test cannot measure`); + return Number(found[1]) * (found[2] === "rem" ? ROOT_FONT : 1); +} + +/** The readable column, the gap and the terrain tile, added up. */ +function needed() { + const main = rule(".daily-text-main"); + const basis = declaration(main, "flex", ".daily-text-main flex").split(/\s+/); + assert.equal( + basis.length, + 3, + ".daily-text-main flex is not grow/shrink/basis", + ); + + const column = length(basis[2], ".daily-text-main flex-basis"); + const gap = length( + declaration(rule(".daily-text"), "gap", ".daily-text gap"), + ".daily-text gap", + ); + const tile = length( + declaration( + rule(".daily-terrain-tile"), + "width", + ".daily-terrain-tile width", + ), + ".daily-terrain-tile width", + ); + return { column, gap, tile, total: column + gap + tile }; +} + +test("the readable column can hold the launch control", () => { + const { column } = needed(); + assert.ok( + column >= LAUNCH_CONTROL, + `the readable column may be as narrow as ${column}px, and PLAY beside ` + + `Download needs about ${LAUNCH_CONTROL}px - the text will run on under ` + + `the terrain tile`, + ); +}); + +test("the card leaves room for the ground beside the text", () => { + const { column, gap, tile, total } = needed(); + const columns = declaration( + rule(".daily"), + "grid-template-columns", + ".daily grid-template-columns", + ); + const found = /minmax\((\d+(?:\.\d+)?(?:px|rem)),\s*1fr\)\s*$/.exec(columns); + assert.ok( + found, + `.daily's text column is "${columns}" and has no floor under it: a share ` + + `of the card is not enough, which is how the text came to be read ` + + `through the terrain tile`, + ); + const floor = length(found[1], ".daily text column floor"); + assert.ok( + floor >= total, + `.daily gives the text side a floor of ${floor}px, and it holds a ` + + `${column}px column, a ${gap}px gap and a ${tile}px tile - ${total}px`, + ); +}); + +test("nothing readable can outgrow the column it is in", () => { + const track = declaration( + rule(".daily-text-main"), + "grid-template-columns", + ".daily-text-main grid-template-columns", + ); + assert.match( + track, + /^minmax\(0,/, + "an auto track sizes to the launch control, which does not wrap, and the " + + "column overflows whatever is beside it", + ); +}); + +test("the ground still has somewhere to go on a phone", () => { + const row = rule(".daily-text"); + assert.equal( + declaration(row, "display", ".daily-text display"), + "flex", + ".daily-text is a grid again: its tracks cannot give way, so on a screen " + + "too narrow for two columns the text will run on under the tile", + ); + assert.equal( + declaration(row, "flex-wrap", ".daily-text flex-wrap"), + "wrap", + "without wrapping there is nowhere for the terrain tile to go", + ); +}); diff --git a/web/src/styles.css b/web/src/styles.css index 3e389ba..1846e60 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1694,9 +1694,9 @@ body:has(.hero) { .daily-eyebrow { margin: 0; color: var(--brand); - font-size: 0.72rem; + font-size: 0.62rem; font-weight: 700; - letter-spacing: 0.12em; + letter-spacing: 0.16em; text-transform: uppercase; } @@ -1797,7 +1797,7 @@ body:has(.hero) { .daily-download { display: inline-flex; align-items: center; - padding: 0.4rem 1rem; + padding: 0.4rem 0.8rem; text-decoration: none; /* Small tracked capitals in the display face: the same treatment the site's chrome labels get, so the secondary half is quiet without being a @@ -5198,31 +5198,56 @@ footer .debug { the art's own 672px - so every machine on it was being scaled down. */ .daily { display: grid; - grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); + /* The text side has a floor rather than a share: it holds the readable + column *and* the ground beside it, and a share of a card that is itself a + share of the page left it too narrow for both - which is how the text + came to be read through the terrain tile. 24rem is 15rem of readable + column, the 1rem between them and the tile's 8rem. The board gives up the + difference; it is the thing on the card that scales. */ + grid-template-columns: minmax(0, 1.25fr) minmax(24rem, 1fr); gap: 1.5rem; align-items: stretch; } +/* Everything readable on the left, the ground beside it on the right. That is + where the ground belongs, and the card's floor above is what keeps it there. + + Flex rather than a two-column grid: what decides whether both fit is this + box's own width, not the window's, and a grid holds its two tracks whatever + is left over - which is how the text came to run on under the tile while + the window was still 1440px wide. The line here gives way on its own, and + only where there is genuinely no room for two columns: a phone. */ .daily-text { - display: grid; - /* Everything readable in the first column, the ground in the second. Two - children, so the tile can simply stretch to the column's height rather - than trying to span rows the grid does not know about yet. */ - grid-template-columns: minmax(0, 1fr) auto; + display: flex; + flex-wrap: wrap; gap: 1rem; - align-items: stretch; + align-items: center; } .daily-text-main { display: grid; - gap: 0.5rem; + /* Capped rather than auto: the launch control does not wrap, so an auto + track sizes to it and the column runs on under whatever is beside it + instead of the layout giving way. */ + grid-template-columns: minmax(0, 1fr); + gap: 0.35rem; align-content: center; + /* 15rem is what the launch control needs on one line, and so the width at + which this column would start crowding the ground beside it. The card's + floor is this plus the gap and the tile. */ + flex: 1 1 15rem; min-width: 0; + /* The type below sizes off this column rather than the window: the card is + drawn at three widths - front page, lobby, archive - and a size chosen + for one of them is wrong in the other two. */ + container-type: inline-size; } .daily-text h2 { margin: 0; - font-size: 1.5rem; + line-height: 1.1; + /* Fluid between the archive's wide column and the lobby's narrow one. */ + font-size: clamp(1.15rem, 9cqi, 1.6rem); } .daily-blurb { @@ -5240,8 +5265,8 @@ footer .debug { .daily-bv { display: flex; align-items: center; - gap: 0.9rem; - margin: 0.25rem 0; + gap: 0.6rem; + margin: 0.15rem 0; } .daily-bv-side { @@ -5255,7 +5280,9 @@ footer .debug { .daily-bv-n { margin: 0; - font-size: 1.85rem; + /* Fluid, and smaller than it was: 1.85rem was set when this had the column + to itself, and it is the figure the row is widest at. */ + font-size: clamp(1.15rem, 8cqi, 1.6rem); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; @@ -5336,12 +5363,12 @@ footer .debug { than a link, which is what the capitals and the air between them buy. The left padding carries the extra to clear the notch cut out of that corner, so the word still sits centred inside the shape. */ - padding: 0.55rem 1.5rem 0.55rem 1.7rem; + padding: 0.5rem 1.15rem 0.5rem 1.35rem; font-family: var(--font-display); text-transform: uppercase; font-size: 1rem; font-weight: 700; - letter-spacing: 0.18em; + letter-spacing: 0.12em; white-space: nowrap; filter: drop-shadow(0 0 0 transparent); transition: filter 150ms ease; @@ -5455,7 +5482,11 @@ footer .debug { screen actually drew, so the frame hugs the ground and there is no letterbox inside it to read as wasted space. */ align-self: center; + flex: 0 0 auto; width: 8rem; + /* Centred once it has wrapped onto a line of its own, and inert while it is + beside the text: there the text has taken every spare pixel already. */ + margin-inline: auto; border: 1px solid color-mix(in srgb, var(--brand) 28%, transparent); border-radius: 0.25rem; overflow: hidden; @@ -5470,14 +5501,12 @@ footer .debug { height: 100%; } -/* Narrow: the ground goes under the text rather than squeezing it. */ +/* On a phone the ground has already wrapped under the text and has the whole + width to itself, so it is drawn larger than the 8rem it gets beside a + column of type. */ @media (max-width: 34rem) { - .daily-text { - grid-template-columns: minmax(0, 1fr); - } .daily-terrain-tile { - justify-self: center; - width: 60%; + width: min(60%, 12rem); } }