927 lines
27 KiB
Svelte
927 lines
27 KiB
Svelte
<script>
|
||
import { onMount, onDestroy } from 'svelte';
|
||
import { browser } from '$app/environment';
|
||
import { gamesOverlay, hasGame, currentGame, gameMode } from '$lib/stores/gamesOverlay';
|
||
import { nakama, ChessOpCode } from '$lib/stores/nakama';
|
||
import { auth } from '$lib/stores/auth';
|
||
|
||
// Chess state
|
||
let game = null;
|
||
let Chess = null;
|
||
let boardSquares = [];
|
||
let selectedSquare = null;
|
||
let legalMoves = [];
|
||
let moveHistory = [];
|
||
|
||
// Match state from overlay store
|
||
let myColor = null;
|
||
|
||
// Drag state
|
||
let isDragging = false;
|
||
let dragOffset = { x: 0, y: 0 };
|
||
let position = { x: null, y: null };
|
||
let isResizing = false;
|
||
let resizeDirection = '';
|
||
let dimensions = { width: 450, height: 520 };
|
||
let startDimensions = { width: 0, height: 0 };
|
||
let startPos = { x: 0, y: 0 };
|
||
|
||
// Callbacks cleanup
|
||
let unsubscribeMatch = null;
|
||
let isDestroyed = false;
|
||
|
||
// Load/save position
|
||
function loadPosition() {
|
||
if (!browser) return;
|
||
try {
|
||
const saved = localStorage.getItem('chessGameOverlayPosition');
|
||
if (saved) {
|
||
const parsed = JSON.parse(saved);
|
||
position = parsed.position || { x: null, y: null };
|
||
dimensions = parsed.dimensions || { width: 450, height: 520 };
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
function savePosition() {
|
||
if (!browser) return;
|
||
try {
|
||
localStorage.setItem('chessGameOverlayPosition', JSON.stringify({
|
||
position,
|
||
dimensions
|
||
}));
|
||
} catch (e) {}
|
||
}
|
||
|
||
// Computed position (default to center-right if not set)
|
||
$: computedPosition = {
|
||
right: position.x === null ? '1rem' : 'auto',
|
||
top: position.y === null ? '50%' : 'auto',
|
||
left: position.x !== null ? `${position.x}px` : 'auto',
|
||
bottom: position.y !== null ? `${position.y}px` : 'auto',
|
||
transform: position.y === null ? 'translateY(-50%)' : 'none'
|
||
};
|
||
|
||
// Calculate chess board cell size based on container dimensions
|
||
// Account for header (~45px), footer (~45px), board border/padding (~20px)
|
||
$: cellSize = Math.floor(Math.min(dimensions.width - 30, dimensions.height - 120) / 8);
|
||
|
||
async function initChess() {
|
||
console.log('[ChessOverlay] initChess called, matchId:', $gamesOverlay.matchId);
|
||
if (!browser) return;
|
||
|
||
try {
|
||
const chessModule = await import('chess.js');
|
||
Chess = chessModule.Chess;
|
||
game = new Chess();
|
||
|
||
// Set up match event handler
|
||
console.log('[ChessOverlay] Registering match event handler...');
|
||
unsubscribeMatch = nakama.onMatchEvent(handleMatchEvent);
|
||
console.log('[ChessOverlay] Handler registered');
|
||
|
||
updateBoardDisplay();
|
||
} catch (e) {
|
||
console.error('[ChessOverlay] Failed to initialize chess:', e);
|
||
}
|
||
}
|
||
|
||
function handleMatchEvent(type, data) {
|
||
if (isDestroyed) return;
|
||
|
||
console.log('[Chess] handleMatchEvent:', type, 'op_code:', data?.op_code);
|
||
|
||
if (type === 'disconnect') {
|
||
gamesOverlay.setMode('finished');
|
||
gamesOverlay.updateState({ result: 'disconnect', reason: 'Disconnected from server' });
|
||
return;
|
||
}
|
||
|
||
if (type !== 'matchdata') return;
|
||
|
||
try {
|
||
const payload = JSON.parse(new TextDecoder().decode(data.data));
|
||
// Convert op_code to number for safe comparison (Nakama SDK uses snake_case)
|
||
const opCode = Number(data.op_code);
|
||
|
||
console.log('[Chess] Received op_code:', opCode, 'payload:', payload);
|
||
|
||
if (opCode === ChessOpCode.GAME_STATE) {
|
||
console.log('[Chess] Processing GAME_STATE with status:', payload.status);
|
||
handleGameState(payload);
|
||
} else if (opCode === ChessOpCode.MOVE) {
|
||
handleOpponentMove(payload);
|
||
} else if (opCode === ChessOpCode.GAME_OVER) {
|
||
handleGameOver(payload);
|
||
} else {
|
||
console.log('[Chess] Unknown opCode:', opCode);
|
||
}
|
||
} catch (e) {
|
||
console.error('Error handling match event:', e, data);
|
||
}
|
||
}
|
||
|
||
function handleGameState(payload) {
|
||
if (payload.status === 'waiting') {
|
||
myColor = payload.yourColor;
|
||
|
||
// Load the Chess960 starting position
|
||
if (game && payload.fen) {
|
||
game.load(payload.fen);
|
||
updateBoardDisplay();
|
||
}
|
||
|
||
gamesOverlay.setMode('waiting');
|
||
gamesOverlay.updateState({
|
||
positionId: payload.positionId,
|
||
fen: payload.fen,
|
||
whiteName: payload.whiteName,
|
||
blackName: payload.blackName
|
||
});
|
||
} else if (payload.status === 'playing') {
|
||
const session = nakama.getSession();
|
||
myColor = payload.whiteId === session?.user_id ? 'w' : 'b';
|
||
|
||
if (game) {
|
||
game.load(payload.fen);
|
||
}
|
||
|
||
gamesOverlay.setMode('playing');
|
||
gamesOverlay.updateState({
|
||
positionId: payload.positionId,
|
||
fen: payload.fen,
|
||
turn: payload.turn || game?.turn(),
|
||
whiteId: payload.whiteId,
|
||
blackId: payload.blackId,
|
||
whiteName: payload.whiteName,
|
||
blackName: payload.blackName,
|
||
myColor
|
||
});
|
||
|
||
updateBoardDisplay();
|
||
} else if (payload.status === 'spectating') {
|
||
myColor = null; // Spectator has no color
|
||
|
||
if (game) {
|
||
game.load(payload.fen);
|
||
}
|
||
|
||
gamesOverlay.setMode('spectating');
|
||
gamesOverlay.updateState({
|
||
positionId: payload.positionId,
|
||
fen: payload.fen,
|
||
turn: payload.turn || game?.turn(),
|
||
whiteId: payload.whiteId,
|
||
blackId: payload.blackId,
|
||
whiteName: payload.whiteName,
|
||
blackName: payload.blackName
|
||
});
|
||
|
||
updateBoardDisplay();
|
||
}
|
||
}
|
||
|
||
function handleOpponentMove(payload) {
|
||
if (game) {
|
||
game.load(payload.fen);
|
||
}
|
||
moveHistory = [...moveHistory, payload.move];
|
||
|
||
gamesOverlay.updateState({
|
||
fen: payload.fen,
|
||
turn: game?.turn()
|
||
});
|
||
|
||
updateBoardDisplay();
|
||
}
|
||
|
||
function handleGameOver(payload) {
|
||
gamesOverlay.setMode('finished');
|
||
gamesOverlay.updateState({
|
||
result: payload.result,
|
||
reason: payload.reason
|
||
});
|
||
}
|
||
|
||
function updateBoardDisplay() {
|
||
if (!game) return;
|
||
|
||
const board2d = [];
|
||
const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
||
// Spectators see from white's perspective, players see from their own side
|
||
const viewColor = myColor || 'w';
|
||
const ranks = viewColor === 'b' ? ['1', '2', '3', '4', '5', '6', '7', '8'] : ['8', '7', '6', '5', '4', '3', '2', '1'];
|
||
|
||
for (const rank of ranks) {
|
||
const row = [];
|
||
for (const file of files) {
|
||
const square = file + rank;
|
||
const piece = game.get(square);
|
||
row.push({
|
||
square,
|
||
piece: piece ? getPieceSymbol(piece) : null,
|
||
color: piece?.color || null,
|
||
isLight: (files.indexOf(file) + parseInt(rank)) % 2 === 1
|
||
});
|
||
}
|
||
board2d.push(row);
|
||
}
|
||
|
||
boardSquares = board2d;
|
||
}
|
||
|
||
function getPieceSymbol(piece) {
|
||
const symbols = {
|
||
'wk': '\u2654', 'wq': '\u2655', 'wr': '\u2656', 'wb': '\u2657', 'wn': '\u2658', 'wp': '\u2659',
|
||
'bk': '\u265A', 'bq': '\u265B', 'br': '\u265C', 'bb': '\u265D', 'bn': '\u265E', 'bp': '\u265F'
|
||
};
|
||
return symbols[piece.color + piece.type] || '';
|
||
}
|
||
|
||
function handleSquareClick(square) {
|
||
if ($gameMode !== 'playing') return;
|
||
if (!game || game.turn() !== myColor) return;
|
||
|
||
const piece = game.get(square);
|
||
|
||
if (selectedSquare) {
|
||
if (legalMoves.includes(square)) {
|
||
makeMove(selectedSquare, square);
|
||
}
|
||
selectedSquare = null;
|
||
legalMoves = [];
|
||
} else if (piece && piece.color === myColor) {
|
||
selectedSquare = square;
|
||
const moves = game.moves({ square, verbose: true });
|
||
legalMoves = moves.map(m => m.to);
|
||
}
|
||
|
||
updateBoardDisplay();
|
||
}
|
||
|
||
async function makeMove(from, to) {
|
||
if (!game) return;
|
||
|
||
const piece = game.get(from);
|
||
const isPromotion = piece.type === 'p' &&
|
||
((piece.color === 'w' && to[1] === '8') || (piece.color === 'b' && to[1] === '1'));
|
||
|
||
const moveData = { from, to };
|
||
if (isPromotion) {
|
||
moveData.promotion = 'q';
|
||
}
|
||
|
||
const result = game.move(moveData);
|
||
if (!result) {
|
||
console.error('Invalid move');
|
||
return;
|
||
}
|
||
|
||
await nakama.sendMatchData($gamesOverlay.matchId, ChessOpCode.MOVE, moveData);
|
||
|
||
moveHistory = [...moveHistory, result];
|
||
gamesOverlay.updateState({
|
||
fen: game.fen(),
|
||
turn: game.turn()
|
||
});
|
||
updateBoardDisplay();
|
||
}
|
||
|
||
async function resign() {
|
||
if ($gameMode !== 'playing') return;
|
||
if (confirm('Are you sure you want to resign?')) {
|
||
await nakama.sendMatchData($gamesOverlay.matchId, ChessOpCode.RESIGN, {});
|
||
}
|
||
}
|
||
|
||
function copyInviteLink() {
|
||
const url = `${window.location.origin}/games/chess?match=${$gamesOverlay.matchId}`;
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
alert('Invite link copied!');
|
||
});
|
||
}
|
||
|
||
function handleClose() {
|
||
if ($gamesOverlay.matchId) {
|
||
nakama.leaveMatch($gamesOverlay.matchId);
|
||
}
|
||
|
||
if (unsubscribeMatch) {
|
||
unsubscribeMatch();
|
||
unsubscribeMatch = null;
|
||
}
|
||
|
||
game = null;
|
||
boardSquares = [];
|
||
selectedSquare = null;
|
||
legalMoves = [];
|
||
moveHistory = [];
|
||
myColor = null;
|
||
|
||
gamesOverlay.closeGame();
|
||
}
|
||
|
||
function handleMinimize() {
|
||
gamesOverlay.toggleMinimized();
|
||
}
|
||
|
||
// Drag handling
|
||
function startDrag(e) {
|
||
if (e.target.closest('button')) return;
|
||
|
||
isDragging = true;
|
||
const rect = e.currentTarget.closest('.chess-overlay').getBoundingClientRect();
|
||
dragOffset = {
|
||
x: e.clientX - rect.left,
|
||
y: e.clientY - rect.top
|
||
};
|
||
e.preventDefault();
|
||
}
|
||
|
||
function handleMouseMove(e) {
|
||
if (isDragging) {
|
||
position = {
|
||
x: e.clientX - dragOffset.x,
|
||
y: window.innerHeight - (e.clientY - dragOffset.y) - dimensions.height
|
||
};
|
||
} else if (isResizing) {
|
||
const dx = e.clientX - startPos.x;
|
||
const dy = e.clientY - startPos.y;
|
||
|
||
let newWidth = startDimensions.width;
|
||
let newHeight = startDimensions.height;
|
||
|
||
if (resizeDirection.includes('e')) {
|
||
newWidth = Math.max(400, startDimensions.width + dx);
|
||
}
|
||
if (resizeDirection.includes('w')) {
|
||
newWidth = Math.max(400, startDimensions.width - dx);
|
||
}
|
||
if (resizeDirection.includes('s')) {
|
||
newHeight = Math.max(450, startDimensions.height + dy);
|
||
}
|
||
if (resizeDirection.includes('n')) {
|
||
newHeight = Math.max(450, startDimensions.height - dy);
|
||
}
|
||
|
||
dimensions = { width: newWidth, height: newHeight };
|
||
}
|
||
}
|
||
|
||
function stopDrag() {
|
||
if (isDragging || isResizing) {
|
||
savePosition();
|
||
}
|
||
isDragging = false;
|
||
isResizing = false;
|
||
resizeDirection = '';
|
||
}
|
||
|
||
function startResize(e, direction) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
isResizing = true;
|
||
resizeDirection = direction;
|
||
startPos = { x: e.clientX, y: e.clientY };
|
||
startDimensions = { ...dimensions };
|
||
}
|
||
|
||
// Watch for overlay changes
|
||
let currentMatchId = null;
|
||
$: if (browser && $gamesOverlay.enabled && $gamesOverlay.matchId && $gamesOverlay.matchId !== currentMatchId) {
|
||
console.log('[ChessOverlay] Reactive: matchId changed from', currentMatchId, 'to', $gamesOverlay.matchId);
|
||
currentMatchId = $gamesOverlay.matchId;
|
||
initChess();
|
||
}
|
||
|
||
// Cleanup when game is closed
|
||
$: if (!$hasGame) {
|
||
currentMatchId = null;
|
||
if (unsubscribeMatch) {
|
||
unsubscribeMatch();
|
||
unsubscribeMatch = null;
|
||
}
|
||
game = null;
|
||
boardSquares = [];
|
||
moveHistory = [];
|
||
myColor = null;
|
||
}
|
||
|
||
onMount(() => {
|
||
loadPosition();
|
||
if (browser) {
|
||
window.addEventListener('mousemove', handleMouseMove);
|
||
window.addEventListener('mouseup', stopDrag);
|
||
|
||
if ($gamesOverlay.enabled && $gamesOverlay.matchId) {
|
||
currentMatchId = $gamesOverlay.matchId;
|
||
initChess();
|
||
}
|
||
}
|
||
});
|
||
|
||
onDestroy(() => {
|
||
isDestroyed = true;
|
||
if (unsubscribeMatch) {
|
||
unsubscribeMatch();
|
||
}
|
||
if (browser) {
|
||
window.removeEventListener('mousemove', handleMouseMove);
|
||
window.removeEventListener('mouseup', stopDrag);
|
||
}
|
||
});
|
||
|
||
// Computed values for display
|
||
$: statusMessage = (() => {
|
||
switch ($gameMode) {
|
||
case 'waiting': return 'Waiting for opponent...';
|
||
case 'playing':
|
||
if (!game) return 'Loading...';
|
||
return game.turn() === myColor ? 'Your turn' : "Opponent's turn";
|
||
case 'spectating':
|
||
if (!$currentGame) return 'Spectating...';
|
||
return `${$currentGame.turn === 'w' ? 'White' : 'Black'} to move`;
|
||
case 'finished':
|
||
return getResultText();
|
||
default: return 'Connecting...';
|
||
}
|
||
})();
|
||
|
||
function getResultText() {
|
||
if (!$currentGame) return 'Game over';
|
||
const result = $currentGame.result;
|
||
const reason = $currentGame.reason || '';
|
||
|
||
if ($gameMode === 'spectating') {
|
||
if (result === '1-0') return `White wins${reason ? ` (${reason})` : ''}`;
|
||
if (result === '0-1') return `Black wins${reason ? ` (${reason})` : ''}`;
|
||
return `Draw${reason ? ` (${reason})` : ''}`;
|
||
}
|
||
|
||
if (result === '1-0') {
|
||
return myColor === 'w' ? 'You win!' : 'You lose';
|
||
} else if (result === '0-1') {
|
||
return myColor === 'b' ? 'You win!' : 'You lose';
|
||
} else if (result === 'timeout') {
|
||
return reason || 'Match timed out';
|
||
}
|
||
return `Draw${reason ? ` (${reason})` : ''}`;
|
||
}
|
||
|
||
$: headerTitle = (() => {
|
||
if (!$currentGame) return 'Chess960';
|
||
const white = $currentGame.whiteName || 'Waiting...';
|
||
const black = $currentGame.blackName || 'Waiting...';
|
||
return `${white} vs ${black}`;
|
||
})();
|
||
|
||
$: positionLabel = $currentGame?.positionId !== undefined
|
||
? `#${$currentGame.positionId}`
|
||
: '';
|
||
</script>
|
||
|
||
{#if $gamesOverlay.enabled && $gamesOverlay.matchId}
|
||
<div
|
||
class="chess-overlay"
|
||
class:minimized={$gamesOverlay.minimized}
|
||
style="
|
||
{computedPosition.right !== 'auto' ? `right: ${computedPosition.right};` : ''}
|
||
{computedPosition.top !== 'auto' ? `top: ${computedPosition.top};` : ''}
|
||
{computedPosition.left !== 'auto' ? `left: ${computedPosition.left};` : ''}
|
||
{computedPosition.bottom !== 'auto' ? `bottom: ${computedPosition.bottom};` : ''}
|
||
{computedPosition.transform !== 'none' ? `transform: ${computedPosition.transform};` : ''}
|
||
width: {dimensions.width}px;
|
||
{!$gamesOverlay.minimized ? `height: ${dimensions.height}px;` : ''}
|
||
"
|
||
>
|
||
<div class="overlay-header" on:mousedown={startDrag}>
|
||
<div class="header-left">
|
||
<span class="header-icon">♛</span>
|
||
<div class="header-info">
|
||
<span class="header-title">{headerTitle}</span>
|
||
{#if positionLabel}
|
||
<span class="header-meta">Position {positionLabel}</span>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
<div class="header-controls">
|
||
{#if $gameMode === 'spectating'}
|
||
<span class="mode-badge spectator">Spectating</span>
|
||
{/if}
|
||
<button class="control-btn" on:click={handleMinimize} title={$gamesOverlay.minimized ? 'Expand' : 'Minimize'}>
|
||
{$gamesOverlay.minimized ? '□' : '−'}
|
||
</button>
|
||
<button class="control-btn close" on:click={handleClose} title="Close">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
{#if !$gamesOverlay.minimized}
|
||
<div class="overlay-body">
|
||
<!-- Status Bar -->
|
||
<div class="status-bar" class:your-turn={$gameMode === 'playing' && game?.turn() === myColor}>
|
||
{#if $gameMode === 'playing' && myColor}
|
||
<span class="color-indicator" class:white={myColor === 'w'} class:black={myColor === 'b'}>
|
||
{myColor === 'w' ? 'White' : 'Black'}
|
||
</span>
|
||
{/if}
|
||
<span class="status-text">{statusMessage}</span>
|
||
</div>
|
||
|
||
<!-- Chess Board -->
|
||
<div class="board-wrapper">
|
||
<div class="chess-board">
|
||
{#each boardSquares as row}
|
||
<div class="board-row">
|
||
{#each row as cell}
|
||
<div
|
||
class="board-cell"
|
||
class:light={cell.isLight}
|
||
class:dark={!cell.isLight}
|
||
class:selected={selectedSquare === cell.square}
|
||
class:legal-move={legalMoves.includes(cell.square)}
|
||
class:clickable={$gameMode === 'playing'}
|
||
style="width: {cellSize}px; height: {cellSize}px;"
|
||
on:click={() => handleSquareClick(cell.square)}
|
||
>
|
||
{#if cell.piece}
|
||
<span class="piece" style="font-size: {cellSize * 0.7}px;" class:white-piece={cell.color === 'w'} class:black-piece={cell.color === 'b'}>
|
||
{cell.piece}
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overlay-footer">
|
||
{#if $gameMode === 'waiting'}
|
||
<button class="action-btn" on:click={copyInviteLink}>Copy Invite Link</button>
|
||
{:else if $gameMode === 'playing'}
|
||
<button class="action-btn danger" on:click={resign}>Resign</button>
|
||
{:else if $gameMode === 'finished'}
|
||
<button class="action-btn" on:click={handleClose}>Close</button>
|
||
{/if}
|
||
|
||
<div class="move-count">
|
||
{moveHistory.length} moves
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Resize handles -->
|
||
<div class="resize-handle resize-e" on:mousedown={(e) => startResize(e, 'e')}></div>
|
||
<div class="resize-handle resize-s" on:mousedown={(e) => startResize(e, 's')}></div>
|
||
<div class="resize-handle resize-se" on:mousedown={(e) => startResize(e, 'se')}></div>
|
||
<div class="resize-handle resize-w" on:mousedown={(e) => startResize(e, 'w')}></div>
|
||
<div class="resize-handle resize-sw" on:mousedown={(e) => startResize(e, 'sw')}></div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
|
||
<style>
|
||
.chess-overlay {
|
||
position: fixed;
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||
z-index: 9998;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||
min-width: 400px;
|
||
min-height: 450px;
|
||
max-width: calc(100vw - 2rem);
|
||
max-height: calc(100vh - 2rem);
|
||
}
|
||
|
||
.chess-overlay.minimized {
|
||
height: auto !important;
|
||
min-height: auto;
|
||
}
|
||
|
||
.overlay-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem 0.75rem;
|
||
background: #0d1117;
|
||
border-bottom: 1px solid #30363d;
|
||
cursor: move;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.header-icon {
|
||
font-size: 1.2rem;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.header-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.1rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.header-title {
|
||
color: #c9d1d9;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.header-meta {
|
||
color: #6e7681;
|
||
font-size: 0.65rem;
|
||
}
|
||
|
||
.header-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mode-badge {
|
||
font-size: 0.6rem;
|
||
padding: 0.15rem 0.4rem;
|
||
border-radius: 3px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.mode-badge.spectator {
|
||
background: rgba(139, 92, 246, 0.2);
|
||
color: #a78bfa;
|
||
}
|
||
|
||
.control-btn {
|
||
width: 22px;
|
||
height: 22px;
|
||
border: none;
|
||
background: none;
|
||
color: #8b949e;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.control-btn:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: #c9d1d9;
|
||
}
|
||
|
||
.control-btn.close:hover {
|
||
color: #f85149;
|
||
}
|
||
|
||
.overlay-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0.5rem;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.4rem 0.6rem;
|
||
background: #1a1a2e;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.status-bar.your-turn {
|
||
background: #2a4a2a;
|
||
}
|
||
|
||
.color-indicator {
|
||
font-weight: bold;
|
||
font-size: 0.7rem;
|
||
padding: 0.15rem 0.4rem;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.color-indicator.white {
|
||
background: #fff;
|
||
color: #000;
|
||
}
|
||
|
||
.color-indicator.black {
|
||
background: #333;
|
||
color: #fff;
|
||
}
|
||
|
||
.status-text {
|
||
color: #ccc;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.board-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 0;
|
||
}
|
||
|
||
.chess-board {
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 3px solid #333;
|
||
background: #333;
|
||
}
|
||
|
||
.board-row {
|
||
display: flex;
|
||
}
|
||
|
||
.board-cell {
|
||
width: 42px;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.board-cell.clickable {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.board-cell.light {
|
||
background: #f0d9b5;
|
||
}
|
||
|
||
.board-cell.dark {
|
||
background: #b58863;
|
||
}
|
||
|
||
.board-cell.selected {
|
||
background: #7fc97f !important;
|
||
}
|
||
|
||
.board-cell.legal-move::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 30%;
|
||
height: 30%;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.board-cell.legal-move:hover {
|
||
background: #aad4aa !important;
|
||
}
|
||
|
||
.piece {
|
||
font-size: 2rem;
|
||
line-height: 1;
|
||
user-select: none;
|
||
}
|
||
|
||
.white-piece {
|
||
color: #fff;
|
||
text-shadow: 0 0 2px #000;
|
||
}
|
||
|
||
.black-piece {
|
||
color: #000;
|
||
text-shadow: 0 0 2px #fff;
|
||
}
|
||
|
||
.overlay-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.5rem 0.75rem;
|
||
background: #0d1117;
|
||
border-top: 1px solid #30363d;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 0.4rem 0.8rem;
|
||
border: 1px solid #30363d;
|
||
background: #161b22;
|
||
color: #c9d1d9;
|
||
font-size: 0.7rem;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
background: #21262d;
|
||
border-color: #484f58;
|
||
}
|
||
|
||
.action-btn.danger {
|
||
border-color: rgba(248, 81, 73, 0.4);
|
||
color: #f85149;
|
||
}
|
||
|
||
.action-btn.danger:hover {
|
||
background: rgba(248, 81, 73, 0.15);
|
||
border-color: #f85149;
|
||
}
|
||
|
||
.move-count {
|
||
color: #6e7681;
|
||
font-size: 0.65rem;
|
||
}
|
||
|
||
/* Resize handles */
|
||
.resize-handle {
|
||
position: absolute;
|
||
background: transparent;
|
||
}
|
||
|
||
.resize-e {
|
||
top: 0;
|
||
right: 0;
|
||
width: 6px;
|
||
height: 100%;
|
||
cursor: ew-resize;
|
||
}
|
||
|
||
.resize-w {
|
||
top: 0;
|
||
left: 0;
|
||
width: 6px;
|
||
height: 100%;
|
||
cursor: ew-resize;
|
||
}
|
||
|
||
.resize-s {
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 6px;
|
||
cursor: ns-resize;
|
||
}
|
||
|
||
.resize-se {
|
||
bottom: 0;
|
||
right: 0;
|
||
width: 12px;
|
||
height: 12px;
|
||
cursor: se-resize;
|
||
}
|
||
|
||
.resize-sw {
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 12px;
|
||
height: 12px;
|
||
cursor: sw-resize;
|
||
}
|
||
|
||
.resize-handle:hover {
|
||
background: rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
@media (max-width: 500px) {
|
||
.chess-overlay {
|
||
right: 0.5rem !important;
|
||
left: 0.5rem !important;
|
||
top: auto !important;
|
||
bottom: 0.5rem !important;
|
||
width: auto !important;
|
||
transform: none !important;
|
||
max-width: none;
|
||
}
|
||
|
||
.board-cell {
|
||
width: 36px;
|
||
height: 36px;
|
||
}
|
||
|
||
.piece {
|
||
font-size: 1.7rem;
|
||
}
|
||
|
||
.resize-handle {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|