npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

chess-analysis-board

v1.0.4

Published

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

Readme

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

npm install @mliebelt/pgn-parser chess.js react-chessboard use-immer

Basic Usage

Standalone Component

import React from 'react';
import AnalysisBoard from './components/AnalysisBoard';
import 'chess-analysis-board/dist/chess-analysis-board.css'; // If consuming as a package

function App() {
  return (
    <div className="App">
      <AnalysisBoard />
    </div>
  );
}

export default App;

Accessing Generated PGN

The component can notify your app whenever the PGN changes:

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:

import React, { useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import AnalysisBoard from './components/AnalysisBoard';
import 'chess-analysis-board/dist/chess-analysis-board.css'; // Important: include library styles

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>
  );
}

Using in another app (import the CSS)

When consuming this component from another project (e.g., a Tauri app), you must import the packaged CSS for proper layout and styling:

import AnalysisBoard from 'chess-analysis-board';
import 'chess-analysis-board/dist/chess-analysis-board.css';

export default function App() {
  return <AnalysisBoard containerMode="embedded" />;
}

Without the CSS import, the move list and text areas will appear unstyled/squashed.

With External Settings (Recommended for Apps)

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

// 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)

// 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 | | startingPgn | string \| null | null | Load a complete game/analysis from PGN notation | | onPgnChange | Function \| null | null | Callback when PGN changes (for external save functionality) | | onError | Function \| null | null | Callback for error reporting (validation, parsing, conflicts) | | 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

{
  flipBoard: 'f',        // Key to flip board orientation
  previousMove: 'k',     // Key to go to previous move
  nextMove: 'j'          // Key to go to next move
}

Error Handling

The component validates input and reports errors via the onError callback:

const handleError = (error) => {
  console.error('Chess Analysis Error:', error);
  
  switch (error.type) {
    case 'fen_pgn_conflict':
      showUserMessage('The starting position conflicts with the PGN');
      break;
    case 'invalid_pgn':
      showUserMessage('Invalid PGN format');
      break;
    case 'invalid_pgn_moves':
      showUserMessage('PGN moves are not legal from the starting position');
      break;
    case 'invalid_fen_in_pgn':
      showUserMessage('Invalid FEN in PGN header');
      break;
    default:
      showUserMessage('Chess analysis error occurred');
  }
};

<AnalysisBoard 
  startingFen={customFen}
  startingPgn={customPgn}
  onError={handleError}
/>

Error Types

| Type | Description | Details | |------|-------------|---------| | fen_pgn_conflict | startingFen prop conflicts with PGN's FEN header | providedFen, pgnFen, pgn | | invalid_pgn | PGN contains no valid moves or is malformed | pgn | | invalid_pgn_moves | First move in PGN is illegal from starting position | startingFen, firstMove, error | | invalid_fen_in_pgn | FEN header in PGN is invalid | fen, error | | pgn_parse_error | Failed to parse PGN syntax | pgn, error |

Container Modes

The component supports two layout modes for different integration scenarios:

Standalone Mode (Default)

<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

<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:

<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

// 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

// 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

// 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

// 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/4k3/8/8/4P3/4K3/8 w - - 0 1"
/>

// Load a complete game with moves, variations, and comments
<AnalysisBoard 
  startingPgn={`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 *`}
/>

// Load from a custom starting position with PGN
<AnalysisBoard 
  startingPgn={`[FEN "rnbqkb1r/ppp2ppp/4pn2/3p4/3P1B2/2N5/PPP1PPPP/R2QKBNR w KQkq - 0 1"]

1. Nb5 Na6 (1... Bd6 2. Nxd6+ cxd6 3. e3) 2. e3 c6 3. Nc3 *`}
/>

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

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:

