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.
  • Puzzle Mode Dashboard – Filter curated tactics, request hints, and inspect persistence/analytics hooks with the new HUD.

🚀 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, { useMemo, useState } from 'react';
import { NeoChessBoard } from 'neo-chess-board/react';
import rawCollection from '../puzzles/daily-tactics.json';
import { loadPuzzleCollection } from 'neo-chess-board/utils/puzzleCollections';

const DIFFICULTY_FILTERS = ['all', 'beginner', 'intermediate', 'advanced'] as const;

export function PuzzleCollections() {
  const [difficulty, setDifficulty] =
    useState<(typeof DIFFICULTY_FILTERS)[number]>('all');
  const [activePuzzleId, setActivePuzzleId] = useState<string | undefined>(undefined);

  const collectionView = useMemo(
    () =>
      loadPuzzleCollection(rawCollection, {
        sortBy: 'difficulty',
        filters: difficulty === 'all' ? undefined : { difficulty: [difficulty] },
      }),
    [difficulty],
  );

  const puzzleMode = useMemo(
    () => ({
      collectionId: 'daily',
      startPuzzleId: activePuzzleId,
      puzzles: collectionView.puzzles,
      allowHints: true,
      autoAdvance: true,
    }),
    [activePuzzleId, collectionView.puzzles],
  );

  return (
    <section>
      <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem' }}>
        {DIFFICULTY_FILTERS.map((option) => (
          <button key={option} onClick={() => setDifficulty(option)}>
            {option}
          </button>
        ))}
      </div>
      <NeoChessBoard
        size={520}
        puzzleMode={puzzleMode}
        onPuzzleComplete={({ nextPuzzleId }) => {
          setActivePuzzleId(nextPuzzleId);
        }}
        onPuzzleEvent={({ type, payload }) => {
          if (type === 'puzzle:hint') {
            console.info('Hint used', payload);
          }
        }}
        onPuzzlePersistenceWarning={({ error }) => {
          console.warn('Puzzle progress not persisted', error);
        }}
      />
    </section>
  );
}

See examples/chess-puzzles.tsx for a feature-complete reference (progress HUD, hint counters, persistence reset, telemetry logging).

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

Use the allowDragging prop to toggle piece movement while editing. Using draggable is not supported on NeoChessBoard and will cause a TypeScript unknown-prop error. The helper ChessJsRules keeps the FEN string valid every time you add or clear a piece, so the textarea always shows a position the board can load. When responding to squareClick, rely on the provided payload—file/rank pairs are not sent and the handler must use the square string directly to stay in sync with board events.

Click any square with the “Clear” option (or with no piece selected) to remove its content. Whenever you leave edit mode, the component reloads the current FEN to ensure the rendered board matches the editor buffer.

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

type PieceTool = { type: 'p' | 'n' | 'b' | 'r' | 'q' | 'k'; color: 'w' | 'b' };

function PositionEditor() {
  const initialFen = START_FEN;
  const [editMode, setEditMode] = useState(false);
  const [selectedPiece, setSelectedPiece] = useState<PieceTool | null>(null);
  const [currentFEN, setCurrentFEN] = useState(initialFen);
  const boardRef = useRef<NeoChessRef>(null);
  const rulesRef = useRef(new ChessJsRules(initialFen));

  const pieces: PieceTool[] = [
    { type: 'k', color: 'w' },
    { type: 'q', color: 'w' },
    { type: 'r', color: 'w' },
    { type: 'b', color: 'w' },
    { type: 'n', color: 'w' },
    { type: 'p', color: 'w' },
    { type: 'k', color: 'b' },
    { type: 'q', color: 'b' },
    { type: 'r', color: 'b' },
    { type: 'b', color: 'b' },
    { type: 'n', color: 'b' },
    { type: 'p', color: 'b' },
  ];

  const handleSquareClick = (event: BoardEventMap['squareClick']) => {
    const square: Square = event.square;

    if (editMode && selectedPiece) {
      const { type, color } = selectedPiece;
      rulesRef.current.getChessInstance().put({ type, color }, square);
    } else {
      rulesRef.current.getChessInstance().remove(square);
    }

    const nextFen = rulesRef.current.getFEN();
    setCurrentFEN(nextFen);
    boardRef.current?.getBoard()?.setPosition(nextFen, true);
  };

  const syncFen = (newFen: string) => {
    rulesRef.current.setFEN(newFen);
    setCurrentFEN(newFen);
  };

  useEffect(() => {
    rulesRef.current.setFEN(currentFEN);
    if (!editMode) {
      boardRef.current?.getBoard()?.setPosition(currentFEN, true);
    }
  }, [editMode, currentFEN]);

  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' }}>
            <button
              onClick={() => setSelectedPiece(null)}
              style={{
                padding: '10px',
                backgroundColor: selectedPiece === null ? '#007bff' : '#f8f9fa',
                color: selectedPiece === null ? 'white' : 'black',
                border: '1px solid #dee2e6',
                borderRadius: '4px',
              }}
            >
              Clear
            </button>
            {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 === 'w' ? 'white' : 'black'}{' '}
                {{ k: 'king', q: 'queen', r: 'rook', b: 'bishop', n: 'knight', p: 'pawn' }[piece.type]}
              </button>
            ))}
          </div>
        </div>
      )}
      <div style={{ display: 'flex', gap: '20px' }}>
        <div>
          <NeoChessBoard
            ref={boardRef}
            position={currentFEN}
            allowDragging={!editMode}
            onSquareClick={editMode ? handleSquareClick : undefined}
          />
        </div>
        <div style={{ minWidth: '300px' }}>
          <h3>FEN String</h3>
          <textarea
            value={currentFEN}
            onChange={(e) => syncFen(e.target.value)}
            style={{
              width: '100%',
              height: '100px',
              fontFamily: 'monospace',
              fontSize: '12px'
            }}
          />
          <div style={{ marginTop: '10px' }}>
            <button onClick={() => syncFen(initialFen)}>
              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 !