accessibility

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-11-11 23:39:16 +01:00
padre dd7c7b97e2
commit 7c4087a34c
Se han modificado 6 ficheros con 70 adiciones y 22 borrados

Ver fichero

@@ -131,7 +131,7 @@ export default function Home() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200">
{/* Header */}
<header className="bg-white shadow-md">
<header className="bg-white shadow-md" role="banner">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
@@ -148,12 +148,15 @@ export default function Home() {
<button
onClick={() => setShowRules(!showRules)}
className="text-gray-600 hover:text-gray-900 text-sm font-medium"
aria-label="Toggle game rules"
aria-expanded={showRules}
>
Rules
</button>
<button
onClick={leaveRoom}
className="bg-red-500 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-red-600 transition-colors"
aria-label="Leave current game"
>
Leave Game
</button>
@@ -169,6 +172,9 @@ export default function Home() {
animate={{ y: 0, opacity: 1 }}
exit={{ y: -100, opacity: 0 }}
className="fixed top-20 left-1/2 transform -translate-x-1/2 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
{error}
</motion.div>
@@ -184,6 +190,9 @@ export default function Home() {
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setShowRules(false)}
role="dialog"
aria-modal="true"
aria-labelledby="rules-title"
>
<motion.div
initial={{ scale: 0.9 }}
@@ -192,7 +201,7 @@ export default function Home() {
className="bg-white rounded-lg p-6 max-w-md"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-xl font-bold mb-4">How to Play</h3>
<h3 id="rules-title" className="text-xl font-bold mb-4">How to Play</h3>
<ul className="space-y-2 text-sm text-gray-700">
<li> Click on a tile to select it</li>
<li> Click &quot;Place Left&quot; or &quot;Place Right&quot; to place it</li>
@@ -204,6 +213,7 @@ export default function Home() {
<button
onClick={() => setShowRules(false)}
className="mt-4 w-full bg-blue-500 text-white py-2 rounded-lg"
aria-label="Close rules dialog"
>
Close
</button>
@@ -213,12 +223,12 @@ export default function Home() {
</AnimatePresence>
{/* Main game area */}
<div className="max-w-7xl mx-auto px-4 py-6">
<main className="max-w-7xl mx-auto px-4 py-6" role="main">
<div className="grid lg:grid-cols-[1fr_300px] gap-6">
{/* Left side - Game board and controls */}
<div className="space-y-4">
{/* Game info */}
<div className="bg-white rounded-lg shadow-md p-4">
<div className="bg-white rounded-lg shadow-md p-4" role="status" aria-live="polite" aria-atomic="true">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600">Current Turn</div>
@@ -255,11 +265,12 @@ export default function Home() {
)}
</div>
)}
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-3 flex-wrap" role="group" aria-label="Game controls">
<button
onClick={() => handlePlaceTile('left')}
disabled={!selectedTile}
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-lg transition-shadow"
aria-label={selectedTile ? `Place tile ${selectedTile.left}-${selectedTile.right} on the left side` : 'Place tile on left side (select a tile first)'}
>
Place Left
</button>
@@ -267,6 +278,7 @@ export default function Home() {
onClick={() => handlePlaceTile('right')}
disabled={!selectedTile}
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-lg transition-shadow"
aria-label={selectedTile ? `Place tile ${selectedTile.left}-${selectedTile.right} on the right side` : 'Place tile on right side (select a tile first)'}
>
Place Right
</button>
@@ -274,6 +286,7 @@ export default function Home() {
<button
onClick={drawTile}
className="flex-1 bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-shadow"
aria-label={`Draw a tile from the boneyard (${gameState.boneyard.length} tiles remaining)`}
>
Draw Tile
</button>
@@ -282,6 +295,7 @@ export default function Home() {
<button
onClick={handlePass}
className="flex-1 bg-gradient-to-r from-yellow-500 to-yellow-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-shadow"
aria-label="Pass your turn (no valid moves available)"
>
Pass Turn
</button>
@@ -303,7 +317,7 @@ export default function Home() {
</div>
{/* Right side - Other players */}
<div className="space-y-4">
<aside className="space-y-4" role="complementary" aria-label="Other players">
<h3 className="text-lg font-semibold text-gray-700">Players</h3>
{gameState.players
.filter(p => p.id !== currentPlayerId)
@@ -338,10 +352,10 @@ export default function Home() {
))}
</div>
</div>
))}
</div>
</div>
)}
</aside>
</div>
</main>
{/* Game over modal */}
{gameState.isGameOver && (

Ver fichero

@@ -163,7 +163,7 @@ export function GameBoard({ placedTiles, width = 1200, height = 700, className =
};
return (
<div className={className}>
<div className={className} role="region" aria-label="Game board">
<canvas
ref={canvasRef}
width={width}
@@ -173,6 +173,8 @@ export function GameBoard({ placedTiles, width = 1200, height = 700, className =
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
role="img"
aria-label={`Domino board with ${placedTiles.length} tiles placed`}
/>
{placedTiles.length > 0 && (
<div className="mt-2 text-center text-sm text-gray-600">

Ver fichero

@@ -17,6 +17,9 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="game-over-title"
>
<motion.div
initial={{ scale: 0.8, y: 50 }}
@@ -32,7 +35,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
>
{winner ? '🏆' : '🤝'}
</motion.div>
<h2 className="text-3xl font-bold text-gray-800 mb-2">
<h2 id="game-over-title" className="text-3xl font-bold text-gray-800 mb-2">
{winner ? 'Game Over!' : 'Game Blocked!'}
</h2>
{winner && (
@@ -91,6 +94,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
whileTap={{ scale: 0.98 }}
onClick={onPlayAgain}
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow"
aria-label="Start a new game"
>
Play Again
</motion.button>
@@ -100,8 +104,9 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
whileTap={{ scale: 0.98 }}
onClick={onLeave}
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
aria-label="Leave game and return to main menu"
>
Back to Menu
Leave Game
</motion.button>
</div>
</motion.div>

Ver fichero

@@ -32,7 +32,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
if (mode === 'menu') {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@@ -48,23 +48,28 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
<p className="text-center text-gray-600 mb-8">Online Multiplayer Game</p>
<div className="space-y-4 mb-6">
<label htmlFor="player-name" className="sr-only">Your name</label>
<input
id="player-name"
type="text"
placeholder="Enter your name"
value={playerName}
onChange={(e) => setPlayerName(e.target.value)}
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none transition-colors"
maxLength={20}
aria-label="Enter your player name"
aria-required="true"
/>
</div>
<div className="space-y-3">
<div className="space-y-3" role="group" aria-label="Game mode selection">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleCreate}
disabled={!playerName.trim()}
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Create a new multiplayer room"
>
Create Room
</motion.button>
@@ -75,6 +80,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
onClick={() => setMode('join')}
disabled={!playerName.trim()}
className="w-full bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Join an existing multiplayer room"
>
Join Room
</motion.button>
@@ -85,6 +91,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
onClick={handleAI}
disabled={!playerName.trim()}
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Start a game against computer AI"
>
Play vs AI
</motion.button>
@@ -94,13 +101,13 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
<p>Built with Next.js, Canvas & Socket.IO</p>
</div>
</motion.div>
</div>
</main>
);
}
if (mode === 'join') {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@@ -109,13 +116,17 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
<h2 className="text-3xl font-bold text-center mb-6 text-gray-800">Join Room</h2>
<div className="space-y-4 mb-6">
<label htmlFor="room-id" className="sr-only">Room ID</label>
<input
id="room-id"
type="text"
placeholder="Enter Room ID"
value={joinRoomId}
onChange={(e) => setJoinRoomId(e.target.value.toUpperCase())}
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-purple-500 focus:outline-none transition-colors uppercase"
maxLength={6}
aria-label="Enter the 6-character room ID"
aria-required="true"
/>
</div>
@@ -126,6 +137,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
onClick={handleJoin}
disabled={!joinRoomId.trim()}
className="w-full bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Join the room with entered ID"
>
Join Game
</motion.button>
@@ -135,12 +147,13 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
whileTap={{ scale: 0.98 }}
onClick={() => setMode('menu')}
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
aria-label="Go back to main menu"
>
Back
</motion.button>
</div>
</motion.div>
</div>
</main>
);
}

Ver fichero

@@ -20,7 +20,7 @@ export function PlayerHand({
validTileIds,
}: PlayerHandProps) {
return (
<div className="bg-white rounded-lg shadow-lg p-4">
<div className="bg-white rounded-lg shadow-lg p-4" role="region" aria-label="Your tiles">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-white ${
@@ -63,6 +63,16 @@ export function PlayerHand({
className={`relative ${
isPlayable && isCurrentPlayer ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
role="button"
tabIndex={isPlayable && isCurrentPlayer ? 0 : -1}
aria-label={`Tile ${tile.left}-${tile.right}${isSelected ? ' (selected)' : ''}${!isPlayable ? ' (cannot be played)' : ''}`}
aria-pressed={isSelected}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && isPlayable && isCurrentPlayer) {
e.preventDefault();
onTileClick(tile.id);
}
}}
>
<DominoTileSVG
tile={tile}
@@ -96,6 +106,8 @@ function DominoTileSVG({ tile, isSelected, isPlayable }: DominoTileSVGProps) {
className={`transition-all ${
isSelected ? 'ring-4 ring-blue-500 rounded' : ''
} ${!isPlayable ? 'opacity-50' : ''}`}
role="img"
aria-label={`Domino tile: ${tile.left} and ${tile.right}`}
>
{/* Background */}
<rect

Ver fichero

@@ -18,7 +18,7 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
const canStart = players.length >= 2 && players.every(p => p.isReady);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@@ -102,11 +102,12 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
whileTap={{ scale: 0.98 }}
onClick={onReady}
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow"
aria-label="Mark yourself as ready to start the game"
>
Ready to Play
</motion.button>
) : (
<div className="w-full bg-green-100 border-2 border-green-500 text-green-700 py-3 rounded-lg font-semibold text-center">
<div className="w-full bg-green-100 border-2 border-green-500 text-green-700 py-3 rounded-lg font-semibold text-center" role="status" aria-live="polite">
{canStart ? 'Starting game...' : players.length < 2 ? 'Waiting for at least 1 more player...' : 'Waiting for other players...'}
</div>
)}
@@ -116,12 +117,13 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
whileTap={{ scale: 0.98 }}
onClick={onLeave}
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
aria-label="Leave the waiting room and return to menu"
>
Leave Room
</motion.button>
</div>
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg" role="region" aria-label="Game rules">
<h4 className="font-semibold text-blue-900 mb-2">Game Rules</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> 2-4 players can play (minimum 2 required)</li>
@@ -132,6 +134,6 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
</ul>
</div>
</motion.div>
</div>
</main>
);
}