board consumed in example
This commit is contained in:
Executable
+59
@@ -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/
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
src/
|
||||||
|
node_modules/
|
||||||
|
tsconfig.json
|
||||||
|
bun.lockb
|
||||||
|
*.log
|
||||||
|
|
||||||
@@ -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 (
|
||||||
|
<section className="mx-auto w-fit space-y-4">
|
||||||
|
<h1 className="text-2xl font-bold">VIMazing Snake</h1>
|
||||||
|
<div ref={containerRef} className="relative" />
|
||||||
|
{gameStatus === "game-won" && <p>🎉 You won!</p>}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **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.
|
||||||
@@ -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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+34
@@ -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
|
||||||
Executable
+15
@@ -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.
|
||||||
Executable
+348
@@ -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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
declare module "@vitejs/plugin-react";
|
||||||
|
declare module "@tailwindcss/vite";
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>VIMazing Maze Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Executable
+27
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+16
@@ -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 (
|
||||||
|
<div className="relative mx-auto my-4 w-fit space-y-4">
|
||||||
|
<h1 className="text-2xl font-bold text-center">VIMazing Snake</h1>
|
||||||
|
<div ref={containerRef} className="relative" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
Executable
+111
@@ -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 <MobileGameOverlay gameStatus={gameStatus} scoreManager={scoreManager} />
|
||||||
|
|
||||||
|
// 🧠 Only show overlay when the game has ended
|
||||||
|
if (['started', 'hasKey'].includes(gameStatus)) return null;
|
||||||
|
// console.log('gameStatus:', gameStatus)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameOverlayContainer>
|
||||||
|
{gameStatus === "waiting" && <GameWaitingContent />}
|
||||||
|
{gameStatus === "game-over" && <GameOverContent />}
|
||||||
|
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
|
||||||
|
</GameOverlayContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameWaitingContent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#c8d3f5] text-[2em] font-mono text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<p className="text-[3rem]">Press <strong className="text-[#ffc777]">i</strong> to play...</p>
|
||||||
|
<p><strong className="text-[#ffc777]">h</strong>,<strong className="text-[#ffc777]">j</strong>,<strong className="text-[#ffc777]">k</strong>,<strong className="text-[#ffc777]">l</strong> to move around</p>
|
||||||
|
<p>try other vim motions <br /> you're familiar with</p>
|
||||||
|
<p>
|
||||||
|
<strong className="text-[#ffc777]">$</strong>,<strong className="text-[#ffc777]">^</strong>,<strong className="text-[#ffc777]">0</strong>,<strong className="text-[#ffc777]">G</strong>,<strong className="text-[#ffc777]">gg</strong> only valid if <br />
|
||||||
|
the edges are reachable
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
|
||||||
|
const finalScore = scoreManager.finalScore ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#c8d3f5] text-[2em] grid gap-4 font-mono uppercase text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
<p className="text-[#c3e88d]">Congratulations!</p>
|
||||||
|
<p>Final Score:</p>
|
||||||
|
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
||||||
|
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
||||||
|
<span className="text-[#c099ff]">min</span>
|
||||||
|
<span className="text-[#c8d3f5]">(</span>
|
||||||
|
<span className="text-[#ff966c]">100000</span>
|
||||||
|
<span className="text-[#c8d3f5]">, (</span>
|
||||||
|
<span className="text-[#82aaff]">optimalTime</span>
|
||||||
|
<span className="text-[#c8d3f5]"> / </span>
|
||||||
|
<span className="text-[#82aaff]">actualTime</span>
|
||||||
|
<span className="text-[#c8d3f5]">) * </span>
|
||||||
|
<span className="text-[#ff966c]">100000</span>
|
||||||
|
<span className="text-[#c8d3f5]">)</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[#828bb8]">Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameOverContent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#ff757f] font-mono uppercase text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
<p className="text-[5em]">Game Over</p>
|
||||||
|
<p className="text-[2em] text-[#828bb8]">Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameOverlayContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
absolute inset-0
|
||||||
|
flex justify-center items-center
|
||||||
|
z-40
|
||||||
|
pointer-events-none
|
||||||
|
"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(27, 29, 43, 0.75)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Executable
+99
@@ -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 (
|
||||||
|
<GameOverlayContainer>
|
||||||
|
{gameStatus === "waiting" && <GameWaitingContent />}
|
||||||
|
{gameStatus === "game-over" && <GameOverContent />}
|
||||||
|
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
|
||||||
|
</GameOverlayContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameWaitingContent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#c8d3f5] text-[1em] font-mono text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-[1.5rem]">Press here to play</p>
|
||||||
|
<p>all the vim motions <br /> available on desktop</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
|
||||||
|
const finalScore = scoreManager.finalScore ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#c8d3f5] text-[1em] grid gap-4 font-mono uppercase text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
<p className="text-[#c3e88d]">Congratulations!</p>
|
||||||
|
<p>Final Score:</p>
|
||||||
|
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
||||||
|
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
||||||
|
<span className="text-[#c099ff]">min</span>
|
||||||
|
<span className="text-[#c8d3f5]">(</span>
|
||||||
|
<span className="text-[#ff966c]">100000</span>
|
||||||
|
<span className="text-[#c8d3f5]">, (</span>
|
||||||
|
<span className="text-[#82aaff]">optimalTime</span>
|
||||||
|
<span className="text-[#c8d3f5]"> / </span>
|
||||||
|
<span className="text-[#82aaff]">actualTime</span>
|
||||||
|
<span className="text-[#c8d3f5]">) * </span>
|
||||||
|
<span className="text-[#ff966c]">100000</span>
|
||||||
|
<span className="text-[#c8d3f5]">)</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[#828bb8]">Press to play again</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameOverContent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-[#ff757f] text-[5em] font-mono uppercase text-center select-none"
|
||||||
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
|
>
|
||||||
|
Game Over
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameOverlayContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
absolute inset-0
|
||||||
|
flex justify-center items-center
|
||||||
|
z-40
|
||||||
|
pointer-events-none
|
||||||
|
"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(27, 29, 43, 0.75)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
Executable
+11
@@ -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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
Executable
+46
@@ -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]);
|
||||||
|
};
|
||||||
|
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useMounted = () => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
};
|
||||||
Executable
+40
@@ -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"]
|
||||||
|
}
|
||||||
Executable
+22
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module "*.css";
|
||||||
Executable
+62
@@ -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 <andre.padez@gmail.com>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+33
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './useGame';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { useGame } from "./useGame";
|
||||||
|
|
||||||
|
export type UseGameType = ReturnType<typeof useGame>;
|
||||||
|
|
||||||
|
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[];
|
||||||
|
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './useGame';
|
||||||
|
export * from './useKeyBindings';
|
||||||
Executable
Executable
Executable
+24
@@ -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";
|
||||||
Executable
+55
@@ -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<GameStatus>("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<typeof useGameStatus>;
|
||||||
|
|
||||||
Executable
+259
@@ -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<KeyLogEntry[]>([]);
|
||||||
|
const logRef = useRef<KeyLogEntry[]>([]);
|
||||||
|
const countRef = useRef<string>("");
|
||||||
|
const lastMotionRef = useRef<Motion | null>(null);
|
||||||
|
const lastKeyRef = useRef<string>("");
|
||||||
|
|
||||||
|
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<typeof useKeyBindings>;
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
export type BoardData = {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useRenderer() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(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<typeof useRenderer>;
|
||||||
Executable
Executable
+85
@@ -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<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const offsetRef = useRef(0);
|
||||||
|
const startTimeRef = useRef<number | null>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from './useScore';
|
||||||
Executable
+143
@@ -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<number | null>(null);
|
||||||
|
const optimalRef = useRef<number>(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<number | null>(null);
|
||||||
|
const [logicLast, setLogicLast] = useState<number | null>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Executable
+38
@@ -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<GameStatus | null>(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]);
|
||||||
|
}
|
||||||
|
|
||||||
Executable
+21
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user