Aller au contenu

Usage Examples

This document provides comprehensive examples for using Neo Chess Board in various scenarios.

🔗 Live Example Pages

Experience the library directly in your browser with these hosted demos:

  • 🌐 Vanilla JS Starter – Standalone HTML setup featuring theme switching, move history, and PGN export helpers.
  • Chess.js Integration – Demonstrates the ChessJsRules adapter synchronized with the chess.js engine.
  • 📈 PGN + Evaluation HUD – Import annotated games, auto-sync the orientation, and follow the evaluation bar.
  • Advanced Features Showcase – Explore puzzles, analysis helpers, and keyboard-driven workflows.

🚀 Quick Start Examples

Basic Vanilla JavaScript Setup

import { NeoChessBoard } from '@magicolala/neo-chess-board';
// Get canvas element
const canvas = document.getElementById('chess-board') as HTMLCanvasElement;
// Create board with default options
const board = new NeoChessBoard(canvas);
// Listen for moves
board.on('move', (move) => {
  console.log(`Move: ${move.from}${move.to}`);
});
// Load a specific position
board.loadPosition('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1');

Basic React Setup

import React, { useRef, useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';
import { ChessJsRules, START_FEN } from '@magicolala/neo-chess-board';

const INITIAL_FEN = START_FEN;

function ChessGame() {
  const [fen, setFen] = useState(INITIAL_FEN);
  const [moves, setMoves] = useState<string[]>([]);
  const rules = useRef(new ChessJsRules(INITIAL_FEN));

  const addMove = (event: { from: string; to: string; fen: string }) => {
    rules.current.setFEN(event.fen);
    setFen(event.fen);
    setMoves((previous) => [...previous, `${event.from}${event.to}`]);
  };

  return (
    <div>
      <NeoChessBoard
        fen={fen}
        theme="neo"
        interactive
        showCoordinates
        onMove={addMove}
        onUpdate={(event) => {
          rules.current.setFEN(event.fen);
          setFen(event.fen);
        }}
        onIllegal={(event) => {
          console.warn('Illegal move blocked:', event.reason);
        }}
      />
      <p>Moves played: {moves.length}</p>
    </div>
  );
}

🎮 Interactive Examples

Game with Move History

import React, { useCallback, useMemo, useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';
import { ChessJsRules, START_FEN } from '@magicolala/neo-chess-board';
import type { Move as ChessMove } from 'chess.js';

interface GameMove {
  fen: string;
  lan: string;
  san: string;
}

function ChessGameWithHistory() {
  const [gameHistory, setGameHistory] = useState<GameMove[]>([]);
  const [currentFen, setCurrentFen] = useState(START_FEN);
  const rules = useMemo(() => new ChessJsRules(START_FEN), []);

  const handleMove = useCallback((event: { from: string; to: string; fen: string }) => {
    rules.setFEN(event.fen);
    const verboseHistory = rules.getChessInstance().history({ verbose: true }) as ChessMove[];
    const last = verboseHistory.at(-1);
    setGameHistory((previous) => [
      ...previous,
      {
        fen: event.fen,
        lan: `${event.from}-${event.to}`,
        san: last?.san ?? `${event.from}-${event.to}`,
      },
    ]);
    setCurrentFen(event.fen);
  }, [rules]);

  const goToMove = useCallback(
    (moveIndex: number) => {
      if (moveIndex < 0) {
        setCurrentFen(START_FEN);
        rules.setFEN(START_FEN);
        return;
      }
      const snapshot = gameHistory[moveIndex];
      if (snapshot) {
        setCurrentFen(snapshot.fen);
        rules.setFEN(snapshot.fen);
      }
    },
    [gameHistory, rules],
  );

  return (
    <div style={{ display: 'flex', gap: '20px' }}>
      <div>
        <NeoChessBoard
          fen={currentFen}
          onMove={handleMove}
          onUpdate={(event) => {
            setCurrentFen(event.fen);
            rules.setFEN(event.fen);
          }}
          theme="wood"
          showCoordinates
        />
      </div>
      <div style={{ minWidth: '200px' }}>
        <h3>Move History</h3>
        <button onClick={() => goToMove(-1)}>Start Position</button>
        <div style={{ maxHeight: '300px', overflow: 'auto' }}>
          {gameHistory.map((gameMove, index) => (
            <div
              key={gameMove.lan + index}
              onClick={() => goToMove(index)}
              style={{
                padding: '4px 8px',
                cursor: 'pointer',
                backgroundColor: '#f5f5f5',
                marginBottom: '2px',
              }}
            >
              {Math.floor(index / 2) + 1}
              {index % 2 === 0 ? '.' : '...'} {gameMove.san}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Puzzle Mode

import React, { useEffect, useRef, useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';
import { ChessJsRules } from '@magicolala/neo-chess-board';
import type { Move as ChessMove } from 'chess.js';

interface ChessPuzzle {
  fen: string;
  solution: string[]; // SAN moves
  description: string;
}

const puzzles: ChessPuzzle[] = [
  {
    fen: 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4',
    solution: ['Ng5', 'd6', 'Nf7#'],
    description: 'White to play and win material',
  },
];

function ChessPuzzle() {
  const [currentPuzzle, setCurrentPuzzle] = useState(0);
  const [solutionIndex, setSolutionIndex] = useState(0);
  const [hint, setHint] = useState('Select the best move.');
  const [boardFen, setBoardFen] = useState(puzzles[0].fen);
  const rules = useRef(new ChessJsRules(puzzles[0].fen));
  const puzzle = puzzles[currentPuzzle];

  useEffect(() => {
    rules.current = new ChessJsRules(puzzle.fen);
    setSolutionIndex(0);
    setHint('Select the best move.');
    setBoardFen(puzzle.fen);
  }, [puzzle]);

  const handleMove = (event: { from: string; to: string; fen: string }) => {
    rules.current.setFEN(event.fen);
    const verboseHistory = rules.current.getChessInstance().history({ verbose: true }) as ChessMove[];
    const lastSan = verboseHistory.at(-1)?.san ?? `${event.from}-${event.to}`;
    const expectedSan = puzzle.solution[solutionIndex];
    if (lastSan === expectedSan) {
      if (solutionIndex + 1 === puzzle.solution.length) {
        setHint('Puzzle solved! 🎉');
      } else {
        setHint(`Correct! Next move ${solutionIndex + 1}/${puzzle.solution.length}`);
      }
      setSolutionIndex((value) => value + 1);
      setBoardFen(event.fen);
    } else {
      setHint(`Try again — expected ${expectedSan}.`);
      setBoardFen(puzzle.fen);
      rules.current.setFEN(puzzle.fen);
    }
  };

  return (
    <div>
      <h2>Chess Puzzle {currentPuzzle + 1}</h2>
      <p>{puzzle.description}</p>
      <NeoChessBoard
        fen={boardFen}
        theme="glass"
        allowPremoves={false}
        onMove={handleMove}
        onUpdate={(event) => {
          rules.current.setFEN(event.fen);
          setBoardFen(event.fen);
        }}
      />
      <div style={{ marginTop: '10px' }}>
        <p>{hint}</p>
        <button
          onClick={() => {
            rules.current.setFEN(puzzle.fen);
            setBoardFen(puzzle.fen);
            setSolutionIndex(0);
            setHint('Select the best move.');
          }}
        >
          Reset
        </button>
        <button
          onClick={() => setCurrentPuzzle((value) => (value + 1) % puzzles.length)}
          style={{ marginLeft: '8px' }}
        >
          Next Puzzle
        </button>
      </div>
    </div>
  );
}

Multi-Board Analysis

import React, { useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';

const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

function MultiboardAnalysis() {
  const [mainFen, setMainFen] = useState(START_FEN);
  const [variations, setVariations] = useState<string[]>([
    'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
    'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2',
  ]);

  return (
    <div>
      <h2>Position Analysis</h2>
      <div style={{ marginBottom: '20px' }}>
        <h3>Main Line</h3>
        <NeoChessBoard
          fen={mainFen}
          theme="light"
          showCoordinates
          onMove={(event) => {
            setMainFen(event.fen);
          }}
        />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '20px' }}>
        {variations.map((variation, index) => (
          <div key={variation + index}>
            <h4>Variation {index + 1}</h4>
            <NeoChessBoard
              fen={variation}
              theme="dark"
              interactive={false}
              style={{ width: '300px', maxWidth: '100%' }}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

🎯 Advanced Usage

Custom Event Handling

import { NeoChessBoard, ChessJsRules } from '@magicolala/neo-chess-board';

const canvas = document.getElementById('board') as HTMLCanvasElement;
const board = new NeoChessBoard(canvas, { interactive: true });
const rules = new ChessJsRules();

board.on('move', (move) => {
  rules.setFEN(move.fen);
  updateMoveList(move.from, move.to, move.fen);
  if (rules.isCheckmate()) {
    endGame(rules.turn() === 'w' ? '0-1' : '1-0');
  } else if (rules.isStalemate()) {
    endGame('1/2-1/2');
  } else if (rules.inCheck()) {
    showCheckWarning(rules.turn() === 'w' ? 'black' : 'white');
  }
});

board.on('illegal', (event) => {
  console.warn(`Illegal move ${event.from}${event.to}: ${event.reason}`);
  showIllegalMoveWarning(event.reason);
});

board.on('update', (event) => {
  rules.setFEN(event.fen);
  updateStatusBanner(event.fen);
});

board.on('promotion', (request) => {
  showPromotionDialog(request);
});

function updateMoveList(from: string, to: string, fen: string) {
  const movesList = document.getElementById('moves-list');
  const moveElement = document.createElement('div');
  moveElement.textContent = `${from}${to} (${fen})`;
  movesList?.appendChild(moveElement);
}

function showIllegalMoveWarning(reason: string) {
  const banner = document.getElementById('game-status');
  if (banner) {
    banner.textContent = `Illegal move: ${reason}`;
    banner.className = 'illegal-warning';
  }
}

function showCheckWarning(color: string) {
  const banner = document.getElementById('game-status');
  if (banner) {
    banner.textContent = `${color} is in check!`;
    banner.className = 'check-warning';
  }
}

function updateStatusBanner(fen: string) {
  const banner = document.getElementById('fen-status');
  if (banner) {
    banner.textContent = fen;
  }
}

function endGame(result: string) {
  const banner = document.getElementById('game-status');
  if (banner) {
    banner.textContent = `Game over: ${result}`;
    banner.className = 'game-over';
  }
}

PGN Import/Export

import { NeoChessBoard } from '@magicolala/neo-chess-board';

class PGNManager {
  private board: NeoChessBoard;
  constructor(canvas: HTMLCanvasElement) {
    this.board = new NeoChessBoard(canvas);
  }

  loadPGN(pgnString: string) {
    try {
      // Parse PGN and apply moves
      const moves = this.parsePGN(pgnString);
      this.board.reset();
      moves.forEach((move) => {
        this.board.makeMove(move.from, move.to);
      });
      console.log('PGN loaded successfully');
    } catch (error) {
      console.error('Error loading PGN:', error);
    }
  }

  exportCurrentGame(): string {
    const pgn = this.board.exportPGN();
    // Add additional metadata
    const headers = {
      Event: 'Casual Game',
      Site: 'Neo Chess Board Demo',
      Date: new Date().toISOString().split('T')[0],
      Round: '1',
      White: 'Player 1',
      Black: 'Player 2',
      Result: '*',
    };
    return this.formatPGN(headers, pgn);
  }

  private parsePGN(pgn: string) {
    // Implement PGN parsing logic
    // This is a simplified version
    const moves = [];
    const movePattern = /\b([NBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:=[NBRQ])?[+#]?)\b/g;
    let match;
    while ((match = movePattern.exec(pgn)) !== null) {
      moves.push(this.sanToMove(match[1]));
    }
    return moves;
  }

  private formatPGN(headers: object, moves: string): string {
    let pgn = '';
    // Add headers
    for (const [key, value] of Object.entries(headers)) {
      pgn += `[${key} "${value}"]\n`;
    }
    pgn += '\n' + moves + '\n';
    return pgn;
  }
}

// Usage
const pgnManager = new PGNManager(canvas);
// Load PGN from file
document.getElementById('load-pgn').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = () => pgnManager.loadPGN(reader.result as string);
    reader.readAsText(file);
  }
});
// Export current game
document.getElementById('export-pgn').addEventListener('click', () => {
  const pgn = pgnManager.exportCurrentGame();
  downloadPGN(pgn, 'game.pgn');
});

function downloadPGN(content: string, filename: string) {
  const blob = new Blob([content], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

Theme Switcher Component

import React, { useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';

const availableThemes = ['light', 'dark', 'wood', 'glass', 'neon', 'retro'];

function ThemeSwitcher() {
  const [currentTheme, setCurrentTheme] = useState('light');
  return (
    <div>
      <div style={{ marginBottom: '20px' }}>
        <label>Choose Theme: </label>
        <select
          value={currentTheme}
          onChange={(e) => setCurrentTheme(e.target.value)}
        >
          {availableThemes.map(theme => (
            <option key={theme} value={theme}>
              {theme.charAt(0).toUpperCase() + theme.slice(1)}
            </option>
          ))}
        </select>
      </div>
      <NeoChessBoard
        theme={currentTheme}
        showCoordinates={true}
        onMove={(move) => console.log('Move:', move)}
      />
    </div>
  );
}

🏆 Complete Game Implementation

import React, { useMemo, useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';
import { ChessJsRules, START_FEN } from '@magicolala/neo-chess-board';
import type { Move as ChessMove } from 'chess.js';

type GameStatus = 'playing' | 'check' | 'checkmate' | 'stalemate';

interface GameState {
  fen: string;
  moves: string[];
  status: GameStatus;
  currentPlayer: 'white' | 'black';
  winner?: 'white' | 'black' | 'draw';
  result?: '1-0' | '0-1' | '1/2-1/2';
}

const THEMES = ['light', 'dark', 'wood', 'glass', 'neon', 'retro'] as const;

function FullChessGame() {
  const [gameState, setGameState] = useState<GameState>({
    fen: START_FEN,
    moves: [],
    status: 'playing',
    currentPlayer: 'white',
  });
  const [selectedTheme, setSelectedTheme] = useState<(typeof THEMES)[number]>('light');
  const [orientation, setOrientation] = useState<'white' | 'black'>('white');
  const [showCoordinates, setShowCoordinates] = useState(true);
  const [lastError, setLastError] = useState<string | null>(null);
  const rules = useMemo(() => new ChessJsRules(START_FEN), []);

  const evaluateBoard = (fen: string) => {
    rules.setFEN(fen);
    if (rules.isCheckmate()) {
      const winner = rules.turn() === 'w' ? 'black' : 'white';
      return { status: 'checkmate' as const, winner, result: winner === 'white' ? '1-0' : '0-1' };
    }
    if (rules.isStalemate()) {
      return { status: 'stalemate' as const, winner: 'draw' as const, result: '1/2-1/2' as const };
    }
    if (rules.inCheck()) {
      return { status: 'check' as const, winner: undefined, result: undefined };
    }
    return { status: 'playing' as const, winner: undefined, result: undefined };
  };

  const applyFen = (fen: string, san?: string) => {
    const { status, winner, result } = evaluateBoard(fen);
    const nextPlayer = rules.turn() === 'w' ? 'white' : 'black';
    setGameState((previous) => ({
      fen,
      moves: san ? [...previous.moves, san] : previous.moves,
      status,
      winner,
      result,
      currentPlayer: nextPlayer,
    }));
  };

  const handleMove = (event: { from: string; to: string; fen: string }) => {
    rules.setFEN(event.fen);
    const history = rules.getChessInstance().history({ verbose: true }) as ChessMove[];
    const san = history.at(-1)?.san ?? `${event.from}-${event.to}`;
    setLastError(null);
    applyFen(event.fen, san);
  };

  const handleUpdate = (event: { fen: string }) => {
    applyFen(event.fen);
  };

  const resetGame = () => {
    rules.setFEN(START_FEN);
    setLastError(null);
    setGameState({
      fen: START_FEN,
      moves: [],
      status: 'playing',
      currentPlayer: 'white',
    });
    setOrientation('white');
  };

  const exportPGN = () => {
    console.log(rules.getChessInstance().pgn());
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
      <h1>Neo Chess Board - Full Game</h1>
      <div style={{ display: 'flex', gap: '20px', marginBottom: '20px', flexWrap: 'wrap' }}>
        <div>
          <label>Theme: </label>
          <select
            value={selectedTheme}
            onChange={(event) => setSelectedTheme(event.target.value as (typeof THEMES)[number])}
          >
            {THEMES.map((theme) => (
              <option key={theme} value={theme}>
                {theme.charAt(0).toUpperCase() + theme.slice(1)}
              </option>
            ))}
          </select>
        </div>
        <div>
          <label>Orientation: </label>
          <select value={orientation} onChange={(event) => setOrientation(event.target.value as 'white' | 'black')}>
            <option value="white">White</option>
            <option value="black">Black</option>
          </select>
        </div>
        <label>
          <input
            type="checkbox"
            checked={showCoordinates}
            onChange={(event) => setShowCoordinates(event.target.checked)}
          />
          Show Coordinates
        </label>
        <button onClick={() => setOrientation((value) => (value === 'white' ? 'black' : 'white'))}>
          Flip Board
        </button>
      </div>
      <div style={{ display: 'flex', gap: '30px', alignItems: 'flex-start' }}>
        <div>
          <NeoChessBoard
            fen={gameState.fen}
            theme={selectedTheme}
            orientation={orientation}
            showCoordinates={showCoordinates}
            allowPremoves
            onMove={handleMove}
            onUpdate={handleUpdate}
            onIllegal={(event) => setLastError(event.reason)}
            style={{ maxWidth: '500px' }}
          />
        </div>
        <div style={{ minWidth: '300px', padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
          <h3>Game Information</h3>
          <div style={{ marginBottom: '12px' }}>
            <strong>Status:</strong>
            <span
              style={{
                marginLeft: '10px',
                padding: '4px 8px',
                borderRadius: '4px',
                backgroundColor:
                  gameState.status === 'check'
                    ? '#ffe6e6'
                    : gameState.status === 'checkmate'
                      ? '#ffcccc'
                      : gameState.status === 'stalemate'
                        ? '#e6f3ff'
                        : '#e6ffe6',
              }}
            >
              {gameState.status.charAt(0).toUpperCase() + gameState.status.slice(1)}
            </span>
          </div>
          <div style={{ marginBottom: '12px' }}>
            <strong>Current Player:</strong>
            <span style={{ marginLeft: '10px', textTransform: 'capitalize' }}>{gameState.currentPlayer}</span>
          </div>
          <div style={{ marginBottom: '12px' }}>
            <strong>Moves:</strong> {gameState.moves.length}
          </div>
          {gameState.winner && (
            <div style={{ marginBottom: '12px' }}>
              <strong>Result:</strong>
              <span style={{ marginLeft: '10px' }}>
                {gameState.winner === 'draw' ? 'Draw' : `${gameState.winner} wins`} ({gameState.result})
              </span>
            </div>
          )}
          {lastError && (
            <div style={{ marginBottom: '12px', color: '#b91c1c' }}>Illegal move: {lastError}</div>
          )}
          <div style={{ display: 'flex', gap: '10px', flexDirection: 'column' }}>
            <button onClick={resetGame} style={{ padding: '8px 16px' }}>
              New Game
            </button>
            <button onClick={exportPGN} style={{ padding: '8px 16px' }}>
              Export PGN
            </button>
          </div>
        </div>
      </div>
      <div style={{ marginTop: '24px' }}>
        <h3>Move List</h3>
        <ol>
          {gameState.moves.map((san, index) => (
            <li key={`${san}-${index}`}>{san}</li>
          ))}
        </ol>
      </div>
    </div>
  );
}

Position Setup Tool

import React, { useState } from 'react';
import { NeoChessBoard } from '@magicolala/neo-chess-board/react';

interface Piece {
  type: string;
  color: 'white' | 'black';
}

interface Square {
  file: string;
  rank: number;
}

function PositionEditor() {
  const [editMode, setEditMode] = useState(false);
  const [selectedPiece, setSelectedPiece] = useState<Piece | null>(null);
  const [currentFEN, setCurrentFEN] = useState('start');

  const pieces = [
    { type: 'king', color: 'white' },
    { type: 'queen', color: 'white' },
    { type: 'rook', color: 'white' },
    { type: 'bishop', color: 'white' },
    { type: 'knight', color: 'white' },
    { type: 'pawn', color: 'white' },
    { type: 'king', color: 'black' },
    { type: 'queen', color: 'black' },
    { type: 'rook', color: 'black' },
    { type: 'bishop', color: 'black' },
    { type: 'knight', color: 'black' },
    { type: 'pawn', color: 'black' },
  ];

  const handleSquareClick = (square: Square) => {
    if (editMode && selectedPiece) {
      // Place selected piece on clicked square
      // Implementation depends on board API
      console.log(`Placing ${selectedPiece.color} ${selectedPiece.type} on ${square.file}${square.rank}`);
    }
  };

  return (
    <div>
      <h2>Position Editor</h2>
      <div style={{ marginBottom: '20px' }}>
        <button
          onClick={() => setEditMode(!editMode)}
          style={{
            padding: '8px 16px',
            backgroundColor: editMode ? '#ff6b6b' : '#4ecdc4',
            color: 'white',
            border: 'none',
            borderRadius: '4px'
          }}
        >
          {editMode ? 'Exit Edit Mode' : 'Enter Edit Mode'}
        </button>
      </div>
      {editMode && (
        <div style={{ marginBottom: '20px' }}>
          <h3>Select Piece to Place:</h3>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '10px', maxWidth: '400px' }}>
            {pieces.map((piece, index) => (
              <button
                key={index}
                onClick={() => setSelectedPiece(piece)}
                style={{
                  padding: '10px',
                  backgroundColor: selectedPiece === piece ? '#007bff' : '#f8f9fa',
                  color: selectedPiece === piece ? 'white' : 'black',
                  border: '1px solid #dee2e6',
                  borderRadius: '4px',
                  textTransform: 'capitalize'
                }}
              >
                {piece.color} {piece.type}
              </button>
            ))}
          </div>
        </div>
      )}
      <div style={{ display: 'flex', gap: '20px' }}>
        <div>
          <NeoChessBoard
            position={currentFEN}
            draggable={!editMode}
            onSquareClick={editMode ? handleSquareClick : undefined}
          />
        </div>
        <div style={{ minWidth: '300px' }}>
          <h3>FEN String</h3>
          <textarea
            value={currentFEN}
            onChange={(e) => setCurrentFEN(e.target.value)}
            style={{
              width: '100%',
              height: '100px',
              fontFamily: 'monospace',
              fontSize: '12px'
            }}
          />
          <div style={{ marginTop: '10px' }}>
            <button onClick={() => setCurrentFEN('start')}>
              Reset to Start
            </button>
            <button
              onClick={() => {
                navigator.clipboard.writeText(currentFEN);
                alert('FEN copied to clipboard!');
              }}
              style={{ marginLeft: '10px' }}
            >
              Copy FEN
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

Pour plus d’exemples, consultez l’application de démonstration complète dans le répertoire demo/ et explorez les fichiers de test dans tests/ pour d’autres modèles d’utilisation !