feat(ui): Board component — rotated grid, coloured pieces, highlights, ghosts
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import type { BoardId, Player, Square } from '../engine/types';
|
||||
import type { SelectionHighlight } from '../engine/legality';
|
||||
import { BOARD_ROTATION, BOARD_PLAYERS } from '../engine/boards';
|
||||
|
||||
interface Props {
|
||||
id: BoardId;
|
||||
fen: string;
|
||||
/** Player colours, e.g. { N:'#4a90d9', ... }. */
|
||||
colors: Record<Player, string>;
|
||||
ghosts: Square[];
|
||||
/** Highlight for this board, or null if no piece is grabbed / not active. */
|
||||
highlight: { playable: Square[]; local: Square[]; selected: Square | null } | null;
|
||||
active: boolean;
|
||||
onSquare: (square: Square) => void;
|
||||
}
|
||||
|
||||
let { id, fen, colors, ghosts, highlight, active, onSquare }: Props = $props();
|
||||
|
||||
const FILES = 'abcdefgh';
|
||||
const GLYPH: Record<string, string> = {
|
||||
k: '♚', q: '♛', r: '♜', b: '♝', n: '♞', p: '♟',
|
||||
};
|
||||
|
||||
interface Cell { square: Square; piece: { glyph: string; color: string } | null; }
|
||||
|
||||
let cells = $derived.by<Cell[]>(() => {
|
||||
const placement = fen.split(' ')[0];
|
||||
const white = colors[BOARD_PLAYERS[id].w];
|
||||
const black = colors[BOARD_PLAYERS[id].b];
|
||||
const map: Record<string, { glyph: string; color: string }> = {};
|
||||
placement.split('/').forEach((row, ri) => {
|
||||
const rank = 8 - ri;
|
||||
let file = 0;
|
||||
for (const ch of row) {
|
||||
if (/\d/.test(ch)) { file += Number(ch); continue; }
|
||||
const isWhite = ch === ch.toUpperCase();
|
||||
map[`${FILES[file]}${rank}`] = {
|
||||
glyph: GLYPH[ch.toLowerCase()],
|
||||
color: isWhite ? white : black,
|
||||
};
|
||||
file += 1;
|
||||
}
|
||||
});
|
||||
const out: Cell[] = [];
|
||||
for (let rank = 8; rank >= 1; rank--) {
|
||||
for (let f = 0; f < 8; f++) {
|
||||
const square = `${FILES[f]}${rank}`;
|
||||
out.push({ square, piece: map[square] ?? null });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
function classes(cell: Cell, index: number): string {
|
||||
const dark = (index + Math.floor(index / 8)) % 2 === 1;
|
||||
const hl = highlight;
|
||||
const list = ['sq', dark ? 'dark' : 'light'];
|
||||
if (ghosts.includes(cell.square)) list.push('ghost-sq');
|
||||
if (hl?.playable.includes(cell.square)) list.push(cell.piece ? 'play occ' : 'play');
|
||||
if (hl?.local.includes(cell.square)) list.push('local');
|
||||
if (hl?.selected === cell.square) list.push('selected');
|
||||
return list.join(' ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="board" class:active style="--rot:{BOARD_ROTATION[id]}deg">
|
||||
{#each cells as cell, i (cell.square)}
|
||||
<button class={classes(cell, i)} onclick={() => onSquare(cell.square)} aria-label={cell.square}>
|
||||
{#if cell.piece}
|
||||
<span class="pc" class:ghost={ghosts.includes(cell.square)}
|
||||
style="color:{cell.piece.color}">{cell.piece.glyph}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, var(--sq, 34px));
|
||||
grid-template-rows: repeat(8, var(--sq, 34px));
|
||||
transform: rotate(var(--rot));
|
||||
border: 1px solid #20232b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.board.active { box-shadow: 0 0 0 3px var(--glow, #4a90d9), 0 0 20px 2px var(--glow, #4a90d9); }
|
||||
.sq {
|
||||
position: relative; padding: 0; border: 0; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.sq.light { background: #cabf9f; }
|
||||
.sq.dark { background: #7d6f55; }
|
||||
.pc {
|
||||
font-size: calc(var(--sq, 34px) * 0.76); line-height: 1;
|
||||
text-shadow: -1px -1px 0 #15171c, 1px -1px 0 #15171c,
|
||||
-1px 1px 0 #15171c, 1px 1px 0 #15171c;
|
||||
}
|
||||
.pc.ghost { opacity: 0.42; }
|
||||
.ghost-sq { outline: 2px dashed #888; outline-offset: -2px; }
|
||||
.sq.play::after, .sq.local::after {
|
||||
content: ''; position: absolute; border-radius: 50%;
|
||||
width: 32%; height: 32%;
|
||||
}
|
||||
.sq.play::after { background: #46c24f; box-shadow: 0 0 7px #46c24f; }
|
||||
.sq.play.occ::after {
|
||||
width: 84%; height: 84%; background: transparent;
|
||||
border: 3px solid #46c24f; box-shadow: 0 0 7px #46c24f;
|
||||
}
|
||||
.sq.local::after { background: transparent; border: 2px dashed #9aa0aa; }
|
||||
.sq.selected { outline: 3px solid #3fd9d9; outline-offset: -3px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user