From 8acc16fc1caf6ce71f6c9e504301a071d19a7ca8 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Wed, 15 Oct 2025 17:29:53 +0100 Subject: [PATCH] working on useGame --- .gitignore | 59 ++ .npmignore | 6 + README.md | 140 ++++ bun.lock | 29 + example/.gitignore | 34 + example/README.md | 15 + example/bun.lock | 348 +++++++++ example/global.d.ts | 2 + example/index.html | 13 + example/package-lock.json | 1166 +++++++++++++++++++++++++++++ example/package.json | 27 + example/src/App.tsx | 19 + example/src/GameOverlay.tsx | 111 +++ example/src/MobileGameOverlay.tsx | 99 +++ example/src/index.css | 1 + example/src/main.tsx | 11 + example/src/useIsMobile.ts | 30 + example/src/useKeyBindings.ts | 46 ++ example/src/useMounted.ts | 11 + example/tsconfig.json | 40 + example/vite.config.ts | 22 + global.d.ts | 1 + package-lock.json | 43 ++ package.json | 52 ++ src/index.ts | 3 + src/lib/Sudoku.ts | 371 +++++++++ src/sudoku.css | 144 ++++ src/types.ts | 18 + src/useGame/index.ts | 1 + src/useGame/useBoardState.ts | 55 ++ src/useGame/useCursor.ts | 52 ++ src/useGame/useGame.ts | 88 +++ src/useGame/useGameStatus.ts | 44 ++ src/useGame/useKeyBindings.ts | 114 +++ src/useGame/useSudokuRenderer.ts | 83 ++ src/useScore/index.ts | 1 + tsconfig.json | 21 + 37 files changed, 3320 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100644 example/bun.lock create mode 100644 example/global.d.ts create mode 100644 example/index.html create mode 100644 example/package-lock.json create mode 100644 example/package.json create mode 100644 example/src/App.tsx create mode 100644 example/src/GameOverlay.tsx create mode 100644 example/src/MobileGameOverlay.tsx create mode 100644 example/src/index.css create mode 100644 example/src/main.tsx create mode 100644 example/src/useIsMobile.ts create mode 100644 example/src/useKeyBindings.ts create mode 100644 example/src/useMounted.ts create mode 100644 example/tsconfig.json create mode 100644 example/vite.config.ts create mode 100644 global.d.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/lib/Sudoku.ts create mode 100644 src/sudoku.css create mode 100644 src/types.ts create mode 100644 src/useGame/index.ts create mode 100644 src/useGame/useBoardState.ts create mode 100644 src/useGame/useCursor.ts create mode 100644 src/useGame/useGame.ts create mode 100644 src/useGame/useGameStatus.ts create mode 100644 src/useGame/useKeyBindings.ts create mode 100644 src/useGame/useSudokuRenderer.ts create mode 100644 src/useScore/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6051801 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules/ +bower_components/ + +# Build outputs +dist/ + +# TypeScript +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +*.log + +# Coverage & reports +coverage/ +*.lcov + +# Caches +.vite/ +.next/ +.nuxt/ +.svelte-kit/ +.angular/ +.cache/ +.parcel-cache/ +.turbo/ +.eslintcache +.stylelintcache + +# IDE & editor +.vscode/ +.idea/ +*.sw? +*.sublime-workspace +*.sublime-project + +# OS files +.DS_Store +Thumbs.db + +# Env files +.env +.env.* + +# Misc +*.pem +*.pfx +*.p12 +*.crt +*.key +*.cer + +# Bun +.bun/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a2cf60a --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +src/ +node_modules/ +tsconfig.json +bun.lockb +*.log + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9528d7a --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# @vimazing/vim-sudoku + +Lightweight, typed **React hooks** and utilities for building interactive sudoku games with VIM-inspired controls. + +Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project. + +--- + +## Contents +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Hooks](#core-hooks) +- [Utilities](#utilities) +- [Example App](#example-app) +- [Build & Release](#build--release) +- [License](#license) + +--- + +## Features +- **Drop-in hooks** for sudoku game logic, player control, scoring, and key logging. +- **Typed API** with generated declaration files for editor IntelliSense. +- **VIM-style navigation** (`h`, `j`, `k`, `l`, counts, `i` to start, etc.). +- **Composable architecture** – bring your own rendering and platform-specific bindings. +- **Tree-shakeable exports** to keep bundles lean. + +--- + +## Installation + +Using **npm**: + +```bash +npm install @vimazing/vim-sudoku +``` + +Or with **bun**: + +```bash +bun add @vimazing/vim-sudoku +``` + +--- + +## Quick Start + +```tsx +import { useEffect } from "react"; +import { useGame } from "@vimazing/vim-sudoku"; + +export function SudokuGame() { + const { containerRef, gameStatus, startGame } = useGame(); + + useEffect(() => { + startGame(); + }, [startGame]); + + return ( +
+

VIMazing Sudoku

