commit 855d4eda9edaafa46e7c260c6a714408550b83de Author: Ali Mousavi Date: Thu Feb 12 00:09:45 2026 +0330 v1.0.0 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 0000000..7a381db Binary files /dev/null and b/screenshots/banner.png differ diff --git a/src/board-manager.ts b/src/board-manager.ts new file mode 100644 index 0000000..bb8e0c4 --- /dev/null +++ b/src/board-manager.ts @@ -0,0 +1,639 @@ +// src/board-manager.ts +import { Chess } from 'chess.js'; +import { Chessground } from 'chessground'; +import { Api } from 'chessground/api'; +import { Config } from 'chessground/config'; +import { Color, Key } from 'chessground/types'; + +import { + ChessViewSettings, + ParsedChessData, + MoveData, + Arrow, + Circle, + BOARD_THEMES, + NAG_CLASSES +} from './types'; +import { + isValidSquare, + getValidMoves, + isInCheck, + squareToPosition +} from './utils'; + +export class BoardManager { + private ground: Api | null = null; + private boardEl: HTMLElement | null = null; + private boardWrapperEl: HTMLElement | null = null; + private nagOverlayEl: HTMLElement | null = null; + private resizeObserver: ResizeObserver | null = null; + + private isFlipped: boolean; + private settings: ChessViewSettings; + private data: ParsedChessData; + private container: HTMLElement; + + // Style tracking for cleanup + private _appliedStyleProps: string[] = []; + + constructor( + container: HTMLElement, + settings: ChessViewSettings, + data: ParsedChessData, + isFlipped: boolean + ) { + this.container = container; + this.settings = settings; + this.data = data; + this.isFlipped = isFlipped; + } + + // =========================================================================== + // INITIALIZATION + // =========================================================================== + + createBoard(boardSection: HTMLElement): void { + this.boardWrapperEl = boardSection; + this.boardEl = boardSection.createDiv({ cls: 'cv-board cg-wrap' }); + this.applyBoardSize(); + this.createNagOverlay(boardSection); + this.setupResizeObserver(); + } + + initChessground( + chess: Chess, + onUserMove?: (orig: Key, dest: Key) => 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"] +}