The menu text reduction: the design handoff, built

Sixty words on the wall became a dozen. The handoff's own summary of the rule:
Jersey 15 NAMES (screen titles, rooms, cards), Bangers CALLS exactly once per
screen (the fixture, the primary button, a modal headline), Silkscreen INFORMS
(numbers, labels, chips) — and a sentence survives only where it carries a
decision. Everything else moved into a bar, a pip or the modal that opens with
the mode.

  · Jersey 15 bundled subset (9 KB) and Bungee retired after one afternoon
  · SPLASH: the plaque is one row, the goal line is gone (the fixture button
    below said it already), the two doors carry a number each
  · PICK A GAME: rooms are one word, posters are their names, the fixture is a
    hero with the only second line — the whole wall fits one screen now
  · MATCH SETUP: unnumbered blocks, the meters' legend once instead of a
    description per court, no summary footer
  · DIFFICULTY: three pips instead of three sentences
  · TRAINING: the revanche is a full-width row, rungs are numbers, the list
    ends on the league that opens the rest of the ladder
  · VS: three paragraphs became YOU / HOST / JOIN
  · MODALS: one rule per row (the sub was the lead again in other words), the
    blood pool as pips on that same line, and the walk-on's two levels in ONE
    face at ONE size — a comparison drawn at two sizes is a headline with a
    footnote (Oliver)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-31 14:41:25 +02:00
1 parent 4c0ede33db
commit a331a1ae37
44 files changed
+3840 -187

No files matched your search

+32 -29
View File
@@ -2368,16 +2368,21 @@ function modalRows(m, inner, skipBody) {
const rows = [];
const push = r => { rows.push(r); return r; };
if (m.foe) {
push({ kind: 'split', h: 32 });
push({ kind: 'split', h: 26 });
const sub = [];
for (const l of m.foe.sub || []) sub.push(...wrapText(l, F(8, 0), inner));
if (sub.length) push({ kind: 'text', lines: sub, dim: true, h: sub.length * 12 + 2 });
} else if (m.rules) {
for (const r of m.rules) {
const lead = wrapText(r.lead, F(8, 700), inner);
const sub = r.sub ? wrapText(r.sub, F(8, 0), inner) : [];
push({ kind: 'rule', lead, sub, hot: r.hot, pips: r.pips,
h: lead.length * 12 + sub.length * 12 + (r.pips ? 12 : 0) + 2 });
// ONE RULE, ONE LINE (2026-08-31, the menu-text-reduction handoff). Every row used to carry a
// statement and a consequence, and the consequence was usually the statement again in other
// words ("YOUR LEAGUE, ROUND BY ROUND" / "WIN IT ALL — MOVE UP A LEAGUE"). The line that
// names the RESULT is the one that survives. A rule is a string, or an object when it needs
// the threat colour or the blood pool. `pips` ride the SAME line, hard right.
for (const raw of m.rules) {
const r = typeof raw === 'string' ? { lead: raw } : raw;
const room = inner - (r.pips ? r.pips * 9 + 12 : 0);
const lead = wrapText(r.lead, F(8, 700), room);
push({ kind: 'rule', lead, hot: r.hot, pips: r.pips, h: lead.length * 12 + 4 });
}
} else if (!skipBody) {
const lines = [];
@@ -2415,7 +2420,11 @@ function modalLayout(m) {
// ...and only when it actually FITS there: a line that wraps in the narrow column belongs under
// the head as an ordinary row, not as a ragged column beside a picture
const beside = pairs && wrapped.length <= bodyLines.length + 1;
const strap = m.foe ? wrapText(m.foe.line, F(8, 0), headW) : beside ? wrapped : [];
// ONE line under the title: the body's slogan on a walk-on, the mode's own fact on a rules card
// (`m.strap` — LEAGUE 3 · 4 ROUNDS, the round count the fixture button gave up), or the two
// lines of an illustrated announcement.
const strap = m.foe ? wrapText(m.foe.line, F(8, 0), headW)
: m.strap ? wrapText(m.strap, F(8, 0), headW) : beside ? wrapped : [];
const headH = Math.max(artH, 22 + strap.length * 12);
const rows = modalRows(m, boxW - pad * 2, beside);
let h = pad + headH + MODAL_GAP + MODAL_FOOT + MODAL_BTN + pad;
@@ -2464,34 +2473,28 @@ function drawModal(m) {
} else if (r.kind === 'rule') {
ctx.textAlign = 'left';
ctx.font = F(8, 700);
r.lead.forEach((l, i) => shadowText(l, inX, y + 7 + i * 12, UI.cream));
let ry = y + 7 + r.lead.length * 12;
// the consequence line: DIM for information, the modal's hot colour only when it threatens
ctx.font = F(8, 0);
r.sub.forEach((l, i) => shadowText(l, inX, ry + i * 12, r.hot ? col : UI.dim));
if (r.pips) { // the pool, in the HUD's own pip language
const py = ry + r.sub.length * 12 + 4;
// the rule wears the modal's hot colour only when it threatens; otherwise it is a statement
r.lead.forEach((l, i) => shadowText(l, inX, y + 8 + i * 12, r.hot ? col : UI.cream));
if (r.pips) { // the pool, in the HUD's own pip language, ON THE SAME LINE
const px = x + L.boxW - pad - (r.pips - 1) * 9;
for (let k = 0; k < r.pips; k++) {
ctx.beginPath(); ctx.arc(inX + 3 + k * 9, py, 2.6, 0, 7);
ctx.beginPath(); ctx.arc(px + k * 9, y + 8, 2.6, 0, 7);
ctx.fillStyle = '#8a1a12'; ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.stroke();
}
}
} else if (r.kind === 'split') {
// HIS LEVEL AGAINST YOURS — one frame, two figures, a hairline between them. The number
// wears the modal's hot colour while the body outranks you and the pick green once it
// does not, which is the whole point of showing it.
const half = Math.round((L.boxW - pad * 2) / 2), f = m.foe;
ctx.strokeStyle = MODAL_RULE; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(inX + half + 0.5, y + 4); ctx.lineTo(inX + half + 0.5, y + r.h - 4);
ctx.stroke();
ctx.textAlign = 'center';
ctx.font = F(7, 0);
shadowText('ITS LEVEL', inX + half / 2, y + 8, UI.dim);
shadowText('YOURS', inX + half * 1.5, y + 8, UI.dim);
ctx.font = F(13, 700);
shadowText(String(f.lvl), inX + half / 2, y + 23, f.lvl > f.mine ? col : UI.pick);
shadowText(String(f.mine), inX + half * 1.5, y + 23, UI.cream);
// HIS LEVEL AGAINST YOURS, on ONE line: his on the left, yours hard right. SAME FACE, SAME
// SIZE for both (Oliver) — a comparison drawn at two sizes is not a comparison, it is a
// headline with a footnote. Only the COLOUR differs, and only where it means something: his
// number wears the modal's hot colour while he outranks you, the pick green once he does not.
const f = m.foe;
ctx.font = F(10, 700);
ctx.textAlign = 'left';
shadowText('LEVEL ' + f.lvl, inX, y + 9, f.lvl > f.mine ? col : UI.pick);
ctx.textAlign = 'right';
shadowText('YOURS ' + f.mine, x + L.boxW - pad, y + 9, UI.dim);
ctx.textAlign = 'left';
}
y += r.h + MODAL_GAP;
}