diff --git a/frontend.css b/frontend.css index ed778c3..f4d791b 100644 --- a/frontend.css +++ b/frontend.css @@ -344,6 +344,44 @@ body { max-height: 220px; overflow-y: auto; flex-shrink: 0; + position: relative; +} + +.standings--side { + border-top: none; +} + +.standings--bottom { + border-top: 1px solid var(--border); + border-left: none; + max-height: none; +} + +.standings__resize-handle { + position: absolute; + background: transparent; + z-index: 10; +} + +.standings__resize-handle:hover, +.standings__resize-handle:active { + background: var(--accent); +} + +.standings__resize-handle--side { + left: 0; + top: 0; + bottom: 0; + width: 4px; + cursor: ew-resize; +} + +.standings__resize-handle--bottom { + top: 0; + left: 0; + right: 0; + height: 4px; + cursor: ns-resize; } .standings__head { @@ -355,6 +393,24 @@ body { .standings__links { display: flex; gap: 10px; + align-items: center; +} + +.standings__toggle-btn { + background: none; + border: 1px solid var(--border); + color: var(--text-muted); + font-family: var(--mono); + font-size: 11px; + padding: 2px 6px; + cursor: pointer; + border-radius: 3px; + transition: all 0.2s; +} + +.standings__toggle-btn:hover { + color: var(--text); + border-color: var(--text-dim); } .standings__title { @@ -605,12 +661,20 @@ body { overflow: hidden; } + .layout--sidebar-bottom { + flex-direction: column; + } + .main { padding: 88px 48px 32px; position: relative; align-items: center; } + .layout--sidebar-bottom .main { + padding-bottom: 16px; + } + .header { position: absolute; top: 32px; @@ -636,7 +700,7 @@ body { .contestant { flex: 1; } - .standings { + .standings--side { width: 280px; border-top: none; border-left: 1px solid var(--border); @@ -644,4 +708,23 @@ body { overflow-y: auto; padding: 24px; } + + .standings--bottom { + width: 100%; + border-top: 1px solid var(--border); + border-left: none; + max-height: 400px; + padding: 16px 48px; + } + + .standings--bottom .standings__list { + flex-direction: row; + flex-wrap: wrap; + gap: 8px 24px; + } + + .standings--bottom .standing { + width: calc(25% - 18px); + min-width: 200px; + } } diff --git a/frontend.tsx b/frontend.tsx index 9357f83..9638c36 100644 --- a/frontend.tsx +++ b/frontend.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { createRoot } from "react-dom/client"; import "./frontend.css"; @@ -327,12 +327,24 @@ function GameOver({ scores }: { scores: Record }) { // ── Standings ──────────────────────────────────────────────────────────────── +const SIDEBAR_MIN_WIDTH = 200; +const SIDEBAR_MAX_WIDTH = 500; +const SIDEBAR_DEFAULT_WIDTH = 280; + function Standings({ scores, activeRound, + position, + width, + onTogglePosition, + onResize, }: { scores: Record; activeRound: RoundState | null; + position: "side" | "bottom"; + width: number; + onTogglePosition: () => void; + onResize: (width: number) => void; }) { const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const maxScore = sorted[0]?.[1] || 1; @@ -344,11 +356,54 @@ function Standings({ ]) : new Set(); + const resizeRef = useRef(null); + const [isResizing, setIsResizing] = useState(false); + + useEffect(() => { + if (!isResizing) return; + + function handleMouseMove(e: MouseEvent) { + if (position === "side") { + const newWidth = window.innerWidth - e.clientX; + onResize(Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, newWidth))); + } else { + const newHeight = window.innerHeight - e.clientY; + onResize(Math.min(400, Math.max(120, newHeight))); + } + } + + function handleMouseUp() { + setIsResizing(false); + } + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing, position, onResize]); + return ( -