working on useGame
This commit is contained in:
+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/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
src/
|
||||||
|
node_modules/
|
||||||
|
tsconfig.json
|
||||||
|
bun.lockb
|
||||||
|
*.log
|
||||||
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# @vimazing/vim-sudoku
|
||||||
|
|
||||||
|
Lightweight, typed **React hooks** and utilities for building interactive sudoku games with VIM-inspired controls.
|
||||||
|
|
||||||
|
Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Core Hooks](#core-hooks)
|
||||||
|
- [Utilities](#utilities)
|
||||||
|
- [Example App](#example-app)
|
||||||
|
- [Build & Release](#build--release)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Drop-in hooks** for sudoku game logic, player control, scoring, and key logging.
|
||||||
|
- **Typed API** with generated declaration files for editor IntelliSense.
|
||||||
|
- **VIM-style navigation** (`h`, `j`, `k`, `l`, counts, `i` to start, etc.).
|
||||||
|
- **Composable architecture** – bring your own rendering and platform-specific bindings.
|
||||||
|
- **Tree-shakeable exports** to keep bundles lean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Using **npm**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @vimazing/vim-sudoku
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with **bun**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @vimazing/vim-sudoku
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useGame } from "@vimazing/vim-sudoku";
|
||||||
|
|
||||||
|
export function SudokuGame() {
|
||||||
|
const { containerRef, gameStatus, startGame } = useGame();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
startGame();
|
||||||
|
}, [startGame]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mx-auto w-fit space-y-4">
|
||||||
|
<h1 className="text-2xl font-bold">VIMazing Sudoku</h1>
|
||||||
|
<div ref={containerRef} className="relative" />
|
||||||
|
{gameStatus === "game-won" && <p>🎉 You solved it!</p>}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Default controls:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| ------------------- | --------------- |
|
||||||
|
| `i` | Start game |
|
||||||
|
| `h` / `j` / `k` / `l` | Move cursor |
|
||||||
|
| `<number>` | Enter number |
|
||||||
|
| `x` / `d` | Clear cell |
|
||||||
|
| `q` | Quit game |
|
||||||
|
| `Esc` | Cancel / pause |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Hooks
|
||||||
|
|
||||||
|
| Hook | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| `useGame(platformHook?)` | One-stop hook that wires sudoku generation, rendering, player state, scoring, and keyboard bindings. Returns refs, status helpers, and managers you can compose with your UI. |
|
||||||
|
| `useKeyBindings()` | Provides VIM-style key bindings, including repeat counts and movement helpers. |
|
||||||
|
| `useScore()` | Tracks timers, moves, and final score calculation. |
|
||||||
|
|
||||||
|
Each hook is exported individually, so you can cherry-pick only what you need:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useScore } from "@vimazing/vim-sudoku/useScore";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
Besides the hooks, the library exports:
|
||||||
|
|
||||||
|
- `SudokuGenerator` – procedural sudoku puzzle generation class.
|
||||||
|
- `SudokuRenderer` – canvas renderer tailored for sudoku grids.
|
||||||
|
- `types` – shared TypeScript types such as `GameStatus`, `SudokuCell`, and `KeyLogEntry`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SudokuGenerator, GameStatus } from "@vimazing/vim-sudoku";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example App
|
||||||
|
|
||||||
|
A demo application lives under `example/` and consumes the package directly.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd example
|
||||||
|
bun install
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
During local development the Vite config aliases `@vimazing/vim-sudoku` to the source folder so you can iterate without rebuilding. When publishing, run the build first (see below) so editors consume the generated declarations in `dist/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & Release
|
||||||
|
|
||||||
|
Build the distributable bundle and type declarations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes JavaScript, type definitions, and styles to `dist/`. The `prepublishOnly` hook reuses the same command to guarantee fresh artifacts before publishing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © [André Padez](https://github.com/andrepadez)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "@vimazing/vim-maze",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.7.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.1.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "vimazing-vim-maze-example",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vimazing/vim-maze": "file:../",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.7.2",
|
||||||
|
"@types/react": "^19.1.1",
|
||||||
|
"@types/react-dom": "^19.1.1",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||||
|
|
||||||
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="],
|
||||||
|
|
||||||
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
|
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||||
|
|
||||||
|
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
||||||
|
|
||||||
|
"@vimazing/vim-maze": ["@vimazing/vim-maze@file:..", { "devDependencies": { "typescript": "~5.9.3" }, "peerDependencies": { "react": "^19.1.1" } }],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="],
|
||||||
|
|
||||||
|
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.235", "", {}, "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
|
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="],
|
||||||
|
|
||||||
|
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
declare module "@vitejs/plugin-react";
|
||||||
|
declare module "@tailwindcss/vite";
|
||||||
@@ -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>
|
||||||
|
|
||||||
Generated
+1166
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "vimazing-vim-maze-example",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.7.2",
|
||||||
|
"@types/react": "^19.1.1",
|
||||||
|
"@types/react-dom": "^19.1.1",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^5.4.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vimazing/vim-maze": "file:../",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useGame } from "../../src/useGame/useGame";
|
||||||
|
import { useKeyBindings } from './useKeyBindings';
|
||||||
|
import "../../src/sudoku.css";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { containerRef, initGame, solveBoard } = useGame(useKeyBindings);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16 }}>
|
||||||
|
<h1>VIMazing Sudoku</h1>
|
||||||
|
<div ref={containerRef} />
|
||||||
|
<div className="mt-20">
|
||||||
|
<button onClick={initGame}>New Puzzle</button>
|
||||||
|
<button onClick={solveBoard}>Solve</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import type { UseGameType } from "../../src/useGame/useGame";
|
||||||
|
|
||||||
|
export const useKeyBindings = (gameManager: UseGameType) => {
|
||||||
|
const { gameStatus, initGame, stopGame, clearLog, resetCount } = gameManager;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
// start game
|
||||||
|
if (e.key === "i" || e.key === "I") {
|
||||||
|
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
|
||||||
|
clearLog();
|
||||||
|
resetCount();
|
||||||
|
if (gameStatus === "game-over" || gameStatus === "game-won") {
|
||||||
|
stopGame();
|
||||||
|
setTimeout(() => initGame(true), 0);
|
||||||
|
} else {
|
||||||
|
initGame(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore other keys while waiting
|
||||||
|
if (gameStatus === "waiting") return;
|
||||||
|
|
||||||
|
// cancel count or stop game
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
stopGame();
|
||||||
|
resetCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// quit
|
||||||
|
if (e.key === "q" || e.key === "Q") {
|
||||||
|
stopGame();
|
||||||
|
resetCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, [gameStatus, initGame, stopGame, clearLog, resetCount]);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useMounted = () => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* === Environment === */
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
/* === Bundler / Vite === */
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* === Code Style & Safety === */
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
|
||||||
|
/* === Module Behavior === */
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
/* === Path aliases (optional) === */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@vimazing/vim-maze": ["../dist/index.d.ts"],
|
||||||
|
"@vimazing/vim-maze/*": ["../dist/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts", "global.d.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const libSource = resolve(__dirname, "../src");
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@vimazing/vim-maze": libSource, // use source during dev
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module "*.css";
|
||||||
Generated
+43
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@vimazing/vim-maze",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@vimazing/vim-maze",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~5.9.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "19.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "@vimazing/vim-sudoku",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "VIMazing VIM Sudoku engine — a lightweight, typed React hook set for sudoku games.",
|
||||||
|
"author": "André Padez <you@example.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/vimazing/vim-sudoku.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/vimazing/vim-sudoku/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/vimazing/vim-sudoku#readme",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./useGame": {
|
||||||
|
"import": "./dist/useGame/index.js",
|
||||||
|
"types": "./dist/useGame/index.d.ts"
|
||||||
|
},
|
||||||
|
"./useScore": {
|
||||||
|
"import": "./dist/useScore/index.js",
|
||||||
|
"types": "./dist/useScore/index.d.ts"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./dist/*.js",
|
||||||
|
"types": "./dist/*.d.ts"
|
||||||
|
},
|
||||||
|
"./*.css": "./dist/*.css"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --outDir dist --declaration --emitDeclarationOnly false && node -e \"const fs = require('node:fs'); fs.mkdirSync('dist', { recursive: true }); fs.copyFileSync('src/sudoku.css', 'dist/sudoku.css');\"",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.7.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"typescript": "~5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import './sudoku.css';
|
||||||
|
export * from './types';
|
||||||
|
export * from "./useGame";
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
// src/lib/Sudoku.ts
|
||||||
|
// Direct TypeScript + ES module version of Robatron's sudoku.js
|
||||||
|
// https://github.com/robatron/sudoku.js
|
||||||
|
// No algorithmic changes – only syntax, structure, and typing updates.
|
||||||
|
|
||||||
|
export type DifficultyName =
|
||||||
|
| "easy"
|
||||||
|
| "medium"
|
||||||
|
| "hard"
|
||||||
|
| "very-hard"
|
||||||
|
| "insane"
|
||||||
|
| "inhuman";
|
||||||
|
|
||||||
|
export type Difficulty = DifficultyName | number;
|
||||||
|
|
||||||
|
const sudoku: any = {};
|
||||||
|
|
||||||
|
sudoku.DIGITS = "123456789";
|
||||||
|
const ROWS = "ABCDEFGHI";
|
||||||
|
const COLS = sudoku.DIGITS;
|
||||||
|
let SQUARES: string[] = [];
|
||||||
|
|
||||||
|
let UNITS: string[][] = [];
|
||||||
|
let SQUARE_UNITS_MAP: Record<string, string[][]> = {};
|
||||||
|
let SQUARE_PEERS_MAP: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
const MIN_GIVENS = 17;
|
||||||
|
const NR_SQUARES = 81;
|
||||||
|
|
||||||
|
const DIFFICULTY: Record<DifficultyName, number> = {
|
||||||
|
easy: 62,
|
||||||
|
medium: 53,
|
||||||
|
hard: 44,
|
||||||
|
"very-hard": 35,
|
||||||
|
insane: 26,
|
||||||
|
inhuman: 17,
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku.BLANK_CHAR = ".";
|
||||||
|
sudoku.BLANK_BOARD = sudoku.BLANK_CHAR.repeat(NR_SQUARES);
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
SQUARES = sudoku._cross(ROWS, COLS);
|
||||||
|
UNITS = sudoku._get_all_units(ROWS, COLS);
|
||||||
|
SQUARE_UNITS_MAP = sudoku._get_square_units_map(SQUARES, UNITS);
|
||||||
|
SQUARE_PEERS_MAP = sudoku._get_square_peers_map(SQUARES, SQUARE_UNITS_MAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Generate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||||
|
if (typeof difficulty === "string" || typeof difficulty === "undefined") {
|
||||||
|
difficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty = sudoku._force_range(difficulty, NR_SQUARES + 1, MIN_GIVENS);
|
||||||
|
unique = unique ?? true;
|
||||||
|
|
||||||
|
const blank_board = sudoku.BLANK_BOARD;
|
||||||
|
let candidates = sudoku._get_candidates_map(blank_board);
|
||||||
|
|
||||||
|
const shuffled_squares = sudoku._shuffle(SQUARES);
|
||||||
|
|
||||||
|
for (const square of shuffled_squares) {
|
||||||
|
const rand_candidate_idx = sudoku._rand_range(candidates[square].length);
|
||||||
|
const rand_candidate = candidates[square][rand_candidate_idx];
|
||||||
|
if (!sudoku._assign(candidates, square, rand_candidate)) break;
|
||||||
|
|
||||||
|
const single_candidates: string[] = [];
|
||||||
|
for (const s of SQUARES) {
|
||||||
|
if (candidates[s].length === 1) single_candidates.push(candidates[s]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
single_candidates.length >= difficulty &&
|
||||||
|
sudoku._strip_dups(single_candidates).length >= 8
|
||||||
|
) {
|
||||||
|
let board = "";
|
||||||
|
let givens_idxs: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < SQUARES.length; i++) {
|
||||||
|
const s = SQUARES[i];
|
||||||
|
if (candidates[s].length === 1) {
|
||||||
|
board += candidates[s];
|
||||||
|
givens_idxs.push(i);
|
||||||
|
} else {
|
||||||
|
board += sudoku.BLANK_CHAR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (givens_idxs.length > difficulty) {
|
||||||
|
givens_idxs = sudoku._shuffle(givens_idxs);
|
||||||
|
for (let i = 0; i < givens_idxs.length - difficulty; ++i) {
|
||||||
|
const target = givens_idxs[i];
|
||||||
|
board =
|
||||||
|
board.substring(0, target) +
|
||||||
|
sudoku.BLANK_CHAR +
|
||||||
|
board.substring(target + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sudoku.solve(board)) return board;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sudoku.generate(difficulty);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Solve
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku.solve = function (board: string, reverse?: boolean): string | false {
|
||||||
|
const report = sudoku.validate_board(board);
|
||||||
|
if (report !== true) throw report;
|
||||||
|
|
||||||
|
let nr_givens = 0;
|
||||||
|
for (const ch of board) {
|
||||||
|
if (ch !== sudoku.BLANK_CHAR && sudoku._in(ch, sudoku.DIGITS)) nr_givens++;
|
||||||
|
}
|
||||||
|
if (nr_givens < MIN_GIVENS) throw "Too few givens";
|
||||||
|
|
||||||
|
reverse = reverse || false;
|
||||||
|
const candidates = sudoku._get_candidates_map(board);
|
||||||
|
const result = sudoku._search(candidates, reverse);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
let solution = "";
|
||||||
|
for (const s of SQUARES) solution += result[s];
|
||||||
|
return solution;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Get candidates
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku.get_candidates = function (board: string): string[][] | false {
|
||||||
|
const report = sudoku.validate_board(board);
|
||||||
|
if (report !== true) throw report;
|
||||||
|
|
||||||
|
const map = sudoku._get_candidates_map(board);
|
||||||
|
if (!map) return false;
|
||||||
|
|
||||||
|
const rows: string[][] = [];
|
||||||
|
let cur_row: string[] = [];
|
||||||
|
let i = 0;
|
||||||
|
for (const square in map) {
|
||||||
|
cur_row.push(map[square]);
|
||||||
|
if (i % 9 === 8) {
|
||||||
|
rows.push(cur_row);
|
||||||
|
cur_row = [];
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._get_candidates_map = function (board: string): Record<string, string> | false {
|
||||||
|
const report = sudoku.validate_board(board);
|
||||||
|
if (report !== true) throw report;
|
||||||
|
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const squares_values = sudoku._get_square_vals_map(board);
|
||||||
|
|
||||||
|
for (const s of SQUARES) map[s] = sudoku.DIGITS;
|
||||||
|
|
||||||
|
for (const s in squares_values) {
|
||||||
|
const val = squares_values[s];
|
||||||
|
if (sudoku._in(val, sudoku.DIGITS)) {
|
||||||
|
const new_candidates = sudoku._assign(map, s, val);
|
||||||
|
if (!new_candidates) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Search
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku._search = function (candidates: Record<string, string>, reverse: boolean): any {
|
||||||
|
if (!candidates) return false;
|
||||||
|
reverse = reverse || false;
|
||||||
|
|
||||||
|
let max_nr_candidates = 0;
|
||||||
|
let max_square = "";
|
||||||
|
for (const s of SQUARES) {
|
||||||
|
const n = candidates[s].length;
|
||||||
|
if (n > max_nr_candidates) {
|
||||||
|
max_nr_candidates = n;
|
||||||
|
max_square = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (max_nr_candidates === 1) return candidates;
|
||||||
|
|
||||||
|
let min_nr_candidates = 10;
|
||||||
|
let min_square = "";
|
||||||
|
for (const s of SQUARES) {
|
||||||
|
const n = candidates[s].length;
|
||||||
|
if (n < min_nr_candidates && n > 1) {
|
||||||
|
min_nr_candidates = n;
|
||||||
|
min_square = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const min_candidates = candidates[min_square];
|
||||||
|
if (!reverse) {
|
||||||
|
for (const v of min_candidates) {
|
||||||
|
const copy = JSON.parse(JSON.stringify(candidates));
|
||||||
|
const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse);
|
||||||
|
if (next) return next;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = min_candidates.length - 1; i >= 0; --i) {
|
||||||
|
const v = min_candidates[i];
|
||||||
|
const copy = JSON.parse(JSON.stringify(candidates));
|
||||||
|
const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse);
|
||||||
|
if (next) return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Assignment / elimination logic
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku._assign = function (candidates: Record<string, string>, square: string, val: string) {
|
||||||
|
const others = candidates[square].replace(val, "");
|
||||||
|
for (const v of others) {
|
||||||
|
const next = sudoku._eliminate(candidates, square, v);
|
||||||
|
if (!next) return false;
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._eliminate = function (candidates: Record<string, string>, square: string, val: string) {
|
||||||
|
if (!sudoku._in(val, candidates[square])) return candidates;
|
||||||
|
|
||||||
|
candidates[square] = candidates[square].replace(val, "");
|
||||||
|
const n = candidates[square].length;
|
||||||
|
|
||||||
|
if (n === 1) {
|
||||||
|
const target_val = candidates[square];
|
||||||
|
for (const peer of SQUARE_PEERS_MAP[square]) {
|
||||||
|
const next = sudoku._eliminate(candidates, peer, target_val);
|
||||||
|
if (!next) return false;
|
||||||
|
}
|
||||||
|
} else if (n === 0) return false;
|
||||||
|
|
||||||
|
for (const unit of SQUARE_UNITS_MAP[square]) {
|
||||||
|
const places: string[] = [];
|
||||||
|
for (const sq of unit) {
|
||||||
|
if (sudoku._in(val, candidates[sq])) places.push(sq);
|
||||||
|
}
|
||||||
|
if (places.length === 0) return false;
|
||||||
|
else if (places.length === 1) {
|
||||||
|
const next = sudoku._assign(candidates, places[0], val);
|
||||||
|
if (!next) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data structures
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku._get_square_vals_map = function (board: string) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
if (board.length !== SQUARES.length) throw "Board length mismatch";
|
||||||
|
for (let i = 0; i < SQUARES.length; i++) map[SQUARES[i]] = board[i];
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._get_square_units_map = function (squares: string[], units: string[][]) {
|
||||||
|
const map: Record<string, string[][]> = {};
|
||||||
|
for (const s of squares) {
|
||||||
|
const list: string[][] = [];
|
||||||
|
for (const u of units) if (u.includes(s)) list.push(u);
|
||||||
|
map[s] = list;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._get_square_peers_map = function (
|
||||||
|
squares: string[],
|
||||||
|
units_map: Record<string, string[][]>
|
||||||
|
) {
|
||||||
|
const map: Record<string, string[]> = {};
|
||||||
|
for (const s of squares) {
|
||||||
|
const peers: string[] = [];
|
||||||
|
for (const unit of units_map[s]) {
|
||||||
|
for (const sq of unit) {
|
||||||
|
if (sq !== s && !peers.includes(sq)) peers.push(sq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map[s] = peers;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._get_all_units = function (rows: string, cols: string) {
|
||||||
|
const units: string[][] = [];
|
||||||
|
for (const r of rows) units.push(sudoku._cross(r, cols));
|
||||||
|
for (const c of cols) units.push(sudoku._cross(rows, c));
|
||||||
|
const row_blocks = ["ABC", "DEF", "GHI"];
|
||||||
|
const col_blocks = ["123", "456", "789"];
|
||||||
|
for (const rb of row_blocks)
|
||||||
|
for (const cb of col_blocks)
|
||||||
|
units.push(sudoku._cross(rb, cb));
|
||||||
|
return units;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Utils
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sudoku.validate_board = function (board: string): true | string {
|
||||||
|
if (!board) return "Empty board";
|
||||||
|
if (board.length !== NR_SQUARES) return "Invalid board size";
|
||||||
|
for (let i = 0; i < board.length; i++) {
|
||||||
|
const c = board[i];
|
||||||
|
if (!sudoku._in(c, sudoku.DIGITS) && c !== sudoku.BLANK_CHAR)
|
||||||
|
return `Invalid char at ${i}: ${c}`;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._cross = function (a: string, b: string) {
|
||||||
|
const res: string[] = [];
|
||||||
|
for (const ai of a) for (const bi of b) res.push(ai + bi);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._in = function (v: string, seq: string) {
|
||||||
|
return seq.indexOf(v) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._shuffle = function <T>(seq: T[]): T[] {
|
||||||
|
const arr = [...seq];
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._rand_range = function (max: number, min = 0) {
|
||||||
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._strip_dups = function <T>(seq: T[]): T[] {
|
||||||
|
return Array.from(new Set(seq));
|
||||||
|
};
|
||||||
|
|
||||||
|
sudoku._force_range = function (nr: number, max: number, min = 0) {
|
||||||
|
return Math.min(Math.max(nr || 0, min), max);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
export { sudoku };
|
||||||
|
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
/* =========================================================
|
||||||
|
🔧 Controls (tweak these)
|
||||||
|
========================================================= */
|
||||||
|
:root {
|
||||||
|
--cell-size: 32px;
|
||||||
|
--cell-font-size: 16px;
|
||||||
|
--grid-gap: 2px;
|
||||||
|
--box-separator: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Container & Grid Base
|
||||||
|
========================================================= */
|
||||||
|
#sudoku_container {
|
||||||
|
position: relative;
|
||||||
|
margin: 1em auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sudoku {
|
||||||
|
position: relative;
|
||||||
|
background-color: #1e2030; /* Tokyo Night card background */
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px rgba(130, 170, 255, 0.05);
|
||||||
|
padding: 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Grid Layout
|
||||||
|
========================================================= */
|
||||||
|
#sudoku .sudoku-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(9, var(--cell-size));
|
||||||
|
gap: var(--grid-gap);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Cell Base Styles
|
||||||
|
========================================================= */
|
||||||
|
#sudoku .sudoku-cell {
|
||||||
|
width: var(--cell-size);
|
||||||
|
height: var(--cell-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
font-size: var(--cell-font-size);
|
||||||
|
background: #2f334d; /* Tokyo Night muted */
|
||||||
|
color: #c8d3f5; /* Tokyo Night foreground */
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
box-shadow: inset 0 0 3px rgba(27, 29, 43, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
3x3 Box Separators
|
||||||
|
========================================================= */
|
||||||
|
/* Add thicker border to right of columns 2 and 5 (0-indexed) */
|
||||||
|
#sudoku .sudoku-cell[data-c="2"],
|
||||||
|
#sudoku .sudoku-cell[data-c="5"] {
|
||||||
|
border-right: var(--box-separator) solid #1e2030;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add thicker border to bottom of rows 2 and 5 (0-indexed) */
|
||||||
|
#sudoku .sudoku-cell[data-r="2"],
|
||||||
|
#sudoku .sudoku-cell[data-r="5"] {
|
||||||
|
border-bottom: var(--box-separator) solid #1e2030;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
User-Entered Cells
|
||||||
|
========================================================= */
|
||||||
|
#sudoku .sudoku-cell.user-entered {
|
||||||
|
color: #82aaff; /* Tokyo Night primary blue */
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Active Cell Highlight
|
||||||
|
========================================================= */
|
||||||
|
#sudoku .sudoku-cell.active {
|
||||||
|
outline: 2px solid #ffc777; /* Tokyo Night yellow/warning */
|
||||||
|
outline-offset: -2px;
|
||||||
|
background: #3d3836;
|
||||||
|
box-shadow:
|
||||||
|
0 0 16px rgba(255, 199, 119, 0.7),
|
||||||
|
0 0 24px rgba(255, 199, 119, 0.4),
|
||||||
|
inset 0 0 12px rgba(255, 199, 119, 0.3);
|
||||||
|
animation: active-cell-glow 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes active-cell-glow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 16px rgba(255, 199, 119, 0.7),
|
||||||
|
0 0 24px rgba(255, 199, 119, 0.4),
|
||||||
|
inset 0 0 12px rgba(255, 199, 119, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 20px rgba(255, 199, 119, 0.9),
|
||||||
|
0 0 32px rgba(255, 199, 119, 0.6),
|
||||||
|
inset 0 0 16px rgba(255, 199, 119, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Visual Feedback Effects
|
||||||
|
========================================================= */
|
||||||
|
/* Invalid move effect */
|
||||||
|
#sudoku .sudoku-cell.invalid-move {
|
||||||
|
animation: sudoku-invalid-pulse 180ms ease-out;
|
||||||
|
box-shadow:
|
||||||
|
0 0 12px rgba(255, 117, 127, 0.85),
|
||||||
|
0 0 24px rgba(255, 117, 127, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sudoku-invalid-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow:
|
||||||
|
0 0 16px rgba(255, 117, 127, 0.95),
|
||||||
|
0 0 28px rgba(255, 117, 127, 0.55);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trail effect for moves */
|
||||||
|
#sudoku .sudoku-cell.trail {
|
||||||
|
box-shadow: inset 0 0 6px rgba(130, 170, 255, 0.6), 0 0 6px rgba(130, 170, 255, 0.35);
|
||||||
|
animation: sudoku-trail-fade 220ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sudoku-trail-fade {
|
||||||
|
from { background-color: rgba(130, 170, 255, 0.25); }
|
||||||
|
to { background-color: transparent; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useGame } from "./useGame";
|
||||||
|
export type UseGameType = ReturnType<typeof useGame>;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Difficulty,
|
||||||
|
DifficultyName,
|
||||||
|
BoardString,
|
||||||
|
} from "./lib/Sudoku";
|
||||||
|
|
||||||
|
export type GamePhase = "idle" | "playing";
|
||||||
|
export type PlayStatus = "started" | "game-over" | "game-won";
|
||||||
|
export type GameStatus = "waiting" | "started" | "game-over" | "game-won";
|
||||||
|
|
||||||
|
export const isPlaying = (status: GameStatus): status is PlayStatus =>
|
||||||
|
status === "started" || status === "game-over" || status === "game-won";
|
||||||
|
|
||||||
|
export const getGamePhase = (status: GameStatus): GamePhase =>
|
||||||
|
status === "waiting" ? "idle" : "playing";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useGame';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { sudoku } from "../lib/Sudoku";
|
||||||
|
import type { UseCursorType } from "./useCursor";
|
||||||
|
|
||||||
|
export function useBoardState(cursorManager: UseCursorType) {
|
||||||
|
const { cursor, setCursor } = cursorManager;
|
||||||
|
const [board, setBoard] = useState<string>(sudoku.generate("medium"));
|
||||||
|
const [initialBoard, setInitialBoard] = useState<string>(board);
|
||||||
|
const [solution, setSolution] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const setDigit = useCallback(
|
||||||
|
(d: string) => {
|
||||||
|
const i = cursor.r * 9 + cursor.c;
|
||||||
|
const arr = board.split("");
|
||||||
|
arr[i] = d;
|
||||||
|
setBoard(arr.join(""));
|
||||||
|
},
|
||||||
|
[cursor, board]
|
||||||
|
);
|
||||||
|
|
||||||
|
const erase = useCallback(() => {
|
||||||
|
const i = cursor.r * 9 + cursor.c;
|
||||||
|
const arr = board.split("");
|
||||||
|
arr[i] = ".";
|
||||||
|
setBoard(arr.join(""));
|
||||||
|
}, [cursor, board]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
const newBoard = sudoku.generate("medium");
|
||||||
|
setBoard(newBoard);
|
||||||
|
setInitialBoard(newBoard);
|
||||||
|
setSolution(null);
|
||||||
|
setCursor({ r: 0, c: 0 });
|
||||||
|
}, [setCursor]);
|
||||||
|
|
||||||
|
const solveBoard = useCallback(() => {
|
||||||
|
const solved = sudoku.solve(board);
|
||||||
|
if (solved) {
|
||||||
|
setSolution(solved);
|
||||||
|
setBoard(solved);
|
||||||
|
}
|
||||||
|
}, [board]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
board,
|
||||||
|
initialBoard,
|
||||||
|
solution,
|
||||||
|
setDigit,
|
||||||
|
erase,
|
||||||
|
reset,
|
||||||
|
solveBoard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseBoardStateType = ReturnType<typeof useBoardState>;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useRef, useState, useCallback } from "react";
|
||||||
|
import type { UseRenderer } from "./useSudokuRenderer";
|
||||||
|
|
||||||
|
export function useCursor(rendererManager: UseRenderer) {
|
||||||
|
const { containerRef } = rendererManager;
|
||||||
|
const [cursor, setCursor] = useState<{ r: number; c: number }>({ r: 0, c: 0 });
|
||||||
|
|
||||||
|
const animatingRef = useRef(false);
|
||||||
|
|
||||||
|
const getCellEl = useCallback((r: number, c: number): HTMLElement | null => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return null;
|
||||||
|
const sudokuDiv = container.querySelector("#sudoku");
|
||||||
|
if (!sudokuDiv) return null;
|
||||||
|
const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`;
|
||||||
|
return sudokuDiv.querySelector(selector) as HTMLElement | null;
|
||||||
|
}, [containerRef]);
|
||||||
|
|
||||||
|
const flashInvalid = useCallback(() => {
|
||||||
|
const el = getCellEl(cursor.r, cursor.c);
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.add("invalid-move");
|
||||||
|
setTimeout(() => el.classList.remove("invalid-move"), 180);
|
||||||
|
}, [cursor, getCellEl]);
|
||||||
|
|
||||||
|
const moveCursor = useCallback(
|
||||||
|
(dr: number, dc: number, steps = 1) => {
|
||||||
|
if (animatingRef.current) return;
|
||||||
|
|
||||||
|
setCursor((prev) => {
|
||||||
|
let r = prev.r;
|
||||||
|
let c = prev.c;
|
||||||
|
for (let i = 0; i < steps; i++) {
|
||||||
|
r = Math.max(0, Math.min(8, r + dr));
|
||||||
|
c = Math.max(0, Math.min(8, c + dc));
|
||||||
|
}
|
||||||
|
return { r, c };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursor,
|
||||||
|
setCursor,
|
||||||
|
moveCursor,
|
||||||
|
getCellEl,
|
||||||
|
flashInvalid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseCursorType = ReturnType<typeof useCursor>;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRenderer } from "./useSudokuRenderer";
|
||||||
|
import { useCursor } from "./useCursor";
|
||||||
|
import { useGameStatus } from "./useGameStatus";
|
||||||
|
import { useBoardState } from "./useBoardState";
|
||||||
|
import { useKeyBindings } from "./useKeyBindings";
|
||||||
|
|
||||||
|
export function useGame(platformHook?: unknown) {
|
||||||
|
const rendererManager = useRenderer();
|
||||||
|
const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager;
|
||||||
|
const cursorManager = useCursor(rendererManager);
|
||||||
|
const { cursor, moveCursor } = cursorManager;
|
||||||
|
const gameManager = useGameStatus(rendererManager, cursorManager);
|
||||||
|
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
||||||
|
const boardManager = useBoardState(cursorManager);
|
||||||
|
const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager;
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Rendering
|
||||||
|
// ---------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
renderEmptyBoard();
|
||||||
|
}, [renderEmptyBoard]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus === "waiting") {
|
||||||
|
renderEmptyBoard();
|
||||||
|
} else {
|
||||||
|
renderBoard(board, initialBoard);
|
||||||
|
highlightCursor(cursor.r, cursor.c);
|
||||||
|
}
|
||||||
|
}, [gameStatus, board, initialBoard, cursor, renderEmptyBoard, renderBoard, highlightCursor]);
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Keybindings
|
||||||
|
// ---------------------------
|
||||||
|
const keyBindings = useKeyBindings({
|
||||||
|
moveCursor,
|
||||||
|
setDigit,
|
||||||
|
erase,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Game Control
|
||||||
|
// ---------------------------
|
||||||
|
const initGame = () => {
|
||||||
|
reset();
|
||||||
|
startGame();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Full Game Manager
|
||||||
|
// ---------------------------
|
||||||
|
const fullGameManager = {
|
||||||
|
// core
|
||||||
|
containerRef,
|
||||||
|
gameManager,
|
||||||
|
gameStatus,
|
||||||
|
setGameStatus,
|
||||||
|
startGame,
|
||||||
|
stopGame,
|
||||||
|
initGame,
|
||||||
|
|
||||||
|
// cursor
|
||||||
|
cursorManager,
|
||||||
|
cursor,
|
||||||
|
moveCursor,
|
||||||
|
|
||||||
|
// board state
|
||||||
|
boardManager,
|
||||||
|
board,
|
||||||
|
solution,
|
||||||
|
setDigit,
|
||||||
|
erase,
|
||||||
|
reset,
|
||||||
|
solveBoard,
|
||||||
|
|
||||||
|
// keybindings (vim-maze compatible)
|
||||||
|
...keyBindings,
|
||||||
|
};
|
||||||
|
|
||||||
|
// allow platform-level injection
|
||||||
|
if (typeof platformHook === "function") {
|
||||||
|
platformHook(fullGameManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullGameManager;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useState, useMemo } from "react";
|
||||||
|
import type { UseRenderer } from "./useSudokuRenderer";
|
||||||
|
import type { UseCursorType } from "./useCursor";
|
||||||
|
import type { GameStatus } from "../types";
|
||||||
|
import { getGamePhase, isPlaying } from "../types";
|
||||||
|
|
||||||
|
export type { GameStatus } from "../types";
|
||||||
|
|
||||||
|
export function useGameStatus(
|
||||||
|
rendererManager: UseRenderer,
|
||||||
|
cursorManager: UseCursorType
|
||||||
|
) {
|
||||||
|
const { containerRef } = rendererManager;
|
||||||
|
const { setCursor } = cursorManager;
|
||||||
|
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||||
|
|
||||||
|
const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]);
|
||||||
|
const playStatus = useMemo(
|
||||||
|
() => (isPlaying(gameStatus) ? gameStatus : null),
|
||||||
|
[gameStatus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const startGame = useCallback(() => {
|
||||||
|
setGameStatus("started");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopGame = useCallback(() => {
|
||||||
|
setCursor({ r: 0, c: 0 });
|
||||||
|
setGameStatus("waiting");
|
||||||
|
const container = containerRef.current;
|
||||||
|
container?.querySelectorAll(".active").forEach((el) => el.classList.remove("active"));
|
||||||
|
}, [setCursor, containerRef]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameStatus,
|
||||||
|
gamePhase,
|
||||||
|
playStatus,
|
||||||
|
setGameStatus,
|
||||||
|
startGame,
|
||||||
|
stopGame,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseGameStatusType = ReturnType<typeof useGameStatus>;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export type KeyLogEntry = { key: string; timestamp: number };
|
||||||
|
|
||||||
|
type Ctx = {
|
||||||
|
moveCursor: (dr: number, dc: number, steps?: number) => void;
|
||||||
|
setDigit: (d: string) => void;
|
||||||
|
erase: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useKeyBindings(ctx: Ctx) {
|
||||||
|
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
|
||||||
|
const logRef = useRef<KeyLogEntry[]>([]);
|
||||||
|
const countRef = useRef<string>(""); // Vim-style numeric prefix
|
||||||
|
const lastMotionRef = useRef<{ dr: number; dc: number; steps: number } | null>(null);
|
||||||
|
|
||||||
|
// Record keypresses for score/time analytics
|
||||||
|
const recordKey = (key: string) => {
|
||||||
|
const entry = { key, timestamp: performance.now() };
|
||||||
|
logRef.current.push(entry);
|
||||||
|
setKeyLog([...logRef.current]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLog = () => {
|
||||||
|
logRef.current = [];
|
||||||
|
setKeyLog([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLog = () => logRef.current;
|
||||||
|
|
||||||
|
const resetCount = () => {
|
||||||
|
countRef.current = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const k = e.key;
|
||||||
|
|
||||||
|
// --- Digits 1–9 fill a cell ---
|
||||||
|
if (/^[1-9]$/.test(k)) {
|
||||||
|
recordKey(k);
|
||||||
|
ctx.setDigit(k);
|
||||||
|
resetCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Erase ---
|
||||||
|
if (k === "x" || k === "Backspace") {
|
||||||
|
recordKey(k);
|
||||||
|
ctx.erase();
|
||||||
|
resetCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Numeric prefixes ---
|
||||||
|
if (/^[0-9]$/.test(k)) {
|
||||||
|
recordKey(k);
|
||||||
|
countRef.current += k;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vim-style navigation ---
|
||||||
|
let dr = 0;
|
||||||
|
let dc = 0;
|
||||||
|
if (k === "h") dc = -1;
|
||||||
|
else if (k === "l") dc = 1;
|
||||||
|
else if (k === "j") dr = 1;
|
||||||
|
else if (k === "k") dr = -1;
|
||||||
|
else if (k === "0" || k === "^") dc = -999; // go to col 0
|
||||||
|
else if (k === "$") dc = 999; // go to col 8
|
||||||
|
else if (k === ".") {
|
||||||
|
recordKey(".");
|
||||||
|
const last = lastMotionRef.current;
|
||||||
|
if (last) ctx.moveCursor(last.dr, last.dc, last.steps);
|
||||||
|
return;
|
||||||
|
} else return;
|
||||||
|
|
||||||
|
recordKey(k);
|
||||||
|
|
||||||
|
const steps = Math.max(1, parseInt(countRef.current || "1", 10));
|
||||||
|
resetCount();
|
||||||
|
|
||||||
|
if (dc === -999) {
|
||||||
|
ctx.moveCursor(0, -1, 99); // move far left
|
||||||
|
lastMotionRef.current = { dr: 0, dc: -1, steps: 99 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dc === 999) {
|
||||||
|
ctx.moveCursor(0, 1, 99); // move far right
|
||||||
|
lastMotionRef.current = { dr: 0, dc: 1, steps: 99 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.moveCursor(dr, dc, steps);
|
||||||
|
lastMotionRef.current = { dr, dc, steps };
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [ctx]);
|
||||||
|
|
||||||
|
// ✅ same interface as vim-maze useKeyBindings
|
||||||
|
return {
|
||||||
|
keyLog,
|
||||||
|
getLog,
|
||||||
|
clearLog,
|
||||||
|
resetCount,
|
||||||
|
countRef,
|
||||||
|
lastMotionRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseKeyBindingsType = ReturnType<typeof useKeyBindings>;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useRef, useCallback } from "react";
|
||||||
|
|
||||||
|
export function useRenderer() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const renderEmptyBoard = useCallback(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
const sudokuDiv = document.createElement("div");
|
||||||
|
sudokuDiv.id = "sudoku";
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "sudoku-grid";
|
||||||
|
|
||||||
|
for (let i = 0; i < 81; i++) {
|
||||||
|
const r = Math.floor(i / 9);
|
||||||
|
const c = i % 9;
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "sudoku-cell";
|
||||||
|
cell.dataset.r = String(r);
|
||||||
|
cell.dataset.c = String(c);
|
||||||
|
cell.textContent = "";
|
||||||
|
grid.appendChild(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
sudokuDiv.appendChild(grid);
|
||||||
|
container.appendChild(sudokuDiv);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderBoard = useCallback((board: string, initialBoard?: string) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
const sudokuDiv = document.createElement("div");
|
||||||
|
sudokuDiv.id = "sudoku";
|
||||||
|
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "sudoku-grid";
|
||||||
|
|
||||||
|
for (let i = 0; i < 81; i++) {
|
||||||
|
const r = Math.floor(i / 9);
|
||||||
|
const c = i % 9;
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "sudoku-cell";
|
||||||
|
cell.dataset.r = String(r);
|
||||||
|
cell.dataset.c = String(c);
|
||||||
|
cell.textContent = board[i] === "." ? "" : board[i];
|
||||||
|
|
||||||
|
if (initialBoard && board[i] !== "." && board[i] !== initialBoard[i]) {
|
||||||
|
cell.classList.add("user-entered");
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.appendChild(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
sudokuDiv.appendChild(grid);
|
||||||
|
container.appendChild(sudokuDiv);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const highlightCursor = useCallback((r: number, c: number) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const sudokuDiv = container.querySelector("#sudoku");
|
||||||
|
if (!sudokuDiv) return;
|
||||||
|
|
||||||
|
sudokuDiv.querySelectorAll(".active").forEach((el) => el.classList.remove("active"));
|
||||||
|
|
||||||
|
const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`;
|
||||||
|
const cell = sudokuDiv.querySelector(selector);
|
||||||
|
if (cell) {
|
||||||
|
cell.classList.add("active");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { containerRef, renderEmptyBoard, renderBoard, highlightCursor };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseRenderer = ReturnType<typeof useRenderer>;
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useScore';
|
||||||
@@ -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