-
@@ -3934,6 +3956,8 @@ function BracketViewport({ children }) {
start.moved = true
bracketPan.dragged = true
el.classList.add('is-grabbing')
+ // Drop any text selection the initial press may have started.
+ window.getSelection()?.removeAllRanges()
}
if (start.moved) {
el.scrollLeft = start.sl - dx
@@ -3979,6 +4003,7 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
// `tops` places each match; once placed we read back the DOM to draw connectors.
const [layout, setLayout] = useState({ tops: new Map(), height: 0 })
const [connectors, setConnectors] = useState({ width: 0, height: 0, lines: [], byes: [] })
+ const [collapsed, setCollapsed] = useState(false)
// Pass 1: measure card heights, then position every match centred on its feeders.
useLayoutEffect(() => {
@@ -3994,7 +4019,7 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
const observer = new ResizeObserver(relayout)
observer.observe(grid)
return () => observer.disconnect()
- }, [side])
+ }, [side, collapsed])
// Pass 2: with matches positioned, read real edges and draw elbow connectors.
// Each connector carries the teams on either end so a hovered team's whole run
@@ -4040,13 +4065,22 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
}
}
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, lines, byes })
- }, [side, layout])
+ }, [side, layout, collapsed])
const active = Boolean(highlight)
return (
-
-
{side.label}
+
+
+ {collapsed ? null : (
)
}
@@ -4263,7 +4298,7 @@ function TournamentDetailPage({ tournamentId, navigate }) {
) : null}
{bracketSides.length ? (
-
+
{hasStandings || listSides.length ? (
Playoffs
) : null}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 00d4897..71d013e 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -834,20 +834,43 @@ h3 {
/* The bracket reads as a campaign map you drag around. The viewport breaks the
page's max-width so there's room to manoeuvre; the surface carries a faint
tactical grid that pans with the bracket (it lives on the scrolling content). */
+/* The whole side (heading + canvas) breaks out to 85vw and centres on the
+ viewport, so the WINNER/LOSER heading aligns with the canvas's left edge. */
+.bracket-side {
+ width: 85vw;
+ margin-left: calc(50% - 42.5vw);
+}
+
+.bracket-caret {
+ display: inline-block;
+ font-size: 0.7em;
+ transition: transform 0.18s ease;
+}
+
+.bracket-caret.is-collapsed {
+ transform: rotate(-90deg);
+}
+
.bracket-viewport {
position: relative;
- width: 100vw;
- margin-left: calc(50% - 50vw);
+ width: 100%;
max-height: 78vh;
overflow: auto;
overscroll-behavior: contain;
cursor: grab;
- border-block: 1px solid var(--color-border);
+ border: 1px solid var(--color-border);
+ border-radius: 0.5rem;
background-color: color-mix(in srgb, var(--color-bg) 86%, #000);
}
.bracket-viewport.is-grabbing {
cursor: grabbing;
+ user-select: none;
+}
+
+/* While panning, don't let the drag turn into a text selection. */
+.bracket-viewport.is-grabbing * {
+ user-select: none;
}
.tournament-bracket-grid {