UNPKG

chess-analysis-board

Version:

A comprehensive chess analysis board React component with move navigation, variation support, PGN import/export, and customizable keyboard shortcuts

533 lines (417 loc) 14.6 kB
# Chess Analysis Board React Component A comprehensive chess analysis board built with React, featuring move navigation, variation support, PGN import/export, and customizable keyboard shortcuts. ## Features ### Core Chess Functionality - **Interactive Chessboard**: Drag-and-drop piece movement with validation - **Move Navigation**: Navigate through games with arrow keys or custom shortcuts - **Variation Support**: Full support for chess variations and sub-variations - **PGN Import/Export**: Load and save games in standard PGN format with comments and variations - **Move Comments**: Add and edit comments for any move - **Auto-scroll**: Selected moves automatically scroll into view ### User Interface - **Modern Design**: Clean, professional interface similar to Lichess - **Responsive Layout**: Adapts to different screen sizes - **Variation Display**: Clear visual hierarchy for main lines and variations - **Comment Integration**: Comments appear inline with proper spacing ### Customization - **Board Flipping**: Toggle between white and black perspectives - **Keyboard Shortcuts**: Fully customizable navigation keys - **Settings Panel**: User-friendly settings interface (Cmd+, or Ctrl+,) ## Installation ```bash npm install @mliebelt/pgn-parser chess.js react-chessboard use-immer ``` ## Basic Usage ### Standalone Component ```jsx import React from 'react'; import AnalysisBoard from './components/AnalysisBoard'; function App() { return ( <div className="App"> <AnalysisBoard /> </div> ); } export default App; ``` ### Accessing Generated PGN The component can notify your app whenever the PGN changes: ```jsx import React, { useState } from 'react'; import AnalysisBoard from './components/AnalysisBoard'; function App() { const [currentPgn, setCurrentPgn] = useState(''); // This runs on every change - just store it const handlePgnChange = (pgn) => { setCurrentPgn(pgn); }; // This runs only when user clicks save const handleSaveStudy = async () => { await invoke('save_study', { pgn: currentPgn }); }; return ( <div> <AnalysisBoard onPgnChange={handlePgnChange} /> <button onClick={handleSaveStudy}>Save Study</button> </div> ); } ``` ### Desktop App Integration (Tauri) For desktop applications, you can hide the PGN box and handle saving externally: ```jsx import React, { useState } from 'react'; import { invoke } from '@tauri-apps/api/tauri'; import AnalysisBoard from './components/AnalysisBoard'; function App() { const [currentPgn, setCurrentPgn] = useState(''); const handlePgnChange = (newPgn) => { setCurrentPgn(newPgn); }; const handleSaveStudy = async () => { try { await invoke('save_study', { pgn: currentPgn }); console.log('Study saved successfully'); } catch (error) { console.error('Failed to save study:', error); } }; return ( <div> <AnalysisBoard enablePgnBox={false} // Hide PGN box, handle saving externally onPgnChange={handlePgnChange} /> <button onClick={handleSaveStudy}>Save Study</button> </div> ); } ``` ### With External Settings (Recommended for Apps) ```jsx import React, { useState, useEffect } from 'react'; import AnalysisBoard from './components/AnalysisBoard'; function App() { const [appSettings, setAppSettings] = useState({ keyboard: { flipBoard: 'f', previousMove: 'k', nextMove: 'j' } }); const [showSettingsModal, setShowSettingsModal] = useState(false); const handleSettingsChange = (newKeyboardSettings) => { setAppSettings({ ...appSettings, keyboard: newKeyboardSettings }); // Save to localStorage, database, etc. localStorage.setItem('chessSettings', JSON.stringify(appSettings)); }; return ( <div className="App"> <AnalysisBoard externalSettings={appSettings.keyboard} onSettingsChange={handleSettingsChange} showExternalSettings={showSettingsModal} onToggleSettings={setShowSettingsModal} /> </div> ); } ``` ## Tauri Desktop App Integration For desktop applications using Tauri, the component can be fully integrated with native menus and persistent settings. ### Frontend Integration ```jsx // App.jsx import React, { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/tauri'; import { listen } from '@tauri-apps/api/event'; import AnalysisBoard from './components/AnalysisBoard'; function App() { const [appSettings, setAppSettings] = useState({ keyboard: { flipBoard: 'f', previousMove: 'k', nextMove: 'j' } }); const [showSettingsModal, setShowSettingsModal] = useState(false); // Load settings from file when app starts useEffect(() => { loadSettings(); setupMenuHandlers(); }, []); const loadSettings = async () => { try { const savedSettings = await invoke('load_settings'); if (savedSettings) { setAppSettings(savedSettings); } } catch (error) { console.log('No saved settings found, using defaults'); } }; const setupMenuHandlers = async () => { // Listen for menu events await listen('menu', (event) => { if (event.payload === 'settings') { setShowSettingsModal(true); } }); }; const handleAppSettingsChange = async (newKeyboardSettings) => { const updatedSettings = { ...appSettings, keyboard: newKeyboardSettings }; setAppSettings(updatedSettings); // Save to file via Tauri try { await invoke('save_settings', { settings: updatedSettings }); } catch (error) { console.error('Failed to save settings:', error); } }; return ( <div className="app"> <AnalysisBoard externalSettings={appSettings.keyboard} onSettingsChange={handleAppSettingsChange} showExternalSettings={showSettingsModal} onToggleSettings={setShowSettingsModal} /> </div> ); } export default App; ``` ### Tauri Backend (Rust) ```rust // src-tauri/src/main.rs use tauri::Manager; use serde::{Deserialize, Serialize}; use std::fs; #[derive(Serialize, Deserialize)] struct KeyboardSettings { #[serde(rename = "flipBoard")] flip_board: String, #[serde(rename = "previousMove")] previous_move: String, #[serde(rename = "nextMove")] next_move: String, } #[derive(Serialize, Deserialize)] struct AppSettings { keyboard: KeyboardSettings, } #[tauri::command] fn load_settings() -> Result<AppSettings, String> { let app_dir = tauri::api::path::app_data_dir(&tauri::Config::default()) .ok_or("Failed to get app data directory")?; let settings_path = app_dir.join("settings.json"); match fs::read_to_string(settings_path) { Ok(contents) => { serde_json::from_str(&contents) .map_err(|e| format!("Failed to parse settings: {}", e)) } Err(_) => Err("Settings file not found".to_string()) } } #[tauri::command] fn save_settings(settings: AppSettings) -> Result<(), String> { let app_dir = tauri::api::path::app_data_dir(&tauri::Config::default()) .ok_or("Failed to get app data directory")?; fs::create_dir_all(&app_dir) .map_err(|e| format!("Failed to create app directory: {}", e))?; let settings_path = app_dir.join("settings.json"); let json = serde_json::to_string_pretty(&settings) .map_err(|e| format!("Failed to serialize settings: {}", e))?; fs::write(settings_path, json) .map_err(|e| format!("Failed to write settings: {}", e)) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![load_settings, save_settings]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` ## API Reference ### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `externalSettings` | `Object \| null` | `null` | External keyboard settings object | | `onSettingsChange` | `Function \| null` | `null` | Callback when settings change | | `showExternalSettings` | `boolean` | `false` | Whether to show settings modal externally | | `onToggleSettings` | `Function \| null` | `null` | Callback to toggle settings modal | | `startingFen` | `string \| null` | `null` | Custom starting position in FEN notation | | `onPgnChange` | `Function \| null` | `null` | Callback when PGN changes (for external save functionality) | | `enableFenInput` | `boolean` | `true` | Whether to enable FEN input functionality | | `enablePgnBox` | `boolean` | `true` | Whether to show the PGN input/output box | | `containerMode` | `string` | `'standalone'` | Layout mode: `'standalone'` (viewport-based) or `'embedded'` (container-relative) | ### Settings Object Structure ```javascript { flipBoard: 'f', // Key to flip board orientation previousMove: 'k', // Key to go to previous move nextMove: 'j' // Key to go to next move } ``` ## Container Modes The component supports two layout modes for different integration scenarios: ### Standalone Mode (Default) ```jsx <AnalysisBoard /> // or explicitly <AnalysisBoard containerMode="standalone" /> ``` - Uses viewport-based sizing (`vw`, `vh`) - Designed for full-page applications - Components size themselves relative to the browser window - Best for dedicated chess analysis applications ### Embedded Mode ```jsx <AnalysisBoard containerMode="embedded" /> ``` - Uses container-relative sizing (`%`, `px`) - Designed for integration into existing applications - Components adapt to their container size - Perfect for Tauri desktop apps, dashboards, or embedded widgets **Key differences in embedded mode:** - Board and moves panel are limited to reasonable max sizes - Sections stack vertically on smaller screens - No viewport units - works within any container - Reduced padding and margins for compact layouts **Example for Tauri integration:** ```jsx <div style={{ width: '1200px', height: '800px' }}> <AnalysisBoard containerMode="embedded" enablePgnBox={false} onPgnChange={handlePgnChange} /> </div> ``` ### Default Keyboard Shortcuts | Action | Default Key | Alternative | Description | |--------|-------------|-------------|-------------| | Next Move | `j` | `→` | Navigate to next move | | Previous Move | `k` | `←` | Navigate to previous move | | Jump to Start | `↑` | - | Jump to beginning of game | | Jump to End | `↓` | - | Jump to end of main line | | Flip Board | `f` | - | Toggle board orientation | | Toggle FEN Input | `Shift+F` | - | Show/hide FEN input section | | Open Settings | `Cmd+,` / `Ctrl+,` | - | Open settings panel | | Close Settings | `Esc` | Click outside | Close settings panel | ### Customizable Settings All keyboard shortcuts and UI behavior can be customized via the settings panel: - **Access**: Press `Cmd+,` (or `Ctrl+,`) to open settings - **Keyboard Shortcuts**: Change any keyboard shortcut to your preference - **Board Orientation**: View current board orientation - **Auto-scroll**: Toggle automatic scrolling to keep selected move in view (default: enabled) - **Close**: Press `Esc` or click outside to close ## UI Component Control The component supports selective enabling/disabling of UI sections for different use cases: ### Disabling FEN Input ```jsx // Completely disable FEN input functionality <AnalysisBoard enableFenInput={false} /> ``` When `enableFenInput={false}`: - The FEN input section never appears - The "Toggle FEN Input" option is removed from settings - The Shift+F keyboard shortcut is disabled - Users cannot change the starting position via the UI ### Disabling PGN Box ```jsx // Hide the PGN input/output box entirely <AnalysisBoard enablePgnBox={false} /> ``` When `enablePgnBox={false}`: - The entire PGN box is hidden (no textarea, copy button, or load button) - PGN generation still works internally for `onPgnChange` callback - Perfect for desktop apps that handle PGN saving externally ### Combined Usage ```jsx // For embedded use cases - minimal UI with external PGN handling <AnalysisBoard enableFenInput={false} enablePgnBox={false} onPgnChange={handlePgnUpdate} // Just tracking changes startingFen="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" /> ``` ## FEN Support The component supports custom starting positions via FEN (Forsyth-Edwards Notation): ### User Interface (when enabled)t - **Toggle Display**: Press `Shift+F` (or customize in settings) to show/hide the FEN input section - **FEN Input**: Paste FEN notation to set custom starting positions - **Validation**: Invalid FEN strings are rejected with user feedback - **Optional Display**: The FEN input section is hidden by default to keep the UI clean ### Programmatic Control ```jsx // Set a custom starting position programmatically <AnalysisBoard startingFen="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" /> // Example: King and Pawn endgame <AnalysisBoard startingFen="8/8/8/8/8/8/4K1k1/8 w - - 0 1" /> ``` ## PGN Support The component supports full PGN import and export with: - **Move annotations**: Comments, NAGs - **Variations**: Nested variations and sub-variations - **Headers**: Standard PGN headers - **Live updates**: PGN updates as you play/navigate - **Custom starting positions**: PGNs work with any starting FEN ### Example PGN with Variations ```pgn 1. e4 e5 2. Nf3 Nc6 3. Bb5 {The Spanish Opening} a6 (3... f5 {The Schliemann Defense} 4. Nc3 fxe4 5. Nxe4) 4. Ba4 Nf6 5. O-O Be7 * ``` ## Styling The component uses CSS classes that can be customized: ```css /* Main container */ .analysis-board-container { } /* Chessboard area */ .analysis-board { } /* Moves panel */ .move-history { } /* Individual moves */ .move { } .selected-move { } /* Variations */ .variation-line { } .variation-row { } /* Comments */ .comment { } .inline-comment { } /* Settings modal */ .settings-overlay { } .settings-modal { } ``` ## Dependencies - `react` (^19.1.0) - `chess.js` (^1.4.0) - Chess game logic and validation - `react-chessboard` (^4.7.3) - Interactive chessboard component - `use-immer` (^0.11.0) - Immutable state management - `@mliebelt/pgn-parser` - PGN parsing for variations and comments ## Browser Compatibility - Chrome/Edge 88+ - Firefox 85+ - Safari 14+ ## Contributing 1. Fork the repository 2. Create a feature branch 3. Make your changes 4. Add tests if applicable 5. Submit a pull request ## License MIT License - see LICENSE file for details.