/* 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+

Testing

Below is the unit testing setup used

Testing Framework

  • Vitest: Fast unit test runner with Vite integration
  • React Testing Library: Component testing with user-centric queries
  • JSDOM: DOM simulation for browser-like testing environment
  • User Event: Realistic user interaction simulation

Test Structure

src/
├── components/
│   ├── __tests__/
│   │   ├── AnalysisBoard.test.jsx    # Main component tests
│   │   └── pgn-parsing.test.js       # PGN parsing logic tests
│   └── AnalysisBoard.jsx
└── test/
    └── setup.js                      # Test environment configuration

Running Tests

# Run all tests once
npm run test:run

# Run tests in watch mode (development)
npm test

# Run specific test by name
npm run test:run -- -t "loads PGN via UI"

# Run tests with UI (requires @vitest/ui) - Spins up a UI accessible at localhost:51204 for investigating failed unit tests
npm run test:ui

# Run tests with coverage // NOT CURRENTLY SET UP
npm run test:run -- --coverage

Test Categories

1. Component Rendering Tests

  • Basic component rendering with default props
  • Conditional rendering based on props (enablePgnBox, enableFenInput)
  • Container mode variations (standalone vs embedded)

2. PGN Functionality Tests

  • Loading PGN from props (startingPgn)
  • PGN parsing with variations and comments
  • PGN generation and callback notifications
  • Error handling for invalid PGN

3. FEN Support Tests

  • Custom starting positions via startingFen prop
  • FEN validation and error reporting
  • FEN header conflicts with PGN

4. User Interaction Tests

  • PGN loading via UI (textarea input)
  • FEN input toggle (Shift+F keyboard shortcut)
  • Move navigation and selection
  • Settings panel interactions

5. Integration Tests

  • PGN with complex variations and sub-variations
  • Comments and move annotations
  • Error boundary and validation

Test Configuration

Vitest Configuration (vitest.config.js)

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',           // Browser-like environment
    setupFiles: ['./src/test/setup.js'],
    globals: true,                  // Global test functions (describe, it, expect)
  },
})

Test Setup (src/test/setup.js)

import '@testing-library/jest-dom'

// Mock ResizeObserver for component tests
global.ResizeObserver = class ResizeObserver {
  constructor(callback) { this.callback = callback }
  observe() {}
  unobserve() {}
  disconnect() {}
}

// Mock getBoundingClientRect for layout tests
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
  value: () => ({ width: 500, height: 500, top: 0, left: 0, bottom: 500, right: 500 }),
  writable: true,
})

// Mock navigator.clipboard for user interactions
Object.defineProperty(navigator, 'clipboard', {
  value: { writeText: () => Promise.resolve(), readText: () => Promise.resolve('') },
  writable: true,
  configurable: true,
})

Testing Best Practices

1. User-Centric Testing

// ✅ Good: Test from user's perspective
const textarea = screen.getByRole('textbox', { name: /pgn/i })
await user.type(textarea, '1. e4 e5 *')

// ❌ Avoid: Testing implementation details
const pgnInput = component.state.pgnInput

2. Async Testing

// ✅ Good: Wait for async operations
await waitFor(() => {
  expect(screen.getByText('e4')).toBeInTheDocument()
})

// ✅ Good: Use proper async/await
it('loads PGN via UI', async () => {
  const user = userEvent.setup()
  // ... test code
})

3. Mocking External Dependencies

// Mock react-chessboard for faster, more reliable tests
vi.mock('react-chessboard', () => ({
  Chessboard: ({ position, onPieceDrop }) => (
    <div data-testid="chessboard" data-position={position}>
      Mock Chessboard
    </div>
  ),
}))

4. Testing Error States

it('reports error for invalid PGN', async () => {
  const mockOnError = vi.fn()
  render(<AnalysisBoard startingPgn="invalid pgn" onError={mockOnError} />)
  
  await waitFor(() => {
    expect(mockOnError).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'pgn_parse_error' })
    )
  })
})

Common Testing Patterns

Testing Keyboard Shortcuts

// For complex keyboard combinations, use fireEvent
fireEvent.keyDown(document, { key: 'F', shiftKey: true })

// For simple typing, use userEvent
await user.keyboard('{Shift>}F{/Shift}')

Testing Multiple Elements

// When expecting multiple instances of the same text
const e3Moves = within(movesList).getAllByText('e3')
expect(e3Moves).toHaveLength(2) // Verify exactly 2 instances

Testing Complex User Flows

it('loads PGN with variations via UI', async () => {
  const user = userEvent.setup()
  render(<AnalysisBoard />)
  
  // 1. Get UI elements
  const textarea = document.querySelector('.pgn-textarea')
  const loadButton = document.querySelector('.load-pgn-button')
  
  // 2. Simulate user input
  fireEvent.change(textarea, {
    target: { value: '1. d4 d5 2. Nc3 Nf6 3. Bf4 { comment } e6 *' }
  })
  
  // 3. Trigger action
  await user.click(loadButton)
  
  // 4. Verify results
  await waitFor(() => {
    const movesList = document.querySelector('.moves-list')
    expect(within(movesList).getByText('d4')).toBeInTheDocument()
    expect(within(movesList).getByText('comment')).toBeInTheDocument()
  })
})

Test Coverage

The test suite covers:

  • ✅ Component rendering and props
  • ✅ PGN parsing and generation
  • ✅ FEN validation and conflicts
  • ✅ User interactions (keyboard, mouse)
  • ✅ Error handling and edge cases
  • ✅ Settings and customization
  • ✅ Container modes and responsive behavior

Debugging Tests

Visual Test Output

# Use screen.debug() in tests to see DOM structure
screen.debug() // Prints current DOM state

Test UI for Interactive Debugging

# Install and use Vitest UI for visual test debugging
npm install --save-dev @vitest/ui
npm run test:ui

Running Specific Tests

# Run tests matching a pattern
npm run test:run -- -t "UI Interactions"

# Run tests in a specific file
npm run test:run -- AnalysisBoard.test.jsx

# Run tests with verbose output
npm run test:run -- --reporter=verbose

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass: npm run test:run
  6. Submit a pull request

License

MIT License - see LICENSE file for details.