Skip to content

feat: draw any tournament in fixtures - #132

Merged
starc007 merged 7 commits into
starc007:mainfrom
chakravartiavinit:feat/fixtures-tournament-data
Aug 1, 2026
Merged

feat: draw any tournament in fixtures#132
starc007 merged 7 commits into
starc007:mainfrom
chakravartiavinit:feat/fixtures-tournament-data

Conversation

@chakravartiavinit

Copy link
Copy Markdown
Contributor

Both fixture styles were built around one World Cup draw: Team.code was required, so every team loaded a country flag from flagcdn.com, and matches had to carry a date, a status and a football-shaped "FT" chip. This makes both take any single-elimination tournament as data, with no change to how the sample renders.

What changed

Team artwork. logo takes any image URL and wins over code. A team with neither falls back to its initials; an empty slot keeps the TBD shield. Flags stay cropped to fill their slot, a logo is fitted whole inside it.

Optional match metadata. date, time and status are optional now: a match with a winner counts as finished without one. A per-match badge replaces the derived chip ("AET", "BO5", "Forfeit"), and thirdPlaceLabel renames the play-off.

Layout guard. A round that doesn't hold exactly half the matches of the one before it used to read past the end of the feeder array and position its cards at NaN, rendering nothing. Feeder lookups fall back to the fixed rhythm instead.

Contrast. The initials fallback uses text-foreground: 14.6:1 light and 11.5:1 dark, against 4.44:1 and 3.55:1 for text-muted-foreground at 9px, which was under AA in both themes. In the wheel, initials hold a floor of 15 viewBox units, about 10px once the 32rem stage scales the 760-unit box. Below that they were rendering at 7.6px on the inner ring.

Both components still take the same rounds array, so one dataset feeds either style.

Not addressed

Depth-1 nodes in the wheel are 19px at the 32rem floor, under the 24px WCAG 2.5.8 target minimum. That predates this branch and applies to the shipping 32-team sample too. Raising NODE_MIN to ~17.8 clears it but reflows the whole wheel, so it's left out.

Notes

The credit entry on the Fixtures registry item is removed in its own commit at the contributor's request. That leaves ComponentEntry.credit and the "Built by" branch in app/components/[category]/[slug]/page.tsx unused; that branch also hardcodes "Fixtures was created by" instead of the component name, which will misname any future credit.

Testing

bun run check (typecheck, biome, registry) and bun test pass. Each of the six commits typechecks and passes its tests on its own. New coverage: a bracket test with a three-round esports draw (logos, initials, no dates, custom play-off label, no NaN offsets) and a wheel case with a two-round artwork-free draw, which has the smallest nodes of any draw and is where the size floor has to hold.

`Team.code` was required, so every team loaded a country flag from
flagcdn.com and nothing else could be drawn. `logo` now takes any image
URL and wins over `code`; a team with neither falls back to its
initials, and an empty slot keeps the TBD shield.

Flags stay cropped to fill their slot. A logo is fitted whole instead,
since a crest cropped to a circle loses its shape. Initials use
text-foreground (14.6:1 light, 11.5:1 dark; muted-foreground was under
AA on both) and hold a floor of 15 viewBox units in the wheel, which is
~10px once the 32rem stage scales the 760-unit box.
A draw that isn't football shouldn't have to invent a kick-off date or
label its results "FT". `date`, `time` and `status` are now optional: a
match with a `winner` counts as finished without one, and the card
header holds its height so a dateless draw doesn't sit top-heavy inside
the fixed card height. A per-match `badge` replaces the derived chip
("AET", "BO5", "Forfeit"), and `thirdPlaceLabel` renames the play-off.

The `rounds` contract is now spelled out on the prop itself, so the API
reference states how the tree pairs up.
Every later match sits at the midpoint of its two feeders, so a round
that doesn't hold exactly half the matches of the one before it read
past the end of the feeder array and positioned its cards at NaN, which
renders nothing. Feeder lookups now fall back to the fixed rhythm, so a
malformed or partial draw degrades into a readable column instead of
disappearing.
The bracket had no test at all. It now renders a three-round esports
draw with logos, initials, no dates and a custom play-off label, and
asserts no card lands at a NaN offset.

The wheel gains a two-round, artwork-free case: it takes the smallest
nodes of any draw, so it's where the initials size floor and the fixed
stage width have to hold.
The descriptions repeated the same "any single-elimination draw" line
three times and leaned on em dashes. Each one now says something
concrete instead: the parent notes both styles read the same array of
rounds, the wheel covers flags, logos and initials, the bracket lists
what comes from the data.

The Usage blocks show how to build a `Round[]` for a tournament that
isn't the bundled World Cup sample, and the sample data headers say
they're a starting shape rather than fixed content. One stale claim
goes with them: the wheel holds a 32rem stage at every size, it does
not size itself to the rim.
Requested by the author: they contributed the component rather than
authoring the page, so the "Built by" block shouldn't name them.
@starc007

Copy link
Copy Markdown
Owner

Great work making both fixture styles reusable across different tournament data. I found three areas worth addressing before merge:

  • Uneven-round fallback can overlap cards. In knockout-bracket.tsx, fallback positions don't account for previously positioned matches. For example, a 2 → 2 draw places later cards only 72px apart although each card is 124px tall. This conflicts with the goal of keeping malformed or partial draws readable.

  • Failed artwork remains failed after its URL or team changes. TeamCrest and TeamMark store a permanent failed boolean. If a temporarily broken logo is corrected while the match ID remains stable, the component keeps showing initials and never attempts the new source. Consider tracking the failed URL or resetting the failure state when the resolved source changes.

  • Time-only matches lose their time in the accessible label. The card renders time independently, but matchLabel only includes it when date exists. A match with only { time: "4:00 pm" } visibly shows the time while its aria-label omits it.

bun run check and all 73 tests pass locally.

@chakravartiavinit

Copy link
Copy Markdown
Contributor Author

Thanks for the review @starc007
checking these

An unfed match in an uneven round fell back to a fixed rhythm measured
from the top of the stage, which could land on top of a card already
placed at a feeder midpoint — a 2 → 2 draw put two 124px cards 72px
apart. Unfed matches now stack a full row under the last card placed in
their own round, and the stage height is measured from the lowest
visible card instead of derived from the base round's count, so a
stacked fallback isn't clipped.

TeamCrest and TeamMark stored a permanent `failed` boolean, so a logo
that was temporarily broken stayed as initials for the life of the
component even after the URL was corrected. They now hold the URL that
failed and retry whenever the resolved source changes.

matchLabel only announced the kick-off when a date was present, so a
match carrying just a time showed it on the card and omitted it from
the aria-label. It now joins the same date/time pair the card renders.
@chakravartiavinit

Copy link
Copy Markdown
Contributor Author

Thanks for flagging these issues, @starc007. I've addressed all the feedback.

@starc007
starc007 merged commit f3275d9 into starc007:main Aug 1, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants