commit 892e85ecf56c612bb32be045f59432e72d8fd442 Author: Andre Padez Date: Thu Oct 16 12:25:52 2025 +0100 board consumed in example diff --git a/.gitignore b/.gitignore new file mode 100755 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 100755 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 100755 index 0000000..e866609 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# @vimazing/vim-snake + +Lightweight, typed **React hooks** and utilities for building interactive snake games with VIM-inspired controls. + +Part of the [VIMazing](https://vimazing.com) project - [GitHub](https://github.com/andrepadez/vimazing-vimaze). + +--- + +## 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) +- [Acknowledgements](#acknowledgements) + +--- + +## Features +- **Drop-in hooks** for snake game mechanics, scoring, and key logging. +- **Typed API** with generated declaration files for editor IntelliSense. +- **VIM-style controls** (`h`, `j`, `k`, `l` for direction changes). +- **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-snake +``` + +Or with **bun**: + +```bash +bun add @vimazing/vim-snake +``` + +--- + +## Quick Start + +```tsx +import { useEffect } from "react"; +import { useGame } from "@vimazing/vim-snake"; +import "@vimazing/vim-snake/game.css"; + +export function SnakeGame() { + const { containerRef, gameStatus, startGame } = useGame(30, 20); + + useEffect(() => { + startGame(); + }, [startGame]); + + return ( +
+

VIMazing Snake

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

🎉 You won!

} +
+ ); +} +``` + +> **Note:** You must manually import the CSS file. The package exports styles but does not auto-import them, giving you control over when and how styles are loaded. + +Default controls: + +| Key | Action | +| ------------------- | --------------- | +| `i` | Start game | +| `h` / `j` / `k` / `l` | Change direction | +| `q` | Quit game | +| `Esc` | Cancel / pause | + +--- + +## Core Hooks + +| Hook | Description | +| ---- | ----------- | +| `useGame(cols, rows, platformHook?)` | One-stop hook that wires board rendering, snake state, food spawning, scoring, and keyboard bindings. Returns refs, status helpers, and managers you can compose with your UI. | +| `useScore()` | Tracks timers, level progression, and final score calculation. | + +Each hook is exported individually, so you can cherry-pick only what you need: + +```ts +import { useScore } from "@vimazing/vim-snake/useScore"; +``` + +--- + +## Utilities + +Besides the hooks, the library exports: + +- `types` – shared TypeScript types such as `GameStatus`, `Direction`, and `Position`. + +```ts +import { GameStatus } from "@vimazing/vim-snake"; +``` + +--- + +## 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-snake` 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) + +--- + +## Acknowledgements + +Classic Snake game reimagined for the VIMazing platform with Vim-style controls. diff --git a/bun.lock b/bun.lock new file mode 100755 index 0000000..cc4f80c --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@vimazing/vim-snake", + "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 100755 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 100755 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 100755 index 0000000..3d521d2 --- /dev/null +++ b/example/bun.lock @@ -0,0 +1,348 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vimazing-vim-snake-example", + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-snake": "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-snake": ["@vimazing/vim-snake@file:..", { "devDependencies": { "@types/node": "^24.7.2", "@types/react": "^19.2.2", "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.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], + + "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.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="], + + "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.25", "", {}, "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA=="], + + "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 100755 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 100755 index 0000000..691ef74 --- /dev/null +++ b/example/index.html @@ -0,0 +1,13 @@ + + + + + + VIMazing Maze Demo + + +
+ + + + diff --git a/example/package.json b/example/package.json new file mode 100755 index 0000000..1539da2 --- /dev/null +++ b/example/package.json @@ -0,0 +1,27 @@ +{ + "name": "vimazing-vim-snake-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-snake": "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 100755 index 0000000..4d25c6a --- /dev/null +++ b/example/src/App.tsx @@ -0,0 +1,16 @@ +import { useGame } from "@vimazing/vim-snake"; +import "@vimazing/vim-snake/game.css"; +import { useKeyBindings } from "./useKeyBindings"; + +function App() { + const { containerRef } = useGame(30, 20, useKeyBindings); + + return ( +
+

VIMazing Snake

+
+
+ ); +} + +export default App; diff --git a/example/src/GameOverlay.tsx b/example/src/GameOverlay.tsx new file mode 100755 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 100755 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 100755 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 100755 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 100755 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 100755 index 0000000..91e6895 --- /dev/null +++ b/example/src/useKeyBindings.ts @@ -0,0 +1,46 @@ +import { useEffect } from "react"; +import type { UseGameType } from "@vimazing/vim-maze"; + +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 100755 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 100755 index 0000000..fe97753 --- /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-snake": ["../src/index.ts"], + "@vimazing/vim-snake/*": ["../src/*"] + } + }, + "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 100755 index 0000000..cc87210 --- /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-snake": libSource, // use source during dev + }, + }, + server: { + port: 5173, + open: true, + }, +}); + diff --git a/global.d.ts b/global.d.ts new file mode 100755 index 0000000..cbe652d --- /dev/null +++ b/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/package.json b/package.json new file mode 100755 index 0000000..6188afe --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "@vimazing/vim-snake", + "version": "0.1.0", + "description": "Classic Snake reimagined with Vim controls — a lightweight, typed React hook library for building snake games with hjkl navigation.", + "author": "André Padez ", + "keywords": [ + "snake", + "vim", + "react", + "hooks", + "game", + "modal-editing", + "typescript", + "vimazing" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vimazing/vim-snake.git" + }, + "bugs": { + "url": "https://github.com/vimazing/vim-snake/issues" + }, + "homepage": "https://github.com/vimazing/vim-snake#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/game.css', 'dist/game.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/game.css b/src/game.css new file mode 100755 index 0000000..5ebff21 --- /dev/null +++ b/src/game.css @@ -0,0 +1,33 @@ +/* ========================================================= + 🔧 Controls (tweak these) + ========================================================= */ +:root { + --snake-cell-size: 16px; +} + +/* ========================================================= + Container & Board Base + ========================================================= */ +#snake-board { + position: relative; + background-color: var(--card, #1e2030); + border-radius: 0.5rem; + box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 5%, transparent); + padding: 4px; +} + +/* ========================================================= + Snake Board Layout + ========================================================= */ +.snake-row { + display: flex; +} + +.snake-cell { + width: var(--snake-cell-size); + height: var(--snake-cell-size); + background-color: var(--muted, #2f334d); + border: 1px solid var(--border, #3b3f5c); + box-sizing: border-box; +} + diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..b677a0a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './useGame'; +export * from './types'; + diff --git a/src/types.ts b/src/types.ts new file mode 100755 index 0000000..1a73470 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,18 @@ +import { useGame } from "./useGame"; + +export type UseGameType = ReturnType; + +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"; + +export type Direction = "up" | "down" | "left" | "right"; +export type Position = { r: number; c: number }; +export type SnakeBody = Position[]; + diff --git a/src/useGame/index.ts b/src/useGame/index.ts new file mode 100755 index 0000000..5b2f75f --- /dev/null +++ b/src/useGame/index.ts @@ -0,0 +1,2 @@ +export * from './useGame'; +export * from './useKeyBindings'; diff --git a/src/useGame/useBoard.ts b/src/useGame/useBoard.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/useGame/useFood.ts b/src/useGame/useFood.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts new file mode 100755 index 0000000..e0ad4c7 --- /dev/null +++ b/src/useGame/useGame.ts @@ -0,0 +1,24 @@ +import { useEffect } from "react"; +import { useRenderer } from "./useRenderer"; + +export function useGame(cols: number, rows: number, platformHook?: unknown) { + const rendererManager = useRenderer(); + const { containerRef, renderBoard } = rendererManager; + + useEffect(() => { + renderBoard(cols, rows); + }, [cols, rows]); + + const fullGameManager = { + containerRef, + renderBoard, + }; + + if (typeof platformHook === "function") { + platformHook(fullGameManager); + } + + return fullGameManager; +} + +export type { GameStatus } from "../types"; diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts new file mode 100755 index 0000000..8db99a4 --- /dev/null +++ b/src/useGame/useGameStatus.ts @@ -0,0 +1,55 @@ +import { useCallback, useState, useMemo } from "react"; +import type { UseMazeType } from "./useMaze"; +import type { UsePlayerType } from "./usePlayer"; +import type { GameStatus } from "../types"; +import { getGamePhase, isPlaying } from "../types"; + +export type { GameStatus } from "../types"; + +export function useGameStatus(mazeManager: UseMazeType, playerManager: UsePlayerType) { + const { mazeRef, containerRef } = mazeManager; + const { setPlayerPos } = playerManager; + const [gameStatus, setGameStatus] = useState("waiting"); + + const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]); + const playStatus = useMemo( + () => (isPlaying(gameStatus) ? gameStatus : null), + [gameStatus] + ); + + // 🕹 Start the game (place hero) + const startGame = useCallback(() => { + const m = mazeRef.current; + if (!m.length) return; + + for (let c = 0; c < m[0].length; c++) { + if (m[m.length - 1][c].includes("entrance")) { + setPlayerPos({ r: m.length - 1, c }); + break; + } + } + + setGameStatus("started"); + }, [mazeRef, setPlayerPos]); + + // 🧍 Stop / exit game + const stopGame = useCallback(() => { + setPlayerPos(null); + setGameStatus("waiting"); + const container = containerRef.current; + container?.querySelector("#maze")?.classList.remove("finished"); + container?.querySelectorAll(".hero").forEach((el) => el.classList.remove("hero")); + }, [setPlayerPos, 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 100755 index 0000000..87cf73d --- /dev/null +++ b/src/useGame/useKeyBindings.ts @@ -0,0 +1,259 @@ +import type { GameStatus } from "../types"; +import { useEffect, useRef, useState } from "react"; +import type { KeyLogEntry } from "../types"; + +export type GameBindingContext = { + movePlayer: (dr: number, dc: number, count?: number) => void; + gameStatus: GameStatus; + mazeManager: any; + playerManager: any; +}; + +type UseMazeKeyBindingsParams = { + gameManager: GameBindingContext; +}; + +type Motion = { dr: number; dc: number; steps: number }; + +const RELEVANT_KEYS = new Set([ + "h", "H", "j", "J", "k", "K", "l", "L", ".", "^", "$", "g", "G", "0", + "1", "2", "3", "4", "5", "6", "7", "8", "9" +]); + +export const useKeyBindings = ({ gameManager }: UseMazeKeyBindingsParams) => { + const { movePlayer, gameStatus } = gameManager; + + const [keyLog, setKeyLog] = useState([]); + const logRef = useRef([]); + const countRef = useRef(""); + const lastMotionRef = useRef(null); + const lastKeyRef = useRef(""); + + const relevantKeys = RELEVANT_KEYS; + + const clearLog = () => { + logRef.current = []; + setKeyLog([]); + }; + + const recordKey = (key: string) => { + const entry = { key, timestamp: performance.now() }; + logRef.current.push(entry); + setKeyLog([...logRef.current]); + }; + + const resetCount = () => { countRef.current = ""; }; + + const findAnchorTarget = (dir: "left" | "right") => { + const m = gameManager.mazeManager.mazeRef.current; + const pos = gameManager.playerManager.playerPos; + if (!m.length || !pos) return null; + + const colsCount = m[0]?.length ?? 0; + const r = pos.r; + const c = pos.c; + + if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; + + const step = dir === "left" ? -1 : 1; + let cc = c; + + while (true) { + const next = cc + step; + if (next < 0 || next >= colsCount) return null; + if (m[r][next].includes("wall")) { + const wallIdx = next; + const expectedWallIdx = dir === "left" ? 0 : colsCount - 1; + const expectedCellIdx = dir === "left" ? 1 : colsCount - 2; + + if (wallIdx !== expectedWallIdx) return null; + if (cc !== expectedCellIdx) return null; + if (cc === c) return null; + + return { r, cTarget: cc }; + } + cc = next; + } + }; + + const findColumnAnchorTarget = (dir: "top" | "bottom") => { + const m = gameManager.mazeManager.mazeRef.current; + const pos = gameManager.playerManager.playerPos; + if (!m.length || !pos) return null; + + const rowsCount = m.length; + const r = pos.r; + const c = pos.c; + + if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; + + const step = dir === "top" ? -1 : 1; + let rr = r; + + while (true) { + const next = rr + step; + if (next < 0 || next >= rowsCount) return null; + if (m[next][c].includes("wall")) { + const wallIdx = next; + const expectedWallIdx = dir === "top" ? 0 : rowsCount - 1; + const expectedCellIdx = dir === "top" ? 1 : rowsCount - 2; + + if (wallIdx !== expectedWallIdx) return null; + if (rr !== expectedCellIdx) return null; + if (rr === r) return null; + + return { rTarget: rr, c }; + } + rr = next; + } + }; + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (["waiting", "game-over", "game-won"].includes(gameStatus)) return; + + // "0" behaves like Vim: beginning of line + if (e.key === "0" && countRef.current === "") { + recordKey("0"); + resetCount(); + + const anchor = findAnchorTarget("left"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = 0; + const dc = anchor.cTarget > pos.c ? 1 : -1; + const steps = Math.abs(anchor.cTarget - pos.c); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + + const isDigit = e.key >= "0" && e.key <= "9"; + if (isDigit) { + if (e.key === "0" && countRef.current === "") return; + recordKey(e.key); + countRef.current += e.key; + return; + } + + if (e.key === ".") { + recordKey("."); + const last = lastMotionRef.current; + if (!last) return; + const steps = Math.max(1, parseInt(countRef.current || String(last.steps), 10)); + resetCount(); + movePlayer(last.dr, last.dc, steps); + lastMotionRef.current = { dr: last.dr, dc: last.dc, steps }; + return; + } + + if (e.key === "^" || e.key === "$") { + recordKey(e.key); + resetCount(); + + const dir = e.key === "^" ? "left" : "right"; + const anchor = findAnchorTarget(dir); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = 0; + const dc = anchor.cTarget > pos.c ? 1 : -1; + const steps = Math.abs(anchor.cTarget - pos.c); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + + if (e.key === "g" || e.key === "G") { + if (e.key === "g") { + if (lastKeyRef.current === "g") { + recordKey(e.key); + resetCount(); + lastKeyRef.current = ""; + + const anchor = findColumnAnchorTarget("top"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = anchor.rTarget > pos.r ? 1 : -1; + const dc = 0; + const steps = Math.abs(anchor.rTarget - pos.r); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } else { + recordKey(e.key); + lastKeyRef.current = "g"; + return; + } + } else { + recordKey(e.key); + resetCount(); + lastKeyRef.current = ""; + + const anchor = findColumnAnchorTarget("bottom"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = anchor.rTarget > pos.r ? 1 : -1; + const dc = 0; + const steps = Math.abs(anchor.rTarget - pos.r); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + } + + if (!relevantKeys.has(e.key)) return; + + recordKey(e.key); + + let dr = 0, dc = 0; + switch (e.key) { + case "h": + case "H": dr = 0; dc = -1; break; + case "j": + case "J": dr = 1; dc = 0; break; + case "k": + case "K": dr = -1; dc = 0; break; + case "l": + case "L": dr = 0; dc = 1; break; + default: return; + } + + const steps = Math.max(1, parseInt(countRef.current || "1", 10)); + resetCount(); + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + }; + + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [movePlayer, gameStatus, gameManager]); + + return { + keyLog, + clearLog, + resetCount, + getLog: () => logRef.current, + }; +}; + +export type UseKeyBindingsType = ReturnType; diff --git a/src/useGame/useRenderer.ts b/src/useGame/useRenderer.ts new file mode 100755 index 0000000..3fe7949 --- /dev/null +++ b/src/useGame/useRenderer.ts @@ -0,0 +1,46 @@ +import { useRef } from "react"; + +export type BoardData = { + cols: number; + rows: number; +}; + +export function useRenderer() { + const containerRef = useRef(null); + + const renderBoard = (cols: number, rows: number) => { + const container = containerRef.current; + if (!container) return; + + container.innerHTML = ""; + + const board = document.createElement("div"); + board.id = "snake-board"; + board.className = "snake-board"; + + for (let r = 0; r < rows; r++) { + const rowDiv = document.createElement("div"); + rowDiv.className = "snake-row"; + rowDiv.setAttribute("data-r", String(r)); + + for (let c = 0; c < cols; c++) { + const cellDiv = document.createElement("div"); + cellDiv.className = "snake-cell"; + cellDiv.setAttribute("data-r", String(r)); + cellDiv.setAttribute("data-c", String(c)); + rowDiv.appendChild(cellDiv); + } + + board.appendChild(rowDiv); + } + + container.appendChild(board); + }; + + return { + containerRef, + renderBoard, + }; +} + +export type UseRendererType = ReturnType; diff --git a/src/useGame/useSnake.ts b/src/useGame/useSnake.ts new file mode 100755 index 0000000..e69de29 diff --git a/src/useScore/hooks/useTimer.ts b/src/useScore/hooks/useTimer.ts new file mode 100755 index 0000000..44b0e9e --- /dev/null +++ b/src/useScore/hooks/useTimer.ts @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +export type UseTimerReturn = { + timeValue: number; + startTimer: () => void; + stopTimer: () => void; + resetTimer: () => void; +}; + +export type UseTimerOptions = { + /** + * Interval (in ms) between updates. Defaults to 100ms. + */ + interval?: number; +}; + +export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { + const { interval = 100 } = options; + const [timeValue, setTimeValue] = useState(0); + + const intervalIdRef = useRef | null>(null); + const offsetRef = useRef(0); + const startTimeRef = useRef(null); + + const update = useCallback(() => { + if (startTimeRef.current === null) { + return; + } + + const now = performance.now(); + setTimeValue(offsetRef.current + (now - startTimeRef.current)); + }, []); + + const startTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + return; + } + + if (startTimeRef.current === null) { + startTimeRef.current = performance.now(); + } + + update(); + intervalIdRef.current = setInterval(update, Math.max(16, interval)); + }, [interval, update]); + + const stopTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + } + + if (startTimeRef.current !== null) { + update(); + offsetRef.current += performance.now() - startTimeRef.current; + startTimeRef.current = null; + } + }, [update]); + + const resetTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + } + + startTimeRef.current = null; + offsetRef.current = 0; + setTimeValue(0); + }, []); + + useEffect(() => { + return () => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + } + }; + }, []); + + return { + timeValue, + startTimer, + stopTimer, + resetTimer, + }; +} diff --git a/src/useScore/index.ts b/src/useScore/index.ts new file mode 100755 index 0000000..525ce75 --- /dev/null +++ b/src/useScore/index.ts @@ -0,0 +1 @@ +export * from './useScore'; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts new file mode 100755 index 0000000..8514393 --- /dev/null +++ b/src/useScore/useScore.ts @@ -0,0 +1,143 @@ +import { useEffect, useRef, useState } from "react"; +import { useTimer } from "../hooks/useTimer"; +import { useScoreTime } from "./useScoreTime"; +import { useScorePaths } from "./useScorePaths"; +import type { KeyLogProvider, GameScoreContext } from "../types"; + +type UseScoreParams = { + gameContext: GameScoreContext; + keyManager?: KeyLogProvider; +}; + +export function useScore({ gameContext, keyManager }: UseScoreParams) { + const timer = useTimer(); + const { gameStatus, setGameStatus } = gameContext; + const { timeValue, startTimer, stopTimer, resetTimer } = timer; + + // ✅ useScorePaths always returns distances under these names + const { + entranceToKey = 0, + entranceToExit = 0, + heroToKey = 0, + heroToExit = 0, + } = gameContext.mazeManager + ? useScorePaths({ gameManager: gameContext as any }) + : {}; + + // ✅ use unified naming for internal logic + const distToKey = entranceToKey || heroToKey; + const distToExit = entranceToExit || heroToExit; + + const [finalScore, setFinalScore] = useState(null); + const optimalRef = useRef(0); + const keystrokes = keyManager ? keyManager.keyLog.length : 0; + + // 1️⃣ Manage visible wall-clock timer + useScoreTime({ gameStatus, timer }); + + // 2️⃣ Compute static optimal path when maze (re)starts + useEffect(() => { + const maze = gameContext.mazeManager?.mazeInstanceRef?.current; + if (!maze) return; + + const k = maze.getDistance("entrance", "key"); + const e = maze.getDistance("key", "exit"); + const total = k + e; + + if (Number.isFinite(total) && total > 0) { + optimalRef.current = total; + } + }, [gameStatus, gameContext.mazeManager?.mazeInstanceRef]); + + const optimalSteps = optimalRef.current; + + // 3️⃣ Efficiency (keystrokes vs optimal steps) + const efficiency = + optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0; + + // 4️⃣ Logic time (internal motion time) + const [logicFirst, setLogicFirst] = useState(null); + const [logicLast, setLogicLast] = useState(null); + + useEffect(() => { + if (gameStatus === "started") { + setLogicFirst(null); + setLogicLast(null); + setPlayerSteps(0); + prevPosRef.current = null; + } + }, [gameStatus]); + + useEffect(() => { + const onTick = (e: Event) => { + const t = + (e as CustomEvent<{ t?: number }>).detail?.t ?? performance.now(); + setLogicFirst((prev) => prev ?? t); + setLogicLast(t); + }; + window.addEventListener("maze-logic-tick", onTick as EventListener); + return () => + window.removeEventListener("maze-logic-tick", onTick as EventListener); + }, []); + + // 5️⃣ Actual player steps + const [playerSteps, setPlayerSteps] = useState(0); + const prevPosRef = useRef<{ r: number; c: number } | null>(null); + + useEffect(() => { + const pos = gameContext.playerManager?.playerPos; + if (!pos) { + prevPosRef.current = null; + return; + } + + if (!["started", "hasKey"].includes(gameStatus)) { + prevPosRef.current = pos; + return; + } + + const prev = prevPosRef.current; + prevPosRef.current = pos; + if (!prev) return; + + const delta = Math.abs(pos.r - prev.r) + Math.abs(pos.c - prev.c); + if (delta > 0) setPlayerSteps((s) => s + delta); + }, [gameStatus, gameContext.playerManager?.playerPos]); + + // 6️⃣ Auto game-over on inefficiency + useEffect(() => { + if (gameStatus === "started" && efficiency > 150) { + setGameStatus("game-over"); + } + }, [efficiency, gameStatus, setGameStatus]); + + // 7️⃣ Final score on win + useEffect(() => { + if (gameStatus !== "game-won") return; + + const logicMs = + logicFirst != null && logicLast != null + ? Math.max(1, logicLast - logicFirst) + : timeValue; + + const optimalMs = optimalSteps * 200; + const ratio = Math.max(0.1, optimalMs / logicMs); + const score = Math.min(100000, Math.round(ratio * 100000)); + + setFinalScore(score); + }, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps, playerSteps]); + + // ✅ Return clean, typed object + return { + timeValue, + startTimer, + stopTimer, + resetTimer, + distToKey, + distToExit, + keystrokes, + optimalSteps, + efficiency, + finalScore, + }; +} diff --git a/src/useScore/useScoreTime.ts b/src/useScore/useScoreTime.ts new file mode 100755 index 0000000..fe2514b --- /dev/null +++ b/src/useScore/useScoreTime.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from "react"; +import type { UseTimerReturn } from "./hooks/useTimer"; +import type { GameStatus } from "../types"; + +type UseScoreTimeParams = { + gameStatus: GameStatus; + timer: UseTimerReturn; +}; + +export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { + const prevStatusRef = useRef(null); + const { startTimer, stopTimer, resetTimer } = timer; + + useEffect(() => { + // const prevStatus = prevStatusRef.current; + prevStatusRef.current = gameStatus; + + switch (gameStatus) { + case "started": + // Always reset and start a fresh timer when a new game begins + resetTimer(); + startTimer(); + break; + + case "hasKey": + // Keep running + break; + + case "game-over": + case "game-won": + case "waiting": + // Stop timer when game finishes or is exited + stopTimer(); + break; + } + }, [gameStatus, startTimer, stopTimer, resetTimer]); +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 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"] +}