---
title: Redesign Matchup Page Information Architecture
type: task
status: completed
description: >-
  Redesign of the league /leagues/:lid/matchups page into a scoreboard with a state-aware sidebar on
  wide screens and a tab set on narrow ones.
base_uri: user:task/league/redesign-matchup-page-information-architecture.md
created_at: '2026-09-13T21:47:44.455Z'
entity_id: 53771700-25f1-4611-beef-90c25b010b80
finished_at: '2026-09-13T22:20:38.480Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
started_at: '2026-09-14T03:03:13.262Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-11T20:59:33.020Z'
visibility_analyzed_at: '2026-11-06T00:17:59.880Z'
---

## Purpose

Improve the look, feel and layout of `/leagues/:lid/matchups`, with a standing mandate to reconsider the page's information architecture rather than only polish the arrangement it has. The current shape — a section stack ending in a tab set — was reached by successive local fixes, not by a design pass, and each step was constrained by what the existing panels already were.

## Outcome

Landed 2026-09-13 across four league commits, suite green at 8881 passing. **Above 600px** the page is a scoreboard with a state-aware 380px sidebar; **below 600px** it is a tab set of all five panels with matchup as the default view. Full detail is in [[user:text/league/matchup-page-architecture.md]], reconciled alongside.

| Commit | What |
| --- | --- |
| `16bc95ed0` | the sidebar arrangement, the three matchup states, the reopened rankings column set |
| `d52376a1c` | the live plays feed's head, bounds and per-play rule |
| `9f35b1f64` | the phone tab set, ratchet 7 to 11 |
| `979844d5e` | scoping the header's anti-tab rejection to the width it governs |

**Deployed.** The 2026-09-13 note that nothing here had shipped is superseded: measured 2026-10-09, the league host runs `656b4be94`, which contains all four commits above.

### The three open questions, answered

- **Is a tab set the right primitive?** Not above 600px, and yes below it — width is the whole answer. A tab is for ALTERNATIVES. At 1168px the week's scoring, the game's trajectory and the franchises' history are three different questions with room for all three, so tabbing them hid two; the tab also handed the 380px rankings table the whole measure, making the imbalance total rather than fixing it. At 390px only one panel can be on screen whatever the arrangement, so they are alternatives in fact and a tab is exactly right. The rejection's premise — a reader wanting more than one at once — simply does not hold at phone width.
- **Does the week ranking belong here?** Yes, and as the sidebar rather than a panel. It is week-scoped and reads the same selector as the score beside it; standings answers a season question. Putting it beside the rosters is what finally lets a reader compare their score to the league's without a click.
- **What is the page for, in its three states?** Upcoming: who is favoured and is my lineup set. Live: the score, what just happened, how it compares. Complete: who won, how it ranked, the series after it. `get_matchup_state` names the three and the page varies its sidebar and its sections on them — previously only the play feed varied.

### What the redesign had to reopen

**The fixed column set, exactly as this task predicted.** The rankings panel no longer carries a measure: three fixed number columns, a team column taking the remainder. Abbreviating the heads to `#`, `Pts` and `Proj` freed 106px, which is what pays for the names. `matchup-history` keeps its fixed 678px deliberately — it is a mirror about the two scores, so a column that grows on one side must grow identically on the other.

### Findings from the original pass, resolved

- **Every rank read `1` on an unplayed week** — fixed. The metric is chosen on the data (`some(points > 0)`), not the clock, because the fantasy week goes live on the Tuesday. An unplayed week ranks by projection, labels itself `Week N projected`, and drops the all-zero points column. Verified as two readings that differ: week 6 of 2026 and week 5 of 2025.
- **Team names truncated at 200px** — fixed for nine of ten. The one that still ellipsises is thirty-seven characters and no sidebar measure will hold it.
- **The page read unbalanced** — fixed. The 380px table fills a 380px aside; the rosters take the remaining 754px.
- **The selected-matchup highlight was very subtle** — fixed with three channels, none of them hue: a `$control_fill` ground, full weight, and a 6px inset rule spanning the row instead of 2px on the rank cell alone.
- **21px descendant clip at 390px** — did not reproduce. The only overflows now measured at 390px are the scores rail (deliberately horizontally scrolling) and intended name ellipsis; page-level overflow is 0.

### Three defects found by measuring, not by reading

- `.table__row` in `app/styles/table.styl` is `width: max-content`, which defeated the flexible column and pushed the projected value off the right edge as a clipped NUMBER — not as anything that looked like a layout fault.
- `flex-basis: auto` let the longest name set the row's basis, so the row summed past its container.
- The panel's own top padding dropped its rule 24px below the scoreboard beside it.

### Left open, and not caused by this work

**An anonymous session loads only two teams into the redux store**, so the rankings render 2 rows of 10 and eight names in the scores rail render blank. It reproduces identically on deployed `xo.football`, and a previous session's retraction of it as a mid-load frame was wrong. The evidence and the next candidate to check are recorded in [[user:text/league/matchup-page-architecture.md]] under "Open defect". This needs its own fix; it is not a matchup-page layout question.

**The mobile plays feed opens with its scrollbar about three times too long.** The virtualizer starts at roughly 32px a row against a measured 113px card, and the thumb grows as rows measure. Neither `defaultHeight` nor `estimatedRowSize` drives that initial extent — both measured inert. The evidence is in the comment in `scoreboard-play-by-play.js`.

## Plan

- [x] **Decide the information architecture before moving any pixels**
  - Answer the three open questions above against how the page is actually used, not against what its panels currently are.
  - Verify: a written statement of what the page is for in each of its three states, and which surface owns each piece of information.

- [x] **Rework the arrangement**
  - Files: `app/views/pages/matchup/`, the panel components it composes, and whichever fixed column sets the new arrangement reopens.
  - Verify: browser pass at 1440px and 390px with `window.innerWidth` asserted from inside the page, per the headed-floor clamp noted in the browser-automation skill.

- [x] **Resolve the unplayed-week ranking**
  - Verify: the rankings panel on a pre-kickoff week shows a column that distinguishes teams, and a control on a completed week shows a different ordering — two readings that must differ.

- [x] **Reconcile the page documentation**
  - File: `text/league/matchup-page-architecture.md`
  - Verify: its section-stack diagram matches the shipped arrangement, and no retired measure or breakpoint survives a grep.
