From 855d4eda9edaafa46e7c260c6a714408550b83de Mon Sep 17 00:00:00 2001 From: Ali Mousavi Date: Thu, 12 Feb 2026 00:09:45 +0330 Subject: [PATCH] v1.0.0 --- .github/workflows/release.yml | 29 + .gitignore | 26 + LICENSE | 21 + README.md | 245 ++++++ assets/pieces/alpha/bB.svg | 1 + assets/pieces/alpha/bK.svg | 1 + assets/pieces/alpha/bN.svg | 1 + assets/pieces/alpha/bP.svg | 1 + assets/pieces/alpha/bQ.svg | 1 + assets/pieces/alpha/bR.svg | 1 + assets/pieces/alpha/wB.svg | 1 + assets/pieces/alpha/wK.svg | 1 + assets/pieces/alpha/wN.svg | 1 + assets/pieces/alpha/wP.svg | 1 + assets/pieces/alpha/wQ.svg | 1 + assets/pieces/alpha/wR.svg | 1 + assets/pieces/california/bB.svg | 1 + assets/pieces/california/bK.svg | 1 + assets/pieces/california/bN.svg | 1 + assets/pieces/california/bP.svg | 1 + assets/pieces/california/bQ.svg | 1 + assets/pieces/california/bR.svg | 1 + assets/pieces/california/wB.svg | 1 + assets/pieces/california/wK.svg | 1 + assets/pieces/california/wN.svg | 1 + assets/pieces/california/wP.svg | 1 + assets/pieces/california/wQ.svg | 1 + assets/pieces/california/wR.svg | 1 + assets/pieces/cardinal/bB.svg | 1 + assets/pieces/cardinal/bK.svg | 1 + assets/pieces/cardinal/bN.svg | 1 + assets/pieces/cardinal/bP.svg | 1 + assets/pieces/cardinal/bQ.svg | 1 + assets/pieces/cardinal/bR.svg | 1 + assets/pieces/cardinal/wB.svg | 1 + assets/pieces/cardinal/wK.svg | 1 + assets/pieces/cardinal/wN.svg | 1 + assets/pieces/cardinal/wP.svg | 1 + assets/pieces/cardinal/wQ.svg | 1 + assets/pieces/cardinal/wR.svg | 1 + assets/pieces/cburnett/bB.svg | 1 + assets/pieces/cburnett/bK.svg | 1 + assets/pieces/cburnett/bN.svg | 1 + assets/pieces/cburnett/bP.svg | 1 + assets/pieces/cburnett/bQ.svg | 1 + assets/pieces/cburnett/bR.svg | 1 + assets/pieces/cburnett/wB.svg | 1 + assets/pieces/cburnett/wK.svg | 1 + assets/pieces/cburnett/wN.svg | 1 + assets/pieces/cburnett/wP.svg | 1 + assets/pieces/cburnett/wQ.svg | 1 + assets/pieces/cburnett/wR.svg | 1 + assets/pieces/chess7/bB.svg | 1 + assets/pieces/chess7/bK.svg | 1 + assets/pieces/chess7/bN.svg | 1 + assets/pieces/chess7/bP.svg | 1 + assets/pieces/chess7/bQ.svg | 1 + assets/pieces/chess7/bR.svg | 1 + assets/pieces/chess7/wB.svg | 1 + assets/pieces/chess7/wK.svg | 1 + assets/pieces/chess7/wN.svg | 1 + assets/pieces/chess7/wP.svg | 1 + assets/pieces/chess7/wQ.svg | 1 + assets/pieces/chess7/wR.svg | 1 + assets/pieces/dubrovny/bB.svg | 1 + assets/pieces/dubrovny/bK.svg | 1 + assets/pieces/dubrovny/bN.svg | 1 + assets/pieces/dubrovny/bP.svg | 1 + assets/pieces/dubrovny/bQ.svg | 1 + assets/pieces/dubrovny/bR.svg | 1 + assets/pieces/dubrovny/wB.svg | 1 + assets/pieces/dubrovny/wK.svg | 1 + assets/pieces/dubrovny/wN.svg | 1 + assets/pieces/dubrovny/wP.svg | 1 + assets/pieces/dubrovny/wQ.svg | 1 + assets/pieces/dubrovny/wR.svg | 1 + assets/pieces/fantasy/bB.svg | 1 + assets/pieces/fantasy/bK.svg | 1 + assets/pieces/fantasy/bN.svg | 1 + assets/pieces/fantasy/bP.svg | 1 + assets/pieces/fantasy/bQ.svg | 1 + assets/pieces/fantasy/bR.svg | 1 + assets/pieces/fantasy/wB.svg | 1 + assets/pieces/fantasy/wK.svg | 1 + assets/pieces/fantasy/wN.svg | 1 + assets/pieces/fantasy/wP.svg | 1 + assets/pieces/fantasy/wQ.svg | 1 + assets/pieces/fantasy/wR.svg | 1 + assets/pieces/gioco/bB.svg | 1 + assets/pieces/gioco/bK.svg | 1 + assets/pieces/gioco/bN.svg | 1 + assets/pieces/gioco/bP.svg | 1 + assets/pieces/gioco/bQ.svg | 1 + assets/pieces/gioco/bR.svg | 1 + assets/pieces/gioco/wB.svg | 1 + assets/pieces/gioco/wK.svg | 1 + assets/pieces/gioco/wN.svg | 1 + assets/pieces/gioco/wP.svg | 1 + assets/pieces/gioco/wQ.svg | 1 + assets/pieces/gioco/wR.svg | 1 + assets/pieces/governor/bB.svg | 1 + assets/pieces/governor/bK.svg | 1 + assets/pieces/governor/bN.svg | 1 + assets/pieces/governor/bP.svg | 1 + assets/pieces/governor/bQ.svg | 1 + assets/pieces/governor/bR.svg | 1 + assets/pieces/governor/wB.svg | 1 + assets/pieces/governor/wK.svg | 1 + assets/pieces/governor/wN.svg | 1 + assets/pieces/governor/wP.svg | 1 + assets/pieces/governor/wQ.svg | 1 + assets/pieces/governor/wR.svg | 1 + assets/pieces/horsey/bB.svg | 1 + assets/pieces/horsey/bK.svg | 1 + assets/pieces/horsey/bN.svg | 1 + assets/pieces/horsey/bP.svg | 1 + assets/pieces/horsey/bQ.svg | 1 + assets/pieces/horsey/bR.svg | 1 + assets/pieces/horsey/wB.svg | 1 + assets/pieces/horsey/wK.svg | 1 + assets/pieces/horsey/wN.svg | 1 + assets/pieces/horsey/wP.svg | 1 + assets/pieces/horsey/wQ.svg | 1 + assets/pieces/horsey/wR.svg | 1 + assets/pieces/icpieces/bB.svg | 1 + assets/pieces/icpieces/bK.svg | 1 + assets/pieces/icpieces/bN.svg | 1 + assets/pieces/icpieces/bP.svg | 1 + assets/pieces/icpieces/bQ.svg | 1 + assets/pieces/icpieces/bR.svg | 1 + assets/pieces/icpieces/wB.svg | 1 + assets/pieces/icpieces/wK.svg | 1 + assets/pieces/icpieces/wN.svg | 1 + assets/pieces/icpieces/wP.svg | 1 + assets/pieces/icpieces/wQ.svg | 1 + assets/pieces/icpieces/wR.svg | 1 + assets/pieces/kosal/bB.svg | 1 + assets/pieces/kosal/bK.svg | 1 + assets/pieces/kosal/bN.svg | 1 + assets/pieces/kosal/bP.svg | 1 + assets/pieces/kosal/bQ.svg | 1 + assets/pieces/kosal/bR.svg | 1 + assets/pieces/kosal/wB.svg | 1 + assets/pieces/kosal/wK.svg | 1 + assets/pieces/kosal/wN.svg | 1 + assets/pieces/kosal/wP.svg | 1 + assets/pieces/kosal/wQ.svg | 1 + assets/pieces/kosal/wR.svg | 1 + assets/pieces/leipzig/bB.svg | 1 + assets/pieces/leipzig/bK.svg | 1 + assets/pieces/leipzig/bN.svg | 1 + assets/pieces/leipzig/bP.svg | 1 + assets/pieces/leipzig/bQ.svg | 1 + assets/pieces/leipzig/bR.svg | 1 + assets/pieces/leipzig/wB.svg | 1 + assets/pieces/leipzig/wK.svg | 1 + assets/pieces/leipzig/wN.svg | 1 + assets/pieces/leipzig/wP.svg | 1 + assets/pieces/leipzig/wQ.svg | 1 + assets/pieces/leipzig/wR.svg | 1 + assets/pieces/libra/bB.svg | 1 + assets/pieces/libra/bK.svg | 1 + assets/pieces/libra/bN.svg | 1 + assets/pieces/libra/bP.svg | 1 + assets/pieces/libra/bQ.svg | 1 + assets/pieces/libra/bR.svg | 1 + assets/pieces/libra/wB.svg | 1 + assets/pieces/libra/wK.svg | 1 + assets/pieces/libra/wN.svg | 1 + assets/pieces/libra/wP.svg | 1 + assets/pieces/libra/wQ.svg | 1 + assets/pieces/libra/wR.svg | 1 + assets/pieces/maestro/bB.svg | 1 + assets/pieces/maestro/bK.svg | 1 + assets/pieces/maestro/bN.svg | 1 + assets/pieces/maestro/bP.svg | 1 + assets/pieces/maestro/bQ.svg | 1 + assets/pieces/maestro/bR.svg | 1 + assets/pieces/maestro/wB.svg | 1 + assets/pieces/maestro/wK.svg | 1 + assets/pieces/maestro/wN.svg | 1 + assets/pieces/maestro/wP.svg | 1 + assets/pieces/maestro/wQ.svg | 1 + assets/pieces/maestro/wR.svg | 1 + assets/pieces/merida/bB.svg | 1 + assets/pieces/merida/bK.svg | 1 + assets/pieces/merida/bN.svg | 1 + assets/pieces/merida/bP.svg | 1 + assets/pieces/merida/bQ.svg | 1 + assets/pieces/merida/bR.svg | 1 + assets/pieces/merida/wB.svg | 1 + assets/pieces/merida/wK.svg | 1 + assets/pieces/merida/wN.svg | 1 + assets/pieces/merida/wP.svg | 1 + assets/pieces/merida/wQ.svg | 1 + assets/pieces/merida/wR.svg | 1 + assets/pieces/monarchy/bB.svg | 1 + assets/pieces/monarchy/bK.svg | 1 + assets/pieces/monarchy/bN.svg | 1 + assets/pieces/monarchy/bP.svg | 1 + assets/pieces/monarchy/bQ.svg | 1 + assets/pieces/monarchy/bR.svg | 1 + assets/pieces/monarchy/wB.svg | 1 + assets/pieces/monarchy/wK.svg | 1 + assets/pieces/monarchy/wN.svg | 1 + assets/pieces/monarchy/wP.svg | 1 + assets/pieces/monarchy/wQ.svg | 1 + assets/pieces/monarchy/wR.svg | 1 + assets/pieces/pirouetti/bB.svg | 1 + assets/pieces/pirouetti/bK.svg | 1 + assets/pieces/pirouetti/bN.svg | 1 + assets/pieces/pirouetti/bP.svg | 1 + assets/pieces/pirouetti/bQ.svg | 1 + assets/pieces/pirouetti/bR.svg | 1 + assets/pieces/pirouetti/wB.svg | 1 + assets/pieces/pirouetti/wK.svg | 1 + assets/pieces/pirouetti/wN.svg | 1 + assets/pieces/pirouetti/wP.svg | 1 + assets/pieces/pirouetti/wQ.svg | 1 + assets/pieces/pirouetti/wR.svg | 1 + assets/pieces/spatial/bB.svg | 1 + assets/pieces/spatial/bK.svg | 1 + assets/pieces/spatial/bN.svg | 1 + assets/pieces/spatial/bP.svg | 1 + assets/pieces/spatial/bQ.svg | 1 + assets/pieces/spatial/bR.svg | 1 + assets/pieces/spatial/wB.svg | 1 + assets/pieces/spatial/wK.svg | 1 + assets/pieces/spatial/wN.svg | 1 + assets/pieces/spatial/wP.svg | 1 + assets/pieces/spatial/wQ.svg | 1 + assets/pieces/spatial/wR.svg | 1 + assets/pieces/staunty/bB.svg | 1 + assets/pieces/staunty/bK.svg | 1 + assets/pieces/staunty/bN.svg | 1 + assets/pieces/staunty/bP.svg | 1 + assets/pieces/staunty/bQ.svg | 1 + assets/pieces/staunty/bR.svg | 1 + assets/pieces/staunty/wB.svg | 1 + assets/pieces/staunty/wK.svg | 1 + assets/pieces/staunty/wN.svg | 1 + assets/pieces/staunty/wP.svg | 1 + assets/pieces/staunty/wQ.svg | 1 + assets/pieces/staunty/wR.svg | 1 + assets/pieces/tatiana/bB.svg | 1 + assets/pieces/tatiana/bK.svg | 1 + assets/pieces/tatiana/bN.svg | 1 + assets/pieces/tatiana/bP.svg | 1 + assets/pieces/tatiana/bQ.svg | 1 + assets/pieces/tatiana/bR.svg | 1 + assets/pieces/tatiana/wB.svg | 1 + assets/pieces/tatiana/wK.svg | 1 + assets/pieces/tatiana/wN.svg | 1 + assets/pieces/tatiana/wP.svg | 1 + assets/pieces/tatiana/wQ.svg | 1 + assets/pieces/tatiana/wR.svg | 1 + esbuild.config.mjs | 39 + manifest.json | 10 + package-lock.json | 622 +++++++++++++++ package.json | 27 + screenshots/banner.png | Bin 0 -> 86839 bytes src/board-manager.ts | 639 ++++++++++++++++ src/chess-renderer.ts | 438 +++++++++++ src/declarations.d.ts | 31 + src/main.ts | 110 +++ src/navigation-controller.ts | 467 ++++++++++++ src/parser.ts | 527 +++++++++++++ src/puzzle-controller.ts | 418 ++++++++++ src/settings.ts | 382 ++++++++++ src/types.ts | 266 +++++++ src/utils.ts | 84 +++ styles.css | 1259 +++++++++++++++++++++++++++++++ tsconfig.json | 20 + 273 files changed, 5912 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/pieces/alpha/bB.svg create mode 100644 assets/pieces/alpha/bK.svg create mode 100644 assets/pieces/alpha/bN.svg create mode 100644 assets/pieces/alpha/bP.svg create mode 100644 assets/pieces/alpha/bQ.svg create mode 100644 assets/pieces/alpha/bR.svg create mode 100644 assets/pieces/alpha/wB.svg create mode 100644 assets/pieces/alpha/wK.svg create mode 100644 assets/pieces/alpha/wN.svg create mode 100644 assets/pieces/alpha/wP.svg create mode 100644 assets/pieces/alpha/wQ.svg create mode 100644 assets/pieces/alpha/wR.svg create mode 100644 assets/pieces/california/bB.svg create mode 100644 assets/pieces/california/bK.svg create mode 100644 assets/pieces/california/bN.svg create mode 100644 assets/pieces/california/bP.svg create mode 100644 assets/pieces/california/bQ.svg create mode 100644 assets/pieces/california/bR.svg create mode 100644 assets/pieces/california/wB.svg create mode 100644 assets/pieces/california/wK.svg create mode 100644 assets/pieces/california/wN.svg create mode 100644 assets/pieces/california/wP.svg create mode 100644 assets/pieces/california/wQ.svg create mode 100644 assets/pieces/california/wR.svg create mode 100644 assets/pieces/cardinal/bB.svg create mode 100644 assets/pieces/cardinal/bK.svg create mode 100644 assets/pieces/cardinal/bN.svg create mode 100644 assets/pieces/cardinal/bP.svg create mode 100644 assets/pieces/cardinal/bQ.svg create mode 100644 assets/pieces/cardinal/bR.svg create mode 100644 assets/pieces/cardinal/wB.svg create mode 100644 assets/pieces/cardinal/wK.svg create mode 100644 assets/pieces/cardinal/wN.svg create mode 100644 assets/pieces/cardinal/wP.svg create mode 100644 assets/pieces/cardinal/wQ.svg create mode 100644 assets/pieces/cardinal/wR.svg create mode 100644 assets/pieces/cburnett/bB.svg create mode 100644 assets/pieces/cburnett/bK.svg create mode 100644 assets/pieces/cburnett/bN.svg create mode 100644 assets/pieces/cburnett/bP.svg create mode 100644 assets/pieces/cburnett/bQ.svg create mode 100644 assets/pieces/cburnett/bR.svg create mode 100644 assets/pieces/cburnett/wB.svg create mode 100644 assets/pieces/cburnett/wK.svg create mode 100644 assets/pieces/cburnett/wN.svg create mode 100644 assets/pieces/cburnett/wP.svg create mode 100644 assets/pieces/cburnett/wQ.svg create mode 100644 assets/pieces/cburnett/wR.svg create mode 100644 assets/pieces/chess7/bB.svg create mode 100644 assets/pieces/chess7/bK.svg create mode 100644 assets/pieces/chess7/bN.svg create mode 100644 assets/pieces/chess7/bP.svg create mode 100644 assets/pieces/chess7/bQ.svg create mode 100644 assets/pieces/chess7/bR.svg create mode 100644 assets/pieces/chess7/wB.svg create mode 100644 assets/pieces/chess7/wK.svg create mode 100644 assets/pieces/chess7/wN.svg create mode 100644 assets/pieces/chess7/wP.svg create mode 100644 assets/pieces/chess7/wQ.svg create mode 100644 assets/pieces/chess7/wR.svg create mode 100644 assets/pieces/dubrovny/bB.svg create mode 100644 assets/pieces/dubrovny/bK.svg create mode 100644 assets/pieces/dubrovny/bN.svg create mode 100644 assets/pieces/dubrovny/bP.svg create mode 100644 assets/pieces/dubrovny/bQ.svg create mode 100644 assets/pieces/dubrovny/bR.svg create mode 100644 assets/pieces/dubrovny/wB.svg create mode 100644 assets/pieces/dubrovny/wK.svg create mode 100644 assets/pieces/dubrovny/wN.svg create mode 100644 assets/pieces/dubrovny/wP.svg create mode 100644 assets/pieces/dubrovny/wQ.svg create mode 100644 assets/pieces/dubrovny/wR.svg create mode 100644 assets/pieces/fantasy/bB.svg create mode 100644 assets/pieces/fantasy/bK.svg create mode 100644 assets/pieces/fantasy/bN.svg create mode 100644 assets/pieces/fantasy/bP.svg create mode 100644 assets/pieces/fantasy/bQ.svg create mode 100644 assets/pieces/fantasy/bR.svg create mode 100644 assets/pieces/fantasy/wB.svg create mode 100644 assets/pieces/fantasy/wK.svg create mode 100644 assets/pieces/fantasy/wN.svg create mode 100644 assets/pieces/fantasy/wP.svg create mode 100644 assets/pieces/fantasy/wQ.svg create mode 100644 assets/pieces/fantasy/wR.svg create mode 100644 assets/pieces/gioco/bB.svg create mode 100644 assets/pieces/gioco/bK.svg create mode 100644 assets/pieces/gioco/bN.svg create mode 100644 assets/pieces/gioco/bP.svg create mode 100644 assets/pieces/gioco/bQ.svg create mode 100644 assets/pieces/gioco/bR.svg create mode 100644 assets/pieces/gioco/wB.svg create mode 100644 assets/pieces/gioco/wK.svg create mode 100644 assets/pieces/gioco/wN.svg create mode 100644 assets/pieces/gioco/wP.svg create mode 100644 assets/pieces/gioco/wQ.svg create mode 100644 assets/pieces/gioco/wR.svg create mode 100644 assets/pieces/governor/bB.svg create mode 100644 assets/pieces/governor/bK.svg create mode 100644 assets/pieces/governor/bN.svg create mode 100644 assets/pieces/governor/bP.svg create mode 100644 assets/pieces/governor/bQ.svg create mode 100644 assets/pieces/governor/bR.svg create mode 100644 assets/pieces/governor/wB.svg create mode 100644 assets/pieces/governor/wK.svg create mode 100644 assets/pieces/governor/wN.svg create mode 100644 assets/pieces/governor/wP.svg create mode 100644 assets/pieces/governor/wQ.svg create mode 100644 assets/pieces/governor/wR.svg create mode 100644 assets/pieces/horsey/bB.svg create mode 100644 assets/pieces/horsey/bK.svg create mode 100644 assets/pieces/horsey/bN.svg create mode 100644 assets/pieces/horsey/bP.svg create mode 100644 assets/pieces/horsey/bQ.svg create mode 100644 assets/pieces/horsey/bR.svg create mode 100644 assets/pieces/horsey/wB.svg create mode 100644 assets/pieces/horsey/wK.svg create mode 100644 assets/pieces/horsey/wN.svg create mode 100644 assets/pieces/horsey/wP.svg create mode 100644 assets/pieces/horsey/wQ.svg create mode 100644 assets/pieces/horsey/wR.svg create mode 100644 assets/pieces/icpieces/bB.svg create mode 100644 assets/pieces/icpieces/bK.svg create mode 100644 assets/pieces/icpieces/bN.svg create mode 100644 assets/pieces/icpieces/bP.svg create mode 100644 assets/pieces/icpieces/bQ.svg create mode 100644 assets/pieces/icpieces/bR.svg create mode 100644 assets/pieces/icpieces/wB.svg create mode 100644 assets/pieces/icpieces/wK.svg create mode 100644 assets/pieces/icpieces/wN.svg create mode 100644 assets/pieces/icpieces/wP.svg create mode 100644 assets/pieces/icpieces/wQ.svg create mode 100644 assets/pieces/icpieces/wR.svg create mode 100644 assets/pieces/kosal/bB.svg create mode 100644 assets/pieces/kosal/bK.svg create mode 100644 assets/pieces/kosal/bN.svg create mode 100644 assets/pieces/kosal/bP.svg create mode 100644 assets/pieces/kosal/bQ.svg create mode 100644 assets/pieces/kosal/bR.svg create mode 100644 assets/pieces/kosal/wB.svg create mode 100644 assets/pieces/kosal/wK.svg create mode 100644 assets/pieces/kosal/wN.svg create mode 100644 assets/pieces/kosal/wP.svg create mode 100644 assets/pieces/kosal/wQ.svg create mode 100644 assets/pieces/kosal/wR.svg create mode 100644 assets/pieces/leipzig/bB.svg create mode 100644 assets/pieces/leipzig/bK.svg create mode 100644 assets/pieces/leipzig/bN.svg create mode 100644 assets/pieces/leipzig/bP.svg create mode 100644 assets/pieces/leipzig/bQ.svg create mode 100644 assets/pieces/leipzig/bR.svg create mode 100644 assets/pieces/leipzig/wB.svg create mode 100644 assets/pieces/leipzig/wK.svg create mode 100644 assets/pieces/leipzig/wN.svg create mode 100644 assets/pieces/leipzig/wP.svg create mode 100644 assets/pieces/leipzig/wQ.svg create mode 100644 assets/pieces/leipzig/wR.svg create mode 100644 assets/pieces/libra/bB.svg create mode 100644 assets/pieces/libra/bK.svg create mode 100644 assets/pieces/libra/bN.svg create mode 100644 assets/pieces/libra/bP.svg create mode 100644 assets/pieces/libra/bQ.svg create mode 100644 assets/pieces/libra/bR.svg create mode 100644 assets/pieces/libra/wB.svg create mode 100644 assets/pieces/libra/wK.svg create mode 100644 assets/pieces/libra/wN.svg create mode 100644 assets/pieces/libra/wP.svg create mode 100644 assets/pieces/libra/wQ.svg create mode 100644 assets/pieces/libra/wR.svg create mode 100644 assets/pieces/maestro/bB.svg create mode 100644 assets/pieces/maestro/bK.svg create mode 100644 assets/pieces/maestro/bN.svg create mode 100644 assets/pieces/maestro/bP.svg create mode 100644 assets/pieces/maestro/bQ.svg create mode 100644 assets/pieces/maestro/bR.svg create mode 100644 assets/pieces/maestro/wB.svg create mode 100644 assets/pieces/maestro/wK.svg create mode 100644 assets/pieces/maestro/wN.svg create mode 100644 assets/pieces/maestro/wP.svg create mode 100644 assets/pieces/maestro/wQ.svg create mode 100644 assets/pieces/maestro/wR.svg create mode 100644 assets/pieces/merida/bB.svg create mode 100644 assets/pieces/merida/bK.svg create mode 100644 assets/pieces/merida/bN.svg create mode 100644 assets/pieces/merida/bP.svg create mode 100644 assets/pieces/merida/bQ.svg create mode 100644 assets/pieces/merida/bR.svg create mode 100644 assets/pieces/merida/wB.svg create mode 100644 assets/pieces/merida/wK.svg create mode 100644 assets/pieces/merida/wN.svg create mode 100644 assets/pieces/merida/wP.svg create mode 100644 assets/pieces/merida/wQ.svg create mode 100644 assets/pieces/merida/wR.svg create mode 100644 assets/pieces/monarchy/bB.svg create mode 100644 assets/pieces/monarchy/bK.svg create mode 100644 assets/pieces/monarchy/bN.svg create mode 100644 assets/pieces/monarchy/bP.svg create mode 100644 assets/pieces/monarchy/bQ.svg create mode 100644 assets/pieces/monarchy/bR.svg create mode 100644 assets/pieces/monarchy/wB.svg create mode 100644 assets/pieces/monarchy/wK.svg create mode 100644 assets/pieces/monarchy/wN.svg create mode 100644 assets/pieces/monarchy/wP.svg create mode 100644 assets/pieces/monarchy/wQ.svg create mode 100644 assets/pieces/monarchy/wR.svg create mode 100644 assets/pieces/pirouetti/bB.svg create mode 100644 assets/pieces/pirouetti/bK.svg create mode 100644 assets/pieces/pirouetti/bN.svg create mode 100644 assets/pieces/pirouetti/bP.svg create mode 100644 assets/pieces/pirouetti/bQ.svg create mode 100644 assets/pieces/pirouetti/bR.svg create mode 100644 assets/pieces/pirouetti/wB.svg create mode 100644 assets/pieces/pirouetti/wK.svg create mode 100644 assets/pieces/pirouetti/wN.svg create mode 100644 assets/pieces/pirouetti/wP.svg create mode 100644 assets/pieces/pirouetti/wQ.svg create mode 100644 assets/pieces/pirouetti/wR.svg create mode 100644 assets/pieces/spatial/bB.svg create mode 100644 assets/pieces/spatial/bK.svg create mode 100644 assets/pieces/spatial/bN.svg create mode 100644 assets/pieces/spatial/bP.svg create mode 100644 assets/pieces/spatial/bQ.svg create mode 100644 assets/pieces/spatial/bR.svg create mode 100644 assets/pieces/spatial/wB.svg create mode 100644 assets/pieces/spatial/wK.svg create mode 100644 assets/pieces/spatial/wN.svg create mode 100644 assets/pieces/spatial/wP.svg create mode 100644 assets/pieces/spatial/wQ.svg create mode 100644 assets/pieces/spatial/wR.svg create mode 100644 assets/pieces/staunty/bB.svg create mode 100644 assets/pieces/staunty/bK.svg create mode 100644 assets/pieces/staunty/bN.svg create mode 100644 assets/pieces/staunty/bP.svg create mode 100644 assets/pieces/staunty/bQ.svg create mode 100644 assets/pieces/staunty/bR.svg create mode 100644 assets/pieces/staunty/wB.svg create mode 100644 assets/pieces/staunty/wK.svg create mode 100644 assets/pieces/staunty/wN.svg create mode 100644 assets/pieces/staunty/wP.svg create mode 100644 assets/pieces/staunty/wQ.svg create mode 100644 assets/pieces/staunty/wR.svg create mode 100644 assets/pieces/tatiana/bB.svg create mode 100644 assets/pieces/tatiana/bK.svg create mode 100644 assets/pieces/tatiana/bN.svg create mode 100644 assets/pieces/tatiana/bP.svg create mode 100644 assets/pieces/tatiana/bQ.svg create mode 100644 assets/pieces/tatiana/bR.svg create mode 100644 assets/pieces/tatiana/wB.svg create mode 100644 assets/pieces/tatiana/wK.svg create mode 100644 assets/pieces/tatiana/wN.svg create mode 100644 assets/pieces/tatiana/wP.svg create mode 100644 assets/pieces/tatiana/wQ.svg create mode 100644 assets/pieces/tatiana/wR.svg create mode 100644 esbuild.config.mjs create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 screenshots/banner.png create mode 100644 src/board-manager.ts create mode 100644 src/chess-renderer.ts create mode 100644 src/declarations.d.ts create mode 100644 src/main.ts create mode 100644 src/navigation-controller.ts create mode 100644 src/parser.ts create mode 100644 src/puzzle-controller.ts create mode 100644 src/settings.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 styles.css create mode 100644 tsconfig.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..891604d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - run: npm install + - run: npm run build + + - uses: softprops/action-gh-release@v1 + with: + files: | + main.js + manifest.json + styles.css + assets/** + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b4b1cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Build output +main.js +main.js.map + +# Dependencies +node_modules/ + +# Obsidian +data.json + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +*.swp +*.swo +*~ + +# Logs +*.log +npm-debug.log* + +# TypeScript +*.tsbuildinfo \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffb36b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ali Mousavi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..52a01ac --- /dev/null +++ b/README.md @@ -0,0 +1,245 @@ +# ♟ ChessView + +An interactive chess board plugin for [Obsidian](https://obsidian.md). Render games from PGN, display positions from FEN, solve puzzles, draw arrows and circles, navigate moves with keyboard shortcuts, and analyze on Lichess or Chess.com — all inside your notes. + +![ChessView Banner](screenshots/banner.png) + + + +## Features + +- **PGN Games** — Paste any PGN and get a fully navigable game viewer +- **FEN Positions** — Display static or interactive positions from FEN strings +- **Puzzle Mode** — Create solvable puzzles with hints and solution reveal +- **Move Annotations** — NAG symbols (!, ?, !!, ??, !?, ?!) with colored overlays like Chess.com/Lichess +- **Arrows & Circles** — Draw annotations on the board using markers or PGN comments +- **Comments** — Display inline PGN comments with move highlighting +- **Branching** — Explore alternative lines in editable mode +- **Themes** — 7 built-in board themes + custom colors +- **Piece Sets** — 20 piece set options +- **Keyboard Navigation** — Arrow keys, Home/End, Space for autoplay +- **Analysis Links** — One-click export to Lichess and Chess.com +- **Mobile Friendly** — Responsive layout that adapts to any screen size +- **Dark Mode** — Full support for Obsidian's dark theme + +## Installation + +### From Obsidian Community Plugins (Recommended) + +1. Open **Settings** → **Community Plugins** → **Browse** +2. Search for **ChessView** +3. Click **Install**, then **Enable** + +### Manual Installation + +1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/tuxitop/chessview/releases) +2. Create a folder `.obsidian/plugins/chessview/` in your vault +3. Copy the three files into that folder +4. Copy the `assets/` folder into that folder (needed for additional piece sets) +5. Restart Obsidian and enable the plugin in **Settings** → **Community Plugins** + +## Usage + +Create a fenced code block with the language `chessview`: + +### Display a PGN Game + +```chessview +[Event "World Championship"] +[White "Kasparov, Garry"] +[Black "Topalov, Veselin"] +[Result "1-0"] + +1. e4 d6 2. d4 Nf6 3. Nc3 g6 4. Be3 Bg7 5. Qd2 c6 6. f3 b5 +2. Nge2 Nbd7 8. Bh6 Bxh6 9. Qxh6 Bb7 10. a3 e5 11. O-O-O Qe7 +3. Kb1 a6 13. Nc1 O-O-O 14. Nb3 exd4 15. Rxd4 c5 16. Rd1 Nb6 +4. g3 Kb8 18. Na5 Ba8 19. Bh3 d5 20. Qf4+ Ka7 21. Rhe1 d4 +5. Nd5 Nbxd5 23. exd5 Qd6 24. Rxd4 cxd4 25. Re7+ Kb6 +6. Qxd4+ Kxa5 27. b4+ Ka4 28. Qc3 Qxd5 29. Ra7 Bb7 +7. Rxb7 Qc4 31. Qxf6 Kxa3 32. Qxa6+ Kxb4 33. c3+ Kxc3 +8. Qa1+ Kd2 35. Qb2+ Kd1 36. Bf1 Rd2 37. Rd7 Rxd7 +9. Bxc4 bxc4 39. Qxh8 Rd3 40. Qa8 c3 41. Qa4+ Ke1 +10. f4 f5 43. Kc1 Rd2 44. Qa7 1-0 +``` + +### Display a FEN Position + +```chessview +rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1 +``` + +### Create a Puzzle + +```chessview +[puzzle] +[rating: 1500] +[title: Scholar's Mate] + +--- + +[FEN "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4"] + +1. Qxf7# +``` + +### Board Orientation + +```chessview +[black] +[Event "My Game"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 +``` + +Available markers: + +- `[white]` — White at bottom (default) +- `[black]` or `[flip]` — Black at bottom + +### Static Board (No Interaction) + +```chessview +[static] +rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1 +``` + +### Arrows and Circles + +Using markers before the PGN/FEN: + +```chessview +[arrow: e2e4 green] +[arrow: d2d4 blue] +[circle: f3 red] + +rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 +``` + +Using standard PGN comment annotations: + +```chessview +[Event "Arrow Demo"] + +1. e4 {[%cal Ge2e4,Rd7d5] [%csl Gd4,Re5]} e5 2. Nf3 +``` + +Arrow/circle color codes: `G` = green, `R` = red, `B` = blue, `Y` = yellow, `O` = orange, `P` = purple + +### Move Annotations (NAGs) + +Inline annotations: + +```chessview + +1. e4! e5 2. Nf3!! Nc6? 3. Bb5?! a6!? 4. Ba4 Nf6?? +``` + +PGN standard NAGs: + +```chessview + +1. e4 $1 e5 $2 2. Nf3 $3 Nc6 $4 3. Bb5 $5 a6 $6 +``` + +| Symbol | NAG | Meaning | +| ------ | ---- | ---------------- | +| `!` | `$1` | Good move | +| `?` | `$2` | Mistake | +| `!!` | `$3` | Brilliant move | +| `??` | `$4` | Blunder | +| `!?` | `$5` | Interesting move | +| `?!` | `$6` | Dubious move | + +### Comments + +```chessview + +1. e4 {The King's Pawn opening} e5 {A solid response} +2. Nf3 {Developing the knight toward the center} Nc6 +``` + +### Start at a Specific Move + +```chessview +[move: 15] +[Event "My Game"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 ... +``` + +### Puzzle with Hints and Themes + +```chessview +[puzzle] +[rating: 2100] +[title: Find the winning combination] +[themes: sacrifice, fork] + +--- + +[FEN "r2qr1k1/ppp2ppp/2np1n2/2b1p1B1/2B1P1b1/2NP1N2/PPP2PPP/R2QR1K1 w - - 0 1"] + +1. Bxf7+ Kh8 2. Ng5 +``` + +## Marker Reference + +| Marker | Description | +| ----------------------- | --------------------------------- | +| `[white]` | White at bottom | +| `[black]` or `[flip]` | Black at bottom | +| `[static]` | No interaction, view only | +| `[noeditable]` | Can navigate but not make moves | +| `[move: N]` | Start at move N | +| `[arrow: e2e4 color]` | Draw arrow (color optional) | +| `[circle: e4 color]` | Draw circle (color optional) | +| `[highlight: e4 color]` | Highlight square (color optional) | +| `[puzzle]` | Enable puzzle mode | +| `[rating: N]` | Puzzle rating | +| `[title: text]` | Puzzle title | +| `[themes: a, b, c]` | Puzzle themes | + +## Keyboard Shortcuts + +| Key | Action | +| ------- | --------------------- | +| `←` | Previous move | +| `→` | Next move | +| `Home` | First move | +| `End` | Last move | +| `Space` | Play / Pause autoplay | +| `F` | Flip board | + +_Click on the board first to focus it for keyboard shortcuts._ + +## Settings + +Access via **Settings** → **Community Plugins** → **ChessView** → ⚙️ + +- **Board Theme** — Brown, Blue, Green, Purple, Gray, Wood, Marble, or Custom +- **Board Size** — Small (280px), Medium (360px), Large (480px), or Auto +- **Piece Set** — 20 options including Cburnett (default), Merida, Alpha, California, and more +- **Notation Style** — Figurine (♞f3) or Letter (Nf3) +- **Coordinates** — Show/hide rank and file labels +- **Colors** — Customize last move highlight, check highlight, arrows, circles +- **Animation Speed** — 0–500ms +- **Autoplay Speed** — 200–3000ms +- **Move List Position** — Right of board or below +- **Analysis Links** — Show/hide Lichess and Chess.com links +- **Default Orientation** — White, Black, or Auto (based on side to move) +- **Puzzle Hints** — Enable/disable hint button in puzzle mode + +## Compatibility + +- **Obsidian** — v1.0.0 and above +- **Platforms** — Desktop (Windows, macOS, Linux) and Mobile (iOS, Android) +- **Themes** — Works with both light and dark themes + +## Credits + +- [Chessground](https://github.com/lichess-org/chessground) — Chess board UI by Lichess +- [chess.js](https://github.com/jhlywa/chess.js) — Chess logic library + +## License + +[MIT](LICENSE) diff --git a/assets/pieces/alpha/bB.svg b/assets/pieces/alpha/bB.svg new file mode 100644 index 0000000..f5dba66 --- /dev/null +++ b/assets/pieces/alpha/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/bK.svg b/assets/pieces/alpha/bK.svg new file mode 100644 index 0000000..f300083 --- /dev/null +++ b/assets/pieces/alpha/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/bN.svg b/assets/pieces/alpha/bN.svg new file mode 100644 index 0000000..e865312 --- /dev/null +++ b/assets/pieces/alpha/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/bP.svg b/assets/pieces/alpha/bP.svg new file mode 100644 index 0000000..33bf130 --- /dev/null +++ b/assets/pieces/alpha/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/bQ.svg b/assets/pieces/alpha/bQ.svg new file mode 100644 index 0000000..46a51a1 --- /dev/null +++ b/assets/pieces/alpha/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/bR.svg b/assets/pieces/alpha/bR.svg new file mode 100644 index 0000000..f00204b --- /dev/null +++ b/assets/pieces/alpha/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wB.svg b/assets/pieces/alpha/wB.svg new file mode 100644 index 0000000..97213cd --- /dev/null +++ b/assets/pieces/alpha/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wK.svg b/assets/pieces/alpha/wK.svg new file mode 100644 index 0000000..1de20c7 --- /dev/null +++ b/assets/pieces/alpha/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wN.svg b/assets/pieces/alpha/wN.svg new file mode 100644 index 0000000..6c27955 --- /dev/null +++ b/assets/pieces/alpha/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wP.svg b/assets/pieces/alpha/wP.svg new file mode 100644 index 0000000..0268b6a --- /dev/null +++ b/assets/pieces/alpha/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wQ.svg b/assets/pieces/alpha/wQ.svg new file mode 100644 index 0000000..185e13d --- /dev/null +++ b/assets/pieces/alpha/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/alpha/wR.svg b/assets/pieces/alpha/wR.svg new file mode 100644 index 0000000..3f9cd91 --- /dev/null +++ b/assets/pieces/alpha/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bB.svg b/assets/pieces/california/bB.svg new file mode 100644 index 0000000..e728923 --- /dev/null +++ b/assets/pieces/california/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bK.svg b/assets/pieces/california/bK.svg new file mode 100644 index 0000000..4db7f7b --- /dev/null +++ b/assets/pieces/california/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bN.svg b/assets/pieces/california/bN.svg new file mode 100644 index 0000000..8bbe500 --- /dev/null +++ b/assets/pieces/california/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bP.svg b/assets/pieces/california/bP.svg new file mode 100644 index 0000000..bb4ce3f --- /dev/null +++ b/assets/pieces/california/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bQ.svg b/assets/pieces/california/bQ.svg new file mode 100644 index 0000000..479e1f9 --- /dev/null +++ b/assets/pieces/california/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/bR.svg b/assets/pieces/california/bR.svg new file mode 100644 index 0000000..aae79ba --- /dev/null +++ b/assets/pieces/california/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wB.svg b/assets/pieces/california/wB.svg new file mode 100644 index 0000000..03c71b7 --- /dev/null +++ b/assets/pieces/california/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wK.svg b/assets/pieces/california/wK.svg new file mode 100644 index 0000000..5478848 --- /dev/null +++ b/assets/pieces/california/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wN.svg b/assets/pieces/california/wN.svg new file mode 100644 index 0000000..45b2bda --- /dev/null +++ b/assets/pieces/california/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wP.svg b/assets/pieces/california/wP.svg new file mode 100644 index 0000000..bba8d0f --- /dev/null +++ b/assets/pieces/california/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wQ.svg b/assets/pieces/california/wQ.svg new file mode 100644 index 0000000..a857fc4 --- /dev/null +++ b/assets/pieces/california/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/california/wR.svg b/assets/pieces/california/wR.svg new file mode 100644 index 0000000..4b2f598 --- /dev/null +++ b/assets/pieces/california/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bB.svg b/assets/pieces/cardinal/bB.svg new file mode 100644 index 0000000..7050e90 --- /dev/null +++ b/assets/pieces/cardinal/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bK.svg b/assets/pieces/cardinal/bK.svg new file mode 100644 index 0000000..2f5ff4a --- /dev/null +++ b/assets/pieces/cardinal/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bN.svg b/assets/pieces/cardinal/bN.svg new file mode 100644 index 0000000..5738910 --- /dev/null +++ b/assets/pieces/cardinal/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bP.svg b/assets/pieces/cardinal/bP.svg new file mode 100644 index 0000000..3a37a7d --- /dev/null +++ b/assets/pieces/cardinal/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bQ.svg b/assets/pieces/cardinal/bQ.svg new file mode 100644 index 0000000..204d149 --- /dev/null +++ b/assets/pieces/cardinal/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/bR.svg b/assets/pieces/cardinal/bR.svg new file mode 100644 index 0000000..a01b04d --- /dev/null +++ b/assets/pieces/cardinal/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wB.svg b/assets/pieces/cardinal/wB.svg new file mode 100644 index 0000000..11450c6 --- /dev/null +++ b/assets/pieces/cardinal/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wK.svg b/assets/pieces/cardinal/wK.svg new file mode 100644 index 0000000..58ec38d --- /dev/null +++ b/assets/pieces/cardinal/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wN.svg b/assets/pieces/cardinal/wN.svg new file mode 100644 index 0000000..23944a3 --- /dev/null +++ b/assets/pieces/cardinal/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wP.svg b/assets/pieces/cardinal/wP.svg new file mode 100644 index 0000000..3b47f67 --- /dev/null +++ b/assets/pieces/cardinal/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wQ.svg b/assets/pieces/cardinal/wQ.svg new file mode 100644 index 0000000..2e904c7 --- /dev/null +++ b/assets/pieces/cardinal/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cardinal/wR.svg b/assets/pieces/cardinal/wR.svg new file mode 100644 index 0000000..8ba39b2 --- /dev/null +++ b/assets/pieces/cardinal/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bB.svg b/assets/pieces/cburnett/bB.svg new file mode 100644 index 0000000..fd71645 --- /dev/null +++ b/assets/pieces/cburnett/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bK.svg b/assets/pieces/cburnett/bK.svg new file mode 100644 index 0000000..8114d17 --- /dev/null +++ b/assets/pieces/cburnett/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bN.svg b/assets/pieces/cburnett/bN.svg new file mode 100644 index 0000000..2c2c534 --- /dev/null +++ b/assets/pieces/cburnett/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bP.svg b/assets/pieces/cburnett/bP.svg new file mode 100644 index 0000000..23b2bc2 --- /dev/null +++ b/assets/pieces/cburnett/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bQ.svg b/assets/pieces/cburnett/bQ.svg new file mode 100644 index 0000000..005c223 --- /dev/null +++ b/assets/pieces/cburnett/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/bR.svg b/assets/pieces/cburnett/bR.svg new file mode 100644 index 0000000..c9339b0 --- /dev/null +++ b/assets/pieces/cburnett/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wB.svg b/assets/pieces/cburnett/wB.svg new file mode 100644 index 0000000..3d7deb0 --- /dev/null +++ b/assets/pieces/cburnett/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wK.svg b/assets/pieces/cburnett/wK.svg new file mode 100644 index 0000000..08189e8 --- /dev/null +++ b/assets/pieces/cburnett/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wN.svg b/assets/pieces/cburnett/wN.svg new file mode 100644 index 0000000..005c337 --- /dev/null +++ b/assets/pieces/cburnett/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wP.svg b/assets/pieces/cburnett/wP.svg new file mode 100644 index 0000000..9949318 --- /dev/null +++ b/assets/pieces/cburnett/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wQ.svg b/assets/pieces/cburnett/wQ.svg new file mode 100644 index 0000000..d5c5ee4 --- /dev/null +++ b/assets/pieces/cburnett/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/cburnett/wR.svg b/assets/pieces/cburnett/wR.svg new file mode 100644 index 0000000..c149104 --- /dev/null +++ b/assets/pieces/cburnett/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bB.svg b/assets/pieces/chess7/bB.svg new file mode 100644 index 0000000..24eab6a --- /dev/null +++ b/assets/pieces/chess7/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bK.svg b/assets/pieces/chess7/bK.svg new file mode 100644 index 0000000..cf5110a --- /dev/null +++ b/assets/pieces/chess7/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bN.svg b/assets/pieces/chess7/bN.svg new file mode 100644 index 0000000..bdb457a --- /dev/null +++ b/assets/pieces/chess7/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bP.svg b/assets/pieces/chess7/bP.svg new file mode 100644 index 0000000..af8fe21 --- /dev/null +++ b/assets/pieces/chess7/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bQ.svg b/assets/pieces/chess7/bQ.svg new file mode 100644 index 0000000..a8c4771 --- /dev/null +++ b/assets/pieces/chess7/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/bR.svg b/assets/pieces/chess7/bR.svg new file mode 100644 index 0000000..19d063a --- /dev/null +++ b/assets/pieces/chess7/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wB.svg b/assets/pieces/chess7/wB.svg new file mode 100644 index 0000000..8c87429 --- /dev/null +++ b/assets/pieces/chess7/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wK.svg b/assets/pieces/chess7/wK.svg new file mode 100644 index 0000000..82a8872 --- /dev/null +++ b/assets/pieces/chess7/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wN.svg b/assets/pieces/chess7/wN.svg new file mode 100644 index 0000000..a84609b --- /dev/null +++ b/assets/pieces/chess7/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wP.svg b/assets/pieces/chess7/wP.svg new file mode 100644 index 0000000..f18dd04 --- /dev/null +++ b/assets/pieces/chess7/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wQ.svg b/assets/pieces/chess7/wQ.svg new file mode 100644 index 0000000..2c23eae --- /dev/null +++ b/assets/pieces/chess7/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/chess7/wR.svg b/assets/pieces/chess7/wR.svg new file mode 100644 index 0000000..9344005 --- /dev/null +++ b/assets/pieces/chess7/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bB.svg b/assets/pieces/dubrovny/bB.svg new file mode 100644 index 0000000..85fe166 --- /dev/null +++ b/assets/pieces/dubrovny/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bK.svg b/assets/pieces/dubrovny/bK.svg new file mode 100644 index 0000000..8dd553f --- /dev/null +++ b/assets/pieces/dubrovny/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bN.svg b/assets/pieces/dubrovny/bN.svg new file mode 100644 index 0000000..25188ed --- /dev/null +++ b/assets/pieces/dubrovny/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bP.svg b/assets/pieces/dubrovny/bP.svg new file mode 100644 index 0000000..86d7e79 --- /dev/null +++ b/assets/pieces/dubrovny/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bQ.svg b/assets/pieces/dubrovny/bQ.svg new file mode 100644 index 0000000..6ecf9a5 --- /dev/null +++ b/assets/pieces/dubrovny/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/bR.svg b/assets/pieces/dubrovny/bR.svg new file mode 100644 index 0000000..5db423c --- /dev/null +++ b/assets/pieces/dubrovny/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wB.svg b/assets/pieces/dubrovny/wB.svg new file mode 100644 index 0000000..82bfad9 --- /dev/null +++ b/assets/pieces/dubrovny/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wK.svg b/assets/pieces/dubrovny/wK.svg new file mode 100644 index 0000000..6c9aeda --- /dev/null +++ b/assets/pieces/dubrovny/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wN.svg b/assets/pieces/dubrovny/wN.svg new file mode 100644 index 0000000..a9079e7 --- /dev/null +++ b/assets/pieces/dubrovny/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wP.svg b/assets/pieces/dubrovny/wP.svg new file mode 100644 index 0000000..727f079 --- /dev/null +++ b/assets/pieces/dubrovny/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wQ.svg b/assets/pieces/dubrovny/wQ.svg new file mode 100644 index 0000000..eedd776 --- /dev/null +++ b/assets/pieces/dubrovny/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/dubrovny/wR.svg b/assets/pieces/dubrovny/wR.svg new file mode 100644 index 0000000..b6cd512 --- /dev/null +++ b/assets/pieces/dubrovny/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bB.svg b/assets/pieces/fantasy/bB.svg new file mode 100644 index 0000000..75d08cf --- /dev/null +++ b/assets/pieces/fantasy/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bK.svg b/assets/pieces/fantasy/bK.svg new file mode 100644 index 0000000..6934638 --- /dev/null +++ b/assets/pieces/fantasy/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bN.svg b/assets/pieces/fantasy/bN.svg new file mode 100644 index 0000000..4de1945 --- /dev/null +++ b/assets/pieces/fantasy/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bP.svg b/assets/pieces/fantasy/bP.svg new file mode 100644 index 0000000..4b91f87 --- /dev/null +++ b/assets/pieces/fantasy/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bQ.svg b/assets/pieces/fantasy/bQ.svg new file mode 100644 index 0000000..c7aa592 --- /dev/null +++ b/assets/pieces/fantasy/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/bR.svg b/assets/pieces/fantasy/bR.svg new file mode 100644 index 0000000..5cab302 --- /dev/null +++ b/assets/pieces/fantasy/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wB.svg b/assets/pieces/fantasy/wB.svg new file mode 100644 index 0000000..2f2a178 --- /dev/null +++ b/assets/pieces/fantasy/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wK.svg b/assets/pieces/fantasy/wK.svg new file mode 100644 index 0000000..4e1fcfc --- /dev/null +++ b/assets/pieces/fantasy/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wN.svg b/assets/pieces/fantasy/wN.svg new file mode 100644 index 0000000..9767054 --- /dev/null +++ b/assets/pieces/fantasy/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wP.svg b/assets/pieces/fantasy/wP.svg new file mode 100644 index 0000000..d267df9 --- /dev/null +++ b/assets/pieces/fantasy/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wQ.svg b/assets/pieces/fantasy/wQ.svg new file mode 100644 index 0000000..df8c3ee --- /dev/null +++ b/assets/pieces/fantasy/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/fantasy/wR.svg b/assets/pieces/fantasy/wR.svg new file mode 100644 index 0000000..cb484f4 --- /dev/null +++ b/assets/pieces/fantasy/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bB.svg b/assets/pieces/gioco/bB.svg new file mode 100644 index 0000000..d54058a --- /dev/null +++ b/assets/pieces/gioco/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bK.svg b/assets/pieces/gioco/bK.svg new file mode 100644 index 0000000..0c142a2 --- /dev/null +++ b/assets/pieces/gioco/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bN.svg b/assets/pieces/gioco/bN.svg new file mode 100644 index 0000000..4dcb444 --- /dev/null +++ b/assets/pieces/gioco/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bP.svg b/assets/pieces/gioco/bP.svg new file mode 100644 index 0000000..0e2a050 --- /dev/null +++ b/assets/pieces/gioco/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bQ.svg b/assets/pieces/gioco/bQ.svg new file mode 100644 index 0000000..7ec3687 --- /dev/null +++ b/assets/pieces/gioco/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/bR.svg b/assets/pieces/gioco/bR.svg new file mode 100644 index 0000000..e39df0c --- /dev/null +++ b/assets/pieces/gioco/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wB.svg b/assets/pieces/gioco/wB.svg new file mode 100644 index 0000000..3155550 --- /dev/null +++ b/assets/pieces/gioco/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wK.svg b/assets/pieces/gioco/wK.svg new file mode 100644 index 0000000..d8c7169 --- /dev/null +++ b/assets/pieces/gioco/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wN.svg b/assets/pieces/gioco/wN.svg new file mode 100644 index 0000000..f0ad3ae --- /dev/null +++ b/assets/pieces/gioco/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wP.svg b/assets/pieces/gioco/wP.svg new file mode 100644 index 0000000..06ea317 --- /dev/null +++ b/assets/pieces/gioco/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wQ.svg b/assets/pieces/gioco/wQ.svg new file mode 100644 index 0000000..95e1e51 --- /dev/null +++ b/assets/pieces/gioco/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/gioco/wR.svg b/assets/pieces/gioco/wR.svg new file mode 100644 index 0000000..e007a75 --- /dev/null +++ b/assets/pieces/gioco/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bB.svg b/assets/pieces/governor/bB.svg new file mode 100644 index 0000000..3a73908 --- /dev/null +++ b/assets/pieces/governor/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bK.svg b/assets/pieces/governor/bK.svg new file mode 100644 index 0000000..c3695fd --- /dev/null +++ b/assets/pieces/governor/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bN.svg b/assets/pieces/governor/bN.svg new file mode 100644 index 0000000..de6b0c0 --- /dev/null +++ b/assets/pieces/governor/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bP.svg b/assets/pieces/governor/bP.svg new file mode 100644 index 0000000..71c050a --- /dev/null +++ b/assets/pieces/governor/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bQ.svg b/assets/pieces/governor/bQ.svg new file mode 100644 index 0000000..07eaab4 --- /dev/null +++ b/assets/pieces/governor/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/bR.svg b/assets/pieces/governor/bR.svg new file mode 100644 index 0000000..1547479 --- /dev/null +++ b/assets/pieces/governor/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wB.svg b/assets/pieces/governor/wB.svg new file mode 100644 index 0000000..5bec1d8 --- /dev/null +++ b/assets/pieces/governor/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wK.svg b/assets/pieces/governor/wK.svg new file mode 100644 index 0000000..7e5ce80 --- /dev/null +++ b/assets/pieces/governor/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wN.svg b/assets/pieces/governor/wN.svg new file mode 100644 index 0000000..8f7dcc1 --- /dev/null +++ b/assets/pieces/governor/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wP.svg b/assets/pieces/governor/wP.svg new file mode 100644 index 0000000..85ee752 --- /dev/null +++ b/assets/pieces/governor/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wQ.svg b/assets/pieces/governor/wQ.svg new file mode 100644 index 0000000..03f46d6 --- /dev/null +++ b/assets/pieces/governor/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/governor/wR.svg b/assets/pieces/governor/wR.svg new file mode 100644 index 0000000..32e9aa9 --- /dev/null +++ b/assets/pieces/governor/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bB.svg b/assets/pieces/horsey/bB.svg new file mode 100644 index 0000000..429d99e --- /dev/null +++ b/assets/pieces/horsey/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bK.svg b/assets/pieces/horsey/bK.svg new file mode 100644 index 0000000..3d6e131 --- /dev/null +++ b/assets/pieces/horsey/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bN.svg b/assets/pieces/horsey/bN.svg new file mode 100644 index 0000000..0261190 --- /dev/null +++ b/assets/pieces/horsey/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bP.svg b/assets/pieces/horsey/bP.svg new file mode 100644 index 0000000..bbd1920 --- /dev/null +++ b/assets/pieces/horsey/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bQ.svg b/assets/pieces/horsey/bQ.svg new file mode 100644 index 0000000..20f67f0 --- /dev/null +++ b/assets/pieces/horsey/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/bR.svg b/assets/pieces/horsey/bR.svg new file mode 100644 index 0000000..4490776 --- /dev/null +++ b/assets/pieces/horsey/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wB.svg b/assets/pieces/horsey/wB.svg new file mode 100644 index 0000000..569893e --- /dev/null +++ b/assets/pieces/horsey/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wK.svg b/assets/pieces/horsey/wK.svg new file mode 100644 index 0000000..3505458 --- /dev/null +++ b/assets/pieces/horsey/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wN.svg b/assets/pieces/horsey/wN.svg new file mode 100644 index 0000000..0177fdb --- /dev/null +++ b/assets/pieces/horsey/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wP.svg b/assets/pieces/horsey/wP.svg new file mode 100644 index 0000000..fd7e877 --- /dev/null +++ b/assets/pieces/horsey/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wQ.svg b/assets/pieces/horsey/wQ.svg new file mode 100644 index 0000000..51d06a4 --- /dev/null +++ b/assets/pieces/horsey/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/horsey/wR.svg b/assets/pieces/horsey/wR.svg new file mode 100644 index 0000000..fb690d6 --- /dev/null +++ b/assets/pieces/horsey/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bB.svg b/assets/pieces/icpieces/bB.svg new file mode 100644 index 0000000..38c0bca --- /dev/null +++ b/assets/pieces/icpieces/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bK.svg b/assets/pieces/icpieces/bK.svg new file mode 100644 index 0000000..570aaaf --- /dev/null +++ b/assets/pieces/icpieces/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bN.svg b/assets/pieces/icpieces/bN.svg new file mode 100644 index 0000000..200d195 --- /dev/null +++ b/assets/pieces/icpieces/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bP.svg b/assets/pieces/icpieces/bP.svg new file mode 100644 index 0000000..bc105a3 --- /dev/null +++ b/assets/pieces/icpieces/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bQ.svg b/assets/pieces/icpieces/bQ.svg new file mode 100644 index 0000000..5559cfd --- /dev/null +++ b/assets/pieces/icpieces/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/bR.svg b/assets/pieces/icpieces/bR.svg new file mode 100644 index 0000000..a1156f4 --- /dev/null +++ b/assets/pieces/icpieces/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wB.svg b/assets/pieces/icpieces/wB.svg new file mode 100644 index 0000000..5516cd1 --- /dev/null +++ b/assets/pieces/icpieces/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wK.svg b/assets/pieces/icpieces/wK.svg new file mode 100644 index 0000000..7652b90 --- /dev/null +++ b/assets/pieces/icpieces/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wN.svg b/assets/pieces/icpieces/wN.svg new file mode 100644 index 0000000..4244d68 --- /dev/null +++ b/assets/pieces/icpieces/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wP.svg b/assets/pieces/icpieces/wP.svg new file mode 100644 index 0000000..fa2bb85 --- /dev/null +++ b/assets/pieces/icpieces/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wQ.svg b/assets/pieces/icpieces/wQ.svg new file mode 100644 index 0000000..52c4854 --- /dev/null +++ b/assets/pieces/icpieces/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/icpieces/wR.svg b/assets/pieces/icpieces/wR.svg new file mode 100644 index 0000000..082c11a --- /dev/null +++ b/assets/pieces/icpieces/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bB.svg b/assets/pieces/kosal/bB.svg new file mode 100644 index 0000000..12b57d8 --- /dev/null +++ b/assets/pieces/kosal/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bK.svg b/assets/pieces/kosal/bK.svg new file mode 100644 index 0000000..1f6ea0d --- /dev/null +++ b/assets/pieces/kosal/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bN.svg b/assets/pieces/kosal/bN.svg new file mode 100644 index 0000000..24c48fb --- /dev/null +++ b/assets/pieces/kosal/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bP.svg b/assets/pieces/kosal/bP.svg new file mode 100644 index 0000000..f8d77de --- /dev/null +++ b/assets/pieces/kosal/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bQ.svg b/assets/pieces/kosal/bQ.svg new file mode 100644 index 0000000..0b99bba --- /dev/null +++ b/assets/pieces/kosal/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/bR.svg b/assets/pieces/kosal/bR.svg new file mode 100644 index 0000000..91ecc34 --- /dev/null +++ b/assets/pieces/kosal/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wB.svg b/assets/pieces/kosal/wB.svg new file mode 100644 index 0000000..b88264f --- /dev/null +++ b/assets/pieces/kosal/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wK.svg b/assets/pieces/kosal/wK.svg new file mode 100644 index 0000000..50b1311 --- /dev/null +++ b/assets/pieces/kosal/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wN.svg b/assets/pieces/kosal/wN.svg new file mode 100644 index 0000000..18ef199 --- /dev/null +++ b/assets/pieces/kosal/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wP.svg b/assets/pieces/kosal/wP.svg new file mode 100644 index 0000000..566a5ee --- /dev/null +++ b/assets/pieces/kosal/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wQ.svg b/assets/pieces/kosal/wQ.svg new file mode 100644 index 0000000..9975311 --- /dev/null +++ b/assets/pieces/kosal/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/kosal/wR.svg b/assets/pieces/kosal/wR.svg new file mode 100644 index 0000000..223d60a --- /dev/null +++ b/assets/pieces/kosal/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bB.svg b/assets/pieces/leipzig/bB.svg new file mode 100644 index 0000000..fa03602 --- /dev/null +++ b/assets/pieces/leipzig/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bK.svg b/assets/pieces/leipzig/bK.svg new file mode 100644 index 0000000..dffec80 --- /dev/null +++ b/assets/pieces/leipzig/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bN.svg b/assets/pieces/leipzig/bN.svg new file mode 100644 index 0000000..b710ff9 --- /dev/null +++ b/assets/pieces/leipzig/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bP.svg b/assets/pieces/leipzig/bP.svg new file mode 100644 index 0000000..671d96c --- /dev/null +++ b/assets/pieces/leipzig/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bQ.svg b/assets/pieces/leipzig/bQ.svg new file mode 100644 index 0000000..f92007a --- /dev/null +++ b/assets/pieces/leipzig/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/bR.svg b/assets/pieces/leipzig/bR.svg new file mode 100644 index 0000000..3bf662b --- /dev/null +++ b/assets/pieces/leipzig/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wB.svg b/assets/pieces/leipzig/wB.svg new file mode 100644 index 0000000..c014c98 --- /dev/null +++ b/assets/pieces/leipzig/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wK.svg b/assets/pieces/leipzig/wK.svg new file mode 100644 index 0000000..9e30e4c --- /dev/null +++ b/assets/pieces/leipzig/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wN.svg b/assets/pieces/leipzig/wN.svg new file mode 100644 index 0000000..a2f68a1 --- /dev/null +++ b/assets/pieces/leipzig/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wP.svg b/assets/pieces/leipzig/wP.svg new file mode 100644 index 0000000..d5d2838 --- /dev/null +++ b/assets/pieces/leipzig/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wQ.svg b/assets/pieces/leipzig/wQ.svg new file mode 100644 index 0000000..e83c9c7 --- /dev/null +++ b/assets/pieces/leipzig/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/leipzig/wR.svg b/assets/pieces/leipzig/wR.svg new file mode 100644 index 0000000..b2cf5c0 --- /dev/null +++ b/assets/pieces/leipzig/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/libra/bB.svg b/assets/pieces/libra/bB.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bB.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/bK.svg b/assets/pieces/libra/bK.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bK.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/bN.svg b/assets/pieces/libra/bN.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bN.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/bP.svg b/assets/pieces/libra/bP.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bP.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/bQ.svg b/assets/pieces/libra/bQ.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bQ.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/bR.svg b/assets/pieces/libra/bR.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/bR.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wB.svg b/assets/pieces/libra/wB.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wB.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wK.svg b/assets/pieces/libra/wK.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wK.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wN.svg b/assets/pieces/libra/wN.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wN.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wP.svg b/assets/pieces/libra/wP.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wP.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wQ.svg b/assets/pieces/libra/wQ.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wQ.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/libra/wR.svg b/assets/pieces/libra/wR.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/libra/wR.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/maestro/bB.svg b/assets/pieces/maestro/bB.svg new file mode 100644 index 0000000..98da810 --- /dev/null +++ b/assets/pieces/maestro/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/bK.svg b/assets/pieces/maestro/bK.svg new file mode 100644 index 0000000..24fc519 --- /dev/null +++ b/assets/pieces/maestro/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/bN.svg b/assets/pieces/maestro/bN.svg new file mode 100644 index 0000000..63178ea --- /dev/null +++ b/assets/pieces/maestro/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/bP.svg b/assets/pieces/maestro/bP.svg new file mode 100644 index 0000000..2ff0cb4 --- /dev/null +++ b/assets/pieces/maestro/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/bQ.svg b/assets/pieces/maestro/bQ.svg new file mode 100644 index 0000000..9f187a9 --- /dev/null +++ b/assets/pieces/maestro/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/bR.svg b/assets/pieces/maestro/bR.svg new file mode 100644 index 0000000..26f079e --- /dev/null +++ b/assets/pieces/maestro/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wB.svg b/assets/pieces/maestro/wB.svg new file mode 100644 index 0000000..1f75018 --- /dev/null +++ b/assets/pieces/maestro/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wK.svg b/assets/pieces/maestro/wK.svg new file mode 100644 index 0000000..ee1c104 --- /dev/null +++ b/assets/pieces/maestro/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wN.svg b/assets/pieces/maestro/wN.svg new file mode 100644 index 0000000..8371d39 --- /dev/null +++ b/assets/pieces/maestro/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wP.svg b/assets/pieces/maestro/wP.svg new file mode 100644 index 0000000..021e6ea --- /dev/null +++ b/assets/pieces/maestro/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wQ.svg b/assets/pieces/maestro/wQ.svg new file mode 100644 index 0000000..7fce1af --- /dev/null +++ b/assets/pieces/maestro/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/maestro/wR.svg b/assets/pieces/maestro/wR.svg new file mode 100644 index 0000000..f9eecf7 --- /dev/null +++ b/assets/pieces/maestro/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bB.svg b/assets/pieces/merida/bB.svg new file mode 100644 index 0000000..ff61c73 --- /dev/null +++ b/assets/pieces/merida/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bK.svg b/assets/pieces/merida/bK.svg new file mode 100644 index 0000000..b0503b3 --- /dev/null +++ b/assets/pieces/merida/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bN.svg b/assets/pieces/merida/bN.svg new file mode 100644 index 0000000..e3fe6bd --- /dev/null +++ b/assets/pieces/merida/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bP.svg b/assets/pieces/merida/bP.svg new file mode 100644 index 0000000..b26e594 --- /dev/null +++ b/assets/pieces/merida/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bQ.svg b/assets/pieces/merida/bQ.svg new file mode 100644 index 0000000..cd8012a --- /dev/null +++ b/assets/pieces/merida/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/bR.svg b/assets/pieces/merida/bR.svg new file mode 100644 index 0000000..f0d0589 --- /dev/null +++ b/assets/pieces/merida/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wB.svg b/assets/pieces/merida/wB.svg new file mode 100644 index 0000000..d162700 --- /dev/null +++ b/assets/pieces/merida/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wK.svg b/assets/pieces/merida/wK.svg new file mode 100644 index 0000000..82b2089 --- /dev/null +++ b/assets/pieces/merida/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wN.svg b/assets/pieces/merida/wN.svg new file mode 100644 index 0000000..f43fec4 --- /dev/null +++ b/assets/pieces/merida/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wP.svg b/assets/pieces/merida/wP.svg new file mode 100644 index 0000000..1351dcb --- /dev/null +++ b/assets/pieces/merida/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wQ.svg b/assets/pieces/merida/wQ.svg new file mode 100644 index 0000000..d16457b --- /dev/null +++ b/assets/pieces/merida/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/merida/wR.svg b/assets/pieces/merida/wR.svg new file mode 100644 index 0000000..55a1c94 --- /dev/null +++ b/assets/pieces/merida/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/monarchy/bB.svg b/assets/pieces/monarchy/bB.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bB.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/bK.svg b/assets/pieces/monarchy/bK.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bK.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/bN.svg b/assets/pieces/monarchy/bN.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bN.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/bP.svg b/assets/pieces/monarchy/bP.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bP.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/bQ.svg b/assets/pieces/monarchy/bQ.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bQ.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/bR.svg b/assets/pieces/monarchy/bR.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/bR.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wB.svg b/assets/pieces/monarchy/wB.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wB.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wK.svg b/assets/pieces/monarchy/wK.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wK.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wN.svg b/assets/pieces/monarchy/wN.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wN.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wP.svg b/assets/pieces/monarchy/wP.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wP.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wQ.svg b/assets/pieces/monarchy/wQ.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wQ.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/monarchy/wR.svg b/assets/pieces/monarchy/wR.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/assets/pieces/monarchy/wR.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/assets/pieces/pirouetti/bB.svg b/assets/pieces/pirouetti/bB.svg new file mode 100644 index 0000000..481ca4d --- /dev/null +++ b/assets/pieces/pirouetti/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/bK.svg b/assets/pieces/pirouetti/bK.svg new file mode 100644 index 0000000..cc55118 --- /dev/null +++ b/assets/pieces/pirouetti/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/bN.svg b/assets/pieces/pirouetti/bN.svg new file mode 100644 index 0000000..29e5e11 --- /dev/null +++ b/assets/pieces/pirouetti/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/bP.svg b/assets/pieces/pirouetti/bP.svg new file mode 100644 index 0000000..efa6316 --- /dev/null +++ b/assets/pieces/pirouetti/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/bQ.svg b/assets/pieces/pirouetti/bQ.svg new file mode 100644 index 0000000..0097d03 --- /dev/null +++ b/assets/pieces/pirouetti/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/bR.svg b/assets/pieces/pirouetti/bR.svg new file mode 100644 index 0000000..0a24c04 --- /dev/null +++ b/assets/pieces/pirouetti/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wB.svg b/assets/pieces/pirouetti/wB.svg new file mode 100644 index 0000000..5a18dcc --- /dev/null +++ b/assets/pieces/pirouetti/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wK.svg b/assets/pieces/pirouetti/wK.svg new file mode 100644 index 0000000..a495a40 --- /dev/null +++ b/assets/pieces/pirouetti/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wN.svg b/assets/pieces/pirouetti/wN.svg new file mode 100644 index 0000000..39cbfd6 --- /dev/null +++ b/assets/pieces/pirouetti/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wP.svg b/assets/pieces/pirouetti/wP.svg new file mode 100644 index 0000000..d29934b --- /dev/null +++ b/assets/pieces/pirouetti/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wQ.svg b/assets/pieces/pirouetti/wQ.svg new file mode 100644 index 0000000..6c01f48 --- /dev/null +++ b/assets/pieces/pirouetti/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/pirouetti/wR.svg b/assets/pieces/pirouetti/wR.svg new file mode 100644 index 0000000..0f0883a --- /dev/null +++ b/assets/pieces/pirouetti/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bB.svg b/assets/pieces/spatial/bB.svg new file mode 100644 index 0000000..65f8b02 --- /dev/null +++ b/assets/pieces/spatial/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bK.svg b/assets/pieces/spatial/bK.svg new file mode 100644 index 0000000..fc5e5de --- /dev/null +++ b/assets/pieces/spatial/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bN.svg b/assets/pieces/spatial/bN.svg new file mode 100644 index 0000000..955f625 --- /dev/null +++ b/assets/pieces/spatial/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bP.svg b/assets/pieces/spatial/bP.svg new file mode 100644 index 0000000..5bcdeb9 --- /dev/null +++ b/assets/pieces/spatial/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bQ.svg b/assets/pieces/spatial/bQ.svg new file mode 100644 index 0000000..1109456 --- /dev/null +++ b/assets/pieces/spatial/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/bR.svg b/assets/pieces/spatial/bR.svg new file mode 100644 index 0000000..79ce913 --- /dev/null +++ b/assets/pieces/spatial/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wB.svg b/assets/pieces/spatial/wB.svg new file mode 100644 index 0000000..6a77f6a --- /dev/null +++ b/assets/pieces/spatial/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wK.svg b/assets/pieces/spatial/wK.svg new file mode 100644 index 0000000..ea44252 --- /dev/null +++ b/assets/pieces/spatial/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wN.svg b/assets/pieces/spatial/wN.svg new file mode 100644 index 0000000..b0dc2f8 --- /dev/null +++ b/assets/pieces/spatial/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wP.svg b/assets/pieces/spatial/wP.svg new file mode 100644 index 0000000..2b88e53 --- /dev/null +++ b/assets/pieces/spatial/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wQ.svg b/assets/pieces/spatial/wQ.svg new file mode 100644 index 0000000..82fb071 --- /dev/null +++ b/assets/pieces/spatial/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/spatial/wR.svg b/assets/pieces/spatial/wR.svg new file mode 100644 index 0000000..de002fe --- /dev/null +++ b/assets/pieces/spatial/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bB.svg b/assets/pieces/staunty/bB.svg new file mode 100644 index 0000000..6bad880 --- /dev/null +++ b/assets/pieces/staunty/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bK.svg b/assets/pieces/staunty/bK.svg new file mode 100644 index 0000000..4b931cb --- /dev/null +++ b/assets/pieces/staunty/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bN.svg b/assets/pieces/staunty/bN.svg new file mode 100644 index 0000000..6106653 --- /dev/null +++ b/assets/pieces/staunty/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bP.svg b/assets/pieces/staunty/bP.svg new file mode 100644 index 0000000..6dd7593 --- /dev/null +++ b/assets/pieces/staunty/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bQ.svg b/assets/pieces/staunty/bQ.svg new file mode 100644 index 0000000..88aeed9 --- /dev/null +++ b/assets/pieces/staunty/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/bR.svg b/assets/pieces/staunty/bR.svg new file mode 100644 index 0000000..32d2bf2 --- /dev/null +++ b/assets/pieces/staunty/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wB.svg b/assets/pieces/staunty/wB.svg new file mode 100644 index 0000000..483963a --- /dev/null +++ b/assets/pieces/staunty/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wK.svg b/assets/pieces/staunty/wK.svg new file mode 100644 index 0000000..c6ac16e --- /dev/null +++ b/assets/pieces/staunty/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wN.svg b/assets/pieces/staunty/wN.svg new file mode 100644 index 0000000..300033c --- /dev/null +++ b/assets/pieces/staunty/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wP.svg b/assets/pieces/staunty/wP.svg new file mode 100644 index 0000000..6520829 --- /dev/null +++ b/assets/pieces/staunty/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wQ.svg b/assets/pieces/staunty/wQ.svg new file mode 100644 index 0000000..93dd7a0 --- /dev/null +++ b/assets/pieces/staunty/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/staunty/wR.svg b/assets/pieces/staunty/wR.svg new file mode 100644 index 0000000..99ed08e --- /dev/null +++ b/assets/pieces/staunty/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bB.svg b/assets/pieces/tatiana/bB.svg new file mode 100644 index 0000000..bd155ba --- /dev/null +++ b/assets/pieces/tatiana/bB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bK.svg b/assets/pieces/tatiana/bK.svg new file mode 100644 index 0000000..9c65dec --- /dev/null +++ b/assets/pieces/tatiana/bK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bN.svg b/assets/pieces/tatiana/bN.svg new file mode 100644 index 0000000..9acd1a9 --- /dev/null +++ b/assets/pieces/tatiana/bN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bP.svg b/assets/pieces/tatiana/bP.svg new file mode 100644 index 0000000..061c1ec --- /dev/null +++ b/assets/pieces/tatiana/bP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bQ.svg b/assets/pieces/tatiana/bQ.svg new file mode 100644 index 0000000..990cba2 --- /dev/null +++ b/assets/pieces/tatiana/bQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/bR.svg b/assets/pieces/tatiana/bR.svg new file mode 100644 index 0000000..3be3aa7 --- /dev/null +++ b/assets/pieces/tatiana/bR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wB.svg b/assets/pieces/tatiana/wB.svg new file mode 100644 index 0000000..13fce87 --- /dev/null +++ b/assets/pieces/tatiana/wB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wK.svg b/assets/pieces/tatiana/wK.svg new file mode 100644 index 0000000..2388b60 --- /dev/null +++ b/assets/pieces/tatiana/wK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wN.svg b/assets/pieces/tatiana/wN.svg new file mode 100644 index 0000000..714b235 --- /dev/null +++ b/assets/pieces/tatiana/wN.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wP.svg b/assets/pieces/tatiana/wP.svg new file mode 100644 index 0000000..2130aee --- /dev/null +++ b/assets/pieces/tatiana/wP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wQ.svg b/assets/pieces/tatiana/wQ.svg new file mode 100644 index 0000000..41ec0b2 --- /dev/null +++ b/assets/pieces/tatiana/wQ.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pieces/tatiana/wR.svg b/assets/pieces/tatiana/wR.svg new file mode 100644 index 0000000..2054080 --- /dev/null +++ b/assets/pieces/tatiana/wR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..dfe08f2 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,39 @@ +import esbuild from 'esbuild'; +import process from 'process'; +import builtins from 'builtin-modules'; + +const prod = process.argv[2] === 'production'; + +const context = await esbuild.context({ + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins + ], + format: 'cjs', + target: 'es2020', + logLevel: 'info', + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: 'main.js' +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..17eef9f --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "chessview", + "name": "ChessView", + "version": "1.0.0", + "minAppVersion": "1.1.9", + "description": "Render interactive chess boards from PGN/FEN notation with puzzles, arrows, and move navigation.", + "author": "Ali Mousavi", + "authorUrl": "https://github.com/tuxitop", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e1cb87a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,622 @@ +{ + "name": "chessview", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chessview", + "version": "1.0.0", + "dependencies": { + "chess.js": "0.13.4", + "chessground": "^9.1.1" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.19.8", + "obsidian": "latest", + "typescript": "^5.3.2" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://npm.partdp.ir/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://npm.partdp.ir/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://npm.partdp.ir/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://npm.partdp.ir/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://npm.partdp.ir/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://npm.partdp.ir/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://npm.partdp.ir/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://npm.partdp.ir/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chess.js": { + "version": "0.13.4", + "resolved": "https://npm.partdp.ir/chess.js/-/chess.js-0.13.4.tgz", + "integrity": "sha512-li8pyEtvfw6V7xvVr5aUh8OuICfviWNT3YXPh56/wNZ8yEdqdAaWcWargK6xTmqsWKKH5c1bvdJ7lh5vqNv2wg==", + "license": "BSD-2-Clause" + }, + "node_modules/chessground": { + "version": "9.2.1", + "resolved": "https://npm.partdp.ir/chessground/-/chessground-9.2.1.tgz", + "integrity": "sha512-KIdxm0zD69e62XRwdnFB0XUNYSzn5HM3pMazRCwpfruRUowFI8y+1OAK5YSyBff29myeV1/4o5Q8T191+Irbog==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "GPL-3.0-or-later", + "funding": { + "url": "https://lichess.org/patron" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://npm.partdp.ir/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://npm.partdp.ir/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://npm.partdp.ir/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obsidian": { + "version": "1.11.4", + "resolved": "https://npm.partdp.ir/obsidian/-/obsidian-1.11.4.tgz", + "integrity": "sha512-n0KD3S+VndgaByrEtEe8NELy0ya6/s+KZ7OcxA6xOm5NN4thxKpQjo6eqEudHEvfGCeT/TYToAKJzitQ1I3XTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://npm.partdp.ir/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://npm.partdp.ir/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://npm.partdp.ir/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://npm.partdp.ir/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8ccfff --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "chessview", + "version": "1.0.0", + "description": "Interactive chess board plugin for Obsidian", + "author": "Ali Mousavi", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tuxitop/chessview" + }, + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "builtin-modules": "^3.3.0", + "esbuild": "^0.19.8", + "obsidian": "latest", + "typescript": "^5.3.2" + }, + "dependencies": { + "chess.js": "0.13.4", + "chessground": "^9.1.1" + } +} \ No newline at end of file diff --git a/screenshots/banner.png b/screenshots/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..7a381dbd02ce7bb31a20afc6e2aff460d01da6b8 GIT binary patch literal 86839 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 void + ): void { + if (!this.boardEl) return; + + const turn: Color = chess.turn() === 'w' ? 'white' : 'black'; + const isEditable = + this.data.isEditable && !this.data.isStatic && !this.data.isPuzzle; + + const config: Config = { + fen: chess.fen(), + orientation: this.isFlipped ? 'black' : 'white', + viewOnly: this.data.isStatic, + coordinates: this.settings.showCoordinates, + highlight: { lastMove: true, check: true }, + animation: { + enabled: this.settings.animationSpeed > 0, + duration: this.settings.animationSpeed + }, + movable: + isEditable && onUserMove + ? { + free: false, + color: 'both' as Color, + dests: getValidMoves(chess), + showDests: true, + events: { after: onUserMove } + } + : { free: false, color: undefined }, + premovable: { enabled: false }, + drawable: { + enabled: true, + visible: true, + autoShapes: this.getAutoShapes(null, 0, []) + } + }; + + this.ground = Chessground(this.boardEl, config); + } + + // =========================================================================== + // BOARD STATE UPDATES + // =========================================================================== + + syncBoard( + chess: Chess, + lastMove: MoveData | null, + options?: { + movable?: Config['movable']; + moves?: MoveData[]; + currentMoveIndex?: number; + } + ): void { + if (!this.ground) return; + + const turn: Color = chess.turn() === 'w' ? 'white' : 'black'; + const inCheck = isInCheck(chess); + + this.ground.set({ + fen: chess.fen(), + turnColor: turn, + check: inCheck, + lastMove: lastMove + ? [lastMove.from as Key, lastMove.to as Key] + : undefined, + movable: options?.movable, + drawable: { + autoShapes: this.getAutoShapes( + options?.moves ?? null, + options?.currentMoveIndex ?? 0, + [] + ) + } + }); + } + + /** + * Full board sync after a user/game move with editable movable config. + */ + syncAfterMove( + chess: Chess, + move: { from: string; to: string }, + moves: MoveData[], + currentMoveIndex: number + ): void { + if (!this.ground) return; + + const turn: Color = chess.turn() === 'w' ? 'white' : 'black'; + const inCheck = isInCheck(chess); + + this.ground.set({ + fen: chess.fen(), + turnColor: turn, + check: inCheck, + lastMove: [move.from as Key, move.to as Key], + movable: { + color: 'both' as Color, + dests: getValidMoves(chess) + }, + drawable: { + autoShapes: this.getAutoShapes(moves, currentMoveIndex, []) + } + }); + } + + /** + * Sync for puzzle mode — no last move display for wrong attempts. + */ + syncPuzzleBoard(chess: Chess, playedMoves: MoveData[]): void { + if (!this.ground) return; + + const turn: Color = chess.turn() === 'w' ? 'white' : 'black'; + const inCheck = isInCheck(chess); + + const lastPlayed = + playedMoves.length > 0 ? playedMoves[playedMoves.length - 1] : null; + + const showLastMove = + lastPlayed && lastPlayed.comment !== 'wrong' + ? [lastPlayed.from as Key, lastPlayed.to as Key] + : undefined; + + this.ground.set({ + fen: chess.fen(), + turnColor: turn, + check: inCheck, + lastMove: showLastMove as [Key, Key] | undefined, + drawable: { autoShapes: this.getAutoShapes(null, 0, []) } + }); + } + + // =========================================================================== + // MOVABLE CONFIG (for puzzle & editable) + // =========================================================================== + + enablePuzzleInput( + chess: Chess, + playerColor: Color, + handler: (orig: Key, dest: Key) => void + ): void { + if (!this.ground) return; + this.ground.set({ + movable: { + free: false, + color: playerColor, + dests: getValidMoves(chess), + showDests: true, + events: { after: handler } + } + }); + } + + disableInput(): void { + if (!this.ground) return; + this.ground.set({ + movable: { color: undefined, dests: new Map() } + }); + } + + setEditableMovable( + chess: Chess, + handler: (orig: Key, dest: Key) => void + ): void { + if (!this.ground) return; + this.ground.set({ + movable: { + free: false, + color: 'both' as Color, + dests: getValidMoves(chess), + showDests: true, + events: { after: handler } + } + }); + } + + // =========================================================================== + // ORIENTATION + // =========================================================================== + + get flipped(): boolean { + return this.isFlipped; + } + + flipBoard(): void { + this.isFlipped = !this.isFlipped; + this.ground?.toggleOrientation(); + } + + // =========================================================================== + // SHAPES (arrows, circles) + // =========================================================================== + + getAutoShapes( + moves: MoveData[] | null, + currentMoveIndex: number, + _extra: any[] + ): any[] { + const shapes: any[] = []; + const defaultArrowColor = this.settings.arrowColor || 'green'; + const defaultCircleColor = this.settings.circleColor || 'green'; + + for (const arrow of this.data.arrows) { + if (isValidSquare(arrow.from) && isValidSquare(arrow.to)) { + shapes.push({ + orig: arrow.from as Key, + dest: arrow.to as Key, + brush: arrow.color || defaultArrowColor + }); + } + } + + for (const circle of this.data.circles) { + if (isValidSquare(circle.square)) { + shapes.push({ + orig: circle.square as Key, + brush: circle.color || defaultCircleColor + }); + } + } + + // Move-specific annotations (non-puzzle) + if ( + !this.data.isPuzzle && + moves && + currentMoveIndex > 0 && + moves[currentMoveIndex - 1]?.annotations + ) { + const ann = moves[currentMoveIndex - 1].annotations!; + for (const arrow of ann.arrows) { + if (isValidSquare(arrow.from) && isValidSquare(arrow.to)) { + shapes.push({ + orig: arrow.from as Key, + dest: arrow.to as Key, + brush: arrow.color || defaultArrowColor + }); + } + } + for (const circle of ann.circles) { + if (isValidSquare(circle.square)) { + shapes.push({ + orig: circle.square as Key, + brush: circle.color || defaultCircleColor + }); + } + } + } + + return shapes; + } + + setAutoShapes(shapes: any[]): void { + this.ground?.setAutoShapes(shapes); + } + + // =========================================================================== + // HINT HIGHLIGHT (puzzle) + // =========================================================================== + + showHintHighlight( + square: string, + moves: MoveData[] | null, + currentMoveIndex: number, + durationMs: number + ): void { + if (!this.ground) return; + const baseShapes = this.getAutoShapes(moves, currentMoveIndex, []); + this.ground.setAutoShapes([ + ...baseShapes, + { orig: square as Key, brush: 'yellow' } + ]); + + setTimeout(() => { + if (this.ground) { + this.ground.setAutoShapes( + this.getAutoShapes(moves, currentMoveIndex, []) + ); + } + }, durationMs); + } + + // =========================================================================== + // NAG OVERLAY + // =========================================================================== + + private createNagOverlay(boardSection: HTMLElement): void { + this.nagOverlayEl = boardSection.createDiv({ cls: 'cv-nag-overlay' }); + } + + updateNagOverlay(moves: MoveData[], currentMoveIndex: number): void { + if (!this.nagOverlayEl) return; + this.nagOverlayEl.empty(); + + if (currentMoveIndex <= 0) return; + + const move = moves[currentMoveIndex - 1]; + if (!move?.nag) return; + + const nagClass = NAG_CLASSES[move.nag]; + if (!nagClass) return; + + const square = move.to; + const pos = squareToPosition(square, this.isFlipped); + if (!pos) return; + + const glyph = this.nagOverlayEl.createDiv({ + cls: `cv-nag-glyph ${nagClass}` + }); + glyph.style.left = `${pos.x}%`; + glyph.style.top = `${pos.y}%`; + + glyph.createSpan({ + cls: 'cv-nag-glyph-inner', + text: move.nag + }); + } + + updateNagHighlight(moves: MoveData[], currentMoveIndex: number): void { + if (!this.boardEl) return; + + // Remove old NAG highlight classes from ALL squares (not just last-move) + const allSquares = this.boardEl.querySelectorAll('cg-board square'); + allSquares.forEach((sq) => { + const el = sq as HTMLElement; + el.className = el.className.replace(/\s*nag-highlight-\S+/g, ''); + }); + + if (currentMoveIndex <= 0) return; + + const move = moves[currentMoveIndex - 1]; + if (!move?.nag) return; + + const nagClass = NAG_CLASSES[move.nag]; + if (!nagClass) return; + + const highlightClass = `nag-highlight-${nagClass.replace('nag-', '')}`; + + // Use a MutationObserver-friendly approach: wait for chessground to update + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (!this.boardEl) return; + const squares = this.boardEl.querySelectorAll( + 'cg-board square.last-move' + ); + squares?.forEach((sq) => { + sq.classList.add(highlightClass); + }); + }); + }); + } + + // =========================================================================== + // BOARD SIZE + // =========================================================================== + + private applyBoardSize(): void { + if (!this.boardEl) return; + const size = this.settings.boardSize; + this.boardEl.addClass(`cv-board-${size}`); + } + + private setupResizeObserver(): void { + if (!this.boardEl) return; + + this.resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = entry.contentRect.width; + if (width > 0 && this.boardEl) { + this.boardEl.style.height = `${width}px`; + this.ground?.redrawAll(); + this.container.style.setProperty('--cv-board-height', `${width}px`); + } + } + }); + + // Always observe so move list height stays synced + if (this.boardEl && this.resizeObserver) { + this.resizeObserver.observe(this.boardEl); + } + } + + // =========================================================================== + // THEME / APPEARANCE + // =========================================================================== + + applyTheme(): void { + const theme = this.settings.boardTheme; + const colors = + theme === 'custom' + ? { + light: this.settings.lightSquareColor, + dark: this.settings.darkSquareColor + } + : BOARD_THEMES[theme]; + + this.container.style.setProperty('--cv-light', colors.light); + this.container.style.setProperty('--cv-dark', colors.dark); + this._appliedStyleProps.push('--cv-light', '--cv-dark'); + + this.container.dataset.theme = theme; + + // Generate board SVG dynamically for ALL themes + const lightHex = colors.light.replace('#', '%23'); + const darkHex = colors.dark.replace('#', '%23'); + const svg = this.generateBoardSvg(lightHex, darkHex); + this.container.style.setProperty( + '--cv-board-svg', + `url('data:image/svg+xml,${svg}')` + ); + this._appliedStyleProps.push('--cv-board-svg'); + } + + applyPieceSet(): void { + const set = this.settings.pieceSet; + this.container.dataset.pieceSet = set; + + if (set === 'cburnett') return; + + const adapter = (window as any).app?.vault?.adapter; + if (!adapter || typeof adapter.getResourcePath !== 'function') return; + + const pluginBase = '.obsidian/plugins/chessview'; + const basePath = `${pluginBase}/assets/pieces/${set}`; + this.applyPieceImages(basePath, adapter); + } + + private generateBoardSvg(light: string, dark: string): string { + const darkSquares: string[] = []; + for (let r = 0; r < 8; r++) { + for (let f = 0; f < 8; f++) { + if ((r + f) % 2 === 1) { + darkSquares.push(``); + } + } + } + return `${darkSquares.join('')}`; + } + + private applyPieceImages( + basePath: string, + adapter: { getResourcePath: (path: string) => string } + ): void { + const pieces = [ + { css: 'piece.white.king', file: 'wK' }, + { css: 'piece.white.queen', file: 'wQ' }, + { css: 'piece.white.rook', file: 'wR' }, + { css: 'piece.white.bishop', file: 'wB' }, + { css: 'piece.white.knight', file: 'wN' }, + { css: 'piece.white.pawn', file: 'wP' }, + { css: 'piece.black.king', file: 'bK' }, + { css: 'piece.black.queen', file: 'bQ' }, + { css: 'piece.black.rook', file: 'bR' }, + { css: 'piece.black.bishop', file: 'bB' }, + { css: 'piece.black.knight', file: 'bN' }, + { css: 'piece.black.pawn', file: 'bP' } + ]; + + const existingStyle = this.container.querySelector('.cv-piece-style'); + if (existingStyle) existingStyle.remove(); + + const rules = pieces.map((p) => { + const resolvedUrl = adapter.getResourcePath(`${basePath}/${p.file}.svg`); + return `.chessview[data-piece-set='${this.settings.pieceSet}'] .cg-wrap ${p.css} { background-image: url('${resolvedUrl}') !important; }`; + }); + + const styleEl = this.container.createEl('style', { cls: 'cv-piece-style' }); + styleEl.textContent = rules.join('\n'); + } + + // =========================================================================== + // PROMOTION DIALOG + // =========================================================================== + + /** + * Check if a move is a promotion and show dialog if so. + * Returns the chosen promotion piece letter, or 'q' for non-promotions. + */ + async getPromotion(chess: Chess, orig: Key, dest: Key): Promise { + const validMoves = chess.moves({ verbose: true }) as Array<{ + from: string; + to: string; + flags: string; + promotion?: string; + }>; + + const isPromotion = validMoves.some( + (m) => m.from === orig && m.to === dest && m.flags.includes('p') + ); + + if (!isPromotion) return 'q'; + + return this.showPromotionDialog( + dest, + chess.turn() === 'w' ? 'white' : 'black' + ); + } + + private showPromotionDialog(square: Key, color: string): Promise { + return new Promise((resolve) => { + if (!this.boardWrapperEl) { + resolve('q'); + return; + } + + const overlay = this.boardWrapperEl.createDiv({ + cls: 'cv-promotion-overlay' + }); + + const dialog = overlay.createDiv({ cls: 'cv-promotion-dialog' }); + + // Position near the promotion square + const pos = squareToPosition(square, this.isFlipped); + if (pos) { + dialog.style.left = `${pos.x + 6.25}%`; + dialog.style.top = this.isFlipped + ? `${pos.y}%` + : `${Math.max(0, pos.y - 37.5)}%`; + } + + const pieceNames: Record = { + q: 'queen', + r: 'rook', + b: 'bishop', + n: 'knight' + }; + + for (const piece of ['q', 'r', 'b', 'n']) { + const btn = dialog.createDiv({ + cls: `cv-promotion-piece piece ${color} ${pieceNames[piece]}`, + attr: { 'aria-label': pieceNames[piece], title: pieceNames[piece] } + }); + btn.onclick = (e) => { + e.stopPropagation(); + overlay.remove(); + resolve(piece); + }; + } + + // Click outside to cancel (default queen) + overlay.onclick = () => { + overlay.remove(); + resolve('q'); + }; + }); + } + + // =========================================================================== + // CLEANUP + // =========================================================================== + + destroy(): void { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + + for (const prop of this._appliedStyleProps) { + this.container.style.removeProperty(prop); + } + this._appliedStyleProps = []; + + const pieceStyle = this.container.querySelector('.cv-piece-style'); + if (pieceStyle) pieceStyle.remove(); + + delete this.container.dataset.theme; + delete this.container.dataset.pieceSet; + + if (this.ground) { + this.ground.destroy(); + this.ground = null; + } + + this.boardEl = null; + this.boardWrapperEl = null; + this.nagOverlayEl = null; + } +} diff --git a/src/chess-renderer.ts b/src/chess-renderer.ts new file mode 100644 index 0000000..c4ffefa --- /dev/null +++ b/src/chess-renderer.ts @@ -0,0 +1,438 @@ +// src/chess-renderer.ts +import { Chess } from 'chess.js'; +import { Key } from 'chessground/types'; + +import { ChessViewSettings, ParsedChessData, MoveData } from './types'; +import { generateAnalysisUrls } from './parser'; +import { BoardManager } from './board-manager'; +import { PuzzleController } from './puzzle-controller'; +import { NavigationController } from './navigation-controller'; + +export class ChessRenderer { + private container: HTMLElement; + private settings: ChessViewSettings; + private data: ParsedChessData; + + private chess: Chess; + private startFen: string; + private isFlipped: boolean = false; + + // Sub-controllers + private board: BoardManager | null = null; + private puzzle: PuzzleController | null = null; + private nav: NavigationController | null = null; + + // Keyboard handler ref + private _keyHandler: ((e: KeyboardEvent) => void) | null = null; + private destroyed: boolean = false; + + constructor( + container: HTMLElement, + data: ParsedChessData, + settings: ChessViewSettings + ) { + this.container = container; + this.data = data; + this.settings = settings; + this.chess = new Chess(); + + // Orientation + if (data.orientation === 'black') { + this.isFlipped = true; + } else if (data.orientation === 'white') { + this.isFlipped = false; + } else if (settings.defaultOrientation === 'black') { + this.isFlipped = true; + } + + // Load starting position + if (data.fen) { + try { + this.chess.load(data.fen); + } catch (e) { + if (!data.error) { + data.error = `Failed to load position: ${e instanceof Error ? e.message : 'Invalid FEN'}`; + } + } + } + + // Auto orientation for non-puzzles + if ( + settings.defaultOrientation === 'auto' && + !data.isPuzzle && + data.orientation === 'white' + ) { + this.isFlipped = this.chess.turn() === 'b'; + } + + // Puzzle orientation defaults to player's side + if (data.isPuzzle && data.orientation === data.playerColor) { + this.isFlipped = data.playerColor === 'black'; + } + + this.startFen = this.chess.fen(); + } + + // =========================================================================== + // RENDER + // =========================================================================== + + render(): void { + // Clean up any previous render + this.destroy(); + this.destroyed = false; + + this.container.empty(); + this.container.addClass('chessview'); + + if (this.data.error) { + this.renderError(this.data.error); + return; + } + + // Re-initialize chess state + this.chess = new Chess(); + if (this.data.fen) { + try { + this.chess.load(this.data.fen); + } catch (e) { + // already validated + } + } + + const mainContainer = this.container.createDiv({ cls: 'cv-container' }); + + // Header + this.renderHeader(mainContainer); + + // Content area + const content = mainContainer.createDiv({ cls: 'cv-content' }); + if (this.settings.moveListPosition === 'bottom') { + content.addClass('cv-content-vertical'); + } + + // Create board manager + this.board = new BoardManager( + this.container, + this.settings, + this.data, + this.isFlipped + ); + + const boardSection = content.createDiv({ cls: 'cv-board-section' }); + this.board.createBoard(boardSection); + + if (this.data.isPuzzle) { + // PUZZLE MODE + this.puzzle = new PuzzleController( + this.chess, + this.data, + this.settings, + this.board, + this.startFen + ); + + this.board.initChessground(this.chess); + + const movesSection = content.createDiv({ cls: 'cv-moves-section' }); + this.puzzle.createMoveList(movesSection); + + this.renderFooter(mainContainer); + this.puzzle.createPuzzleUI(mainContainer); + + this.board.applyTheme(); + this.board.applyPieceSet(); + + this.puzzle.start(); + } else { + // GAME MODE + this.nav = new NavigationController( + this.chess, + this.data, + this.settings, + this.board, + this.startFen + ); + + this.board.initChessground(this.chess, (orig: Key, dest: Key) => + this.nav!.handleUserMove(orig, dest) + ); + + this.nav.createBranchOverlay(boardSection); + + if (this.settings.showMoveList && this.nav.moveCount > 0) { + const movesSection = content.createDiv({ cls: 'cv-moves-section' }); + this.nav.createMoveList(movesSection); + } + + this.renderFooter(mainContainer); + + this.board.applyTheme(); + this.board.applyPieceSet(); + + if (this.data.startMove > 0) { + this.nav.goToStartMove(); + } + } + + this.setupKeyboardShortcuts(); + } + + // =========================================================================== + // HEADER + // =========================================================================== + + private renderError(message: string): void { + const errorContainer = this.container.createDiv({ cls: 'cv-error' }); + errorContainer.createEl('strong', { text: '⚠️ Error' }); + errorContainer.createEl('p', { text: message }); + const details = errorContainer.createEl('details'); + details.createEl('summary', { text: 'Details' }); + const pre = details.createEl('pre'); + pre.textContent = this.data.fen || this.data.pgn || 'No input'; + } + + private renderHeader(container: HTMLElement): void { + const header = container.createDiv({ cls: 'cv-header' }); + const headerText = header.createSpan({ cls: 'cv-header-text' }); + + if (this.data.isPuzzle) { + headerText.createSpan({ cls: 'cv-header-label', text: 'Puzzle' }); + + const infoParts: string[] = []; + if (this.data.puzzleTitle) infoParts.push(this.data.puzzleTitle); + if (this.data.puzzleRating) + infoParts.push(`Rating: ${this.data.puzzleRating}`); + if (this.data.puzzleThemes.length > 0) + infoParts.push(this.data.puzzleThemes.join(', ')); + + const event = this.data.headers['Event']; + if (event && event !== '?') infoParts.push(event); + + headerText.appendText( + infoParts.length > 0 ? infoParts.join(' • ') : 'Solve the puzzle' + ); + } else { + const parts: string[] = []; + + const white = this.data.headers['White']; + const black = this.data.headers['Black']; + if (white || black) parts.push(`${white || '?'} vs ${black || '?'}`); + + const event = this.data.headers['Event']; + if (event && event !== '?') parts.push(event); + + const date = this.data.headers['Date']; + if (date && date !== '????.??.??') parts.push(date.replace(/\./g, '-')); + + const result = this.data.headers['Result']; + if (result && result !== '*') parts.push(result); + + headerText.textContent = + parts.length > 0 ? parts.join(' • ') : 'Chess Position'; + } + } + + // =========================================================================== + // FOOTER + // =========================================================================== + + private renderFooter(container: HTMLElement): void { + const footer = container.createDiv({ cls: 'cv-footer' }); + + // Left side: navigation + counter + const leftGroup = footer.createDiv({ cls: 'cv-footer-left' }); + + if (!this.data.isPuzzle && this.nav && this.nav.moveCount > 0) { + this.createControlBtn(leftGroup, '⏮', 'First move (Home)', () => + this.nav!.goToStart() + ); + this.createControlBtn(leftGroup, '◀', 'Previous move (←)', () => + this.nav!.goBack() + ); + + const playBtn = this.createControlBtn( + leftGroup, + '▶', + 'Play (Space)', + () => this.nav!.toggleAutoPlay() + ); + playBtn.addClass('cv-play-btn'); + this.nav.setPlayBtnEl(playBtn); + + this.createControlBtn(leftGroup, '▶', 'Next move (→)', () => + this.nav!.goForward() + ); + this.createControlBtn(leftGroup, '⏭', 'Last move (End)', () => + this.nav!.goToEnd() + ); + + const counterEl = leftGroup.createSpan({ cls: 'cv-counter' }); + this.nav.setCounterEl(counterEl); + } + + // Right side: flip, copy, links + const rightGroup = footer.createDiv({ cls: 'cv-footer-right' }); + + this.createControlBtn(rightGroup, '⇅', 'Flip board (F)', () => { + this.isFlipped = !this.isFlipped; + this.board?.flipBoard(); + if (this.nav) { + this.board?.updateNagOverlay([], this.nav.currentIndex); + } + }); + + const copyBtn = rightGroup.createEl('button', { + cls: 'cv-action-btn', + attr: { 'aria-label': 'Copy', title: 'Copy to clipboard' } + }); + copyBtn.innerHTML = ``; + copyBtn.onclick = () => this.copyToClipboard(); + + if (this.settings.showAnalysisLinks) { + const urls = generateAnalysisUrls(this.data); + rightGroup.createEl('a', { + cls: 'cv-action-link', + text: 'Lichess', + href: urls.lichess, + attr: { target: '_blank', rel: 'noopener' } + }); + rightGroup.createEl('a', { + cls: 'cv-action-link', + text: 'Chess.com', + href: urls.chessCom, + attr: { target: '_blank', rel: 'noopener' } + }); + } + } + + private createControlBtn( + container: HTMLElement, + icon: string, + label: string, + onClick: () => void + ): HTMLElement { + const btn = container.createEl('button', { + cls: 'cv-btn', + attr: { 'aria-label': label, title: label } + }); + btn.textContent = icon; + btn.onclick = onClick; + return btn; + } + + // =========================================================================== + // CLIPBOARD + // =========================================================================== + + private async copyToClipboard(): Promise { + let text: string; + + if (this.data.isPuzzle) { + const movesText = this.data.solutionMoves + .map((m, i) => { + const str = + i % 2 === 0 ? `${Math.floor(i / 2) + 1}. ${m.san}` : m.san; + return str; + }) + .join(' '); + + const headers = Object.entries(this.data.headers) + .map(([k, v]) => `[${k} "${v}"]`) + .join('\n'); + + text = headers ? `${headers}\n\n${movesText}` : movesText; + } else if (this.nav) { + text = this.nav.getClipboardText(); + } else { + text = this.chess.fen(); + } + + try { + await navigator.clipboard.writeText(text); + const btn = this.container.querySelector('.cv-action-btn'); + if (btn) { + btn.addClass('copied'); + setTimeout(() => btn.removeClass('copied'), 1000); + } + } catch (e) { + console.error('Copy failed:', e); + } + } + + // =========================================================================== + // KEYBOARD + // =========================================================================== + + private setupKeyboardShortcuts(): void { + this.container.setAttribute('tabindex', '0'); + + const handler = (e: KeyboardEvent) => { + if (this.destroyed) return; + + if (this.data.isPuzzle) { + if (e.key === 'f' || e.key === 'F') { + e.preventDefault(); + this.isFlipped = !this.isFlipped; + this.board?.flipBoard(); + } + return; + } + + if (!this.nav) return; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + this.nav.goBack(); + break; + case 'ArrowRight': + e.preventDefault(); + this.nav.goForward(); + break; + case 'Home': + e.preventDefault(); + this.nav.goToStart(); + break; + case 'End': + e.preventDefault(); + this.nav.goToEnd(); + break; + case ' ': + e.preventDefault(); + this.nav.toggleAutoPlay(); + break; + case 'f': + case 'F': + e.preventDefault(); + this.isFlipped = !this.isFlipped; + this.board?.flipBoard(); + break; + } + }; + + this.container.addEventListener('keydown', handler); + this._keyHandler = handler; + } + + // =========================================================================== + // CLEANUP + // =========================================================================== + + destroy(): void { + this.destroyed = true; + + if (this._keyHandler) { + this.container.removeEventListener('keydown', this._keyHandler); + this._keyHandler = null; + } + + this.nav?.destroy(); + this.puzzle?.destroy(); + this.board?.destroy(); + + this.nav = null; + this.puzzle = null; + this.board = null; + } +} diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 0000000..4d97860 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1,31 @@ +// src/declarations.d.ts +declare module '*.css' { + const content: string; + export default content; +} + +declare module 'chess.js' { + export class Chess { + constructor(fen?: string); + load(fen: string): boolean; + fen(): string; + turn(): 'w' | 'b'; + move( + move: string | { from: string; to: string; promotion?: string }, + options?: { sloppy?: boolean } + ): any; + moves(options?: { square?: string; verbose?: boolean }): any[]; + in_check(): boolean; + in_checkmate(): boolean; + in_stalemate(): boolean; + in_draw(): boolean; + game_over(): boolean; + undo(): any; + board(): any[][]; + validate_fen(fen: string): { + valid: boolean; + error_number: number; + error: string; + }; + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3579de3 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,110 @@ +// src/main.ts +import { + Plugin, + MarkdownPostProcessorContext, + MarkdownRenderChild +} from 'obsidian'; +import { ChessViewSettings, DEFAULT_SETTINGS } from './types'; +import { ChessViewSettingTab } from './settings'; +import { parseChessInput } from './parser'; +import { ChessRenderer } from './chess-renderer'; + +class ChessRenderChild extends MarkdownRenderChild { + private renderer: ChessRenderer; + + constructor(containerEl: HTMLElement, renderer: ChessRenderer) { + super(containerEl); + this.renderer = renderer; + } + + onunload(): void { + this.renderer.destroy(); + } +} + +export default class ChessViewPlugin extends Plugin { + settings!: ChessViewSettings; + + async onload(): Promise { + await this.loadSettings(); + + this.registerMarkdownCodeBlockProcessor( + 'chessview', + (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + this.processChessBlock(source, el, ctx); + } + ); + + this.addSettingTab(new ChessViewSettingTab(this.app, this)); + + this.addCommand({ + id: 'insert-chessviewer-game', + name: 'Insert game', + editorCallback: (editor) => { + editor.replaceSelection( + '```chessviewer\n[Event "?"]\n[White "?"]\n[Black "?"]\n\n1. e4 e5 2. Nf3 Nc6 3. Bb5\n```' + ); + } + }); + + this.addCommand({ + id: 'insert-chessviewer-fen', + name: 'Insert position (FEN)', + editorCallback: (editor) => { + editor.replaceSelection( + '```chessviewer\nrnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1\n```' + ); + } + }); + + this.addCommand({ + id: 'insert-chessviewer-puzzle', + name: 'Insert puzzle', + editorCallback: (editor) => { + editor.replaceSelection(`\`\`\`chessviewer +[puzzle] +[rating: 1500] +[title: White to move and win] +--- +[FEN "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 3"] + +1. Qxf7# +\`\`\``); + } + }); + + console.log('ChessView loaded'); + } + + onunload(): void { + console.log('ChessView unloaded'); + } + + async loadSettings(): Promise { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + } + + private processChessBlock( + source: string, + el: HTMLElement, + ctx: MarkdownPostProcessorContext + ): void { + try { + const container = el.createDiv({ cls: 'chessview-container' }); + const data = parseChessInput(source); + const renderer = new ChessRenderer(container, data, this.settings); + renderer.render(); + ctx.addChild(new ChessRenderChild(container, renderer)); + } catch (error) { + console.error('ChessView Error:', error); + el.createDiv({ + cls: 'chessview-error', + text: `Error: ${error instanceof Error ? error.message : String(error)}` + }); + } + } +} diff --git a/src/navigation-controller.ts b/src/navigation-controller.ts new file mode 100644 index 0000000..1f842af --- /dev/null +++ b/src/navigation-controller.ts @@ -0,0 +1,467 @@ +// src/navigation-controller.ts +import { Chess } from 'chess.js'; +import { Color, Key } from 'chessground/types'; + +import { + ChessViewSettings, + ParsedChessData, + MoveData, + FIGURINE_NOTATION, + NAG_SYMBOLS, + NAG_CLASSES +} from './types'; +import { BoardManager } from './board-manager'; +import { getValidMoves, isInCheck } from './utils'; + +interface BranchPoint { + moveIndex: number; + fen: string; + moves: MoveData[]; +} + +export class NavigationController { + private chess: Chess; + private data: ParsedChessData; + private settings: ChessViewSettings; + private board: BoardManager; + private startFen: string; + + // Move state + private moves: MoveData[] = []; + private mainLineMoves: MoveData[] = []; + private currentMoveIndex: number = 0; + + // Branch state + private branchStack: BranchPoint[] = []; + private isInBranch: boolean = false; + + // Autoplay state + private isPlaying: boolean = false; + private playInterval: ReturnType | null = null; + + // DOM + private moveListEl: HTMLElement | null = null; + private commentEl: HTMLElement | null = null; + private counterEl: HTMLElement | null = null; + private playBtnEl: HTMLElement | null = null; + private branchOverlayEl: HTMLElement | null = null; + + private destroyed: boolean = false; + + constructor( + chess: Chess, + data: ParsedChessData, + settings: ChessViewSettings, + board: BoardManager, + startFen: string + ) { + this.chess = chess; + this.data = data; + this.settings = settings; + this.board = board; + this.startFen = startFen; + + if (data.moves && data.moves.length > 0 && !data.isPuzzle) { + this.moves = [...data.moves]; + this.mainLineMoves = [...data.moves]; + } + } + + // =========================================================================== + // SETUP + // =========================================================================== + + createMoveList(movesSection: HTMLElement): void { + this.moveListEl = movesSection.createDiv({ cls: 'cv-moves' }); + this.renderMoveList(); + this.commentEl = movesSection.createDiv({ cls: 'cv-comment hidden' }); + } + + createBranchOverlay(boardSection: HTMLElement): void { + this.branchOverlayEl = boardSection.createDiv({ cls: 'cv-branch-overlay' }); + const returnBtn = this.branchOverlayEl.createEl('button', { + cls: 'cv-branch-return', + text: '↩ Return to main line' + }); + returnBtn.onclick = () => this.returnToMainLine(); + } + + setCounterEl(el: HTMLElement): void { + this.counterEl = el; + this.updateCounter(); + } + + setPlayBtnEl(el: HTMLElement): void { + this.playBtnEl = el; + } + + get moveCount(): number { + return this.moves.length; + } + + get currentIndex(): number { + return this.currentMoveIndex; + } + + // =========================================================================== + // NAVIGATION + // =========================================================================== + + goToMove(index: number): void { + if (this.destroyed) return; + + index = Math.max(0, Math.min(this.moves.length, index)); + + this.chess.load(this.startFen); + for (let i = 0; i < index; i++) { + try { + this.chess.move(this.moves[i].san); + } catch (e) { + break; + } + } + + this.currentMoveIndex = index; + const last = index > 0 ? this.moves[index - 1] : null; + + const isEditable = this.data.isEditable && !this.data.isStatic; + + this.board.syncBoard(this.chess, last, { + movable: isEditable + ? { + free: false, + color: 'both' as Color, + dests: getValidMoves(this.chess), + showDests: true, + events: { + after: (orig: Key, dest: Key) => this.handleUserMove(orig, dest) + } + } + : { free: false, color: undefined }, + moves: this.moves, + currentMoveIndex: this.currentMoveIndex + }); + + this.updateMoveHighlights(); + this.updateCounter(); + this.updateComment(); + this.board.updateNagOverlay(this.moves, this.currentMoveIndex); + this.board.updateNagHighlight(this.moves, this.currentMoveIndex); + this.updateBranchIndicator(); + } + + goToStart(): void { + this.goToMove(0); + } + + goToEnd(): void { + this.goToMove(this.moves.length); + } + + goForward(): void { + this.goToMove(this.currentMoveIndex + 1); + } + + goBack(): void { + this.goToMove(this.currentMoveIndex - 1); + } + + goToStartMove(): void { + if (this.data.startMove > 0) { + this.goToMove(Math.min(this.data.startMove, this.moves.length)); + } + } + + // =========================================================================== + // USER MOVES (editable) + // =========================================================================== + + async handleUserMove(orig: Key, dest: Key): Promise { + if (this.destroyed) return; + + // Get promotion piece via dialog if needed + const promotion = await this.board.getPromotion(this.chess, orig, dest); + + try { + const move = this.chess.move({ from: orig, to: dest, promotion }); + if (!move) return; + + if ( + this.currentMoveIndex < this.mainLineMoves.length && + this.mainLineMoves[this.currentMoveIndex].san === move.san + ) { + // Move matches main line — just advance + this.currentMoveIndex++; + this.board.syncAfterMove( + this.chess, + move, + this.moves, + this.currentMoveIndex + ); + this.updateMoveHighlights(); + this.updateCounter(); + this.updateComment(); + this.board.updateNagOverlay(this.moves, this.currentMoveIndex); + this.board.updateNagHighlight(this.moves, this.currentMoveIndex); + return; + } + + // Entering a branch + if (!this.isInBranch) { + this.branchStack.push({ + moveIndex: this.currentMoveIndex, + fen: this.startFen, + moves: [...this.mainLineMoves] + }); + this.isInBranch = true; + this.moves = this.moves.slice(0, this.currentMoveIndex); + } else { + this.moves = this.moves.slice(0, this.currentMoveIndex); + } + + const moveData: MoveData = { + san: move.san, + from: move.from, + to: move.to, + fen: this.chess.fen() + }; + + this.moves.push(moveData); + this.currentMoveIndex = this.moves.length; + + this.board.syncAfterMove( + this.chess, + move, + this.moves, + this.currentMoveIndex + ); + this.renderMoveList(); + this.updateCounter(); + this.updateComment(); + this.board.updateNagOverlay(this.moves, this.currentMoveIndex); + this.board.updateNagHighlight(this.moves, this.currentMoveIndex); + this.updateBranchIndicator(); + } catch (e) { + this.board.syncBoard(this.chess, null); + } + } + + private returnToMainLine(): void { + if (!this.isInBranch || this.branchStack.length === 0) return; + + const branch = this.branchStack.pop()!; + this.moves = [...branch.moves]; + this.mainLineMoves = [...branch.moves]; + this.isInBranch = this.branchStack.length > 0; + + this.goToMove(branch.moveIndex); + this.renderMoveList(); + this.updateBranchIndicator(); + } + + // =========================================================================== + // AUTOPLAY + // =========================================================================== + + toggleAutoPlay(): void { + if (this.isPlaying) { + this.stopAutoPlay(); + } else { + this.startAutoPlay(); + } + } + + private startAutoPlay(): void { + if (this.isPlaying) return; + this.isPlaying = true; + if (this.playBtnEl) { + this.playBtnEl.textContent = '⏸'; + this.playBtnEl.setAttribute('title', 'Pause (Space)'); + } + this.playInterval = setInterval(() => { + if (this.currentMoveIndex >= this.moves.length) { + this.stopAutoPlay(); + return; + } + this.goToMove(this.currentMoveIndex + 1); + }, this.settings.autoPlaySpeed); + } + + stopAutoPlay(): void { + this.isPlaying = false; + if (this.playBtnEl) { + this.playBtnEl.textContent = '▶'; + this.playBtnEl.setAttribute('title', 'Play (Space)'); + } + if (this.playInterval) { + clearInterval(this.playInterval); + this.playInterval = null; + } + } + + // =========================================================================== + // MOVE LIST UI + // =========================================================================== + + private renderMoveList(): void { + if (!this.moveListEl) return; + this.moveListEl.empty(); + + const list = this.moveListEl.createDiv({ cls: 'cv-moves-list' }); + + for (let i = 0; i < this.moves.length; i += 2) { + const row = list.createDiv({ cls: 'cv-move-row' }); + row.createSpan({ + cls: 'cv-move-num', + text: `${Math.floor(i / 2) + 1}.` + }); + + this.createMoveSpan(row, this.moves[i], i); + + if (i + 1 < this.moves.length) { + this.createMoveSpan(row, this.moves[i + 1], i + 1); + } + } + } + + private createMoveSpan( + container: HTMLElement, + move: MoveData, + index: number + ): void { + const classes = ['cv-move']; + if (move.comment) classes.push('has-comment'); + if (move.annotations) classes.push('has-annotation'); + if (index === this.currentMoveIndex - 1) classes.push('active'); + + const span = container.createSpan({ + cls: classes.join(' '), + attr: move.comment ? { title: move.comment } : {} + }); + + span.createSpan({ text: this.formatMove(move.san) }); + + if (move.nag) { + const nagClass = NAG_CLASSES[move.nag] || ''; + span.createSpan({ + cls: `cv-move-nag ${nagClass}`, + text: move.nag + }); + } + + span.onclick = () => this.goToMove(index + 1); + } + + private formatMove(san: string): string { + if (this.settings.notationType === 'figurine') { + return san.replace(/[KQRBN]/g, (m) => FIGURINE_NOTATION[m] || m); + } + return san; + } + + private updateMoveHighlights(): void { + if (!this.moveListEl) return; + const moves = this.moveListEl.querySelectorAll('.cv-move'); + moves.forEach((el, i) => { + el.removeClass('active'); + if (i === this.currentMoveIndex - 1) { + el.addClass('active'); + el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }); + } + + private updateCounter(): void { + if (this.counterEl) { + this.counterEl.textContent = `${this.currentMoveIndex}/${this.moves.length}`; + } + } + + private updateComment(): void { + if (!this.commentEl) return; + this.commentEl.empty(); + + if (this.currentMoveIndex <= 0) { + this.commentEl.addClass('hidden'); + return; + } + + const move = this.moves[this.currentMoveIndex - 1]; + const hasNag = move?.nag; + const hasComment = move?.comment; + + if (!hasNag && !hasComment) { + this.commentEl.addClass('hidden'); + return; + } + + this.commentEl.removeClass('hidden'); + + if (hasNag) { + const nagClass = NAG_CLASSES[move.nag!] || ''; + const nagInfo = Object.values(NAG_SYMBOLS).find( + (n) => n.symbol === move.nag + ); + const label = nagInfo ? nagInfo.label : move.nag!; + + this.commentEl.createSpan({ + cls: `cv-comment-nag ${nagClass}`, + text: `${move.nag} ${label}` + }); + } + + if (hasComment) { + this.commentEl.createSpan({ + cls: 'cv-comment-text', + text: move.comment! + }); + } + } + + private updateBranchIndicator(): void { + if (!this.branchOverlayEl) return; + if (this.isInBranch) { + this.branchOverlayEl.addClass('visible'); + } else { + this.branchOverlayEl.removeClass('visible'); + } + } + + // =========================================================================== + // CLIPBOARD + // =========================================================================== + + getClipboardText(): string { + if (this.data.type === 'fen' || this.moves.length === 0) { + return this.chess.fen(); + } + + const headers = Object.entries(this.data.headers) + .map(([k, v]) => `[${k} "${v}"]`) + .join('\n'); + + const movesText = this.moves + .map((m, i) => { + let str = i % 2 === 0 ? `${Math.floor(i / 2) + 1}. ${m.san}` : m.san; + if (m.comment && m.comment !== 'wrong') str += ` {${m.comment}}`; + return str; + }) + .join(' '); + + return headers ? `${headers}\n\n${movesText}` : movesText; + } + + // =========================================================================== + // CLEANUP + // =========================================================================== + + destroy(): void { + this.destroyed = true; + this.stopAutoPlay(); + this.moveListEl = null; + this.commentEl = null; + this.counterEl = null; + this.playBtnEl = null; + this.branchOverlayEl = null; + } +} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..316955f --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,527 @@ +import { Chess } from 'chess.js'; +import { stripVariations } from './utils'; +import { + ParsedChessData, + MoveData, + Arrow, + Circle, + Highlight, + MoveAnnotation, + ANNOTATION_COLORS, + NAG_SYMBOLS +} from './types'; + +const SEPARATOR = '---'; + +export function parseChessInput(source: string): ParsedChessData { + const result: ParsedChessData = { + type: 'game', + fen: null, + pgn: null, + moves: [], + orientation: 'white', + isStatic: false, + isEditable: true, + isPuzzle: false, + playerColor: 'white', + solutionMoves: [], + puzzleRating: null, + puzzleThemes: [], + puzzleTitle: null, + headers: {}, + arrows: [], + circles: [], + highlights: [], + startMove: 0, + error: null + }; + + try { + const cleaned = source.trim(); + result.isPuzzle = /^\[puzzle\]/im.test(cleaned); + + if (result.isPuzzle) { + return parsePuzzle(cleaned, result); + } else { + return parseGame(cleaned, result); + } + } catch (e) { + result.error = e instanceof Error ? e.message : 'Unknown parsing error'; + return result; + } +} + +// ========================================================================= +// PUZZLE PARSING +// ========================================================================= + +function parsePuzzle(source: string, result: ParsedChessData): ParsedChessData { + result.type = 'puzzle'; + result.isEditable = false; + + const sepIndex = source.indexOf(SEPARATOR); + let markerSection: string; + let pgnSection: string; + + if (sepIndex !== -1) { + markerSection = source.substring(0, sepIndex).trim(); + pgnSection = source.substring(sepIndex + SEPARATOR.length).trim(); + } else { + // No separator — treat everything as markers, no PGN + markerSection = source; + pgnSection = ''; + } + + // Parse markers (before ---) + const markerLines = markerSection + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l); + + for (const line of markerLines) { + parseMarkerLine(line, result); + } + + if (!pgnSection) { + result.error = 'Puzzle has no PGN data after ---'; + return result; + } + + // Parse PGN section (after ---) — standard PGN + const fenHeader = pgnSection.match(/\[FEN\s+"([^"]+)"\]/i); + if (fenHeader) { + result.fen = normalizeFen(fenHeader[1]); + const validation = validateFen(result.fen); + if (!validation.valid) { + result.error = 'Invalid FEN: ' + validation.error; + return result; + } + } + + // Extract all PGN headers + const headerMatches = pgnSection.matchAll(/\[(\w+)\s+"([^"]+)"\]/g); + for (const match of headerMatches) { + result.headers[match[1]] = match[2]; + } + + // Store raw PGN + result.pgn = pgnSection; + + // Parse moves + result.solutionMoves = parseMovesFromPgn(pgnSection, result.fen); + + if (result.solutionMoves.length === 0) { + result.error = 'Puzzle has no valid moves'; + return result; + } + + // Determine player color: the side that makes the LAST move + const fenTurn = result.fen + ? result.fen.split(/\s+/)[1] === 'b' + ? 'black' + : 'white' + : 'white'; + + if (result.solutionMoves.length % 2 === 0) { + // Even number of moves: first move is opponent, last is player + // Player is opposite of FEN turn + result.playerColor = fenTurn === 'white' ? 'black' : 'white'; + } else { + // Odd number of moves: first move is player, last is player + // Player is same as FEN turn + result.playerColor = fenTurn; + } + + // Default orientation to player's side unless explicitly set + if (!markerSection.match(/\[(white|black|flip)\]/i)) { + result.orientation = result.playerColor; + } + + return result; +} + +// ========================================================================= +// GAME PARSING +// ========================================================================= + +function parseGame(source: string, result: ParsedChessData): ParsedChessData { + const lines = source.split(/\r?\n/); + const markerLines: string[] = []; + const chessLines: string[] = []; + let inChessData = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!inChessData && !trimmed) continue; + + const isMarker = + /^\[(white|black|flip|static|noeditable|debug)\]$/i.test(trimmed) || + /^\[move\s*:\s*\d+\]$/i.test(trimmed) || + /^\[arrow\s*:\s*[^\]]+\]$/i.test(trimmed) || + /^\[highlight\s*:\s*[^\]]+\]$/i.test(trimmed) || + /^\[circle\s*:\s*[^\]]+\]$/i.test(trimmed); + + if (!inChessData && isMarker) { + markerLines.push(trimmed); + } else { + inChessData = true; + chessLines.push(line); + } + } + + for (const marker of markerLines) { + parseMarkerLine(marker, result); + } + + const chessData = chessLines.join('\n').trim(); + + const hasPgnHeaders = /\[\w+\s+"[^"]*"\]/.test(chessData); + const cleanedForDetection = chessData + .replace(/\[[^\]]*"[^\]]*\]/g, '') + .trim(); + const firstToken = cleanedForDetection.split(/\s+/)[0] || ''; + const isFen = + !hasPgnHeaders && + firstToken.split('/').length === 8 && + /^[rnbqkpRNBQKP1-8/]+$/.test(firstToken); + + if (isFen) { + result.type = 'fen'; + result.fen = normalizeFen(chessData.trim()); + const validation = validateFen(result.fen); + if (!validation.valid) { + result.error = 'Invalid FEN: ' + validation.error; + } + } else { + result.type = 'game'; + result.pgn = chessData; + + const fenHeader = chessData.match(/\[FEN\s+"([^"]+)"\]/i); + if (fenHeader) { + result.fen = normalizeFen(fenHeader[1]); + const validation = validateFen(result.fen); + if (!validation.valid) { + result.error = 'Invalid FEN: ' + validation.error; + } + } + + const headerMatches = chessData.matchAll(/\[(\w+)\s+"([^"]+)"\]/g); + for (const match of headerMatches) { + result.headers[match[1]] = match[2]; + } + + result.moves = parseMovesFromPgn(chessData, result.fen); + } + + return result; +} + +// ========================================================================= +// SHARED UTILITIES +// ========================================================================= + +function parseMarkerLine(line: string, result: ParsedChessData): void { + const kvMatch = line.match(/^\[(\w+)\s*:\s*([^\]]+)\]$/i); + if (kvMatch) { + const key = kvMatch[1].toLowerCase(); + const value = kvMatch[2].trim(); + + switch (key) { + case 'rating': + result.puzzleRating = parseInt(value) || null; + break; + case 'themes': + result.puzzleThemes = value.split(/[,\s]+/).filter((t) => t); + break; + case 'title': + result.puzzleTitle = value; + break; + case 'move': + result.startMove = parseInt(value) || 0; + break; + } + } + + if (/^\[(?:black|flip)\]$/i.test(line)) { + result.orientation = 'black'; + } else if (/^\[white\]$/i.test(line)) { + result.orientation = 'white'; + } else if (/^\[static\]$/i.test(line)) { + result.isStatic = true; + result.isEditable = false; + } else if (/^\[noeditable\]$/i.test(line)) { + result.isEditable = false; + } + + parseAnnotationMarker(line, result); +} + +function parseAnnotationMarker(marker: string, result: ParsedChessData): void { + const arrowMatch = marker.match( + /^\[arrow\s*:\s*([a-h][1-8])[-]?([a-h][1-8])(?:\s+(\w+))?\]$/i + ); + if (arrowMatch) { + result.arrows.push({ + from: arrowMatch[1].toLowerCase(), + to: arrowMatch[2].toLowerCase(), + color: arrowMatch[3] + ? ANNOTATION_COLORS[arrowMatch[3]] || arrowMatch[3] + : undefined + }); + } + + const circleMatch = marker.match( + /^\[circle\s*:\s*([a-h][1-8])(?:\s+(\w+))?\]$/i + ); + if (circleMatch) { + result.circles.push({ + square: circleMatch[1].toLowerCase(), + color: circleMatch[2] + ? ANNOTATION_COLORS[circleMatch[2]] || circleMatch[2] + : undefined + }); + } + + const highlightMatch = marker.match( + /^\[highlight\s*:\s*([a-h][1-8])(?:\s+(\w+))?\]$/i + ); + if (highlightMatch) { + result.highlights.push({ + square: highlightMatch[1].toLowerCase(), + color: highlightMatch[2] + ? ANNOTATION_COLORS[highlightMatch[2]] || highlightMatch[2] + : undefined + }); + } +} + +export function parseMovesFromPgn( + pgn: string, + startFen: string | null +): MoveData[] { + const chess = new Chess(); + const moves: MoveData[] = []; + + if (startFen) { + try { + chess.load(normalizeFen(startFen)); + } catch (e) { + return moves; + } + } + + let cleaned = pgn.replace(/\[[^\]]*"[^\]]*"\]/g, ''); + cleaned = stripVariations(cleaned); + cleaned = cleaned + .replace(/\s*(1-0|0-1|1\/2-1\/2|\*)\s*$/g, '') + .replace(/\s+/g, ' ') + .trim(); + + const tokenRegex = + /(\{[^}]*\})|(\$\d+)|([KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:=[QRBN])?[+#]?(?:[!?]{1,2})?|O-O-O[!?]{0,2}|O-O[!?]{0,2})/g; + let match; + let pendingComment: string | undefined; + let pendingAnnotation: MoveAnnotation | undefined; + let pendingNag: string | undefined; + + while ((match = tokenRegex.exec(cleaned)) !== null) { + const token = match[0]; + + if (token.startsWith('{') && token.endsWith('}')) { + const content = token.slice(1, -1).trim(); + const annotation = parseCommentAnnotations(content); + const textComment = content + .replace(/\[%cal\s+[^\]]+\]/gi, '') + .replace(/\[%csl\s+[^\]]+\]/gi, '') + .trim(); + + if (moves.length > 0) { + const lastMove = moves[moves.length - 1]; + if (textComment) lastMove.comment = textComment; + if (annotation.arrows.length > 0 || annotation.circles.length > 0) { + lastMove.annotations = mergeAnnotations( + lastMove.annotations, + annotation + ); + } + } else { + pendingComment = textComment || undefined; + pendingAnnotation = + annotation.arrows.length > 0 || annotation.circles.length > 0 + ? annotation + : undefined; + } + } else if (token.startsWith('$')) { + const nagInfo = NAG_SYMBOLS[token]; + if (nagInfo) { + if (moves.length > 0) { + moves[moves.length - 1].nag = nagInfo.symbol; + } else { + pendingNag = nagInfo.symbol; + } + } + } else { + let moveStr = token; + let inlineNag: string | undefined; + + const nagSuffix = moveStr.match(/([!?]{1,2})$/); + if (nagSuffix) { + inlineNag = nagSuffix[1]; + moveStr = moveStr.replace(/[!?]+$/, ''); + } + + try { + const result = chess.move(moveStr, { sloppy: true }); + if (result) { + const moveData: MoveData = { + san: result.san, + from: result.from, + to: result.to, + fen: chess.fen(), + comment: pendingComment, + nag: pendingNag, + annotations: pendingAnnotation + }; + + if (inlineNag && NAG_SYMBOLS[inlineNag]) { + moveData.nag = NAG_SYMBOLS[inlineNag].symbol; + } + + moves.push(moveData); + pendingComment = undefined; + pendingAnnotation = undefined; + pendingNag = undefined; + } + } catch (e) { + // Skip invalid move + } + } + } + + return moves; +} + +export function parseCommentAnnotations(comment: string): MoveAnnotation { + const annotation: MoveAnnotation = { + arrows: [], + circles: [], + highlights: [] + }; + + const calMatch = comment.match(/\[%cal\s+([^\]]+)\]/i); + if (calMatch) { + const defs = calMatch[1].split(','); + for (const def of defs) { + const m = def.trim().match(/^([RGBYOP]?)([a-h][1-8])([a-h][1-8])$/i); + if (m) { + annotation.arrows.push({ + from: m[2].toLowerCase(), + to: m[3].toLowerCase(), + color: m[1] + ? ANNOTATION_COLORS[m[1].toUpperCase()] || 'green' + : 'green' + }); + } + } + } + + const cslMatch = comment.match(/\[%csl\s+([^\]]+)\]/i); + if (cslMatch) { + const defs = cslMatch[1].split(','); + for (const def of defs) { + const m = def.trim().match(/^([RGBYOP]?)([a-h][1-8])$/i); + if (m) { + annotation.circles.push({ + square: m[2].toLowerCase(), + color: m[1] + ? ANNOTATION_COLORS[m[1].toUpperCase()] || 'green' + : 'green' + }); + } + } + } + + return annotation; +} + +function mergeAnnotations( + existing: MoveAnnotation | undefined, + incoming: MoveAnnotation +): MoveAnnotation { + if (!existing) return incoming; + return { + arrows: [...existing.arrows, ...incoming.arrows], + circles: [...existing.circles, ...incoming.circles], + highlights: [...existing.highlights, ...incoming.highlights] + }; +} + +export function normalizeFen(fen: string): string { + const parts = fen.trim().split(/\s+/); + if (parts.length === 0 || parts[0].split('/').length !== 8) return fen; + + const position = parts[0]; + const turn = parts[1] || 'w'; + const castling = parts[2] || '-'; + const enPassant = parts[3] || '-'; + const halfMove = parts[4] || '0'; + const fullMove = parts[5] || '1'; + + return `${position} ${turn} ${castling} ${enPassant} ${halfMove} ${fullMove}`; +} + +export function validateFen(fen: string): { valid: boolean; error?: string } { + const chess = new Chess(); + + if (typeof chess.validate_fen === 'function') { + const result = chess.validate_fen(fen); + return { + valid: result.valid, + error: result.valid ? undefined : result.error + }; + } + + try { + const loaded = chess.load(fen); + if (loaded === false) { + return { valid: false, error: 'Invalid FEN position' }; + } + return { valid: true }; + } catch (e) { + return { + valid: false, + error: e instanceof Error ? e.message : 'Invalid FEN' + }; + } +} + +export function generateAnalysisUrls(data: ParsedChessData): { + lichess: string; + chessCom: string; +} { + const isFlipped = data.orientation === 'black'; + const colorParam = isFlipped ? '?color=black' : '?color=white'; + const flipParam = isFlipped ? '&flip=true' : ''; + + let lichessUrl: string; + let chessComUrl: string; + + if (data.type === 'fen' && data.fen) { + const fenForLichess = data.fen.replace(/\s+/g, '_'); + lichessUrl = `https://lichess.org/analysis/${fenForLichess}${colorParam}`; + chessComUrl = `https://www.chess.com/analysis?fen=${encodeURIComponent(data.fen)}${flipParam}`; + } else { + const movesSan = + data.moves.length > 0 + ? data.moves.map((m) => m.san).join(' ') + : data.solutionMoves.map((m) => m.san).join(' '); + const pgnForUrl = data.fen + ? `[SetUp "1"][FEN "${data.fen}"] ${movesSan}` + : movesSan; + + lichessUrl = `https://lichess.org/analysis/pgn/${encodeURIComponent(pgnForUrl)}${colorParam}`; + chessComUrl = `https://www.chess.com/analysis?pgn=${encodeURIComponent(pgnForUrl)}${flipParam}`; + } + + return { lichess: lichessUrl, chessCom: chessComUrl }; +} diff --git a/src/puzzle-controller.ts b/src/puzzle-controller.ts new file mode 100644 index 0000000..f9fa154 --- /dev/null +++ b/src/puzzle-controller.ts @@ -0,0 +1,418 @@ +// src/puzzle-controller.ts +import { Chess } from 'chess.js'; +import { Color, Key } from 'chessground/types'; + +import { + ParsedChessData, + MoveData, + ChessViewSettings, + FIGURINE_NOTATION, + NAG_CLASSES +} from './types'; +import { BoardManager } from './board-manager'; + +export type PuzzleState = 'waiting' | 'playing' | 'solved' | 'failed'; + +export class PuzzleController { + private chess: Chess; + private data: ParsedChessData; + private settings: ChessViewSettings; + private board: BoardManager; + private startFen: string; + + private state: PuzzleState = 'waiting'; + private moveIndex: number = 0; + private playedMoves: MoveData[] = []; + private solutionRevealed: boolean = false; + private destroyed: boolean = false; + + // DOM + private moveListEl: HTMLElement | null = null; + private statusEl: HTMLElement | null = null; + private puzzleButtonsEl: HTMLElement | null = null; + + constructor( + chess: Chess, + data: ParsedChessData, + settings: ChessViewSettings, + board: BoardManager, + startFen: string + ) { + this.chess = chess; + this.data = data; + this.settings = settings; + this.board = board; + this.startFen = startFen; + } + + // =========================================================================== + // SETUP + // =========================================================================== + + createMoveList(movesSection: HTMLElement): void { + this.moveListEl = movesSection.createDiv({ cls: 'cv-moves' }); + } + + createPuzzleUI(container: HTMLElement): void { + const puzzleUI = container.createDiv({ cls: 'cv-puzzle-ui' }); + this.statusEl = puzzleUI.createDiv({ cls: 'cv-puzzle-status' }); + this.puzzleButtonsEl = puzzleUI.createDiv({ cls: 'cv-puzzle-buttons' }); + } + + start(): void { + this.state = 'waiting'; + this.moveIndex = 0; + this.playedMoves = []; + this.solutionRevealed = false; + this.destroyed = false; + + this.chess.load(this.startFen); + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + this.updateStatus(); + this.renderMoveList(); + + // Determine if first move is opponent's (auto-play it) + const fenTurn = this.chess.turn() === 'w' ? 'white' : 'black'; + const firstMoveIsOpponent = fenTurn !== this.data.playerColor; + + if (firstMoveIsOpponent && this.data.solutionMoves.length > 0) { + setTimeout(() => { + if (!this.destroyed) this.playOpponentMove(); + }, 600); + } else { + this.state = 'playing'; + this.enableInput(); + this.updateStatus(); + } + } + + // =========================================================================== + // MOVE LOGIC + // =========================================================================== + + private playOpponentMove(): void { + if (this.destroyed) return; + if (this.moveIndex >= this.data.solutionMoves.length) return; + + const expectedMove = this.data.solutionMoves[this.moveIndex]; + + try { + const move = this.chess.move(expectedMove.san); + if (move) { + this.playedMoves.push({ + san: move.san, + from: move.from, + to: move.to, + fen: this.chess.fen() + }); + this.moveIndex++; + } + } catch (e) { + console.warn('Could not play opponent move:', expectedMove.san); + } + + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + this.renderMoveList(); + + if (this.moveIndex < this.data.solutionMoves.length) { + this.state = 'playing'; + this.enableInput(); + this.updateStatus(); + } else { + this.state = 'solved'; + this.board.disableInput(); + this.updateStatus(); + } + } + + async handleMove(orig: Key, dest: Key): Promise { + if (this.state !== 'playing') return; + if (this.moveIndex >= this.data.solutionMoves.length) return; + + const expected = this.data.solutionMoves[this.moveIndex]; + + // Get promotion piece via dialog if needed + const promotion = await this.board.getPromotion(this.chess, orig, dest); + + try { + const move = this.chess.move({ from: orig, to: dest, promotion }); + + if (!move) { + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + return; + } + + if (move.san === expected.san) { + // Correct move + this.playedMoves.push({ + san: move.san, + from: move.from, + to: move.to, + fen: this.chess.fen() + }); + this.moveIndex++; + + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + this.renderMoveList(); + + if (this.moveIndex >= this.data.solutionMoves.length) { + this.state = 'solved'; + this.board.disableInput(); + this.updateStatus(); + } else { + this.state = 'waiting'; + this.board.disableInput(); + this.updateStatus(); + + setTimeout(() => { + if (!this.destroyed) this.playOpponentMove(); + }, 400); + } + } else { + // Wrong move — undo it + this.chess.undo(); + this.state = 'failed'; + + this.playedMoves.push({ + san: move.san, + from: move.from, + to: move.to, + fen: '', + comment: 'wrong' + }); + + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + this.board.disableInput(); + this.updateStatus(); + this.renderMoveList(); + } + } catch (e) { + this.board.syncPuzzleBoard(this.chess, this.playedMoves); + } + } + + private enableInput(): void { + const playerColor: Color = this.data.playerColor as Color; + this.board.enablePuzzleInput( + this.chess, + playerColor, + (orig: Key, dest: Key) => this.handleMove(orig, dest) + ); + } + + // =========================================================================== + // ACTIONS + // =========================================================================== + + private retry(): void { + this.solutionRevealed = false; + this.start(); + this.updateButtons(); + } + + private showSolution(): void { + this.solutionRevealed = true; + this.renderMoveList(); + this.updateButtons(); + } + + private hideSolution(): void { + this.solutionRevealed = false; + this.renderMoveList(); + this.updateButtons(); + } + + showHint(): void { + if (this.state !== 'playing') return; + if (this.moveIndex >= this.data.solutionMoves.length) return; + + const nextMove = this.data.solutionMoves[this.moveIndex]; + const fromSquare = nextMove.from; + + if (fromSquare) { + this.board.showHintHighlight(fromSquare, null, 0, 2000); + } + } + + // =========================================================================== + // UI RENDERING + // =========================================================================== + + private updateStatus(): void { + if (!this.statusEl) return; + this.statusEl.empty(); + this.statusEl.removeClass('success', 'failed', 'waiting', 'playing'); + + const playerLabel = this.data.playerColor === 'white' ? 'White' : 'Black'; + + switch (this.state) { + case 'waiting': + this.statusEl.addClass('waiting'); + this.statusEl.textContent = 'Watch...'; + break; + case 'playing': + this.statusEl.addClass('playing'); + this.statusEl.textContent = `${playerLabel} to move — Your turn`; + break; + case 'solved': + this.statusEl.addClass('success'); + this.statusEl.textContent = '✓ Puzzle solved!'; + break; + case 'failed': + this.statusEl.addClass('failed'); + this.statusEl.textContent = '✗ Incorrect'; + break; + } + + this.updateButtons(); + } + + private updateButtons(): void { + if (!this.puzzleButtonsEl) return; + this.puzzleButtonsEl.empty(); + + if ( + this.state === 'failed' || + this.state === 'solved' || + this.solutionRevealed + ) { + const retryBtn = this.puzzleButtonsEl.createEl('button', { + cls: 'cv-btn cv-puzzle-btn', + text: '↺ Retry' + }); + retryBtn.onclick = () => this.retry(); + } + + if (this.state === 'playing' && this.settings.puzzleShowHints) { + const hintBtn = this.puzzleButtonsEl.createEl('button', { + cls: 'cv-btn cv-puzzle-btn cv-hint-btn', + text: '💡 Hint' + }); + hintBtn.onclick = () => this.showHint(); + } + + if (this.state !== 'solved') { + if (this.solutionRevealed) { + const hideBtn = this.puzzleButtonsEl.createEl('button', { + cls: 'cv-btn cv-puzzle-btn', + text: '🙈 Hide Solution' + }); + hideBtn.onclick = () => this.hideSolution(); + } else { + const solBtn = this.puzzleButtonsEl.createEl('button', { + cls: 'cv-btn cv-puzzle-btn', + text: '👁 Show Solution' + }); + solBtn.onclick = () => this.showSolution(); + } + } + } + + renderMoveList(): void { + if (!this.moveListEl) return; + this.moveListEl.empty(); + + const list = this.moveListEl.createDiv({ cls: 'cv-moves-list' }); + + if (this.solutionRevealed) { + this.renderMoveRows(list, this.data.solutionMoves, true); + } else if (this.playedMoves.length === 0) { + list.createDiv({ cls: 'cv-moves-empty', text: 'Solve the puzzle...' }); + } else { + this.renderMoveRows(list, this.playedMoves, false); + } + } + + private renderMoveRows( + container: HTMLElement, + moves: MoveData[], + isSolution: boolean + ): void { + let startMoveNum = 1; + let startIsBlack = false; + + if (this.data.fen) { + const parts = this.data.fen.split(/\s+/); + if (parts[5]) startMoveNum = parseInt(parts[5]) || 1; + if (parts[1] === 'b') startIsBlack = true; + } + + let moveNum = startMoveNum; + let i = 0; + + if (startIsBlack && moves.length > 0) { + const row = container.createDiv({ cls: 'cv-move-row' }); + row.createSpan({ cls: 'cv-move-num', text: `${moveNum}.` }); + row.createSpan({ cls: 'cv-move cv-move-placeholder', text: '...' }); + + const move = moves[0]; + const cls = this.getPuzzleMoveClass(move, 0, isSolution); + const span = row.createSpan({ cls }); + span.createSpan({ text: this.formatMove(move.san) }); + + i = 1; + moveNum++; + } + + while (i < moves.length) { + const row = container.createDiv({ cls: 'cv-move-row' }); + row.createSpan({ cls: 'cv-move-num', text: `${moveNum}.` }); + + const wMove = moves[i]; + const wCls = this.getPuzzleMoveClass(wMove, i, isSolution); + const wSpan = row.createSpan({ cls: wCls }); + wSpan.createSpan({ text: this.formatMove(wMove.san) }); + i++; + + if (i < moves.length) { + const bMove = moves[i]; + const bCls = this.getPuzzleMoveClass(bMove, i, isSolution); + const bSpan = row.createSpan({ cls: bCls }); + bSpan.createSpan({ text: this.formatMove(bMove.san) }); + i++; + } + + moveNum++; + } + } + + private getPuzzleMoveClass( + move: MoveData, + index: number, + isSolution: boolean + ): string { + const classes = ['cv-move']; + if (move.comment === 'wrong') { + classes.push('cv-move-wrong'); + } else if (isSolution) { + if (index < this.playedMoves.length) { + classes.push('cv-move-played'); + } else { + classes.push('cv-move-unplayed'); + } + } + return classes.join(' '); + } + + private formatMove(san: string): string { + if (this.settings.notationType === 'figurine') { + return san.replace(/[KQRBN]/g, (m) => FIGURINE_NOTATION[m] || m); + } + return san; + } + + // =========================================================================== + // CLEANUP + // =========================================================================== + + destroy(): void { + this.destroyed = true; + this.moveListEl = null; + this.statusEl = null; + this.puzzleButtonsEl = null; + } + + get currentState(): PuzzleState { + return this.state; + } +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..a5a9757 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,382 @@ +// src/settings.ts +import { App, PluginSettingTab, Setting, Notice, Plugin } from 'obsidian'; +import { + ChessViewSettings, + DEFAULT_SETTINGS, + BoardTheme, + BoardSize, + PieceSet, + BOARD_THEMES, + BOARD_SIZES +} from './types'; + +interface ChessViewPluginInterface extends Plugin { + settings: ChessViewSettings; + saveSettings(): Promise; +} + +export class ChessViewSettingTab extends PluginSettingTab { + plugin: ChessViewPluginInterface; + + constructor(app: App, plugin: ChessViewPluginInterface) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + // ==================================================================== + // BOARD APPEARANCE + // ==================================================================== + containerEl.createEl('h2', { text: 'Board Appearance' }); + + new Setting(containerEl) + .setName('Board theme') + .setDesc('Choose the color scheme for the board squares') + .addDropdown((dropdown) => + dropdown + .addOptions({ + brown: 'Brown (Classic)', + blue: 'Blue', + green: 'Green', + purple: 'Purple', + gray: 'Gray', + wood: 'Wood', + marble: 'Marble', + custom: 'Custom Colors' + }) + .setValue(this.plugin.settings.boardTheme) + .onChange(async (value: string) => { + this.plugin.settings.boardTheme = value as BoardTheme; + if (value !== 'custom') { + const theme = BOARD_THEMES[value as BoardTheme]; + if (theme) { + this.plugin.settings.lightSquareColor = theme.light; + this.plugin.settings.darkSquareColor = theme.dark; + } + } + await this.plugin.saveSettings(); + this.display(); + }) + ); + + if (this.plugin.settings.boardTheme === 'custom') { + new Setting(containerEl) + .setName('Light square color') + .setDesc('Color for light squares') + .addColorPicker((picker) => + picker + .setValue(this.plugin.settings.lightSquareColor) + .onChange(async (value) => { + this.plugin.settings.lightSquareColor = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Dark square color') + .setDesc('Color for dark squares') + .addColorPicker((picker) => + picker + .setValue(this.plugin.settings.darkSquareColor) + .onChange(async (value) => { + this.plugin.settings.darkSquareColor = value; + await this.plugin.saveSettings(); + }) + ); + } + + new Setting(containerEl) + .setName('Board size') + .setDesc('Size of the chess board') + .addDropdown((dropdown) => { + const options: Record = {}; + for (const [key, val] of Object.entries(BOARD_SIZES)) { + options[key] = val.label; + } + return dropdown + .addOptions(options) + .setValue(this.plugin.settings.boardSize) + .onChange(async (value: string) => { + this.plugin.settings.boardSize = value as BoardSize; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Piece set') + .setDesc('Choose the style of chess pieces') + .addDropdown((dropdown) => + dropdown + .addOptions({ + cburnett: 'Cburnett (default)', + merida: 'Merida', + alpha: 'Alpha', + pirouetti: 'Pirouetti', + spatial: 'Spatial', + california: 'California', + cardinal: 'Cardinal', + dubrovny: 'Dubrovny', + fantasy: 'Fantasy', + gioco: 'Gioco', + governor: 'Governor', + horsey: 'Horsey', + icpieces: 'IC Pieces', + kosal: 'Kosal', + leipzig: 'Leipzig', + maestro: 'Maestro', + monarchy: 'Monarchy', + staunty: 'Staunty', + tatiana: 'Tatiana', + chess7: 'Chess7' + }) + .setValue(this.plugin.settings.pieceSet) + .onChange(async (value: PieceSet) => { + this.plugin.settings.pieceSet = value; + await this.plugin.saveSettings(); + }) + ); + + // ==================================================================== + // NOTATION + // ==================================================================== + containerEl.createEl('h2', { text: 'Notation' }); + + new Setting(containerEl) + .setName('Move notation style') + .setDesc('How pieces are displayed in the move list') + .addDropdown((dropdown) => + dropdown + .addOptions({ + figurine: 'Figurine (\u2658f3, \u2657c4)', + letter: 'Letters (Nf3, Bc4)' + }) + .setValue(this.plugin.settings.notationType) + .onChange(async (value: string) => { + this.plugin.settings.notationType = value as 'figurine' | 'letter'; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Show coordinates') + .setDesc('Display rank and file labels on the board') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.showCoordinates) + .onChange(async (value) => { + this.plugin.settings.showCoordinates = value; + await this.plugin.saveSettings(); + }) + ); + + // ==================================================================== + // HIGHLIGHTS & COLORS + // ==================================================================== + containerEl.createEl('h2', { text: 'Colors & Highlights' }); + + new Setting(containerEl) + .setName('Last move highlight') + .setDesc('Color for highlighting the last move') + .addColorPicker((picker) => + picker + .setValue(this.rgbaToHex(this.plugin.settings.lastMoveHighlightColor)) + .onChange(async (value) => { + this.plugin.settings.lastMoveHighlightColor = this.hexToRgba( + value, + 0.41 + ); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Check highlight') + .setDesc('Color for highlighting the king in check') + .addColorPicker((picker) => + picker + .setValue(this.rgbaToHex(this.plugin.settings.checkHighlightColor)) + .onChange(async (value) => { + this.plugin.settings.checkHighlightColor = this.hexToRgba( + value, + 0.5 + ); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Default arrow color') + .setDesc('Default color for arrows when no color is specified') + .addColorPicker((picker) => + picker + .setValue(this.rgbaToHex(this.plugin.settings.arrowColor)) + .onChange(async (value) => { + this.plugin.settings.arrowColor = this.hexToRgba(value, 0.8); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Default circle color') + .setDesc('Default color for circles when no color is specified') + .addColorPicker((picker) => + picker + .setValue(this.rgbaToHex(this.plugin.settings.circleColor)) + .onChange(async (value) => { + this.plugin.settings.circleColor = this.hexToRgba(value, 0.8); + await this.plugin.saveSettings(); + }) + ); + + // ==================================================================== + // BEHAVIOR + // ==================================================================== + containerEl.createEl('h2', { text: 'Behavior' }); + + new Setting(containerEl) + .setName('Animation speed') + .setDesc('Speed of piece movement animation (ms). 0 to disable.') + .addSlider((slider) => + slider + .setLimits(0, 500, 50) + .setValue(this.plugin.settings.animationSpeed) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.animationSpeed = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Autoplay speed') + .setDesc('Delay between moves during autoplay (ms)') + .addSlider((slider) => + slider + .setLimits(200, 3000, 100) + .setValue(this.plugin.settings.autoPlaySpeed) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.autoPlaySpeed = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Show move list') + .setDesc('Display the list of moves alongside the board') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.showMoveList) + .onChange(async (value) => { + this.plugin.settings.showMoveList = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Move list position') + .setDesc('Where to display the move list') + .addDropdown((dropdown) => + dropdown + .addOptions({ + right: 'Right of board', + bottom: 'Below board' + }) + .setValue(this.plugin.settings.moveListPosition) + .onChange(async (value: string) => { + this.plugin.settings.moveListPosition = value as 'right' | 'bottom'; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Show analysis links') + .setDesc('Display links to Lichess and Chess.com analysis') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.showAnalysisLinks) + .onChange(async (value) => { + this.plugin.settings.showAnalysisLinks = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName('Default board orientation') + .setDesc('Which side to show at the bottom by default') + .addDropdown((dropdown) => + dropdown + .addOptions({ + white: 'White', + black: 'Black', + auto: 'Auto (based on side to move)' + }) + .setValue(this.plugin.settings.defaultOrientation) + .onChange(async (value: string) => { + this.plugin.settings.defaultOrientation = value as + | 'white' + | 'black' + | 'auto'; + await this.plugin.saveSettings(); + }) + ); + + // ==================================================================== + // PUZZLE SETTINGS + // ==================================================================== + containerEl.createEl('h2', { text: 'Puzzle Mode' }); + + new Setting(containerEl) + .setName('Show hints') + .setDesc('Allow hints in puzzle mode') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.puzzleShowHints) + .onChange(async (value) => { + this.plugin.settings.puzzleShowHints = value; + await this.plugin.saveSettings(); + }) + ); + + // ==================================================================== + // RESET + // ==================================================================== + containerEl.createEl('h2', { text: 'Reset' }); + + new Setting(containerEl) + .setName('Reset to defaults') + .setDesc('Restore all settings to their default values') + .addButton((button) => + button + .setButtonText('Reset') + .setWarning() + .onClick(async () => { + Object.assign(this.plugin.settings, DEFAULT_SETTINGS); + await this.plugin.saveSettings(); + this.display(); + new Notice('ChessView settings reset to defaults'); + }) + ); + } + + private rgbaToHex(rgba: string): string { + const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (match) { + const r = parseInt(match[1]).toString(16).padStart(2, '0'); + const g = parseInt(match[2]).toString(16).padStart(2, '0'); + const b = parseInt(match[3]).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + return rgba.startsWith('#') ? rgba : '#000000'; + } + + private hexToRgba(hex: string, alpha: number): string { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6e4b589 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,266 @@ +// src/types.ts +// ============================================================================ +// SETTINGS TYPES +// ============================================================================ + +export interface ChessViewSettings { + boardTheme: BoardTheme; + boardSize: BoardSize; + pieceSet: PieceSet; + notationType: NotationType; + showCoordinates: boolean; + coordinatePosition: 'inside' | 'outside'; + lightSquareColor: string; + darkSquareColor: string; + lastMoveHighlightColor: string; + checkHighlightColor: string; + selectedSquareColor: string; + arrowColor: string; + circleColor: string; + animationSpeed: number; + autoPlaySpeed: number; + showMoveList: boolean; + moveListPosition: 'right' | 'bottom'; + showAnalysisLinks: boolean; + defaultOrientation: 'white' | 'black' | 'auto'; + puzzleShowHints: boolean; + puzzleSuccessColor: string; + puzzleFailColor: string; +} + +export type BoardTheme = + | 'brown' + | 'blue' + | 'green' + | 'purple' + | 'gray' + | 'wood' + | 'marble' + | 'custom'; + +export type BoardSize = 'small' | 'medium' | 'large' | 'auto'; + +export type PieceSet = + | 'cburnett' + | 'merida' + | 'alpha' + | 'pirouetti' + | 'spatial' + | 'california' + | 'cardinal' + | 'dubrovny' + | 'fantasy' + | 'gioco' + | 'governor' + | 'horsey' + | 'icpieces' + | 'kosal' + | 'leipzig' + | 'maestro' + | 'monarchy' + | 'staunty' + | 'tatiana' + | 'chess7'; + +export type NotationType = 'figurine' | 'letter'; + +export const DEFAULT_SETTINGS: ChessViewSettings = { + boardTheme: 'brown', + boardSize: 'medium', + pieceSet: 'cburnett', + notationType: 'figurine', + showCoordinates: true, + coordinatePosition: 'outside', + lightSquareColor: '#f0d9b5', + darkSquareColor: '#b58863', + lastMoveHighlightColor: 'rgba(155, 199, 0, 0.41)', + checkHighlightColor: 'rgba(255, 0, 0, 0.5)', + selectedSquareColor: 'rgba(20, 85, 30, 0.5)', + arrowColor: 'rgba(0, 128, 0, 0.8)', + circleColor: 'rgba(0, 128, 0, 0.8)', + animationSpeed: 200, + autoPlaySpeed: 1000, + showMoveList: true, + moveListPosition: 'right', + showAnalysisLinks: true, + defaultOrientation: 'auto', + puzzleShowHints: true, + puzzleSuccessColor: '#4CAF50', + puzzleFailColor: '#f44336' +}; + +export const BOARD_THEMES: Record = + { + brown: { light: '#f0d9b5', dark: '#b58863' }, + blue: { light: '#dee3e6', dark: '#8ca2ad' }, + green: { light: '#ffffdd', dark: '#86a666' }, + purple: { light: '#e8e0f0', dark: '#9070a0' }, + gray: { light: '#cccccc', dark: '#888888' }, + wood: { light: '#e8c99b', dark: '#a67d4b' }, + marble: { light: '#f5f5f5', dark: '#a0a0a0' }, + custom: { light: '#f0d9b5', dark: '#b58863' } + }; + +export const BOARD_SIZES: Record = { + small: { base: 280, label: 'Small (280px)' }, + medium: { base: 360, label: 'Medium (360px)' }, + large: { base: 480, label: 'Large (480px)' }, + auto: { base: 0, label: 'Auto (fit container)' } +}; + +export interface ParsedChessData { + type: 'game' | 'puzzle' | 'fen'; + fen: string | null; + pgn: string | null; + moves: MoveData[]; + orientation: 'white' | 'black'; + isStatic: boolean; + isEditable: boolean; + isPuzzle: boolean; + playerColor: 'white' | 'black'; + solutionMoves: MoveData[]; + puzzleRating: number | null; + puzzleThemes: string[]; + puzzleTitle: string | null; + headers: Record; + arrows: Arrow[]; + circles: Circle[]; + highlights: Highlight[]; + startMove: number; + error: string | null; +} + +export interface Arrow { + from: string; + to: string; + color?: string; +} + +export interface Circle { + square: string; + color?: string; +} + +export interface Highlight { + square: string; + color?: string; +} + +export interface MoveAnnotation { + arrows: Arrow[]; + circles: Circle[]; + highlights: Highlight[]; +} + +export interface MoveData { + san: string; + from: string; + to: string; + fen: string; + comment?: string; + nag?: string; + annotations?: MoveAnnotation; +} + +// NAG (Numeric Annotation Glyphs) mapping +export const NAG_SYMBOLS: Record = { + $1: { symbol: '!', label: 'Good move' }, + $2: { symbol: '?', label: 'Mistake' }, + $3: { symbol: '!!', label: 'Brilliant move' }, + $4: { symbol: '??', label: 'Blunder' }, + $5: { symbol: '!?', label: 'Interesting move' }, + $6: { symbol: '?!', label: 'Dubious move' }, + $7: { symbol: '□', label: 'Forced move' }, + $9: { symbol: '✕', label: 'Miss' }, + $10: { symbol: '=', label: 'Equal position' }, + $13: { symbol: '∞', label: 'Unclear position' }, + $14: { symbol: '⩲', label: 'White slightly better' }, + $15: { symbol: '⩱', label: 'Black slightly better' }, + $16: { symbol: '±', label: 'White better' }, + $17: { symbol: '∓', label: 'Black better' }, + $18: { symbol: '+−', label: 'White winning' }, + $19: { symbol: '−+', label: 'Black winning' }, + '!!': { symbol: '!!', label: 'Brilliant move' }, + '!?': { symbol: '!?', label: 'Interesting move' }, + '?!': { symbol: '?!', label: 'Dubious move' }, + '??': { symbol: '??', label: 'Blunder' }, + '!': { symbol: '!', label: 'Good move' }, + '?': { symbol: '?', label: 'Mistake' } +}; + +export const NAG_CLASSES: Record = { + '!': 'nag-good', + '!!': 'nag-brilliant', + '?': 'nag-mistake', + '??': 'nag-blunder', + '!?': 'nag-interesting', + '?!': 'nag-dubious', + '□': 'nag-forced', + '✕': 'nag-miss', + '=': 'nag-equal', + '∞': 'nag-unclear', + '⩲': 'nag-white-slight', + '⩱': 'nag-black-slight', + '±': 'nag-white-better', + '∓': 'nag-black-better', + '+−': 'nag-white-winning', + '−+': 'nag-black-winning' +}; + +export const FIGURINE_NOTATION: Record = { + K: '♔', + Q: '♕', + R: '♖', + B: '♗', + N: '♘', + k: '♚', + q: '♛', + r: '♜', + b: '♝', + n: '♞' +}; + +export const ANNOTATION_COLORS: Record = { + R: 'red', + G: 'green', + B: 'blue', + Y: 'yellow', + O: 'orange', + P: 'purple', + red: 'red', + green: 'green', + blue: 'blue', + yellow: 'yellow', + orange: 'orange', + purple: 'purple' +}; + +export const UNICODE_PIECES: Record = { + wK: '♔', + wQ: '♕', + wR: '♖', + wB: '♗', + wN: '♘', + wP: '♙', + bK: '♚', + bQ: '♛', + bR: '♜', + bB: '♝', + bN: '♞', + bP: '♟' +}; + +export const LETTER_PIECES: Record = { + wK: 'K', + wQ: 'Q', + wR: 'R', + wB: 'B', + wN: 'N', + wP: 'P', + bK: 'K', + bQ: 'Q', + bR: 'R', + bB: 'B', + bN: 'N', + bP: 'P' +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..26ae339 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,84 @@ +// src/utils.ts +import { Chess } from 'chess.js'; +import { Key } from 'chessground/types'; + +/** + * Validate a square string like "e4" + */ +export function isValidSquare(sq: string): boolean { + return /^[a-h][1-8]$/.test(sq); +} + +/** + * Get all legal moves as a Map for chessground + */ +export function getValidMoves(chess: Chess): Map { + const dests = new Map(); + const allMoves = chess.moves({ verbose: true }) as Array<{ + from: string; + to: string; + }>; + for (const move of allMoves) { + const from = move.from as Key; + const existing = dests.get(from); + if (existing) { + existing.push(move.to as Key); + } else { + dests.set(from, [move.to as Key]); + } + } + return dests; +} + +/** + * Safely check if king is in check + */ +export function isInCheck(chess: Chess): boolean { + try { + return chess.in_check(); + } catch { + return false; + } +} + +/** + * Convert a square to percentage-based position on the board + */ +export function squareToPosition( + square: string, + isFlipped: boolean +): { x: number; y: number } | null { + if (square.length !== 2) return null; + const file = square.charCodeAt(0) - 97; + const rank = parseInt(square[1]) - 1; + if (file < 0 || file > 7 || rank < 0 || rank > 7) return null; + + let x: number, y: number; + if (isFlipped) { + x = (7 - file) * 12.5; + y = rank * 12.5; + } else { + x = file * 12.5; + y = (7 - rank) * 12.5; + } + return { x, y }; +} + +/** + * Strip nested variations (parenthesized text) using stack-based parsing. + * Handles arbitrarily nested variations like (1. e4 (1... d5) d6). + */ +export function stripVariations(text: string): string { + let result = ''; + let depth = 0; + for (const ch of text) { + if (ch === '(') { + depth++; + } else if (ch === ')') { + depth = Math.max(0, depth - 1); + } else if (depth === 0) { + result += ch; + } + } + return result; +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..982a7e2 --- /dev/null +++ b/styles.css @@ -0,0 +1,1259 @@ +/* ============================================================================ + CHESSGROUND BASE STYLES + ============================================================================ */ + +.cg-wrap { + box-sizing: content-box; + position: relative; + display: block; +} + +cg-container { + position: absolute; + width: 100%; + height: 100%; + display: block; + top: 0; +} + +cg-board { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + line-height: 0; + background-size: cover; + cursor: pointer; +} + +cg-board square { + position: absolute; + top: 0; + left: 0; + width: 12.5%; + height: 12.5%; + pointer-events: none; +} + +cg-board square.move-dest { + background: radial-gradient( + rgba(20, 85, 30, 0.5) 22%, + #208530 0, + rgba(0, 0, 0, 0.3) 0, + rgba(0, 0, 0, 0) 0 + ); + pointer-events: auto; +} + +cg-board square.oc.move-dest { + background: radial-gradient( + transparent 0%, + transparent 80%, + rgba(20, 85, 0, 0.3) 80% + ); +} + +cg-board square.last-move { + will-change: transform; + background-color: rgba(155, 199, 0, 0.41); +} + +cg-board square.selected { + background-color: rgba(20, 85, 30, 0.5); +} + +cg-board square.check { + background: radial-gradient( + ellipse at center, + rgba(255, 0, 0, 0.9) 0%, + rgba(231, 0, 0, 0.8) 25%, + rgba(169, 0, 0, 0) 89% + ); +} + +.cg-wrap piece { + position: absolute; + top: 0; + left: 0; + width: 12.5%; + height: 12.5%; + background-size: cover; + z-index: 2; + will-change: transform; + pointer-events: none; +} + +cg-board piece.dragging { + cursor: move; + z-index: 11; +} + +cg-board piece.anim { + z-index: 8; +} + +cg-board piece.fading { + z-index: 1; + opacity: 0.5; +} + +.cg-wrap piece.ghost { + opacity: 0.3; +} + +.cg-wrap cg-auto-pieces, +.cg-wrap .cg-shapes, +.cg-wrap .cg-custom-svgs { + overflow: visible; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.cg-wrap cg-auto-pieces { + z-index: 2; +} + +.cg-wrap .cg-shapes { + overflow: hidden; + opacity: 0.6; + z-index: 12; +} + +.cg-wrap .cg-custom-svgs { + z-index: 13; +} + +.cg-wrap .cg-custom-svgs svg { + overflow: visible; +} + +/* ============================================================================ + COORDINATES + ============================================================================ */ + +.cg-wrap coords { + position: absolute; + display: flex; + pointer-events: none; + opacity: 0.85; + font-family: 'Segoe UI', Arial, sans-serif; + font-size: 10px; + font-weight: bold; + z-index: 3; +} + +.cg-wrap coords.ranks { + left: 0; + top: 0; + flex-flow: column-reverse; + height: 100%; +} + +.cg-wrap coords.ranks.black { + flex-flow: column; +} + +.cg-wrap coords.ranks coord { + flex: 1 1 auto; + display: flex; + align-items: flex-start; + justify-content: flex-start; + padding: 1px 0 0 2px; +} + +.cg-wrap coords.files { + bottom: 0; + left: 0; + flex-flow: row; + width: 100%; + text-transform: lowercase; +} + +.cg-wrap coords.files.black { + flex-flow: row-reverse; +} + +.cg-wrap coords.files coord { + flex: 1 1 auto; + display: flex; + align-items: flex-end; + justify-content: flex-end; + padding: 0 2px 1px 0; +} + +.chessview { + --cv-coord-on-light: var(--cv-dark); + --cv-coord-on-dark: var(--cv-light); +} + +.cg-wrap coords.ranks:not(.black) coord:nth-child(odd) { + color: var(--cv-coord-on-dark); +} +.cg-wrap coords.ranks:not(.black) coord:nth-child(even) { + color: var(--cv-coord-on-light); +} +.cg-wrap coords.files:not(.black) coord:nth-child(odd) { + color: var(--cv-coord-on-dark); +} +.cg-wrap coords.files:not(.black) coord:nth-child(even) { + color: var(--cv-coord-on-light); +} + +.cg-wrap coords.ranks.black coord:nth-child(odd) { + color: var(--cv-coord-on-light); +} +.cg-wrap coords.ranks.black coord:nth-child(even) { + color: var(--cv-coord-on-dark); +} +.cg-wrap coords.files.black coord:nth-child(odd) { + color: var(--cv-coord-on-light); +} +.cg-wrap coords.files.black coord:nth-child(even) { + color: var(--cv-coord-on-dark); +} + +/* ============================================================================ + BOARD THEME — single rule driven by CSS vars (all themes use generateBoardSvg) + ============================================================================ */ + +.chessview .cg-wrap cg-board { + background-color: var(--cv-light, #f0d9b5); + background-image: var(--cv-board-svg, none); + background-size: cover; +} + +/* ============================================================================ + CBURNETT PIECE SET (default, embedded) + ============================================================================ */ + +.cg-wrap piece.white.pawn { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.white.knight { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.white.bishop { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.white.rook { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.white.queen { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.white.king { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.pawn { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.knight { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.bishop { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.rook { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.queen { + background-image: url('data:image/svg+xml,'); +} +.cg-wrap piece.black.king { + background-image: url('data:image/svg+xml,'); +} + +/* ============================================================================ + CHESS VIEWER LAYOUT + ============================================================================ */ + +.chessview { + --cv-light: #f0d9b5; + --cv-dark: #b58863; + margin: 1em 0; +} + +.chessview-container { + margin: 1em 0; +} + +.chessview-error { + padding: 1em; + background: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + border-radius: 4px; + color: #c00; +} + +.cv-container { + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + overflow: hidden; + background: var(--background-primary); +} + +.cv-header { + padding: 8px 12px; + background: var(--background-secondary); + border-bottom: 1px solid var(--background-modifier-border); + font-size: 13px; + color: var(--text-muted); +} + +.cv-header-text { + font-weight: 500; +} + +.cv-header-label { + display: inline-block; + font-size: 11px; + font-weight: 700; + padding: 1px 6px; + border-radius: 3px; + background: var(--interactive-accent); + color: var(--text-on-accent); + margin-right: 6px; + vertical-align: middle; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +/* Content: board + moves side by side */ +.cv-content { + display: flex; + flex-direction: row; + justify-content: center; +} + +.cv-content.cv-content-vertical { + flex-direction: column; + align-items: center; +} + +/* Board section */ +.cv-board-section { + flex-shrink: 1; + position: relative; + min-width: 0; + overflow: hidden; +} + +/* Board sizes — fixed dimensions, chessground needs explicit w/h */ +.cv-board.cg-wrap { + width: 360px; + height: 360px; +} + +.cv-board.cv-board-small { + width: 280px; + height: 280px; +} + +.cv-board.cv-board-medium { + width: 360px; + height: 360px; +} + +.cv-board.cv-board-large { + width: 480px; + height: 480px; +} + +.cv-board.cv-board-auto { + width: 480px; + height: 480px; + max-width: 100%; + aspect-ratio: 1 / 1; +} + +.cv-board-section:has(.cv-board-auto) { + flex-shrink: 1; +} + +.cv-board-section:has(.cv-board-auto) .cv-board.cg-wrap { + height: auto !important; + aspect-ratio: 1 / 1; +} + +/* ============================================================================ + PROMOTION DIALOG + ============================================================================ */ + +.cv-promotion-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 20; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.cv-promotion-piece { + width: 20%; + height: 20%; + max-width: 80px; + max-height: 80px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + border-radius: 6px; + background-color: var(--background-primary); + border: 2px solid var(--background-modifier-border); + transition: + transform 0.1s, + box-shadow 0.1s; +} + +.cv-promotion-piece:hover { + transform: scale(1.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border-color: var(--interactive-accent); +} + +/* ============================================================================ + NAG / MOVE CLASSIFICATION OVERLAY + ============================================================================ */ + +.cv-nag-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 14; +} + +.cv-nag-glyph { + position: absolute; + width: 12.5%; + height: 12.5%; + display: flex; + align-items: flex-start; + justify-content: flex-end; + pointer-events: none; +} + +.cv-nag-glyph-inner { + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 12px; + font-weight: 900; + line-height: 1; + color: #fff; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + transform: translate(30%, -30%); + font-family: 'Segoe UI', Arial, Helvetica, sans-serif; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.cv-nag-glyph.nag-brilliant .cv-nag-glyph-inner { + background: #1baca6; +} +.cv-nag-glyph.nag-good .cv-nag-glyph-inner { + background: #5b8c3e; +} +.cv-nag-glyph.nag-interesting .cv-nag-glyph-inner { + background: #e69d00; +} +.cv-nag-glyph.nag-dubious .cv-nag-glyph-inner { + background: #cf8f00; +} +.cv-nag-glyph.nag-mistake .cv-nag-glyph-inner { + background: #e6912c; +} +.cv-nag-glyph.nag-blunder .cv-nag-glyph-inner { + background: #ca3431; +} +.cv-nag-glyph.nag-miss .cv-nag-glyph-inner { + background: #ca3431; +} +.cv-nag-glyph.nag-forced .cv-nag-glyph-inner, +.cv-nag-glyph.nag-equal .cv-nag-glyph-inner, +.cv-nag-glyph.nag-unclear .cv-nag-glyph-inner { + background: #888; +} +.cv-nag-glyph.nag-white-slight .cv-nag-glyph-inner, +.cv-nag-glyph.nag-white-better .cv-nag-glyph-inner, +.cv-nag-glyph.nag-white-winning .cv-nag-glyph-inner { + background: #4a90d9; +} +.cv-nag-glyph.nag-black-slight .cv-nag-glyph-inner, +.cv-nag-glyph.nag-black-better .cv-nag-glyph-inner, +.cv-nag-glyph.nag-black-winning .cv-nag-glyph-inner { + background: #4a90d9; +} + +/* NAG-colored last move highlights — use data attribute set by JS */ +.chessview[data-nag-highlight='brilliant'] cg-board square.last-move { + background-color: rgba(27, 172, 166, 0.35) !important; +} +.chessview[data-nag-highlight='good'] cg-board square.last-move { + background-color: rgba(91, 140, 62, 0.35) !important; +} +.chessview[data-nag-highlight='interesting'] cg-board square.last-move { + background-color: rgba(230, 157, 0, 0.3) !important; +} +.chessview[data-nag-highlight='dubious'] cg-board square.last-move { + background-color: rgba(207, 143, 0, 0.3) !important; +} +.chessview[data-nag-highlight='mistake'] cg-board square.last-move { + background-color: rgba(230, 145, 44, 0.35) !important; +} +.chessview[data-nag-highlight='blunder'] cg-board square.last-move { + background-color: rgba(202, 52, 49, 0.35) !important; +} +.chessview[data-nag-highlight='miss'] cg-board square.last-move { + background-color: rgba(202, 52, 49, 0.35) !important; +} +.chessview[data-nag-highlight='forced'] cg-board square.last-move, +.chessview[data-nag-highlight='equal'] cg-board square.last-move, +.chessview[data-nag-highlight='unclear'] cg-board square.last-move { + background-color: rgba(136, 136, 136, 0.3) !important; +} +.chessview[data-nag-highlight='white-slight'] cg-board square.last-move, +.chessview[data-nag-highlight='white-better'] cg-board square.last-move, +.chessview[data-nag-highlight='white-winning'] cg-board square.last-move { + background-color: rgba(74, 144, 217, 0.3) !important; +} +.chessview[data-nag-highlight='black-slight'] cg-board square.last-move, +.chessview[data-nag-highlight='black-better'] cg-board square.last-move, +.chessview[data-nag-highlight='black-winning'] cg-board square.last-move { + background-color: rgba(74, 144, 217, 0.3) !important; +} + +/* ============================================================================ + MOVES SECTION + ============================================================================ */ + +.cv-moves-section { + display: flex; + flex-direction: column; + flex: 1; + min-width: 180px; + max-width: 220px; + max-height: var(--cv-board-height, 360px); + border-left: 1px solid var(--background-modifier-border); + background: var(--background-secondary); + overflow: hidden; +} + +/* Move list height constraint — match board size */ +.cv-content:has(.cv-board-small) .cv-moves-section { + max-width: 180px; +} + +.cv-content:has(.cv-board-medium) .cv-moves-section { + max-width: 200px; +} + +.cv-content:has(.cv-board-large) .cv-moves-section { + max-width: 240px; +} + +.cv-content:has(.cv-board-auto) .cv-moves-section { + max-width: 240px; + min-width: 180px; +} + +.cv-content-vertical .cv-moves-section { + max-width: none; + width: 100%; + border-left: none; + border-top: 1px solid var(--background-modifier-border); +} + +.cv-moves { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.cv-moves-list { + padding: 8px; +} + +.cv-move-row { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 0; + font-size: 13px; + flex-wrap: wrap; +} + +.cv-move-num { + color: var(--text-muted); + min-width: 28px; + text-align: right; + padding-right: 4px; + flex-shrink: 0; +} + +.cv-move { + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + font-family: var(--font-monospace); + transition: background-color 0.15s; +} + +.cv-move:hover { + background: var(--background-modifier-hover); +} + +.cv-move.active { + background: var(--interactive-accent); + color: var(--text-on-accent) !important; +} + +.cv-move.active.has-annotation { + background: var(--interactive-accent); +} + +.cv-move.active .cv-move-nag { + color: var(--text-on-accent) !important; +} + +.cv-move-nag { + font-size: 11px; + font-weight: 700; + margin-left: 1px; + vertical-align: super; + line-height: 1; +} + +.cv-move-nag.nag-brilliant { + color: #1baca6; +} +.cv-move-nag.nag-good { + color: #5b8c3e; +} +.cv-move-nag.nag-interesting { + color: #e69d00; +} +.cv-move-nag.nag-dubious { + color: #cf8f00; +} +.cv-move-nag.nag-mistake { + color: #e6912c; +} +.cv-move-nag.nag-blunder { + color: #ca3431; +} +.cv-move-nag.nag-miss { + color: #ca3431; +} +.cv-move-nag.nag-forced, +.cv-move-nag.nag-equal, +.cv-move-nag.nag-unclear { + color: #888; +} + +.cv-move.has-comment { + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; +} + +.cv-move.has-annotation { + background: rgba(100, 160, 255, 0.1); +} + +/* Comment display area — room for ~3 lines */ +.cv-comment { + padding: 8px 12px; + border-top: 1px solid var(--background-modifier-border); + background: var(--background-primary-alt); + font-size: 12px; + font-style: italic; + color: var(--text-muted); + flex-shrink: 0; + max-height: 80px; + overflow-y: auto; + overflow-x: hidden; + line-height: 1.5; +} + +.cv-comment.hidden { + display: none; +} + +.cv-comment-text { + display: block; + line-height: 1.5; +} + +/* NAG label in comment area */ +.cv-comment-nag { + display: inline-block; + font-size: 11px; + font-weight: 700; + font-style: normal; + padding: 1px 8px; + border-radius: 3px; + margin-right: 6px; + color: #fff; + vertical-align: middle; + line-height: 1.6; +} + +.cv-comment-nag.nag-brilliant { + background: #1baca6; +} +.cv-comment-nag.nag-good { + background: #5b8c3e; +} +.cv-comment-nag.nag-interesting { + background: #e69d00; +} +.cv-comment-nag.nag-dubious { + background: #cf8f00; +} +.cv-comment-nag.nag-mistake { + background: #e6912c; +} +.cv-comment-nag.nag-blunder { + background: #ca3431; +} +.cv-comment-nag.nag-miss { + background: #ca3431; +} +.cv-comment-nag.nag-forced, +.cv-comment-nag.nag-equal, +.cv-comment-nag.nag-unclear { + background: #888; +} +.cv-comment-nag.nag-white-slight, +.cv-comment-nag.nag-white-better, +.cv-comment-nag.nag-white-winning { + background: #4a90d9; +} +.cv-comment-nag.nag-black-slight, +.cv-comment-nag.nag-black-better, +.cv-comment-nag.nag-black-winning { + background: #4a90d9; +} + +/* ============================================================================ + BRANCH OVERLAY + ============================================================================ */ + +.cv-branch-overlay { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 15; + pointer-events: none; +} + +.cv-branch-overlay.visible { + display: block; +} + +.cv-branch-overlay::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 2px solid var(--interactive-accent); + opacity: 0.4; + pointer-events: none; +} + +.cv-branch-return { + position: absolute; + bottom: 4px; + right: 4px; + padding: 4px 10px; + font-size: 11px; + font-weight: 600; + background: rgba(0, 0, 0, 0.7); + color: #fff; + border: none; + border-radius: 3px; + cursor: pointer; + pointer-events: auto; + white-space: nowrap; + z-index: 16; + opacity: 0.8; + transition: opacity 0.15s; +} + +.cv-branch-return:hover { + opacity: 1; +} + +/* ============================================================================ + FOOTER + ============================================================================ */ + +.cv-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: var(--background-secondary); + border-top: 1px solid var(--background-modifier-border); + gap: 8px; + flex-wrap: wrap; +} + +.cv-footer-left { + display: flex; + gap: 4px; + align-items: center; +} + +.cv-footer-right { + display: flex; + gap: 8px; + align-items: center; +} + +.cv-controls { + display: flex; + gap: 4px; + align-items: center; +} + +.cv-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); + cursor: pointer; + font-size: 14px; + color: var(--text-normal); + transition: all 0.15s; + font-family: 'Segoe UI', 'Apple Color Emoji', 'Noto Sans', Arial, sans-serif; + line-height: 1; + padding: 0; + -webkit-appearance: none; + appearance: none; +} + +.cv-btn:hover { + background: var(--background-modifier-hover); + border-color: var(--background-modifier-border-hover); +} + +.cv-btn:active { + transform: scale(0.95); +} + +.cv-btn.cv-play-btn { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); + font-size: 12px; +} + +.cv-btn.cv-play-btn:hover { + filter: brightness(1.1); +} + +.cv-counter { + font-size: 12px; + color: var(--text-muted); + min-width: 50px; + text-align: center; + font-family: var(--font-monospace); +} + +/* Actions */ +.cv-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.cv-action-btn { + width: 39px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); + cursor: pointer; + color: var(--text-muted); + transition: all 0.15s; +} + +.cv-action-btn svg { + width: 20px; + height: 20px; +} + +.cv-action-btn:hover { + color: var(--text-normal); + background: var(--background-modifier-hover); +} + +.cv-action-btn.copied { + background: var(--interactive-success); + color: white; + border-color: var(--interactive-success); +} + +.cv-action-link { + font-size: 12px; + color: var(--text-accent); + text-decoration: none; +} + +.cv-action-link:hover { + text-decoration: underline; +} + +/* ============================================================================ + PUZZLE + ============================================================================ */ + +.cv-puzzle-ui { + border-top: 1px solid var(--background-modifier-border); + background: var(--background-secondary); +} + +.cv-puzzle-status { + padding: 10px 12px; + text-align: center; + font-weight: 600; + font-size: 14px; +} + +.cv-puzzle-status.waiting { + color: var(--text-muted); +} +.cv-puzzle-status.playing { + color: var(--text-normal); +} +.cv-puzzle-status.success { + background: rgba(76, 175, 80, 0.15); + color: #4caf50; +} +.cv-puzzle-status.failed { + background: rgba(244, 67, 54, 0.15); + color: #f44336; +} + +.cv-puzzle-buttons { + display: flex; + gap: 8px; + padding: 8px 12px; + flex-wrap: wrap; + justify-content: center; +} + +.cv-puzzle-buttons:empty { + display: none; +} + +.cv-puzzle-btn { + width: auto !important; + padding: 6px 14px !important; + font-size: 13px !important; +} + +.cv-hint-btn { + background: var(--background-modifier-warning) !important; + color: var(--text-warning) !important; +} + +.cv-move-wrong { + color: #f44336 !important; + text-decoration: line-through; + opacity: 0.8; +} + +.cv-move-played { + background: rgba(76, 175, 80, 0.15); +} + +.cv-move-unplayed { + opacity: 0.6; +} + +.cv-move-placeholder { + color: var(--text-muted); + padding: 2px 6px; +} + +.cv-moves-empty { + padding: 20px; + text-align: center; + color: var(--text-muted); + font-style: italic; +} + +/* ============================================================================ + ERROR DISPLAY + ============================================================================ */ + +.cv-error { + padding: 16px; + background: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + border-radius: 8px; + color: var(--text-error); +} + +.cv-error strong { + display: block; + margin-bottom: 8px; + color: #c00; +} +.cv-error p { + margin: 0 0 12px 0; +} +.cv-error details { + margin-top: 8px; +} +.cv-error summary { + cursor: pointer; + color: var(--text-muted); + font-size: 12px; +} +.cv-error pre { + margin-top: 8px; + padding: 8px; + background: var(--background-secondary); + border-radius: 4px; + font-size: 11px; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; +} + +/* Focus */ +.chessview:focus { + outline: 2px solid var(--interactive-accent); + outline-offset: 2px; +} +.chessview:focus:not(:focus-visible) { + outline: none; +} + +/* ============================================================================ + MOBILE RESPONSIVE + ============================================================================ */ + +@media (max-width: 600px) { + .cv-content { + flex-direction: column; + align-items: center; + } + + .cv-board-section { + width: 100%; + } + + .cv-board.cg-wrap { + width: 100% !important; + height: auto !important; + aspect-ratio: 1 / 1; + } + + .cv-moves-section { + max-width: none !important; + width: 100%; + max-height: none !important; + border-left: none; + border-top: 1px solid var(--background-modifier-border); + } + + .cv-moves { + max-height: 120px; + } + + .cv-comment { + font-size: 11px; + padding: 6px 10px; + max-height: 60px; + } + + .cv-btn { + width: 36px; + height: 36px; + } + + .cv-footer { + padding: 6px 8px; + } + + .cv-footer-left { + gap: 2px; + } + + .cv-footer-right { + gap: 6px; + } + + .cv-action-link { + font-size: 11px; + } + + .cv-header { + padding: 6px 10px; + font-size: 12px; + } + + .cv-nag-glyph-inner { + width: 16px; + height: 16px; + font-size: 9px; + } + + .cv-branch-return { + font-size: 11px; + padding: 4px 10px; + } + + .cv-promotion-piece { + width: 22%; + height: 22%; + } +} + +@media (max-width: 360px) { + .cv-btn { + width: 32px; + height: 32px; + font-size: 12px; + } + .cv-counter { + display: none; + } + .cv-action-link { + display: none; + } +} + +/* ============================================================================ + LARGER SCREENS + ============================================================================ */ + +@media (min-width: 700px) { + .cv-board.cv-board-medium { + width: 400px; + height: 400px; + } + + .cv-board.cv-board-auto { + width: 480px; + height: 480px; + } + + .cv-content:has(.cv-board-auto) .cv-moves-section { + min-width: 180px; + max-width: 260px; + } + + .cv-comment { + max-height: 90px; + } +} + +@media (min-width: 900px) { + .cv-board.cv-board-medium { + width: 440px; + height: 440px; + } + + .cv-board.cv-board-auto { + width: 520px; + height: 520px; + } + + .cv-moves-section { + max-width: 260px; + } + + .cv-content:has(.cv-board-auto) .cv-moves-section { + min-width: 200px; + max-width: 300px; + } + + .cv-comment { + max-height: 100px; + } +} + +@media (min-width: 1100px) { + .cv-board.cv-board-large { + width: 520px; + height: 520px; + } + + .cv-board.cv-board-auto { + width: 560px; + height: 560px; + } + + .cv-content:has(.cv-board-large) .cv-moves-section { + max-width: 280px; + } + + .cv-content:has(.cv-board-auto) .cv-moves-section { + max-width: 280px; + min-width: 220px; + } + + .cv-moves-section { + max-width: 280px; + } + + .cv-comment { + max-height: 110px; + } +} + +/* ============================================================================ + DARK MODE + ============================================================================ */ + +.theme-dark .cv-move.has-comment { + text-decoration-color: var(--text-accent); +} + +.theme-dark .cv-comment { + background: var(--background-secondary-alt); +} + +.theme-dark .cv-nag-glyph-inner { + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); +} + +.theme-dark .cv-promotion-piece { + background-color: var(--background-secondary); + border-color: var(--background-modifier-border); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d4c6c54 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES2020", + "allowJs": true, + "noImplicitAny": false, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "lib": ["DOM", "ES2020"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"] +}