Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions src/apps/chess/components/ChessTable.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
title, header row, status line, buttons — is castle's
(CastleTable.module.css), as rummy's is. */

/* The table fills the overlay's height, so the board can take what the
rest leaves of it. */
.table {
width: 100%;
max-width: 560px;
height: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -54,24 +57,34 @@
}

.play {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

/* The board fits the narrower of the column and the screen's height less
the header, both clocks and the actions (about 19rem), so the whole of
it is on screen; below 16rem the overlay scrolls instead of shrinking
the squares past a tap. The pieces are sized from the board, not the
screen, so they always fit their squares. */
/* What the clocks, header and actions leave, measured rather than
budgeted: the board is the larger square that fits it. Below 15rem
the overlay scrolls rather than shrink the squares past a tap. */
.boardSlot {
flex: 1 1 0;
min-height: 15rem;
container-type: size;
display: flex;
align-items: center;
justify-content: center;
}

/* The pieces are sized from the board, not the screen, so they always
fit their squares. */
.board {
position: relative;
container-type: inline-size;
display: grid;
grid-template-columns: repeat(8, 1fr);
width: max(16rem, min(100%, calc(100dvh - 19rem)));
width: min(100cqw, 100cqh);
aspect-ratio: 1;
align-self: center;
border: 2px solid rgba(0, 0, 0, 0.5);
border-radius: 4px;
overflow: hidden;
Expand Down Expand Up @@ -365,3 +378,28 @@
background: radial-gradient(circle, #f0c96a 20%, #c27a1a 100%);
outline: none;
}

/* A phone on its side: too short for the board between its clocks, so the
board takes the left and everything else stacks to its right. */
@media (orientation: landscape) and (max-height: 500px) {
.table {
max-width: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-auto-rows: min-content;
align-content: start;
column-gap: 1rem;
}

.play {
display: contents;
}

.boardSlot {
grid-column: 1;
grid-row: 1 / span 6;
width: calc(100dvh - 60px - 4.5rem);
height: calc(100dvh - 60px - 4.5rem);
min-height: 0;
}
}
190 changes: 96 additions & 94 deletions src/apps/chess/components/ChessTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,101 +283,103 @@ const ChessTable = ({ playerId, connected, view, table }: ChessTableProps) => {
<span id={targetNote} className={felt.srOnly}>
a move
</span>
<div
ref={boardRef}
className={styles.board}
role="group"
aria-label="board"
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
>
{squares.map((square, index) => {
const piece = shown.get(square)
const mine = piece !== undefined && colorOfPiece(piece) === myColor
// a1 dark: a square is dark where its file and rank index sum even.
const light = (square.charCodeAt(0) - 97 + Number(square[1])) % 2 === 0
const target = targets.includes(square)
const grab = mine && myTurn && connected && targetsFrom(view.legalMoves, square).length > 0
return (
<button
key={square}
type="button"
data-square={square}
className={`${styles.square} ${light ? styles.light : styles.dark} ${target ? styles.target : ''}`}
aria-label={piece === undefined ? square : `${square}, ${pieceName(piece)}`}
aria-pressed={mine ? from === square : undefined}
aria-disabled={!myTurn || !connected ? true : undefined}
data-shade={light ? 'light' : 'dark'}
aria-describedby={target ? targetNote : undefined}
data-last={last.includes(square) ? 'true' : undefined}
data-check={piece !== undefined && piece === checkedKing ? 'true' : undefined}
data-grab={grab ? 'true' : undefined}
data-dragging={ghost !== null && from === square ? 'true' : undefined}
// Under the picker's scrim, out of reach of the keyboard as of the pointer.
inert={pendingPromotion !== null}
onPointerDown={event => onPointerDown(event, square)}
onClick={() => tap(square)}
<div className={styles.boardSlot}>
<div
ref={boardRef}
className={styles.board}
role="group"
aria-label="board"
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
>
{squares.map((square, index) => {
const piece = shown.get(square)
const mine = piece !== undefined && colorOfPiece(piece) === myColor
// a1 dark: a square is dark where its file and rank index sum even.
const light = (square.charCodeAt(0) - 97 + Number(square[1])) % 2 === 0
const target = targets.includes(square)
const grab = mine && myTurn && connected && targetsFrom(view.legalMoves, square).length > 0
return (
<button
key={square}
type="button"
data-square={square}
className={`${styles.square} ${light ? styles.light : styles.dark} ${target ? styles.target : ''}`}
aria-label={piece === undefined ? square : `${square}, ${pieceName(piece)}`}
aria-pressed={mine ? from === square : undefined}
aria-disabled={!myTurn || !connected ? true : undefined}
data-shade={light ? 'light' : 'dark'}
aria-describedby={target ? targetNote : undefined}
data-last={last.includes(square) ? 'true' : undefined}
data-check={piece !== undefined && piece === checkedKing ? 'true' : undefined}
data-grab={grab ? 'true' : undefined}
data-dragging={ghost !== null && from === square ? 'true' : undefined}
// Under the picker's scrim, out of reach of the keyboard as of the pointer.
inert={pendingPromotion !== null}
onPointerDown={event => onPointerDown(event, square)}
onClick={() => tap(square)}
>
{index % 8 === 0 && (
<span className={styles.rank} aria-hidden="true" data-testid="coordinate">
{square[1]}
</span>
)}
{index >= 56 && (
<span className={styles.file} aria-hidden="true" data-testid="coordinate">
{square[0]}
</span>
)}
{piece !== undefined && (
<span className={colorOfPiece(piece) === 'white' ? styles.whitePiece : styles.blackPiece} aria-hidden="true">
{glyph(piece)}
</span>
)}
</button>
)
})}
{/* Lichess's picker: the pieces stacked down the file from the
promotion square, which is always on the viewer's far rank,
over a dimmed board a tap on which lets the pawn go back. */}
{pendingPromotion !== null && (
<div
className={styles.promotion}
role="group"
aria-label="promote to"
onKeyDown={event => {
if (event.key === 'Escape') {
event.preventDefault()
cancelPromotion()
}
}}
>
{index % 8 === 0 && (
<span className={styles.rank} aria-hidden="true" data-testid="coordinate">
{square[1]}
</span>
)}
{index >= 56 && (
<span className={styles.file} aria-hidden="true" data-testid="coordinate">
{square[0]}
</span>
)}
{piece !== undefined && (
<span className={colorOfPiece(piece) === 'white' ? styles.whitePiece : styles.blackPiece} aria-hidden="true">
{glyph(piece)}
</span>
)}
</button>
)
})}
{/* Lichess's picker: the pieces stacked down the file from the
promotion square, which is always on the viewer's far rank,
over a dimmed board a tap on which lets the pawn go back. */}
{pendingPromotion !== null && (
<div
className={styles.promotion}
role="group"
aria-label="promote to"
onKeyDown={event => {
if (event.key === 'Escape') {
event.preventDefault()
cancelPromotion()
}
}}
>
<ul className={styles.promotionFile} style={{ left: `${(squares.indexOf(pendingPromotion.to) % 8) * 12.5}%` }}>
{PROMOTIONS.map(({ letter, name }, i) => {
const uci = pendingPromotion.moves.find(move => move.endsWith(letter))
if (uci === undefined) return null
return (
<li key={letter}>
<button
type="button"
className={`${styles.promotionPiece} ${myColor === 'white' ? styles.whitePiece : styles.blackPiece}`}
aria-label={name}
autoFocus={i === 0}
onClick={() => {
refocus.current = { square: uci.slice(2, 4) }
setPromoting(null)
table.play(uci)
}}
>
<span aria-hidden="true">{glyph(myColor === 'white' ? letter.toUpperCase() : letter)}</span>
</button>
</li>
)
})}
</ul>
<button type="button" className={styles.promotionScrim} aria-label="Cancel" onClick={cancelPromotion} />
</div>
)}
<ul className={styles.promotionFile} style={{ left: `${(squares.indexOf(pendingPromotion.to) % 8) * 12.5}%` }}>
{PROMOTIONS.map(({ letter, name }, i) => {
const uci = pendingPromotion.moves.find(move => move.endsWith(letter))
if (uci === undefined) return null
return (
<li key={letter}>
<button
type="button"
className={`${styles.promotionPiece} ${myColor === 'white' ? styles.whitePiece : styles.blackPiece}`}
aria-label={name}
autoFocus={i === 0}
onClick={() => {
refocus.current = { square: uci.slice(2, 4) }
setPromoting(null)
table.play(uci)
}}
>
<span aria-hidden="true">{glyph(myColor === 'white' ? letter.toUpperCase() : letter)}</span>
</button>
</li>
)
})}
</ul>
<button type="button" className={styles.promotionScrim} aria-label="Cancel" onClick={cancelPromotion} />
</div>
)}
</div>
</div>
{me?.color !== undefined && <ClockRow view={view} seatId={me.playerId} color={me.color} you />}
{/* On the page, not the board: the board clips its overflow, and a
Expand Down
Loading