+
+ {gameStatus === "game-won" &&

🎉 You solved it!

} +
+ ); +} +``` + +Default controls: + +| Key | Action | +| ------------------- | --------------- | +| `i` | Start game | +| `h` / `j` / `k` / `l` | Move cursor | +| `` | Enter number | +| `x` / `d` | Clear cell | +| `q` | Quit game | +| `Esc` | Cancel / pause | + +--- + +## Core Hooks + +| Hook | Description | +| ---- | ----------- | +| `useGame(platformHook?)` | One-stop hook that wires sudoku generation, rendering, player state, scoring, and keyboard bindings. Returns refs, status helpers, and managers you can compose with your UI. | +| `useKeyBindings()` | Provides VIM-style key bindings, including repeat counts and movement helpers. | +| `useScore()` | Tracks timers, moves, and final score calculation. | + +Each hook is exported individually, so you can cherry-pick only what you need: + +```ts +import { useScore } from "@vimazing/vim-sudoku/useScore"; +``` + +--- + +## Utilities + +Besides the hooks, the library exports: + +- `SudokuGenerator` – procedural sudoku puzzle generation class. +- `SudokuRenderer` – canvas renderer tailored for sudoku grids. +- `types` – shared TypeScript types such as `GameStatus`, `SudokuCell`, and `KeyLogEntry`. + +```ts +import { SudokuGenerator, GameStatus } from "@vimazing/vim-sudoku"; +``` + +--- + +## Example App + +A demo application lives under `example/` and consumes the package directly. + +```bash +cd example +bun install +bun run dev +``` + +During local development the Vite config aliases `@vimazing/vim-sudoku` to the source folder so you can iterate without rebuilding. When publishing, run the build first (see below) so editors consume the generated declarations in `dist/`. + +--- + +## Build & Release + +Build the distributable bundle and type declarations: + +```bash +bun run build +``` + +This writes JavaScript, type definitions, and styles to `dist/`. The `prepublishOnly` hook reuses the same command to guarantee fresh artifacts before publishing. + +--- + +## License + +MIT © [André Padez](https://github.com/andrepadez) diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..935fccc --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@vimazing/vim-maze", + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.2.2", + "typescript": "~5.9.3", + }, + "peerDependencies": { + "react": "^19.1.1", + }, + }, + }, + "packages": { + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a98e6ab --- /dev/null +++ b/example/README.md @@ -0,0 +1,15 @@ +# example + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/example/bun.lock b/example/bun.lock new file mode 100644 index 0000000..b5febc2 --- /dev/null +++ b/example/bun.lock @@ -0,0 +1,348 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vimazing-vim-maze-example", + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1", + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + + "@vimazing/vim-maze": ["@vimazing/vim-maze@file:..", { "devDependencies": { "typescript": "~5.9.3" }, "peerDependencies": { "react": "^19.1.1" } }], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="], + + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], + + "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.235", "", {}, "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + } +} diff --git a/example/global.d.ts b/example/global.d.ts new file mode 100644 index 0000000..72f5ca9 --- /dev/null +++ b/example/global.d.ts @@ -0,0 +1,2 @@ +declare module "@vitejs/plugin-react"; +declare module "@tailwindcss/vite"; diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..691ef74 --- /dev/null +++ b/example/index.html @@ -0,0 +1,13 @@ + + + + + + VIMazing Maze Demo + + +
+ + + + diff --git a/example/package-lock.json b/example/package-lock.json new file mode 100644 index 0000000..939a090 --- /dev/null +++ b/example/package-lock.json @@ -0,0 +1,1166 @@ +{ + "name": "vimazing-vim-maze-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vimazing-vim-maze-example", + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10" + } + }, + "..": { + "name": "@vimazing/vim-maze", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "~5.9.3" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.6.1", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.14", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vimazing/vim-maze": { + "resolved": "..", + "link": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001750", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "3.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..b571444 --- /dev/null +++ b/example/package.json @@ -0,0 +1,27 @@ +{ + "name": "vimazing-vim-maze-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1" + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx new file mode 100644 index 0000000..735b57d --- /dev/null +++ b/example/src/App.tsx @@ -0,0 +1,19 @@ +import { useGame } from "../../src/useGame/useGame"; +import { useKeyBindings } from './useKeyBindings'; +import "../../src/sudoku.css"; + +export default function App() { + const { containerRef, initGame, solveBoard } = useGame(useKeyBindings); + + return ( +
+

VIMazing Sudoku

+
+
+ + +
+
+ ); +} + diff --git a/example/src/GameOverlay.tsx b/example/src/GameOverlay.tsx new file mode 100644 index 0000000..d9d8c59 --- /dev/null +++ b/example/src/GameOverlay.tsx @@ -0,0 +1,111 @@ +import type { GameStatus } from "@vimazing/vim-maze"; +import type { UseScoreType } from "@vimazing/vim-maze"; +import { useIsMobile } from "./useIsMobile"; +import { MobileGameOverlay } from "./MobileGameOverlay"; + +type GameOverlayProps = { + gameStatus: GameStatus; + scoreManager: UseScoreType; +}; + +export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => { + const isMobile = useIsMobile(); + if (isMobile) return + + // 🧠 Only show overlay when the game has ended + if (['started', 'hasKey'].includes(gameStatus)) return null; + // console.log('gameStatus:', gameStatus) + + return ( + + {gameStatus === "waiting" && } + {gameStatus === "game-over" && } + {gameStatus === "game-won" && } + + ); +}; + +export const GameWaitingContent = () => { + return ( +
+
+

Press i to play...

+

h,j,k,l to move around

+

try other vim motions
you're familiar with

+

+ $,^,0,G,gg only valid if
+ the edges are reachable +

+
+
+ ); +}; + +export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => { + const finalScore = scoreManager.finalScore ?? 0; + + return ( +
+

Congratulations!

+

Final Score:

+

{finalScore.toLocaleString()}

+

+ min + ( + 100000 + , ( + optimalTime + / + actualTime + ) * + 100000 + ) +

+

Press i to play again

+
+ ); +}; + +export const GameOverContent = () => { + return ( +
+

Game Over

+

Press i to play again

+
+ ); +}; + +export const GameOverlayContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + + + diff --git a/example/src/MobileGameOverlay.tsx b/example/src/MobileGameOverlay.tsx new file mode 100644 index 0000000..f804e3f --- /dev/null +++ b/example/src/MobileGameOverlay.tsx @@ -0,0 +1,99 @@ +import type { GameStatus } from "@vimazing/vim-maze"; +import type { UseScoreType } from "@vimazing/vim-maze"; + +type MobileGameOverlayProps = { + gameStatus: GameStatus; + scoreManager: UseScoreType; +}; + +export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => { + // 🧠 Only show overlay when the game has ended + if (['started', 'hasKey'].includes(gameStatus)) return null; + // console.log('gameStatus:', gameStatus) + + return ( + + {gameStatus === "waiting" && } + {gameStatus === "game-over" && } + {gameStatus === "game-won" && } + + ); +}; + +export const GameWaitingContent = () => { + return ( +
+
+

Press here to play

+

all the vim motions
available on desktop

+
+
+ ); +}; + +export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => { + const finalScore = scoreManager.finalScore ?? 0; + + return ( +
+

Congratulations!

+

Final Score:

+

{finalScore.toLocaleString()}

+

+ min + ( + 100000 + , ( + optimalTime + / + actualTime + ) * + 100000 + ) +

+

Press to play again

+
+ ); +}; + +export const GameOverContent = () => { + return ( +
+ Game Over +
+ ); +}; + +export const GameOverlayContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + + diff --git a/example/src/index.css b/example/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/example/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/example/src/main.tsx b/example/src/main.tsx new file mode 100644 index 0000000..d9f0159 --- /dev/null +++ b/example/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); + diff --git a/example/src/useIsMobile.ts b/example/src/useIsMobile.ts new file mode 100644 index 0000000..6b77170 --- /dev/null +++ b/example/src/useIsMobile.ts @@ -0,0 +1,30 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" + ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches + : false + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + + const onChange = (event: MediaQueryListEvent) => { + setIsMobile(event.matches); + }; + + // Set initial value + setIsMobile(mql.matches); + + // Add listener + mql.addEventListener("change", onChange); + + // Cleanup + return () => mql.removeEventListener("change", onChange); + }, []); + + return isMobile; +} diff --git a/example/src/useKeyBindings.ts b/example/src/useKeyBindings.ts new file mode 100644 index 0000000..4ec3a28 --- /dev/null +++ b/example/src/useKeyBindings.ts @@ -0,0 +1,46 @@ +import { useEffect } from "react"; +import type { UseGameType } from "../../src/useGame/useGame"; + +export const useKeyBindings = (gameManager: UseGameType) => { + const { gameStatus, initGame, stopGame, clearLog, resetCount } = gameManager; + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + // start game + if (e.key === "i" || e.key === "I") { + if (["waiting", "game-over", "game-won"].includes(gameStatus)) { + clearLog(); + resetCount(); + if (gameStatus === "game-over" || gameStatus === "game-won") { + stopGame(); + setTimeout(() => initGame(true), 0); + } else { + initGame(true); + } + return; + } + } + + // ignore other keys while waiting + if (gameStatus === "waiting") return; + + // cancel count or stop game + if (e.key === "Escape") { + stopGame(); + resetCount(); + return; + } + + // quit + if (e.key === "q" || e.key === "Q") { + stopGame(); + resetCount(); + return; + } + }; + + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [gameStatus, initGame, stopGame, clearLog, resetCount]); +}; + diff --git a/example/src/useMounted.ts b/example/src/useMounted.ts new file mode 100644 index 0000000..7da354e --- /dev/null +++ b/example/src/useMounted.ts @@ -0,0 +1,11 @@ +import { useState, useEffect } from 'react'; + +export const useMounted = () => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return mounted; +}; diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..8538eed --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + /* === Environment === */ + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + + /* === Bundler / Vite === */ + "noEmit": true, + "allowJs": true, + "skipLibCheck": true, + "isolatedModules": true, + "types": ["vite/client"], + + /* === Code Style & Safety === */ + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + + /* === Module Behavior === */ + "allowImportingTsExtensions": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + /* === Path aliases (optional) === */ + "baseUrl": ".", + "paths": { + "@vimazing/vim-maze": ["../dist/index.d.ts"], + "@vimazing/vim-maze/*": ["../dist/*"] + } + }, + "include": ["src", "vite.config.ts", "global.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/example/vite.config.ts b/example/vite.config.ts new file mode 100644 index 0000000..f2794ab --- /dev/null +++ b/example/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import tailwindcss from '@tailwindcss/vite' +import react from "@vitejs/plugin-react"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const libSource = resolve(__dirname, "../src"); + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@vimazing/vim-maze": libSource, // use source during dev + }, + }, + server: { + port: 5173, + open: true, + }, +}); + diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..77bfbe9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "@vimazing/vim-maze", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vimazing/vim-maze", + "version": "1.0.1", + "license": "MIT", + "devDependencies": { + "typescript": "~5.9.3" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa3ffa2 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "@vimazing/vim-sudoku", + "version": "0.1.0", + "description": "VIMazing VIM Sudoku engine — a lightweight, typed React hook set for sudoku games.", + "author": "André Padez ", + "repository": { + "type": "git", + "url": "https://github.com/vimazing/vim-sudoku.git" + }, + "bugs": { + "url": "https://github.com/vimazing/vim-sudoku/issues" + }, + "homepage": "https://github.com/vimazing/vim-sudoku#readme", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./useGame": { + "import": "./dist/useGame/index.js", + "types": "./dist/useGame/index.d.ts" + }, + "./useScore": { + "import": "./dist/useScore/index.js", + "types": "./dist/useScore/index.d.ts" + }, + "./*": { + "import": "./dist/*.js", + "types": "./dist/*.d.ts" + }, + "./*.css": "./dist/*.css" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --outDir dist --declaration --emitDeclarationOnly false && node -e \"const fs = require('node:fs'); fs.mkdirSync('dist', { recursive: true }); fs.copyFileSync('src/sudoku.css', 'dist/sudoku.css');\"", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "react": "^19.1.1" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.2.2", + "typescript": "~5.9.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..630a69f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import './sudoku.css'; +export * from './types'; +export * from "./useGame"; diff --git a/src/lib/Sudoku.ts b/src/lib/Sudoku.ts new file mode 100644 index 0000000..d6c6d00 --- /dev/null +++ b/src/lib/Sudoku.ts @@ -0,0 +1,371 @@ +// src/lib/Sudoku.ts +// Direct TypeScript + ES module version of Robatron's sudoku.js +// https://github.com/robatron/sudoku.js +// No algorithmic changes – only syntax, structure, and typing updates. + +export type DifficultyName = + | "easy" + | "medium" + | "hard" + | "very-hard" + | "insane" + | "inhuman"; + +export type Difficulty = DifficultyName | number; + +const sudoku: any = {}; + +sudoku.DIGITS = "123456789"; +const ROWS = "ABCDEFGHI"; +const COLS = sudoku.DIGITS; +let SQUARES: string[] = []; + +let UNITS: string[][] = []; +let SQUARE_UNITS_MAP: Record = {}; +let SQUARE_PEERS_MAP: Record = {}; + +const MIN_GIVENS = 17; +const NR_SQUARES = 81; + +const DIFFICULTY: Record = { + easy: 62, + medium: 53, + hard: 44, + "very-hard": 35, + insane: 26, + inhuman: 17, +}; + +sudoku.BLANK_CHAR = "."; +sudoku.BLANK_BOARD = sudoku.BLANK_CHAR.repeat(NR_SQUARES); + +function initialize() { + SQUARES = sudoku._cross(ROWS, COLS); + UNITS = sudoku._get_all_units(ROWS, COLS); + SQUARE_UNITS_MAP = sudoku._get_square_units_map(SQUARES, UNITS); + SQUARE_PEERS_MAP = sudoku._get_square_peers_map(SQUARES, SQUARE_UNITS_MAP); +} + +// --------------------------------------------------------------------------- +// Generate +// --------------------------------------------------------------------------- + +sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string { + if (typeof difficulty === "string" || typeof difficulty === "undefined") { + difficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy; + } + + difficulty = sudoku._force_range(difficulty, NR_SQUARES + 1, MIN_GIVENS); + unique = unique ?? true; + + const blank_board = sudoku.BLANK_BOARD; + let candidates = sudoku._get_candidates_map(blank_board); + + const shuffled_squares = sudoku._shuffle(SQUARES); + + for (const square of shuffled_squares) { + const rand_candidate_idx = sudoku._rand_range(candidates[square].length); + const rand_candidate = candidates[square][rand_candidate_idx]; + if (!sudoku._assign(candidates, square, rand_candidate)) break; + + const single_candidates: string[] = []; + for (const s of SQUARES) { + if (candidates[s].length === 1) single_candidates.push(candidates[s]); + } + + if ( + single_candidates.length >= difficulty && + sudoku._strip_dups(single_candidates).length >= 8 + ) { + let board = ""; + let givens_idxs: number[] = []; + + for (let i = 0; i < SQUARES.length; i++) { + const s = SQUARES[i]; + if (candidates[s].length === 1) { + board += candidates[s]; + givens_idxs.push(i); + } else { + board += sudoku.BLANK_CHAR; + } + } + + if (givens_idxs.length > difficulty) { + givens_idxs = sudoku._shuffle(givens_idxs); + for (let i = 0; i < givens_idxs.length - difficulty; ++i) { + const target = givens_idxs[i]; + board = + board.substring(0, target) + + sudoku.BLANK_CHAR + + board.substring(target + 1); + } + } + + if (sudoku.solve(board)) return board; + } + } + + return sudoku.generate(difficulty); +}; + +// --------------------------------------------------------------------------- +// Solve +// --------------------------------------------------------------------------- + +sudoku.solve = function (board: string, reverse?: boolean): string | false { + const report = sudoku.validate_board(board); + if (report !== true) throw report; + + let nr_givens = 0; + for (const ch of board) { + if (ch !== sudoku.BLANK_CHAR && sudoku._in(ch, sudoku.DIGITS)) nr_givens++; + } + if (nr_givens < MIN_GIVENS) throw "Too few givens"; + + reverse = reverse || false; + const candidates = sudoku._get_candidates_map(board); + const result = sudoku._search(candidates, reverse); + + if (result) { + let solution = ""; + for (const s of SQUARES) solution += result[s]; + return solution; + } + return false; +}; + +// --------------------------------------------------------------------------- +// Get candidates +// --------------------------------------------------------------------------- + +sudoku.get_candidates = function (board: string): string[][] | false { + const report = sudoku.validate_board(board); + if (report !== true) throw report; + + const map = sudoku._get_candidates_map(board); + if (!map) return false; + + const rows: string[][] = []; + let cur_row: string[] = []; + let i = 0; + for (const square in map) { + cur_row.push(map[square]); + if (i % 9 === 8) { + rows.push(cur_row); + cur_row = []; + } + ++i; + } + return rows; +}; + +sudoku._get_candidates_map = function (board: string): Record | false { + const report = sudoku.validate_board(board); + if (report !== true) throw report; + + const map: Record = {}; + const squares_values = sudoku._get_square_vals_map(board); + + for (const s of SQUARES) map[s] = sudoku.DIGITS; + + for (const s in squares_values) { + const val = squares_values[s]; + if (sudoku._in(val, sudoku.DIGITS)) { + const new_candidates = sudoku._assign(map, s, val); + if (!new_candidates) return false; + } + } + return map; +}; + +// --------------------------------------------------------------------------- +// Search +// --------------------------------------------------------------------------- + +sudoku._search = function (candidates: Record, reverse: boolean): any { + if (!candidates) return false; + reverse = reverse || false; + + let max_nr_candidates = 0; + let max_square = ""; + for (const s of SQUARES) { + const n = candidates[s].length; + if (n > max_nr_candidates) { + max_nr_candidates = n; + max_square = s; + } + } + if (max_nr_candidates === 1) return candidates; + + let min_nr_candidates = 10; + let min_square = ""; + for (const s of SQUARES) { + const n = candidates[s].length; + if (n < min_nr_candidates && n > 1) { + min_nr_candidates = n; + min_square = s; + } + } + + const min_candidates = candidates[min_square]; + if (!reverse) { + for (const v of min_candidates) { + const copy = JSON.parse(JSON.stringify(candidates)); + const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse); + if (next) return next; + } + } else { + for (let i = min_candidates.length - 1; i >= 0; --i) { + const v = min_candidates[i]; + const copy = JSON.parse(JSON.stringify(candidates)); + const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse); + if (next) return next; + } + } + return false; +}; + +// --------------------------------------------------------------------------- +// Assignment / elimination logic +// --------------------------------------------------------------------------- + +sudoku._assign = function (candidates: Record, square: string, val: string) { + const others = candidates[square].replace(val, ""); + for (const v of others) { + const next = sudoku._eliminate(candidates, square, v); + if (!next) return false; + } + return candidates; +}; + +sudoku._eliminate = function (candidates: Record, square: string, val: string) { + if (!sudoku._in(val, candidates[square])) return candidates; + + candidates[square] = candidates[square].replace(val, ""); + const n = candidates[square].length; + + if (n === 1) { + const target_val = candidates[square]; + for (const peer of SQUARE_PEERS_MAP[square]) { + const next = sudoku._eliminate(candidates, peer, target_val); + if (!next) return false; + } + } else if (n === 0) return false; + + for (const unit of SQUARE_UNITS_MAP[square]) { + const places: string[] = []; + for (const sq of unit) { + if (sudoku._in(val, candidates[sq])) places.push(sq); + } + if (places.length === 0) return false; + else if (places.length === 1) { + const next = sudoku._assign(candidates, places[0], val); + if (!next) return false; + } + } + return candidates; +}; + +// --------------------------------------------------------------------------- +// Data structures +// --------------------------------------------------------------------------- + +sudoku._get_square_vals_map = function (board: string) { + const map: Record = {}; + if (board.length !== SQUARES.length) throw "Board length mismatch"; + for (let i = 0; i < SQUARES.length; i++) map[SQUARES[i]] = board[i]; + return map; +}; + +sudoku._get_square_units_map = function (squares: string[], units: string[][]) { + const map: Record = {}; + for (const s of squares) { + const list: string[][] = []; + for (const u of units) if (u.includes(s)) list.push(u); + map[s] = list; + } + return map; +}; + +sudoku._get_square_peers_map = function ( + squares: string[], + units_map: Record +) { + const map: Record = {}; + for (const s of squares) { + const peers: string[] = []; + for (const unit of units_map[s]) { + for (const sq of unit) { + if (sq !== s && !peers.includes(sq)) peers.push(sq); + } + } + map[s] = peers; + } + return map; +}; + +sudoku._get_all_units = function (rows: string, cols: string) { + const units: string[][] = []; + for (const r of rows) units.push(sudoku._cross(r, cols)); + for (const c of cols) units.push(sudoku._cross(rows, c)); + const row_blocks = ["ABC", "DEF", "GHI"]; + const col_blocks = ["123", "456", "789"]; + for (const rb of row_blocks) + for (const cb of col_blocks) + units.push(sudoku._cross(rb, cb)); + return units; +}; + +// --------------------------------------------------------------------------- +// Utils +// --------------------------------------------------------------------------- + +sudoku.validate_board = function (board: string): true | string { + if (!board) return "Empty board"; + if (board.length !== NR_SQUARES) return "Invalid board size"; + for (let i = 0; i < board.length; i++) { + const c = board[i]; + if (!sudoku._in(c, sudoku.DIGITS) && c !== sudoku.BLANK_CHAR) + return `Invalid char at ${i}: ${c}`; + } + return true; +}; + +sudoku._cross = function (a: string, b: string) { + const res: string[] = []; + for (const ai of a) for (const bi of b) res.push(ai + bi); + return res; +}; + +sudoku._in = function (v: string, seq: string) { + return seq.indexOf(v) !== -1; +}; + +sudoku._shuffle = function (seq: T[]): T[] { + const arr = [...seq]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +}; + +sudoku._rand_range = function (max: number, min = 0) { + return Math.floor(Math.random() * (max - min)) + min; +}; + +sudoku._strip_dups = function (seq: T[]): T[] { + return Array.from(new Set(seq)); +}; + +sudoku._force_range = function (nr: number, max: number, min = 0) { + return Math.min(Math.max(nr || 0, min), max); +}; + +// --------------------------------------------------------------------------- +// Init +// --------------------------------------------------------------------------- +initialize(); + +export { sudoku }; + diff --git a/src/sudoku.css b/src/sudoku.css new file mode 100644 index 0000000..dc00012 --- /dev/null +++ b/src/sudoku.css @@ -0,0 +1,144 @@ +/* ========================================================= + 🔧 Controls (tweak these) + ========================================================= */ +:root { + --cell-size: 32px; + --cell-font-size: 16px; + --grid-gap: 2px; + --box-separator: 3px; +} + +/* ========================================================= + Container & Grid Base + ========================================================= */ +#sudoku_container { + position: relative; + margin: 1em auto; + width: fit-content; +} + +#sudoku { + position: relative; + background-color: #1e2030; /* Tokyo Night card background */ + border-radius: 0.5rem; + box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px rgba(130, 170, 255, 0.05); + padding: 0.5rem; + width: fit-content; +} + +/* ========================================================= + Grid Layout + ========================================================= */ +#sudoku .sudoku-grid { + display: grid; + grid-template-columns: repeat(9, var(--cell-size)); + gap: var(--grid-gap); + user-select: none; +} + +/* ========================================================= + Cell Base Styles + ========================================================= */ +#sudoku .sudoku-cell { + width: var(--cell-size); + height: var(--cell-size); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + font-family: "Fira Code", monospace; + font-size: var(--cell-font-size); + background: #2f334d; /* Tokyo Night muted */ + color: #c8d3f5; /* Tokyo Night foreground */ + transition: all 0.15s ease; + box-shadow: inset 0 0 3px rgba(27, 29, 43, 0.4); +} + +/* ========================================================= + 3x3 Box Separators + ========================================================= */ +/* Add thicker border to right of columns 2 and 5 (0-indexed) */ +#sudoku .sudoku-cell[data-c="2"], +#sudoku .sudoku-cell[data-c="5"] { + border-right: var(--box-separator) solid #1e2030; + margin-right: 1px; +} + +/* Add thicker border to bottom of rows 2 and 5 (0-indexed) */ +#sudoku .sudoku-cell[data-r="2"], +#sudoku .sudoku-cell[data-r="5"] { + border-bottom: var(--box-separator) solid #1e2030; + margin-bottom: 1px; +} + +/* ========================================================= + User-Entered Cells + ========================================================= */ +#sudoku .sudoku-cell.user-entered { + color: #82aaff; /* Tokyo Night primary blue */ + font-weight: 600; +} + +/* ========================================================= + Active Cell Highlight + ========================================================= */ +#sudoku .sudoku-cell.active { + outline: 2px solid #ffc777; /* Tokyo Night yellow/warning */ + outline-offset: -2px; + background: #3d3836; + box-shadow: + 0 0 16px rgba(255, 199, 119, 0.7), + 0 0 24px rgba(255, 199, 119, 0.4), + inset 0 0 12px rgba(255, 199, 119, 0.3); + animation: active-cell-glow 1.5s ease-in-out infinite; +} + +@keyframes active-cell-glow { + 0%, 100% { + box-shadow: + 0 0 16px rgba(255, 199, 119, 0.7), + 0 0 24px rgba(255, 199, 119, 0.4), + inset 0 0 12px rgba(255, 199, 119, 0.3); + } + 50% { + box-shadow: + 0 0 20px rgba(255, 199, 119, 0.9), + 0 0 32px rgba(255, 199, 119, 0.6), + inset 0 0 16px rgba(255, 199, 119, 0.4); + } +} + +/* ========================================================= + Visual Feedback Effects + ========================================================= */ +/* Invalid move effect */ +#sudoku .sudoku-cell.invalid-move { + animation: sudoku-invalid-pulse 180ms ease-out; + box-shadow: + 0 0 12px rgba(255, 117, 127, 0.85), + 0 0 24px rgba(255, 117, 127, 0.45); +} + +@keyframes sudoku-invalid-pulse { + 0% { + transform: scale(1.05); + box-shadow: + 0 0 16px rgba(255, 117, 127, 0.95), + 0 0 28px rgba(255, 117, 127, 0.55); + } + 100% { + transform: none; + box-shadow: none; + } +} + +/* Trail effect for moves */ +#sudoku .sudoku-cell.trail { + box-shadow: inset 0 0 6px rgba(130, 170, 255, 0.6), 0 0 6px rgba(130, 170, 255, 0.35); + animation: sudoku-trail-fade 220ms ease-out; +} + +@keyframes sudoku-trail-fade { + from { background-color: rgba(130, 170, 255, 0.25); } + to { background-color: transparent; } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..db4afb4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,18 @@ +import { useGame } from "./useGame"; +export type UseGameType = ReturnType; + +export type { + Difficulty, + DifficultyName, + BoardString, +} from "./lib/Sudoku"; + +export type GamePhase = "idle" | "playing"; +export type PlayStatus = "started" | "game-over" | "game-won"; +export type GameStatus = "waiting" | "started" | "game-over" | "game-won"; + +export const isPlaying = (status: GameStatus): status is PlayStatus => + status === "started" || status === "game-over" || status === "game-won"; + +export const getGamePhase = (status: GameStatus): GamePhase => + status === "waiting" ? "idle" : "playing"; diff --git a/src/useGame/index.ts b/src/useGame/index.ts new file mode 100644 index 0000000..1163755 --- /dev/null +++ b/src/useGame/index.ts @@ -0,0 +1 @@ +export * from './useGame'; diff --git a/src/useGame/useBoardState.ts b/src/useGame/useBoardState.ts new file mode 100644 index 0000000..dff74a7 --- /dev/null +++ b/src/useGame/useBoardState.ts @@ -0,0 +1,55 @@ +import { useState, useCallback } from "react"; +import { sudoku } from "../lib/Sudoku"; +import type { UseCursorType } from "./useCursor"; + +export function useBoardState(cursorManager: UseCursorType) { + const { cursor, setCursor } = cursorManager; + const [board, setBoard] = useState(sudoku.generate("medium")); + const [initialBoard, setInitialBoard] = useState(board); + const [solution, setSolution] = useState(null); + + const setDigit = useCallback( + (d: string) => { + const i = cursor.r * 9 + cursor.c; + const arr = board.split(""); + arr[i] = d; + setBoard(arr.join("")); + }, + [cursor, board] + ); + + const erase = useCallback(() => { + const i = cursor.r * 9 + cursor.c; + const arr = board.split(""); + arr[i] = "."; + setBoard(arr.join("")); + }, [cursor, board]); + + const reset = useCallback(() => { + const newBoard = sudoku.generate("medium"); + setBoard(newBoard); + setInitialBoard(newBoard); + setSolution(null); + setCursor({ r: 0, c: 0 }); + }, [setCursor]); + + const solveBoard = useCallback(() => { + const solved = sudoku.solve(board); + if (solved) { + setSolution(solved); + setBoard(solved); + } + }, [board]); + + return { + board, + initialBoard, + solution, + setDigit, + erase, + reset, + solveBoard, + }; +} + +export type UseBoardStateType = ReturnType; diff --git a/src/useGame/useCursor.ts b/src/useGame/useCursor.ts new file mode 100644 index 0000000..3e65d30 --- /dev/null +++ b/src/useGame/useCursor.ts @@ -0,0 +1,52 @@ +import { useRef, useState, useCallback } from "react"; +import type { UseRenderer } from "./useSudokuRenderer"; + +export function useCursor(rendererManager: UseRenderer) { + const { containerRef } = rendererManager; + const [cursor, setCursor] = useState<{ r: number; c: number }>({ r: 0, c: 0 }); + + const animatingRef = useRef(false); + + const getCellEl = useCallback((r: number, c: number): HTMLElement | null => { + const container = containerRef.current; + if (!container) return null; + const sudokuDiv = container.querySelector("#sudoku"); + if (!sudokuDiv) return null; + const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`; + return sudokuDiv.querySelector(selector) as HTMLElement | null; + }, [containerRef]); + + const flashInvalid = useCallback(() => { + const el = getCellEl(cursor.r, cursor.c); + if (!el) return; + el.classList.add("invalid-move"); + setTimeout(() => el.classList.remove("invalid-move"), 180); + }, [cursor, getCellEl]); + + const moveCursor = useCallback( + (dr: number, dc: number, steps = 1) => { + if (animatingRef.current) return; + + setCursor((prev) => { + let r = prev.r; + let c = prev.c; + for (let i = 0; i < steps; i++) { + r = Math.max(0, Math.min(8, r + dr)); + c = Math.max(0, Math.min(8, c + dc)); + } + return { r, c }; + }); + }, + [] + ); + + return { + cursor, + setCursor, + moveCursor, + getCellEl, + flashInvalid, + }; +} + +export type UseCursorType = ReturnType; diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts new file mode 100644 index 0000000..2001d20 --- /dev/null +++ b/src/useGame/useGame.ts @@ -0,0 +1,88 @@ +import { useEffect } from "react"; +import { useRenderer } from "./useSudokuRenderer"; +import { useCursor } from "./useCursor"; +import { useGameStatus } from "./useGameStatus"; +import { useBoardState } from "./useBoardState"; +import { useKeyBindings } from "./useKeyBindings"; + +export function useGame(platformHook?: unknown) { + const rendererManager = useRenderer(); + const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager; + const cursorManager = useCursor(rendererManager); + const { cursor, moveCursor } = cursorManager; + const gameManager = useGameStatus(rendererManager, cursorManager); + const { gameStatus, setGameStatus, startGame, stopGame } = gameManager; + const boardManager = useBoardState(cursorManager); + const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager; + + // --------------------------- + // Rendering + // --------------------------- + useEffect(() => { + renderEmptyBoard(); + }, [renderEmptyBoard]); + + useEffect(() => { + if (gameStatus === "waiting") { + renderEmptyBoard(); + } else { + renderBoard(board, initialBoard); + highlightCursor(cursor.r, cursor.c); + } + }, [gameStatus, board, initialBoard, cursor, renderEmptyBoard, renderBoard, highlightCursor]); + + // --------------------------- + // Keybindings + // --------------------------- + const keyBindings = useKeyBindings({ + moveCursor, + setDigit, + erase, + }); + + // --------------------------- + // Game Control + // --------------------------- + const initGame = () => { + reset(); + startGame(); + }; + + // --------------------------- + // Full Game Manager + // --------------------------- + const fullGameManager = { + // core + containerRef, + gameManager, + gameStatus, + setGameStatus, + startGame, + stopGame, + initGame, + + // cursor + cursorManager, + cursor, + moveCursor, + + // board state + boardManager, + board, + solution, + setDigit, + erase, + reset, + solveBoard, + + // keybindings (vim-maze compatible) + ...keyBindings, + }; + + // allow platform-level injection + if (typeof platformHook === "function") { + platformHook(fullGameManager); + } + + return fullGameManager; +} diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts new file mode 100644 index 0000000..b74720f --- /dev/null +++ b/src/useGame/useGameStatus.ts @@ -0,0 +1,44 @@ +import { useCallback, useState, useMemo } from "react"; +import type { UseRenderer } from "./useSudokuRenderer"; +import type { UseCursorType } from "./useCursor"; +import type { GameStatus } from "../types"; +import { getGamePhase, isPlaying } from "../types"; + +export type { GameStatus } from "../types"; + +export function useGameStatus( + rendererManager: UseRenderer, + cursorManager: UseCursorType +) { + const { containerRef } = rendererManager; + const { setCursor } = cursorManager; + const [gameStatus, setGameStatus] = useState("waiting"); + + const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]); + const playStatus = useMemo( + () => (isPlaying(gameStatus) ? gameStatus : null), + [gameStatus] + ); + + const startGame = useCallback(() => { + setGameStatus("started"); + }, []); + + const stopGame = useCallback(() => { + setCursor({ r: 0, c: 0 }); + setGameStatus("waiting"); + const container = containerRef.current; + container?.querySelectorAll(".active").forEach((el) => el.classList.remove("active")); + }, [setCursor, containerRef]); + + return { + gameStatus, + gamePhase, + playStatus, + setGameStatus, + startGame, + stopGame, + }; +} + +export type UseGameStatusType = ReturnType; diff --git a/src/useGame/useKeyBindings.ts b/src/useGame/useKeyBindings.ts new file mode 100644 index 0000000..3122925 --- /dev/null +++ b/src/useGame/useKeyBindings.ts @@ -0,0 +1,114 @@ +import { useEffect, useRef, useState } from "react"; + +export type KeyLogEntry = { key: string; timestamp: number }; + +type Ctx = { + moveCursor: (dr: number, dc: number, steps?: number) => void; + setDigit: (d: string) => void; + erase: () => void; +}; + +export function useKeyBindings(ctx: Ctx) { + const [keyLog, setKeyLog] = useState([]); + const logRef = useRef([]); + const countRef = useRef(""); // Vim-style numeric prefix + const lastMotionRef = useRef<{ dr: number; dc: number; steps: number } | null>(null); + + // Record keypresses for score/time analytics + const recordKey = (key: string) => { + const entry = { key, timestamp: performance.now() }; + logRef.current.push(entry); + setKeyLog([...logRef.current]); + }; + + const clearLog = () => { + logRef.current = []; + setKeyLog([]); + }; + + const getLog = () => logRef.current; + + const resetCount = () => { + countRef.current = ""; + }; + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + const k = e.key; + + // --- Digits 1–9 fill a cell --- + if (/^[1-9]$/.test(k)) { + recordKey(k); + ctx.setDigit(k); + resetCount(); + return; + } + + // --- Erase --- + if (k === "x" || k === "Backspace") { + recordKey(k); + ctx.erase(); + resetCount(); + return; + } + + // --- Numeric prefixes --- + if (/^[0-9]$/.test(k)) { + recordKey(k); + countRef.current += k; + return; + } + + // --- Vim-style navigation --- + let dr = 0; + let dc = 0; + if (k === "h") dc = -1; + else if (k === "l") dc = 1; + else if (k === "j") dr = 1; + else if (k === "k") dr = -1; + else if (k === "0" || k === "^") dc = -999; // go to col 0 + else if (k === "$") dc = 999; // go to col 8 + else if (k === ".") { + recordKey("."); + const last = lastMotionRef.current; + if (last) ctx.moveCursor(last.dr, last.dc, last.steps); + return; + } else return; + + recordKey(k); + + const steps = Math.max(1, parseInt(countRef.current || "1", 10)); + resetCount(); + + if (dc === -999) { + ctx.moveCursor(0, -1, 99); // move far left + lastMotionRef.current = { dr: 0, dc: -1, steps: 99 }; + return; + } + + if (dc === 999) { + ctx.moveCursor(0, 1, 99); // move far right + lastMotionRef.current = { dr: 0, dc: 1, steps: 99 }; + return; + } + + ctx.moveCursor(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + }; + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [ctx]); + + // ✅ same interface as vim-maze useKeyBindings + return { + keyLog, + getLog, + clearLog, + resetCount, + countRef, + lastMotionRef, + }; +} + +export type UseKeyBindingsType = ReturnType; diff --git a/src/useGame/useSudokuRenderer.ts b/src/useGame/useSudokuRenderer.ts new file mode 100644 index 0000000..95cd980 --- /dev/null +++ b/src/useGame/useSudokuRenderer.ts @@ -0,0 +1,83 @@ +import { useRef, useCallback } from "react"; + +export function useRenderer() { + const containerRef = useRef(null); + + const renderEmptyBoard = useCallback(() => { + const container = containerRef.current; + if (!container) return; + + container.innerHTML = ""; + const sudokuDiv = document.createElement("div"); + sudokuDiv.id = "sudoku"; + + const grid = document.createElement("div"); + grid.className = "sudoku-grid"; + + for (let i = 0; i < 81; i++) { + const r = Math.floor(i / 9); + const c = i % 9; + const cell = document.createElement("div"); + cell.className = "sudoku-cell"; + cell.dataset.r = String(r); + cell.dataset.c = String(c); + cell.textContent = ""; + grid.appendChild(cell); + } + + sudokuDiv.appendChild(grid); + container.appendChild(sudokuDiv); + }, []); + + const renderBoard = useCallback((board: string, initialBoard?: string) => { + const container = containerRef.current; + if (!container) return; + + container.innerHTML = ""; + const sudokuDiv = document.createElement("div"); + sudokuDiv.id = "sudoku"; + + const grid = document.createElement("div"); + grid.className = "sudoku-grid"; + + for (let i = 0; i < 81; i++) { + const r = Math.floor(i / 9); + const c = i % 9; + const cell = document.createElement("div"); + cell.className = "sudoku-cell"; + cell.dataset.r = String(r); + cell.dataset.c = String(c); + cell.textContent = board[i] === "." ? "" : board[i]; + + if (initialBoard && board[i] !== "." && board[i] !== initialBoard[i]) { + cell.classList.add("user-entered"); + } + + grid.appendChild(cell); + } + + sudokuDiv.appendChild(grid); + container.appendChild(sudokuDiv); + }, []); + + const highlightCursor = useCallback((r: number, c: number) => { + const container = containerRef.current; + if (!container) return; + + const sudokuDiv = container.querySelector("#sudoku"); + if (!sudokuDiv) return; + + sudokuDiv.querySelectorAll(".active").forEach((el) => el.classList.remove("active")); + + const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`; + const cell = sudokuDiv.querySelector(selector); + if (cell) { + cell.classList.add("active"); + } + }, []); + + return { containerRef, renderEmptyBoard, renderBoard, highlightCursor }; +} + +export type UseRenderer = ReturnType; + diff --git a/src/useScore/index.ts b/src/useScore/index.ts new file mode 100644 index 0000000..525ce75 --- /dev/null +++ b/src/useScore/index.ts @@ -0,0 +1 @@ +export * from './useScore'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..14cd137 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "global.d.ts"], + "exclude": ["example"] +}