brackt/app/components/scoring/BracketTreePaginated.tsx

187 lines
6.1 KiB
TypeScript
Raw Permalink Normal View History

import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "~/components/ui/button";
import { useRoundTransition } from "~/hooks/useRoundTransition";
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
import type { FeederMap } from "~/lib/bracket-layout";
import type { BracketTemplate } from "~/lib/bracket-templates";
import {
TreeColumns,
BracketMatchSlot,
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
bracketGeometry,
windowGeometry,
SLOT_WIDTH,
LABEL_HEIGHT,
DESIRED_CARD_HEIGHT,
MAX_CARD_HEIGHT,
type BracketMatch,
type BracketOwnership,
} from "./BracketTreeView";
interface BracketTreePaginatedProps {
rounds: string[];
matchesByRound: Map<string, BracketMatch[]>;
ownershipMap: Map<string, BracketOwnership>;
userParticipantIds: Set<string>;
/** Index of the first scoring round — default page starts here */
firstScoringRoundIdx?: number;
thirdPlaceRound?: string;
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
feeders?: FeederMap;
template?: BracketTemplate;
}
export function BracketTreePaginated({
rounds,
matchesByRound,
ownershipMap,
userParticipantIds,
firstScoringRoundIdx,
thirdPlaceRound,
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
feeders,
template,
}: BracketTreePaginatedProps) {
const mainRounds = thirdPlaceRound ? rounds.filter((r) => r !== thirdPlaceRound) : rounds;
const thirdPlaceMatch = thirdPlaceRound ? (matchesByRound.get(thirdPlaceRound) ?? [])[0] : undefined;
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
// Pages are pairs of layout columns, not pairs of rounds: a column can mix rounds
// when teams enter the bracket at different points (see computeGroupLayout).
const geometry = bracketGeometry(
mainRounds,
matchesByRound,
feeders,
template?.rounds.map((r) => r.name) ?? mainRounds
);
const columns = geometry.layout.columns;
const lastPage = Math.max(columns.length - 2, 0);
const defaultPage = Math.max(
0,
Math.min(
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
firstScoringRoundIdx !== undefined ? Math.max(0, firstScoringRoundIdx - 1) : lastPage,
lastPage,
),
);
const { page, anim, stripRef, navigate, handleTransitionEnd } = useRoundTransition(
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
lastPage,
defaultPage,
);
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
const pageGeometry = (p: number) => windowGeometry(geometry, p, p + 1);
const labelFor = (p: number) => {
const [a, b] = [columns[p]?.label, columns[p + 1]?.label];
return b ? `${a}${b}` : (a ?? "");
};
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
const label = labelFor(anim ? anim.toPage : page);
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
const pageG = pageGeometry(page);
const animFromG = anim ? pageGeometry(anim.fromPage) : pageG;
const animToG = anim ? pageGeometry(anim.toPage) : pageG;
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
let leftPage: number;
let rightPage: number | null = null;
let leftG = pageG;
let rightG = pageG;
let settlingTransition = false;
if (anim?.phase === "sliding") {
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
leftPage = anim.dir === "right" ? anim.fromPage : anim.toPage;
rightPage = anim.dir === "right" ? anim.toPage : anim.fromPage;
leftG = anim.dir === "right" ? animFromG : animToG;
rightG = anim.dir === "right" ? animToG : animFromG;
} else if (anim?.phase === "settling") {
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
leftPage = anim.toPage;
leftG = animToG;
settlingTransition = true;
} else {
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
leftPage = page;
}
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
const containerMinHeight =
anim?.phase === "settling" ? animToG.bracketHeight : animFromG.bracketHeight;
const initialX = anim?.phase === "sliding" && anim.dir === "left" ? -SLOT_WIDTH : 0;
return (
<div>
<div className="flex items-center gap-2 mb-3">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(page - 1)}
disabled={page === 0 || !!anim}
className="h-7 w-7 shrink-0"
aria-label="Previous rounds"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="flex-1 text-center text-xs font-medium text-muted-foreground uppercase tracking-wide truncate">
{label}
</span>
<Button
variant="ghost"
size="icon"
onClick={() => navigate(page + 1)}
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
disabled={page >= lastPage || !!anim}
className="h-7 w-7 shrink-0"
aria-label="Next rounds"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div style={{ width: SLOT_WIDTH, overflow: "hidden", minHeight: containerMinHeight + LABEL_HEIGHT + 2, transition: settlingTransition ? "min-height 500ms ease" : undefined }}>
<div
ref={anim?.phase === "sliding" ? stripRef : undefined}
style={{
display: "flex",
width: anim?.phase === "sliding" ? SLOT_WIDTH * 2 : SLOT_WIDTH,
transform: anim?.phase === "sliding" ? `translateX(${initialX}px)` : undefined,
}}
onTransitionEnd={handleTransitionEnd}
>
<div style={{ width: SLOT_WIDTH, flexShrink: 0 }}>
<TreeColumns
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
geometry={leftG}
columnRange={[leftPage, leftPage + 1]}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
feeders={feeders}
template={template}
transitionDuration={settlingTransition ? 500 : undefined}
/>
</div>
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
{anim?.phase === "sliding" && rightPage !== null && (
<div style={{ width: SLOT_WIDTH, flexShrink: 0 }}>
<TreeColumns
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
geometry={rightG}
columnRange={[rightPage, rightPage + 1]}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
feeders={feeders}
template={template}
/>
</div>
)}
</div>
</div>
{thirdPlaceMatch && (
<div style={{ marginTop: 20, width: SLOT_WIDTH }}>
<div
className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground text-center"
style={{ height: LABEL_HEIGHT, display: "flex", alignItems: "center", justifyContent: "center" }}
>
3rd Place
</div>
<BracketMatchSlot
match={thirdPlaceMatch}
slotHeight={Math.min(DESIRED_CARD_HEIGHT, MAX_CARD_HEIGHT)}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
Lay out brackets from the feeder graph The LLWS bracket didn't read as a bracket: cards sat above games that don't feed them, connectors joined the wrong pairs, and several games had no line at all. The stored data was correct — LLWS_ADVANCEMENT already matches the official 2026 LLBWS bracket game for game. The renderer was the problem. TreeColumns placed cards at `index * (height / roundSize)` and ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds only for an exact halving. The LLWS winners bracket is not one: two of the four Opening Round games skip Winners Round 2 and go straight to the semifinals, so those two got stranded in column one with nothing beside them, and the halving branch drew confident, wrong connectors for the rest. Lay out from the graph instead. app/lib/bracket-layout.ts inverts a template's advancement into "what fills each slot", then assigns columns by depth from the group's final, orders each column by the parent's slot order, and centres each card on its feeders. Counting back from the final is what makes a printed bracket line up: a team entering late is drawn in the column where it actually plays. This reproduces the official International bracket exactly, and fixes Elimination Round 3, where the official bracket prints the later game on top but match-number sort put it below. Because column is depth, every in-group edge spans exactly one gutter, so connectors now draw for unplayed games too. Cards also take a fixed height rather than stretching to fill their column, which is what made a lone final tower over the rest. Empty slots name their source — "Loser of Winners SF 1" rather than "TBD". That is the only way to show the feeds crossing between the winners and elimination brackets, which render as separate trees. Also: - Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer can import it without pulling the database context into the browser bundle; models/playoff-match re-exports it. - Page the mobile view one group at a time, matching desktop. A whole double-elimination phase is a DAG, not a tree, so its columns would be arbitrary. - Add a clear-bracket admin action. Nothing else could rewrite a match's participants, so a mis-seeded bracket had no repair path at all. - Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the routing and layout tests check against one copy of the official bracket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
feeders={feeders}
template={template}
/>
</div>
)}
</div>
);
}