Rework the chess page into a chess.com-style layout

Board with player bars and captured trays on the left, a game panel with
move list, status, and action buttons on the right. The page is locked to
the viewport (no scrolling); on phones the panel collapses into a slide-up
menu reachable from a slim status bar. Cache-busts the chess scripts too.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Josh-Heaps
2026-06-10 16:58:35 -06:00
co-authored by Claude Opus 4.8
parent c92c46e6d4
commit 6402ef268c
4 changed files with 450 additions and 155 deletions
+50 -17
View File
@@ -6,7 +6,14 @@
} }
<div id="chessContainer"> <div id="chessContainer">
<div id="boardContainer"> <section id="boardArea">
<div class="playerBar" id="topPlayerBar">
<span class="playerDot black"></span>
<span class="playerName">Opponent</span>
<div class="capturedTray" id="captured-top"></div>
<span class="advantage" id="advantage-top"></span>
</div>
<div id="chessBoard"> <div id="chessBoard">
<!-- Placeholder squares --> <!-- Placeholder squares -->
@for (int i = 0; i < 64; i++) @for (int i = 0; i < 64; i++)
@@ -14,18 +21,41 @@
<div id="square-@i" class="chessSquare @( (i + i / 8) % 2 == 0 ? "light" : "dark" )"></div> <div id="square-@i" class="chessSquare @( (i + i / 8) % 2 == 0 ? "light" : "dark" )"></div>
} }
</div> </div>
</div>
<div id="textContainer" class="sideContent"> <div class="playerBar" id="bottomPlayerBar">
<span class="playerDot white"></span>
<span class="playerName">You</span>
<div class="capturedTray" id="captured-bottom"></div>
<span class="advantage" id="advantage-bottom"></span>
</div>
</section>
<aside id="gamePanel">
<header class="panelHeader">
<span class="panelLogo">&#9822;</span>
<h1>Chess</h1> <h1>Chess</h1>
<p>Click a button to start a game :)</p> <button id="menuClose" class="iconBtn" aria-label="Close menu" onclick="closeMenu()">&times;</button>
</header>
<div id="moveList">
<p class="movePlaceholder">Moves will appear here once a game begins.</p>
</div> </div>
<div id="buttonContainer" class="sideContent"> <div id="statusLine">Start a game to play.</div>
<button id="startGameBtn" onclick="startNewGame()">Start New Game</button>
<button id="startCPUGame" onclick="startCPUGame()">Vs CPU</button> <div id="panelButtons">
<button id="copyPgnBtn" onclick="copyPgn()" style="display: none">Copy PGN</button> <button id="startGameBtn" class="btn btn-primary" onclick="startNewGame()">New Game</button>
<a id="watchLink" href="/watch">Watch other games →</a> <button id="startCPUGame" class="btn btn-secondary" onclick="startCPUGame()">Play vs CPU</button>
<button id="copyPgnBtn" class="btn btn-ghost" onclick="copyPgn()" style="display: none">Copy PGN</button>
<a id="watchLink" href="/watch">Watch other games &rarr;</a>
</div>
</aside>
<div id="menuBackdrop" onclick="closeMenu()"></div>
<div id="mobileBar">
<span id="mobileStatus">Start a game to play.</span>
<button id="menuToggle" class="btn btn-primary" onclick="toggleMenu()">Menu</button>
</div> </div>
</div> </div>
@@ -61,17 +91,20 @@
@section Scripts { @section Scripts {
<script src="~/js/signalr/signalr.min.js"></script> <script src="~/js/signalr/signalr.min.js"></script>
<script src="~/js/ChessScripts/GameState.js"></script> <script src="~/js/ChessScripts/GameState.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessUtils.js"></script> <script src="~/js/ChessScripts/ChessUtils.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessAPI.js"></script> <script src="~/js/ChessScripts/ChessAPI.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessSignalR.js"></script> <script src="~/js/ChessScripts/ChessSignalR.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessInteractions.js"></script> <script src="~/js/ChessScripts/ChessInteractions.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessBoard.js"></script> <script src="~/js/ChessScripts/ChessBoard.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/ChessModals.js"></script> <script src="~/js/ChessScripts/ChessModals.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/ChessScripts/chessMain.js"></script> <script src="~/js/ChessScripts/chessMain.js?v=@ViewData["cssVersion"]"></script>
} }
@section Styles { @section Styles {
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="~/css/chess/game.css?v=@ViewData["cssVersion"]" /> <link rel="stylesheet" href="~/css/chess/game.css?v=@ViewData["cssVersion"]" />
<link rel="stylesheet" href="~/css/chess/site.css?v=@ViewData["cssVersion"]" /> <link rel="stylesheet" href="~/css/chess/site.css?v=@ViewData["cssVersion"]" />
} }
+1 -1
View File
@@ -1,6 +1,6 @@
@{ @{
Layout = null; Layout = null;
ViewData["cssVersion"] = "1.0.5"; // <--- change this once to bust cache ViewData["cssVersion"] = "1.0.7"; // <--- change this once to bust cache
} }
<!DOCTYPE html> <!DOCTYPE html>
+1 -9
View File
@@ -1,12 +1,4 @@
#boardContainer { #chessBoard {
position: relative;
width: fit-content;
margin: 2vw auto;
}
#chessBoard {
width: 60vw;
height: 60vw;
display: grid; display: grid;
grid-template-columns: repeat(8, 1fr); grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr); grid-template-rows: repeat(8, 1fr);
+395 -125
View File
@@ -1,159 +1,429 @@
html, body { :root {
background-color: #2b2c30; --bg: #2b2c30;
color: #d6d6d6; --panel: #21232a;
cursor: default; --panel-row: rgba(255, 255, 255, 0.035);
--bar: #1e2026;
--accent: #8cd5ed;
--accent-ink: #10222a;
--text: #d6d6d6;
--muted: #8b8f99;
--line: #34373f;
--bar-h: clamp(34px, 5.5vh, 50px);
--radius: 12px;
--font: 'Outfit', system-ui, -apple-system, Segoe UI, sans-serif;
}
html, body {
height: 100%; height: 100%;
margin: 0; margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font);
cursor: default;
/* The chess page is a fixed, single-screen app: never scroll. */
overflow: hidden;
} }
#startGameBtn { main {
background-color: #8cd5ed; height: 100%;
color: #262626;
border-radius: 30px;
border: 0px;
cursor: pointer;
order: 1;
padding: 2vh;
margin: 2vh;
} }
#startCPUGame { /* ---- Layout ------------------------------------------------------------ */
background-color: #8cd5ed;
color: #262626;
border-radius: 30px;
border: 0px;
cursor: pointer;
order: 1;
padding: 2vh;
margin: 2vh;
}
#copyPgnBtn {
background-color: #8cd5ed;
color: #262626;
border-radius: 30px;
border: 0px;
cursor: pointer;
order: 1;
padding: 2vh;
margin: 2vh;
}
#watchLink {
color: #8cd5ed;
text-decoration: none;
margin: 2vh;
order: 2;
}
#watchLink:hover {
text-decoration: underline;
}
/* Portrait / narrow: board on top, panel stacked below. */
#chessContainer { #chessContainer {
height: 100vh;
height: 100dvh;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-evenly; gap: clamp(8px, 1.6vh, 16px);
height: 100vh; padding: clamp(10px, 2vh, 20px);
box-sizing: border-box; overflow: hidden;
padding: 5vw; }
#boardArea {
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 auto;
width: min(94vw, 56vh);
} }
#chessBoard { #chessBoard {
aspect-ratio: 1 / 1; width: 100%;
width: 90vw; /* use the smaller of width or height */
height: auto; height: auto;
max-height: 90vw; aspect-ratio: 1 / 1;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
} }
#boardContainer { #gamePanel {
align-content: center; width: min(94vw, 56vh);
flex-grow: 1; flex: 1 1 auto;
} min-height: 0;
.sideContent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-content: center; background: var(--panel);
justify-content: center; border-radius: var(--radius);
text-align: center; overflow: hidden;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
} }
#textContainer { /* Landscape / wide: board left, game panel right (chess.com style). */
flex-direction: column;
order: -1;
flex-shrink: 1;
font-size: large;
margin: 5vw;
}
#buttonContainer {
flex-grow: 3;
}
/* Put UI to left/right when screen is short */
@media (min-aspect-ratio: 1/1) { @media (min-aspect-ratio: 1/1) {
#chessContainer { #chessContainer {
display: grid;
grid-template-columns: auto, auto;
grid-template-rows: auto, auto;
align-items: center;
flex-direction: row; flex-direction: row;
align-items: center;
justify-content: center; justify-content: center;
align-items: center; gap: clamp(16px, 3vw, 48px);
height: 100vh; padding: clamp(12px, 3vh, 28px);
padding: 0;
} }
.sideContent { #boardArea {
display: flex; width: auto;
flex-direction: column; height: 100%;
flex-grow: 1; justify-content: center;
flex-shrink: 0; flex: 0 0 auto;
align-items: center;
text-align: center;
padding: min(5vw, 5vh);
}
#boardContainer {
grid-row: 1 / span 2;
grid-column: 1;
}
#textContainer {
grid-row: 1;
grid-column: 2;
text-align: center;
font-size: x-large;
}
#buttonContainer {
grid-row: 2;
grid-column: 2;
text-align: left;
align-self: start;
}
#startGameBtn {
padding: 1vw;
font-size: large;
}
#startCPUGame {
padding: 1vw;
margin: 1vw;
font-size: large;
} }
#chessBoard { #chessBoard {
aspect-ratio: 1 / 1; width: min(72vh, 54vw);
flex-shrink: 1; height: min(72vh, 54vw);
width: min(90vh, 90vw);
max-height: 90vh;
} }
#difficultyButtonContainer { .playerBar {
grid-template-columns: repeat(10, 1fr); width: min(72vh, 54vw);
}
#gamePanel {
width: clamp(300px, 26vw, 380px);
height: min(86vh, 100%);
align-self: center;
}
}
/* ---- Player bars ------------------------------------------------------- */
.playerBar {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
box-sizing: border-box;
height: var(--bar-h);
padding: 0 12px;
background: var(--bar);
border-radius: 8px;
}
.playerDot {
width: 14px;
height: 14px;
border-radius: 50%;
flex: 0 0 auto;
}
.playerDot.white {
background: #ededed;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4) inset;
}
.playerDot.black {
background: #2c2c2c;
box-shadow: 0 0 0 1px #565656 inset;
}
.playerName {
font-weight: 600;
white-space: nowrap;
}
.capturedTray {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1px;
flex: 1;
min-width: 0;
overflow: hidden;
}
.capturedPiece {
height: clamp(15px, calc(var(--bar-h) * 0.58), 26px);
width: auto;
margin-right: -5px;
}
.advantage {
margin-left: auto;
font-weight: 700;
color: var(--accent);
white-space: nowrap;
}
/* ---- Game panel -------------------------------------------------------- */
.panelHeader {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
flex: 0 0 auto;
}
.panelLogo {
font-size: 1.5rem;
color: var(--accent);
line-height: 1;
}
.panelHeader h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
letter-spacing: 0.01em;
}
#moveList {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 6px 0;
}
.movePlaceholder {
margin: 0;
padding: 18px 16px;
color: var(--muted);
font-size: 0.95rem;
}
.moveRow {
display: grid;
grid-template-columns: 2.4em 1fr 1fr;
align-items: center;
gap: 6px;
padding: 4px 14px;
font-size: 0.98rem;
}
.moveRow:nth-child(odd) {
background: var(--panel-row);
}
.moveNum {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.moveSan {
padding: 2px 7px;
border-radius: 5px;
}
.moveSan.latest {
background: var(--accent);
color: var(--accent-ink);
font-weight: 600;
}
#statusLine {
flex: 0 0 auto;
padding: 12px 16px;
border-top: 1px solid var(--line);
color: var(--muted);
font-weight: 500;
}
#statusLine.alert {
color: var(--accent);
font-weight: 700;
}
#panelButtons {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px 16px;
border-top: 1px solid var(--line);
}
.btn {
font-family: inherit;
font-weight: 600;
font-size: clamp(0.92rem, 1vw, 1.05rem);
border: 0;
border-radius: 8px;
padding: clamp(10px, 1.4vh, 14px) 16px;
cursor: pointer;
transition: transform 0.08s ease, filter 0.15s ease, background 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--accent);
color: var(--accent-ink);
}
.btn-primary:hover {
filter: brightness(1.08);
}
.btn-secondary {
background: #363a44;
color: var(--text);
}
.btn-secondary:hover {
background: #424752;
}
.btn-ghost {
background: transparent;
color: var(--accent);
border: 1px solid var(--line);
}
.btn-ghost:hover {
background: #2a2e37;
}
#watchLink {
color: var(--muted);
text-decoration: none;
text-align: center;
font-size: 0.9rem;
margin-top: 2px;
}
#watchLink:hover {
color: var(--accent);
}
/* ---- Mobile chrome (hidden on desktop) --------------------------------- */
.iconBtn {
background: transparent;
border: 0;
color: var(--text);
font-size: 1.7rem;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.iconBtn:hover {
color: var(--accent);
}
#menuClose {
margin-left: auto;
}
/* Default (tablet/desktop): no mobile bar, menu button, or sheet chrome. */
#mobileBar,
#menuBackdrop,
#menuClose {
display: none;
}
/* ---- Phone layout ------------------------------------------------------ */
/* The full panel doesn't fit alongside a usable board on a phone, so we show
the board + a slim status/menu bar, and tuck the move list and secondary
actions into a slide-up sheet. The page itself still never scrolls. */
@media (max-width: 640px) {
#chessContainer {
flex-direction: column;
justify-content: flex-start;
gap: 8px;
padding: 8px;
}
#boardArea {
width: min(94vw, 60vh);
flex: 0 0 auto;
}
.playerBar {
width: 100%;
}
/* Right rail becomes a hidden bottom sheet, revealed by the Menu button. */
#gamePanel {
display: none;
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
width: 100%;
height: auto;
max-height: 85dvh;
border-radius: 16px 16px 0 0;
z-index: 60;
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.5);
}
body.menu-open #gamePanel {
display: flex;
}
/* The slim bar already shows status, so hide the sheet's inline copy. */
#gamePanel #statusLine {
display: none;
}
#menuClose {
display: block;
}
#menuBackdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 55;
}
body.menu-open #menuBackdrop {
display: block;
}
#mobileBar {
display: flex;
align-items: center;
gap: 10px;
width: min(94vw, 60vh);
flex: 0 0 auto;
box-sizing: border-box;
background: var(--panel);
border-radius: 10px;
padding: 8px 10px;
}
#mobileStatus {
flex: 1;
min-width: 0;
color: var(--muted);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#mobileStatus.alert {
color: var(--accent);
}
#mobileBar #menuToggle {
flex: 0 0 auto;
padding: 10px 20px;
} }
} }