commit c0190b59ade6665910f79daf3b2194f2acb72f91 Author: Feror Date: Tue Feb 3 13:48:56 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.well-known/README.md b/.well-known/README.md new file mode 100644 index 0000000..b793226 --- /dev/null +++ b/.well-known/README.md @@ -0,0 +1,27 @@ +# Deep Link Association Placeholders + +Fill the placeholders in the `.well-known` JSON files before deploying. + +## iOS (Universal Links) + +File: `.well-known/apple-app-site-association` + +- `YOUR_IOS_TEAM_ID` + - Where: Apple Developer account → Membership details (Team ID). +- `YOUR_IOS_BUNDLE_ID` + - Where: Expo config `mobile/app.json` → `expo.ios.bundleIdentifier`. + +Format must be: `TEAMID.BUNDLEID` (example: `ABCD1234EF.com.example.app`). + +## Android (App Links) + +File: `.well-known/assetlinks.json` + +- `YOUR_ANDROID_PACKAGE_NAME` + - Where: Expo config `mobile/app.json` → `expo.android.package`. +- `YOUR_SHA256_CERT_FINGERPRINT` + - Where: + - Debug: `keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey` + (password is usually `android`). + - Release/Play App Signing: Play Console → App Integrity → App signing key certificate. + diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association new file mode 100644 index 0000000..6191be7 --- /dev/null +++ b/.well-known/apple-app-site-association @@ -0,0 +1,13 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "VD9WQ6BYX2.fr.negopoly.app", + "paths": [ + "/play/*" + ] + } + ] + } +} diff --git a/.well-known/assetlinks.json b/.well-known/assetlinks.json new file mode 100644 index 0000000..4e7fa60 --- /dev/null +++ b/.well-known/assetlinks.json @@ -0,0 +1,10 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "fr.negopoly.app", + "sha256_cert_fingerprints": ["YOUR_SHA256_CERT_FINGERPRINT"] + } + } +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ce6cddf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# Repository Guidelines + +This is a Companion App for an IRL Board Game. The board game is called Negopoly, a Monopoly parody that runs in a city called NegoCity. The app is built using Bun runtime and TypeScript. The App serves as a fake banking system for players to manage their in-game finances. + +When a user reaches the app, they are greeted with a home page about the game. They can then go to /play, where they can create or join a game session. Each game session has a unique code that players can use to join. When a player creates a game session, they become the banker (sort of the session admin) and can start the game once all players have joined. When the banker is on the lobby screen, it also displays a QR code that other players can scan to join the game session easily (/play/:sessionID). + +Using the app, players can see their current balance, transaction history, and transfer money to other players. They can also chat with other players in the session, and create chat groups for private conversations with multiple players. + +The Banker has complete control over the game session. They can force transfers between players, add or remove money from players, see all chats, and end the game. They can also block all functionality for a set time (there are special cards in the game that cause a complete blackout). + +The Banker can also create "dummy" players for IRL participants who don't have access to the app, allowing them to manage their finances through the Banker. If a user disconnects, their profile becomes a dummy player that the Banker can control. If a user connects mid-game, they can take over a dummy player (the Banker needs to approve). + +The frontend is built with react and TSX files, while the backend uses Bun's built-in server capabilities. The play part of the frontend needs to look like a banking app, with a clean and simple UI, and needs to be mobile-first (for the players, the Banker will probably use a computer most of the time). The home page can be more playful and colorful to match the game's theme. The app will store game sessions in memory, so no database is required. The app should be optimized for performance and low latency, as multiple players will be interacting with it simultaneously. It will need to handle updates in real-time, so consider using WebSockets (using Bun's built in WebSocket server) or similar technologies for communication between the frontend and backend. + +## Project Structure & Module Organization + +- `index.ts` is the current entry point (Bun runtime). +- `front/` exists but is currently empty; place frontend assets or HTML/TSX here if you add a UI. +- `README.md` documents basic Bun usage. +- `node_modules/` is generated by `bun install` and should not be edited directly. + +## Build, Test, and Development Commands + +- `bun install` — install dependencies. +- `bun run index.ts` — run the app (from `README.md`). +- `bun --hot ./index.ts` — optional hot-reload workflow if you add server routes or frontend assets. +- `bun test` — run tests when they exist (see testing section). + +## Coding Style & Naming Conventions + +- Use TypeScript in ESM mode (`"type": "module"`). +- Prefer Bun APIs from `CLAUDE.md` (e.g., `Bun.serve`, `Bun.file`, `bun:sqlite`) instead of Node/Express equivalents. +- Indentation: 2 spaces in JSON; use your editor defaults for TS but keep it consistent. +- Name files by role (e.g., `index.ts`, `frontend.tsx`, `*.test.ts`). + +## Testing Guidelines + +- Testing framework: `bun:test` (see `CLAUDE.md` example). +- Test file naming: `*.test.ts` alongside the code or under a future `tests/` directory. +- Run tests with `bun test`. + +## Commit & Pull Request Guidelines + +- This directory is not a Git repository, so commit conventions are not defined yet. +- If you initialize Git, prefer concise, present-tense messages (e.g., `Add api route for users`). +- PRs (if used): include a short description, steps to verify, and screenshots for UI changes. + +## Configuration & Security Notes + +- Bun auto-loads `.env`; do not add dotenv loaders unless required. +- Keep secrets out of the repo; use `.env.local` or CI secrets where available. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..764c1dd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d2e56b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# créditmabligopapp + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +Hot reload during development: + +```bash +bun --hot ./index.ts +``` + +Run tests: + +```bash +bun test +``` + +This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..51dc0ff --- /dev/null +++ b/bun.lock @@ -0,0 +1,107 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "créditmabligopapp", + "dependencies": { + "qrcode": "^1.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.5", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/qrcode": "^1.5.5", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + + "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + } +} diff --git a/front/home.css b/front/home.css new file mode 100644 index 0000000..ac87691 --- /dev/null +++ b/front/home.css @@ -0,0 +1,281 @@ +@import url("https://fonts.googleapis.com/css2?family=Recursive:wght@300;400;600;700&family=Unbounded:wght@400;600;800&display=swap"); + +:root { + color-scheme: light; + --cream: #f7f2e8; + --ink: #1b1b1f; + --tangerine: #ff8a3d; + --teal: #1d9aa3; + --berry: #f25287; + --sun: #ffd166; + --sky: #c7f0ff; + --shadow: rgba(12, 24, 38, 0.18); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: radial-gradient(circle at top left, #fff5d6 0%, #f7f2e8 45%, #e8f6f8 100%); + color: var(--ink); + font-family: "Recursive", "Segoe UI", sans-serif; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'%3E%3Crect width='140' height='140' fill='none'/%3E%3Ccircle cx='4' cy='4' r='1.2' fill='%23000000' opacity='0.08'/%3E%3Ccircle cx='74' cy='60' r='1.3' fill='%23000000' opacity='0.08'/%3E%3Ccircle cx='120' cy='96' r='1' fill='%23000000' opacity='0.06'/%3E%3C/svg%3E"); + opacity: 0.25; + mix-blend-mode: multiply; + pointer-events: none; + z-index: 0; +} + +.page-home #root { + position: relative; + z-index: 1; +} + +.home { + display: flex; + flex-direction: column; + gap: 3rem; + padding: 3.5rem 1.5rem 4rem; + max-width: 1100px; + margin: 0 auto; +} + +.hero { + display: grid; + gap: 1.8rem; + padding: 2.5rem; + border-radius: 28px; + background: linear-gradient(130deg, #fff4dd 10%, #fff 45%, #e0fbff 100%); + box-shadow: 0 20px 50px var(--shadow); + position: relative; + overflow: hidden; +} + +.hero::after { + content: ""; + position: absolute; + right: -30px; + top: -40px; + width: 240px; + height: 240px; + background: conic-gradient(from 120deg, var(--tangerine), var(--berry), var(--sun)); + border-radius: 50%; + opacity: 0.2; +} + +.hero__badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.9rem; + border-radius: 999px; + font-weight: 600; + font-size: 0.85rem; + letter-spacing: 0.08em; + text-transform: uppercase; + background: #fff; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08); + width: fit-content; +} + +.hero h1 { + font-family: "Unbounded", "Trebuchet MS", sans-serif; + font-size: clamp(2.4rem, 3vw + 1rem, 4rem); + margin: 0; + letter-spacing: -0.02em; +} + +.hero p { + margin: 0; + font-size: 1.1rem; + max-width: 36rem; + line-height: 1.6; +} + +.hero__actions { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.9rem 1.7rem; + font-weight: 600; + font-size: 0.95rem; + text-decoration: none; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.btn.primary { + background: var(--tangerine); + color: #fff; + box-shadow: 0 16px 30px rgba(255, 138, 61, 0.35); +} + +.btn.ghost { + background: rgba(0, 0, 0, 0.08); + color: var(--ink); +} + +.btn:hover { + transform: translateY(-2px); +} + +.grid { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +.card { + background: #fff; + border-radius: 20px; + padding: 1.6rem; + box-shadow: 0 16px 40px rgba(14, 30, 40, 0.08); + display: grid; + gap: 0.6rem; +} + +.card h3 { + font-family: "Unbounded", sans-serif; + margin: 0; + font-size: 1.1rem; +} + +.card p { + margin: 0; + color: rgba(20, 20, 20, 0.8); + line-height: 1.5; +} + +.section-title { + font-family: "Unbounded", sans-serif; + font-size: 1.5rem; + margin: 0; +} + +.map { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + align-items: center; +} + +.map__panel { + background: #111827; + color: #fff; + border-radius: 24px; + padding: 2rem; + position: relative; + overflow: hidden; + min-height: 220px; +} + +.map__panel::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(120deg, rgba(29, 154, 163, 0.35), rgba(255, 138, 61, 0.35)); + opacity: 0.7; +} + +.map__panel-content { + position: relative; + display: grid; + gap: 0.8rem; +} + +.map__panel h2 { + margin: 0; + font-family: "Unbounded", sans-serif; +} + +.map__panel span { + font-size: 0.95rem; + opacity: 0.8; +} + +.map__legend { + display: grid; + gap: 0.8rem; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.7rem; + font-weight: 600; +} + +.legend-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--teal); + box-shadow: 0 0 0 4px rgba(29, 154, 163, 0.2); +} + +.legend-dot.orange { + background: var(--tangerine); + box-shadow: 0 0 0 4px rgba(255, 138, 61, 0.2); +} + +.legend-dot.sun { + background: var(--sun); + box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.25); +} + +.footer { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + align-items: center; + justify-content: space-between; + padding: 2rem; + border-radius: 24px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 10px 25px rgba(12, 24, 38, 0.1); +} + +.footer strong { + font-family: "Unbounded", sans-serif; +} + +@media (min-width: 960px) { + .home { + padding: 5rem 2.5rem 6rem; + } + + .hero { + grid-template-columns: 1.2fr 0.8fr; + align-items: center; + } +} + +.reveal { + opacity: 0; + transform: translateY(24px); + animation: rise 0.8s ease forwards; + animation-delay: var(--delay, 0s); +} + +@keyframes rise { + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/front/home.tsx b/front/home.tsx new file mode 100644 index 0000000..3be8fd6 --- /dev/null +++ b/front/home.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import "./home.css"; + +const features = [ + { + title: "Instant Sessions", + text: "Spin up a banker-led lobby, invite players with a code or QR, and launch the game in seconds.", + }, + { + title: "Real-Time Banking", + text: "Track balances, history, and transfers with live updates designed for fast tabletop play.", + }, + { + title: "Control the Chaos", + text: "Blackouts, forced transfers, and dummy players give the Banker full city authority.", + }, +]; + +const steps = [ + { + label: "Banker opens the vault", + detail: "Create a session, name the banker, and get the invite code instantly.", + }, + { + label: "Citizens join NegoCity", + detail: "Players join from their phones and the lobby updates in real time.", + }, + { + label: "Let the deals fly", + detail: "Transfers, chats, and group deals all flow through the app.", + }, +]; + +function Home() { + return ( +
+
+
Negopoly companion bank
+
+

Welcome to NegoCity.

+

+ Negopoly is a Monopoly parody where every deal goes through the Bank of NegoCity. This + companion app keeps the money moving, the chats flowing, and the Banker in total + control. +

+ +
+
+ +
+ {features.map((feature, index) => ( +
+

{feature.title}

+

{feature.text}

+
+ ))} +
+ +
+
+
+ Route map +

NegoCity money flow

+

+ Banker-driven sessions, private group chats, and live balance sync. Everything you + need for fast-paced tabletop finance. +

+
+
+
+

+ The game loop +

+ {steps.map((step, index) => ( +
+ +
+
{step.label}
+ {step.detail} +
+
+ ))} +
+
+ +
+
+ Ready for NegoCity? +

Make every deal official. Let the Banker run the show.

+
+ + Start a session + +
+
+ ); +} + +const root = createRoot(document.getElementById("root")!); +root.render(); diff --git a/front/index.html b/front/index.html new file mode 100644 index 0000000..800ada6 --- /dev/null +++ b/front/index.html @@ -0,0 +1,16 @@ + + + + + + Negopoly | NegoCity Bank + + + +
+ + + diff --git a/front/play.css b/front/play.css new file mode 100644 index 0000000..23487f9 --- /dev/null +++ b/front/play.css @@ -0,0 +1,1086 @@ +@import url("https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600;700&family=Sora:wght@300;400;600;700&display=swap"); + +:root { + color-scheme: light; + --ink: #0c1824; + --ink-soft: rgba(12, 24, 36, 0.72); + --surface: #f7f5f0; + --surface-strong: #fff; + --accent: #2bb79a; + --accent-dark: #1b8b75; + --gold: #f4b648; + --danger: #e84d4d; + --shadow: rgba(12, 24, 36, 0.15); + --border: rgba(12, 24, 36, 0.08); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: "Sora", "Segoe UI", sans-serif; + color: var(--ink); + background: radial-gradient(circle at top, #f8f7f2 0%, #eef4f3 50%, #e6edf1 100%); +} + +body::before { + content: ""; + position: fixed; + inset: 0; + background-image: linear-gradient(120deg, rgba(43, 183, 154, 0.08), transparent), + repeating-linear-gradient( + 90deg, + rgba(12, 24, 36, 0.05) 0, + rgba(12, 24, 36, 0.05) 1px, + transparent 1px, + transparent 80px + ); + pointer-events: none; + z-index: 0; +} + +.page-play #root { + position: relative; + z-index: 1; +} + +.play-shell { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1.2rem 4rem; + display: grid; + gap: 1.5rem; +} + +.entry-layout { + display: grid; + gap: 1.5rem; +} + +.entry-panel { + order: 2; +} + +.entry-panel.join { + order: 1; +} + +.play-header { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + justify-content: space-between; + padding: 1.2rem 1.5rem; + border-radius: 20px; + background: var(--surface-strong); + box-shadow: 0 15px 35px var(--shadow); +} + +.brand { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.brand h1 { + font-family: "Fraunces", "Georgia", serif; + font-size: 1.6rem; + margin: 0; +} + +.brand span { + color: var(--ink-soft); + font-size: 0.9rem; +} + +.pills { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.pill { + padding: 0.4rem 0.8rem; + border-radius: 999px; + background: rgba(43, 183, 154, 0.12); + color: var(--accent-dark); + font-weight: 600; + font-size: 0.8rem; +} + +.pill.gold { + background: rgba(244, 182, 72, 0.2); + color: #9a6212; +} + +.layout { + display: grid; + gap: 1.5rem; +} + +.tools-container { + display: grid; + gap: 1.5rem; +} + +.tabs-layout { + display: grid; + gap: 1.5rem; +} + +.tabs-nav { + display: flex; + gap: 0.6rem; + background: var(--surface-strong); + border-radius: 16px; + padding: 0.6rem; + border: 1px solid var(--border); + box-shadow: 0 10px 25px var(--shadow); +} + +.tabs-nav a { + flex: 1; + text-align: center; + padding: 0.55rem 0.8rem; + border-radius: 12px; + text-decoration: none; + font-weight: 600; + color: var(--ink-soft); + transition: background 0.2s ease, color 0.2s ease; + position: relative; +} + +.tabs-nav a.active { + background: rgba(43, 183, 154, 0.12); + color: var(--accent-dark); +} + +.tab-unread { + position: absolute; + top: 8px; + right: 12px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--danger); + box-shadow: 0 0 0 2px var(--surface-strong); +} + +.tabs-content { + min-width: 0; +} + +.tools-tabs { + display: flex; + gap: 0.8rem; + padding: 0.4rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid var(--border); + box-shadow: 0 8px 20px var(--shadow); +} + +.tools-tab { + flex: 1; + border: none; + border-radius: 999px; + padding: 0.55rem 1rem; + font-weight: 600; + color: var(--ink-soft); + background: transparent; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.tools-tab.active { + background: rgba(43, 183, 154, 0.16); + color: var(--accent-dark); +} + +.tools-layout { + display: grid; + gap: 1rem; +} + +.player-roster { + display: grid; + gap: 0.6rem; +} + +.player-roster-item { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.7rem 0.9rem; + border-radius: 14px; + border: 1px solid transparent; + background: rgba(12, 24, 36, 0.04); + text-align: left; + cursor: pointer; + transition: border 0.2s ease, background 0.2s ease; +} + +.player-roster-item.active { + border-color: rgba(43, 183, 154, 0.4); + background: rgba(43, 183, 154, 0.12); +} + +.player-roster-main { + display: grid; + gap: 0.2rem; +} + +.player-roster-main strong { + font-size: 0.95rem; +} + +.player-roster-main span { + font-size: 0.8rem; + color: var(--ink-soft); +} + +.player-roster-meta { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +.player-roster-balance { + font-weight: 600; + font-size: 0.9rem; +} + +.player-summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.8rem 1rem; + border-radius: 16px; + background: rgba(43, 183, 154, 0.08); + margin-bottom: 0.8rem; +} + +.player-summary strong { + display: block; + font-size: 1rem; +} + +.player-summary span { + font-size: 0.8rem; + color: var(--ink-soft); +} + +.player-balance { + font-size: 1.3rem; + font-weight: 700; +} + +.card { + background: var(--surface-strong); + border-radius: 20px; + padding: 1.4rem; + box-shadow: 0 14px 30px var(--shadow); + border: 1px solid var(--border); +} + +.card h2, +.card h3 { + font-family: "Fraunces", serif; + margin: 0 0 0.6rem 0; +} + +.card p { + margin: 0; + color: var(--ink-soft); + line-height: 1.5; +} + +.card-grid { + display: grid; + gap: 1rem; +} + +.balance { + display: grid; + gap: 0.4rem; + font-size: 2rem; + font-weight: 700; +} + +.balance span { + font-size: 0.95rem; + font-weight: 500; + color: var(--ink-soft); +} + +.form { + display: grid; + gap: 0.8rem; +} + +.form label { + font-size: 0.85rem; + font-weight: 600; + color: var(--ink-soft); +} + +.form input, +.form select, +.form textarea { + width: 100%; + padding: 0.7rem 0.9rem; + border-radius: 12px; + border: 1px solid var(--border); + font-family: inherit; + font-size: 0.95rem; + background: #fff; +} + +.form select[multiple] { + min-height: 110px; +} + +.form textarea { + min-height: 90px; + resize: vertical; +} + +.checkbox-row { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.9rem; + color: var(--ink); +} + +.checkbox-row input { + width: 16px; + height: 16px; + accent-color: var(--accent); +} + +.autosave-list { + display: grid; + gap: 0.5rem; +} + +.autosave-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + padding: 0.5rem 0.7rem; + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.7); +} + +.autosave-meta { + font-size: 0.85rem; + color: var(--ink-soft); +} + +.button { + border: none; + border-radius: 999px; + padding: 0.7rem 1.4rem; + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + background: var(--accent); + color: #fff; + box-shadow: 0 12px 20px rgba(43, 183, 154, 0.3); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.button.secondary { + background: rgba(12, 24, 36, 0.08); + color: var(--ink); + box-shadow: none; +} + +.button.danger { + background: var(--danger); +} + +.button.small { + padding: 0.4rem 0.9rem; + font-size: 0.8rem; +} + +.button:hover { + transform: translateY(-1px); +} + +.button:disabled { + cursor: not-allowed; + opacity: 0.55; + box-shadow: none; + transform: none; +} + +.list { + display: grid; + gap: 0.75rem; +} + +.list.scrollable { + max-height: 280px; + overflow-y: auto; + padding-right: 0.25rem; +} + +.list-item { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 0.7rem 0.8rem; + border-radius: 14px; + background: rgba(12, 24, 36, 0.04); +} + +.list-item strong { + font-size: 0.9rem; +} + +.list-item span { + font-size: 0.8rem; + color: var(--ink-soft); +} + +.amount { + font-weight: 600; + color: var(--accent-dark); +} + +.amount.negative { + color: var(--danger); +} + +.badge { + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + background: rgba(43, 183, 154, 0.12); + color: var(--accent-dark); +} + +.badge.dummy { + background: rgba(244, 182, 72, 0.15); + color: #9a6212; +} + +.badge.offline { + background: rgba(12, 24, 36, 0.1); + color: #5b6470; +} + +.status { + font-weight: 600; + color: var(--accent-dark); +} + +.status.ended { + color: var(--danger); +} + +.qr { + display: grid; + place-items: center; + padding: 1rem; + background: #fff; + border-radius: 16px; + border: 1px dashed var(--border); +} + +.qr img { + width: 180px; + height: 180px; +} + +.chat-shell { + max-width: 960px; + width: 100%; + margin: 0 auto; +} + +.chat-screen { + position: relative; + display: grid; + grid-template-rows: auto auto 1fr; + background: var(--surface-strong); + border-radius: 26px; + border: 1px solid var(--border); + box-shadow: 0 18px 38px var(--shadow); + overflow: hidden; + min-height: 70vh; +} + +.chat-screen--thread, +.chat-screen--new { + grid-template-rows: auto 1fr; +} + +.chat-screen-header { + display: flex; + align-items: center; + gap: 1.2rem; + padding: 1.2rem 1.6rem; + background: linear-gradient(135deg, #1d9d86, #2bb79a); + color: #fff; +} + +.chat-screen-title h1 { + margin: 0; + font-family: "Fraunces", serif; + font-size: 1.5rem; +} + +.chat-screen-title span { + font-size: 0.85rem; + opacity: 0.9; +} + +.chat-back { + text-decoration: none; + color: #fff; + font-weight: 600; + font-size: 0.9rem; +} + +.chat-search { + padding: 0.9rem 1.6rem; + background: #f4faf8; + border-bottom: 1px solid var(--border); +} + +.chat-search input { + width: 100%; + border-radius: 999px; + border: 1px solid var(--border); + padding: 0.6rem 1rem; + font-family: inherit; + background: #fff; +} + +.chat-list { + display: grid; +} + +.chat-list-item { + display: grid; + grid-template-columns: 52px 1fr; + gap: 0.9rem; + padding: 0.9rem 1.6rem; + text-decoration: none; + color: var(--ink); + border-bottom: 1px solid var(--border); + background: #fff; + transition: background 0.2s ease; +} + +.chat-list-item:hover { + background: rgba(43, 183, 154, 0.08); +} + +.chat-list-item.unread strong { + font-weight: 700; +} + +.chat-list-item.unread .chat-list-bottom p { + color: var(--ink); +} + +.chat-avatar { + width: 52px; + height: 52px; + border-radius: 18px; + display: grid; + place-items: center; + font-weight: 700; + color: #0b3c32; + background: rgba(43, 183, 154, 0.18); +} + +.chat-avatar--direct { + background: rgba(244, 182, 72, 0.2); + color: #9a6212; +} + +.chat-avatar--group { + background: rgba(12, 24, 36, 0.1); + color: var(--ink); +} + +.chat-list-body { + display: grid; + gap: 0.3rem; +} + +.chat-list-top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.chat-list-meta { + display: flex; + align-items: center; + gap: 0.45rem; +} + +.chat-unread-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--danger); + box-shadow: 0 0 0 2px #fff; +} + +.chat-list-top strong { + font-size: 1rem; +} + +.chat-list-top span { + font-size: 0.75rem; + color: var(--ink-soft); +} + +.chat-list-bottom { + display: flex; + align-items: center; + gap: 1rem; + justify-content: space-between; +} + +.chat-list-bottom p { + margin: 0; + color: var(--ink-soft); + font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-pill { + padding: 0.15rem 0.5rem; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.08em; + border-radius: 999px; + background: rgba(12, 24, 36, 0.08); + color: var(--ink-soft); +} + +.chat-pill--direct { + background: rgba(244, 182, 72, 0.25); + color: #9a6212; +} + +.chat-pill--global { + background: rgba(43, 183, 154, 0.18); + color: var(--accent-dark); +} + +.chat-pill--group { + background: rgba(12, 24, 36, 0.12); + color: var(--ink); +} + +.chat-fab { + position: absolute; + right: 1.6rem; + bottom: 1.6rem; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--accent); + color: #fff; + font-size: 2rem; + display: grid; + place-items: center; + text-decoration: none; + box-shadow: 0 16px 24px rgba(27, 139, 117, 0.3); +} + +.chat-thread-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.2rem; + padding: 1.1rem 1.6rem; + background: linear-gradient(135deg, #1d9d86, #2bb79a); + color: #fff; +} + +.chat-thread-leading { + display: flex; + gap: 1rem; + align-items: center; +} + +.chat-thread-header h1 { + margin: 0; + font-family: "Fraunces", serif; + font-size: 1.3rem; +} + +.chat-thread-header span { + font-size: 0.8rem; + opacity: 0.9; +} + +.chat-thread-tag { + padding: 0.2rem 0.7rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.5); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.chat-thread { + display: grid; + grid-template-rows: 1fr auto; + background: #f4faf8; + min-height: 60vh; +} + +.chat-thread-body { + padding: 1.4rem 1.6rem; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.7rem; + overflow-y: auto; + background: linear-gradient(180deg, rgba(43, 183, 154, 0.08), transparent 45%); +} + +.chat-empty { + text-align: center; + color: var(--ink-soft); + padding: 2.4rem 1rem; +} + +.chat-empty span { + font-size: 0.85rem; +} + +.chat-message-row { + display: flex; + align-items: flex-start; +} + +.chat-message-row.me { + justify-content: flex-end; +} + +.chat-bubble { + max-width: 74%; + padding: 0.6rem 0.85rem; + border-radius: 16px 16px 16px 6px; + background: #fff; + box-shadow: 0 8px 18px rgba(12, 24, 36, 0.12); + display: grid; + gap: 0.2rem; +} + +.chat-bubble.me { + background: rgba(43, 183, 154, 0.18); + border-radius: 16px 16px 6px 16px; +} + +.chat-sender { + font-size: 0.7rem; + color: var(--accent-dark); + font-weight: 600; +} + +.chat-bubble p { + margin: 0; + font-size: 0.95rem; +} + +.chat-time { + font-size: 0.65rem; + color: var(--ink-soft); + text-align: right; +} + +.chat-composer { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.6rem; + padding: 0.9rem 1.1rem 1.1rem; + background: #fff; + border-top: 1px solid var(--border); +} + +.chat-composer input { + border-radius: 999px; + border: 1px solid var(--border); + padding: 0.7rem 1rem; + font-family: inherit; +} + +.chat-send { + border: none; + background: var(--accent); + color: #fff; + padding: 0.6rem 1.2rem; + border-radius: 999px; + font-weight: 600; + cursor: pointer; +} + +.chat-new { + padding: 1.5rem 1.6rem 2rem; + display: grid; + gap: 1.4rem; + background: #f7fbfa; +} + +.chat-toggle { + display: grid; + grid-template-columns: 1fr 1fr; + border-radius: 999px; + overflow: hidden; + border: 1px solid var(--border); + background: #fff; +} + +.chat-toggle button { + border: none; + background: transparent; + padding: 0.6rem 0.8rem; + font-weight: 600; + cursor: pointer; +} + +.chat-toggle button.active { + background: var(--accent); + color: #fff; +} + +.chat-field { + display: grid; + gap: 0.4rem; + font-size: 0.85rem; + color: var(--ink-soft); +} + +.chat-field input { + border-radius: 14px; + border: 1px solid var(--border); + padding: 0.7rem 0.9rem; + font-family: inherit; + background: #fff; +} + +.chat-members h2 { + margin: 0 0 0.6rem; + font-size: 1rem; +} + +.chat-members-list { + display: grid; + gap: 0.6rem; +} + +.chat-member { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 0.6rem; + padding: 0.7rem 0.8rem; + border-radius: 14px; + border: 1px solid var(--border); + background: #fff; + cursor: pointer; +} + +.chat-member.selected { + border-color: var(--accent); + box-shadow: 0 8px 18px rgba(27, 139, 117, 0.2); +} + +.chat-member input { + accent-color: var(--accent); +} + +.chat-member-name { + font-weight: 600; +} + +.chat-member-meta { + font-size: 0.75rem; + color: var(--ink-soft); +} + +.chat-error { + color: var(--danger); + margin: 0; + font-size: 0.85rem; +} + +.blackout { + position: fixed; + inset: 0; + background: rgba(12, 24, 36, 0.88); + color: #fff; + display: grid; + place-items: center; + z-index: 10; + text-align: center; + padding: 2rem; +} + +.blackout h2 { + font-family: "Fraunces", serif; + font-size: 2rem; + margin-bottom: 0.6rem; +} + +.blackout span { + color: rgba(255, 255, 255, 0.8); +} + +.split { + display: grid; + gap: 1rem; +} + +.inline { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; +} + +.helper { + font-size: 0.8rem; + color: var(--ink-soft); +} + +@media (min-width: 980px) { + .layout { + grid-template-columns: 2.1fr 1fr; + } + + .tools-layout { + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + } + + .play-shell { + padding: 2.5rem 2rem 4rem; + } + + .entry-layout { + grid-template-columns: 1fr 1fr; + } + + .entry-panel.create { + order: 1; + } + + .entry-panel.join { + order: 2; + } + + .tabs-layout { + grid-template-columns: 220px 1fr; + align-items: start; + } + + .tabs-nav { + flex-direction: column; + position: sticky; + top: 1.5rem; + } + + .tabs-nav a { + text-align: left; + } +} + +@media (max-width: 979px) { + .tabs-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 6; + border-radius: 0; + justify-content: space-around; + padding-top: 0.6rem; + padding-bottom: calc(0.6rem + env(safe-area-inset-bottom)); + } + + .tabs-content { + padding-bottom: calc(5.5rem + env(safe-area-inset-bottom)); + } + + .chat-screen { + min-height: 70vh; + } + + input, + select, + textarea { + font-size: 16px; + } + + .chat-screen-header, + .chat-thread-header { + padding: 1rem 1.1rem; + } + + .chat-search { + padding: 0.8rem 1.1rem; + } + + .chat-list-item { + padding: 0.8rem 1.1rem; + } + + .chat-thread-body { + padding: 1.1rem; + } + + .chat-composer { + padding: 0.8rem 1rem 1rem; + } + + .chat-fab { + right: 1.1rem; + bottom: 1.1rem; + } + + .chat-new { + padding: 1.2rem 1.1rem 1.6rem; + } +} + +.reveal { + opacity: 0; + transform: translateY(18px); + animation: rise 0.7s ease forwards; + animation-delay: var(--delay, 0s); +} + +@keyframes rise { + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/front/play.html b/front/play.html new file mode 100644 index 0000000..20e132a --- /dev/null +++ b/front/play.html @@ -0,0 +1,16 @@ + + + + + + Negopoly | Play + + + +
+ + + diff --git a/front/play.tsx b/front/play.tsx new file mode 100644 index 0000000..cc7f195 --- /dev/null +++ b/front/play.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import "./play.css"; +import PlayApp from "./play/app"; + +const root = createRoot(document.getElementById("root")!); +root.render(); diff --git a/front/play/app.tsx b/front/play/app.tsx new file mode 100644 index 0000000..f8e8ef4 --- /dev/null +++ b/front/play/app.tsx @@ -0,0 +1,2265 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { + BrowserRouter, + Navigate, + NavLink, + Route, + Routes, + useNavigate, + useParams, +} from "react-router-dom"; +import ChatListScreen from "./chat/ChatListScreen"; +import ChatNewScreen from "./chat/ChatNewScreen"; +import ChatThreadScreen from "./chat/ChatThreadScreen"; +import { getLatestThreadTimestamp, getUnreadThreadIds } from "./chat/utils"; +import { + formatConnectionState, + formatStatus, + formatTransactionKind, + getLocale, + tStatic, + useI18n, +} from "./i18n"; +import QRCode from "qrcode"; +import type { SessionSnapshot, Transaction } from "../../shared/types"; + +const STORAGE_KEY = "negopoly:lastSession"; +const CHAT_READ_KEY = "negopoly:chatRead"; + +type StoredSession = { + sessionId: string; + playerId: string; +}; + +type JoinResponse = { + sessionId: string; + sessionCode: string; + playerId: string; + role: string; + status: string; +}; + +type SessionPreviewPlayer = { + id: string; + name: string; + role: string; + isDummy: boolean; + connected: boolean; +}; + +type SessionPreview = { + sessionId: string; + code: string; + status: string; + players: SessionPreviewPlayer[]; +}; + +function readStoredSession(): StoredSession | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as StoredSession; + } catch { + return null; + } +} + +function writeStoredSession(sessionId: string, playerId: string) { + localStorage.setItem(STORAGE_KEY, JSON.stringify({ sessionId, playerId })); +} + +function clearStoredSession() { + localStorage.removeItem(STORAGE_KEY); +} + +function getChatReadKey(sessionId: string, playerId: string) { + return `${CHAT_READ_KEY}:${sessionId}:${playerId}`; +} + +function readChatReadState(sessionId: string, playerId: string): Record { + try { + const raw = localStorage.getItem(getChatReadKey(sessionId, playerId)); + if (!raw) return {}; + return JSON.parse(raw) as Record; + } catch { + return {}; + } +} + +function writeChatReadState(sessionId: string, playerId: string, state: Record) { + try { + localStorage.setItem(getChatReadKey(sessionId, playerId), JSON.stringify(state)); + } catch { + // ignore write failures + } +} + +function formatMoney(amount: number) { + const value = new Intl.NumberFormat(getLocale(), { + maximumFractionDigits: 0, + }).format(amount); + return `₦${value}`; +} + +function formatTransactionTimestamp(value: number) { + const date = new Date(value); + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + const time = date.toLocaleTimeString(getLocale(), { hour: "2-digit", minute: "2-digit" }); + if (sameDay) return time; + const day = date.toLocaleDateString(getLocale(), { month: "short", day: "numeric" }); + return `${day} ${time}`; +} + +function getTransactionLabel( + transaction: Transaction, + t: ReturnType["t"], +) { + if (transaction.kind === "banker_adjust" || transaction.kind === "banker_force_transfer") { + const note = transaction.note?.trim(); + return note || t("common.noReason"); + } + return formatTransactionKind(transaction.kind, t); +} + +function getTransactionDisplay( + transaction: Transaction, + viewerId: string | null | undefined, + players: SessionSnapshot["players"], + t: ReturnType["t"], +) { + const absAmount = Math.abs(transaction.amount); + const label = getTransactionLabel(transaction, t); + const findPlayer = (id: string | null) => players.find((player) => player.id === id); + const from = findPlayer(transaction.fromId); + const to = findPlayer(transaction.toId); + let outgoing = false; + let counterparty = t("common.bank"); + const timeLabel = formatTransactionTimestamp(transaction.createdAt); + + if (transaction.kind === "banker_adjust") { + outgoing = transaction.amount < 0; + counterparty = t("common.bank"); + } else if (transaction.kind === "transfer" || transaction.kind === "banker_force_transfer") { + if (viewerId && transaction.fromId === viewerId) { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } else if (viewerId && transaction.toId === viewerId) { + outgoing = false; + counterparty = from?.name ?? t("common.player"); + } else { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } + } + + return { + label, + subtitle: viewerId + ? `${outgoing ? t("common.to") : t("common.from")} ${counterparty} · ${timeLabel}` + : transaction.kind === "banker_adjust" + ? outgoing + ? `${to?.name ?? t("common.player")} → ${t("common.bank")} · ${timeLabel}` + : `${t("common.bank")} → ${to?.name ?? t("common.player")} · ${timeLabel}` + : `${from?.name ?? t("common.player")} → ${to?.name ?? t("common.player")} · ${timeLabel}`, + amount: `${outgoing ? "-" : ""}${formatMoney(absAmount)}`, + tone: outgoing ? "negative" : "positive", + }; +} + +function formatTime(value: number) { + return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +function getWsUrl(sessionId: string, playerId: string) { + const base = window.location.origin.replace("http", "ws"); + const params = new URLSearchParams({ sessionId, playerId }); + return `${base}/ws?${params.toString()}`; +} + +function getBlackoutState(session: SessionSnapshot | null, tick: number) { + tick; + return { + active: Boolean(session?.blackoutActive), + }; +} + +function useSessionManager() { + const stored = readStoredSession(); + const [sessionId, setSessionId] = useState(stored?.sessionId ?? ""); + const [playerId, setPlayerId] = useState(stored?.playerId ?? ""); + const [session, setSession] = useState(null); + const [error, setError] = useState(null); + const [pendingTakeoverId, setPendingTakeoverId] = useState(null); + const [connectionState, setConnectionState] = useState< + "idle" | "connecting" | "open" | "error" + >("idle"); + const [tick, setTick] = useState(0); + const [chatReadState, setChatReadState] = useState>({}); + + const wsRef = useRef(null); + + useEffect(() => { + const timer = setInterval(() => setTick((value) => value + 1), 1000); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + if (!sessionId || !playerId) { + setChatReadState({}); + return; + } + setChatReadState(readChatReadState(sessionId, playerId)); + }, [sessionId, playerId]); + + useEffect(() => { + if (!sessionId || !playerId) { + setConnectionState("idle"); + setSession(null); + return; + } + + setConnectionState("connecting"); + const ws = new WebSocket(getWsUrl(sessionId, playerId)); + wsRef.current = ws; + + ws.onopen = () => { + setConnectionState("open"); + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + if (message.type === "state") { + setSession(message.session as SessionSnapshot); + } + if (message.type === "error") { + setError(message.message); + } + if (message.type === "takeover_approved") { + const assignedId = message.assignedPlayerId as string; + setPlayerId(assignedId); + writeStoredSession(sessionId, assignedId); + } + } catch { + setError(tStatic("error.parseResponse")); + } + }; + + ws.onerror = () => { + setConnectionState("error"); + }; + + ws.onclose = () => { + setConnectionState("error"); + }; + + const pingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping", sessionId, playerId })); + } + }, 15000); + + return () => { + clearInterval(pingTimer); + ws.close(); + }; + }, [sessionId, playerId]); + + useEffect(() => { + if (!pendingTakeoverId) return; + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + if (!sessionId || !playerId) return; + wsRef.current.send( + JSON.stringify({ + type: "takeover_request", + sessionId, + playerId, + dummyId: pendingTakeoverId, + }), + ); + setPendingTakeoverId(null); + }, [pendingTakeoverId, sessionId, playerId, connectionState]); + + const me = session?.players.find((player) => player.id === playerId) ?? null; + const isBanker = me?.role === "banker"; + + const players = useMemo(() => { + if (!session) return []; + return [...session.players].sort((a, b) => { + if (a.role === b.role) { + return a.name.localeCompare(b.name); + } + return a.role === "banker" ? -1 : 1; + }); + }, [session]); + + async function createSession(bankerName: string) { + setError(null); + setSession(null); + const response = await fetch("/api/session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bankerName }), + }); + if (!response.ok) { + setError(tStatic("error.createSession")); + return null; + } + const data = (await response.json()) as JoinResponse; + setSessionId(data.sessionId); + setPlayerId(data.playerId); + writeStoredSession(data.sessionId, data.playerId); + return data; + } + + async function joinSession(code: string, name: string) { + setError(null); + setSession(null); + if (!code) { + setError(tStatic("entry.alert.enterCode")); + return null; + } + const storedNow = readStoredSession(); + const reusePlayerId = storedNow?.sessionId === code ? storedNow.playerId : undefined; + const response = await fetch(`/api/session/${code}/join`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, playerId: reusePlayerId }), + }); + if (!response.ok) { + setError(tStatic("error.joinSession")); + return null; + } + const data = (await response.json()) as JoinResponse; + setSessionId(data.sessionId); + setPlayerId(data.playerId); + writeStoredSession(data.sessionId, data.playerId); + return data; + } + + function sendMessage(payload: Record) { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + setError(tStatic("error.connectionNotReady")); + return; + } + wsRef.current.send(JSON.stringify(payload)); + } + + function resetSession() { + clearStoredSession(); + setSessionId(""); + setPlayerId(""); + setSession(null); + setError(null); + setPendingTakeoverId(null); + setChatReadState({}); + } + + function markChatRead(threadId: string, timestamp: number) { + if (!sessionId || !playerId) return; + setChatReadState((prev) => { + const next = { + ...prev, + [threadId]: Math.max(prev[threadId] ?? 0, timestamp), + }; + writeChatReadState(sessionId, playerId, next); + return next; + }); + } + + return { + sessionId, + playerId, + session, + chatReadState, + players, + me, + isBanker, + connectionState, + error, + setError, + tick, + setSessionId, + setPlayerId, + setSession, + createSession, + joinSession, + sendMessage, + resetSession, + setPendingTakeoverId, + markChatRead, + }; +} + +function useRouteSessionSync(manager: ReturnType) { + const params = useParams(); + const { sessionId, setSessionId, setSession, setPlayerId } = manager; + useEffect(() => { + const routeSessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + if (!routeSessionId) return; + if (routeSessionId !== sessionId) { + setSessionId(routeSessionId); + setSession(null); + } + const stored = readStoredSession(); + if (!stored || stored.sessionId !== routeSessionId) { + setPlayerId(""); + } + }, [params.sessionId, sessionId, setSessionId, setSession, setPlayerId]); +} + +function GameTabs({ + role, + sessionId, + hasUnread, +}: { + role: "banker" | "player"; + sessionId: string; + hasUnread?: boolean; +}) { + const { t } = useI18n(); + return ( + + ); +} + +function EntryPage({ manager }: { manager: ReturnType }) { + const navigate = useNavigate(); + const { t } = useI18n(); + const [createName, setCreateName] = useState(""); + const [joinCode, setJoinCode] = useState(""); + const [joinName, setJoinName] = useState(""); + const [joinStep, setJoinStep] = useState<"code" | "choice">("code"); + const [joinPreview, setJoinPreview] = useState(null); + const [takeoverDummyId, setTakeoverDummyId] = useState(""); + const [takeoverName, setTakeoverName] = useState(""); + + const stored = readStoredSession(); + const storedPlayer = joinPreview?.players.find((player) => player.id === stored?.playerId); + const takeoverDisabled = storedPlayer?.connected === true; + const dummyOptions = joinPreview?.players.filter((player) => player.isDummy) ?? []; + + useEffect(() => { + if (joinStep === "choice" && joinPreview && joinCode !== joinPreview.code) { + setJoinStep("code"); + setJoinPreview(null); + setJoinName(""); + setTakeoverDummyId(""); + } + }, [joinCode, joinPreview, joinStep]); + + return ( +
+
+
+

{t("app.name")}

+ {t("entry.tagline")} +
+
+ {t("entry.liveSessions")} + {t("entry.bankerControlled")} +
+
+ +
+
+

{t("entry.createTitle")}

+

{t("entry.createSubtitle")}

+
+ + setCreateName(event.target.value)} + placeholder={t("entry.bankerName")} + /> + +
+
+ +
+

{t("entry.joinTitle")}

+

{t("entry.joinSubtitle")}

+
+ + setJoinCode(event.target.value.toUpperCase())} + placeholder={t("entry.codePlaceholder")} + /> + {joinStep === "code" && ( + + )} +
+ + {joinStep === "choice" && joinPreview && ( +
+
+ + setJoinName(event.target.value)} + placeholder={t("entry.playerName")} + /> + +
+ +
+ + {takeoverDisabled && ( +

{t("entry.alreadyConnected")}

+ )} + {!takeoverDisabled && ( + <> + + setTakeoverName(event.target.value)} + placeholder={t("entry.yourNameOptional")} + /> + + + )} + {!takeoverDisabled && dummyOptions.length === 0 && ( +

{t("entry.noDummies")}

+ )} +
+ + +
+ )} +
+
+ + {manager.error && ( +
+ {t("common.notice")} {manager.error} +
+ )} +
+ ); +} + +function LobbyPage({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const { t } = useI18n(); + const { sessionId, playerId, setError } = manager; + const [joinName, setJoinName] = useState(""); + const [qrCode, setQrCode] = useState(""); + const [joinPreview, setJoinPreview] = useState(null); + const [takeoverDummyId, setTakeoverDummyId] = useState(""); + const [takeoverName, setTakeoverName] = useState(""); + const [dummyName, setDummyName] = useState(""); + const [dummyBalance, setDummyBalance] = useState("1500"); + + useEffect(() => { + if (manager.session && manager.me && manager.session.status === "active") { + navigate( + manager.isBanker + ? `/play/${manager.sessionId}/banker/dashboard` + : `/play/${manager.sessionId}/player/home`, + ); + } + }, [manager.session, manager.me, manager.isBanker, manager.sessionId, navigate]); + + useEffect(() => { + if (!sessionId) return; + const joinUrl = `${window.location.origin}/play/${sessionId}`; + QRCode.toDataURL(joinUrl, { width: 240, margin: 1 }) + .then((url) => setQrCode(url)) + .catch(() => setQrCode("")); + }, [sessionId]); + + useEffect(() => { + if (!sessionId || playerId) return; + fetch(`/api/session/${sessionId}/info`) + .then((response) => (response.ok ? response.json() : null)) + .then((data) => { + if (data) { + setJoinPreview(data as SessionPreview); + } + }) + .catch(() => { + setError(t("lobby.errorLoadInfo")); + }); + }, [sessionId, playerId, setError, t]); + + if (!sessionId) { + return ; + } + + if (!playerId) { + const dummyOptions = joinPreview?.players.filter((player) => player.isDummy) ?? []; + return ( +
+
+
+

{t("lobby.title")}

+ {t("lobby.sessionLabel", { id: manager.sessionId })} +
+
+
+

{t("lobby.joinTitle")}

+ {!joinPreview &&

{t("lobby.loadingInfo")}

} + {joinPreview && ( +
+
+ + setJoinName(event.target.value)} + placeholder={t("entry.playerName")} + /> + +
+ +
+ + + setTakeoverName(event.target.value)} + placeholder={t("entry.yourNameOptional")} + /> + + {dummyOptions.length === 0 && ( +

{t("entry.noDummies")}

+ )} +
+
+ )} +
+ {manager.error && ( +
+ {t("common.notice")} {manager.error} +
+ )} +
+ ); + } + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("lobby.title")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+ {formatConnectionState(manager.connectionState, t)} +
+
+
+

{t("lobby.waitingState")}

+
+ +
+
+
+ ); + } + + const players = manager.players; + + return ( +
+
+
+

{t("lobby.header", { code: manager.session.code })}

+ + {t("lobby.statusLine", { + status: formatStatus(manager.session.status, t), + count: players.length, + })} + +
+
+ {manager.me.name} + + {manager.isBanker ? t("common.banker") : t("common.player")} + +
+
+ +
+
+

{t("lobby.roster")}

+
+ {players.map((player) => ( +
+
+ {player.name} + {player.role === "banker" ? t("common.banker") : t("common.player")} +
+
+ {player.isDummy && {t("common.dummy")}} + {!player.connected && ( + {t("common.offline")} + )} +
+
+ ))} +
+ {manager.isBanker && manager.session.status !== "active" && ( + + )} + {!manager.isBanker && ( +

{t("lobby.waitingBanker")}

+ )} + {manager.session.status === "ended" && ( +

{t("lobby.sessionClosed")}

+ )} +
+ + {manager.isBanker && ( + <> +
+

{t("lobby.inviteQr")}

+

{t("lobby.scanToJoin")}

+
+ {qrCode ? {t("lobby.inviteQr")} : ""} +
+
+ +
+

{t("lobby.addDummyTitle")}

+

{t("lobby.addDummySubtitle")}

+
+ + setDummyName(event.target.value)} + placeholder={t("banker.dummyName")} + /> + + setDummyBalance(event.target.value)} + placeholder={t("common.startingBalance")} + /> + +
+
+ + )} +
+ + {manager.error && ( +
+ {t("common.notice")} {manager.error} +
+ )} +
+ ); +} + +function BankerPage({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const params = useParams(); + const { t } = useI18n(); + const tab = params.tab ?? "dashboard"; + const [adjustTarget, setAdjustTarget] = useState(""); + const [adjustAmount, setAdjustAmount] = useState(""); + const [adjustNote, setAdjustNote] = useState(""); + + const [forceFrom, setForceFrom] = useState(""); + const [forceTo, setForceTo] = useState(""); + const [forceAmount, setForceAmount] = useState(""); + const [forceNote, setForceNote] = useState(""); + + const [dummyName, setDummyName] = useState(""); + const [dummyBalance, setDummyBalance] = useState("1500"); + + const [blackoutReason, setBlackoutReason] = useState(""); + const [toolsTab, setToolsTab] = useState<"players" | "admin">("players"); + const [selectedPlayerId, setSelectedPlayerId] = useState(""); + + const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); + const [autoSaveInterval, setAutoSaveInterval] = useState("3"); + const [autoSaveLimit, setAutoSaveLimit] = useState("5"); + const [autoSaveEntries, setAutoSaveEntries] = useState< + { id: string; savedAt: number; state: SessionSnapshot }[] + >([]); + const [autoSaveStatus, setAutoSaveStatus] = useState(null); + const [loadStatus, setLoadStatus] = useState(null); + + const autoSaveKey = useMemo( + () => (manager.sessionId ? `negopoly:autosave:${manager.sessionId}` : ""), + [manager.sessionId], + ); + const autoSaveSettingsKey = useMemo( + () => (manager.sessionId ? `negopoly:autosave:${manager.sessionId}:settings` : ""), + [manager.sessionId], + ); + + useEffect(() => { + if (!autoSaveSettingsKey || !autoSaveKey) return; + try { + const settingsRaw = localStorage.getItem(autoSaveSettingsKey); + if (settingsRaw) { + const settings = JSON.parse(settingsRaw) as { + enabled?: boolean; + intervalMinutes?: number; + maxEntries?: number; + }; + setAutoSaveEnabled(Boolean(settings.enabled)); + if (settings.intervalMinutes) { + setAutoSaveInterval(String(settings.intervalMinutes)); + } + if (settings.maxEntries) { + setAutoSaveLimit(String(settings.maxEntries)); + } + } + } catch { + // ignore invalid settings + } + + try { + const savedRaw = localStorage.getItem(autoSaveKey); + if (savedRaw) { + const entries = JSON.parse(savedRaw) as { + id: string; + savedAt: number; + state: SessionSnapshot; + }[]; + setAutoSaveEntries(entries); + } else { + setAutoSaveEntries([]); + } + } catch { + setAutoSaveEntries([]); + } + }, [autoSaveKey, autoSaveSettingsKey]); + + useEffect(() => { + if (!autoSaveSettingsKey) return; + const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3); + const maxEntries = Math.max(1, Number(autoSaveLimit) || 5); + localStorage.setItem( + autoSaveSettingsKey, + JSON.stringify({ + enabled: autoSaveEnabled, + intervalMinutes, + maxEntries, + }), + ); + }, [autoSaveEnabled, autoSaveInterval, autoSaveLimit, autoSaveSettingsKey]); + + useEffect(() => { + if (!manager.sessionId) { + navigate("/play"); + return; + } + if (!manager.playerId) { + navigate(`/play/${manager.sessionId}/lobby`); + return; + } + if (manager.session && manager.session.status !== "active") { + navigate(`/play/${manager.sessionId}/lobby`); + } + }, [manager.session, manager.sessionId, manager.playerId, navigate]); + + const players = manager.players; + const eligiblePlayers = useMemo( + () => players.filter((player) => player.role !== "banker"), + [players], + ); + const pendingRequests = manager.session?.takeoverRequests.filter( + (request) => request.status === "pending", + ); + const showPending = (pendingRequests ?? []).length > 0; + const { active: blackoutActive } = getBlackoutState( + manager.session, + manager.tick, + ); + const showBlackout = blackoutActive && !manager.isBanker; + const selectedPlayer = + eligiblePlayers.find((player) => player.id === selectedPlayerId) ?? null; + const playerTransactions = + manager.session?.transactions.filter( + (transaction) => + transaction.fromId === selectedPlayerId || transaction.toId === selectedPlayerId, + ) ?? []; + + useEffect(() => { + if (eligiblePlayers.length === 0) { + if (selectedPlayerId) { + setSelectedPlayerId(""); + } + return; + } + if (!selectedPlayerId || !eligiblePlayers.some((player) => player.id === selectedPlayerId)) { + setSelectedPlayerId(eligiblePlayers[0].id); + } + }, [eligiblePlayers, selectedPlayerId]); + + useEffect(() => { + if (!selectedPlayerId) return; + setAdjustTarget(selectedPlayerId); + setForceFrom(selectedPlayerId); + if (!forceTo || forceTo === selectedPlayerId) { + const fallback = eligiblePlayers.find((player) => player.id !== selectedPlayerId); + setForceTo(fallback?.id ?? ""); + } + }, [selectedPlayerId, eligiblePlayers, forceTo]); + + async function fetchGameState(): Promise { + if (!manager.sessionId || !manager.me) return null; + const response = await fetch( + `/api/session/${manager.sessionId}/state?bankerId=${manager.me.id}`, + ); + if (!response.ok) { + return null; + } + return (await response.json()) as SessionSnapshot; + } + + function persistAutoSaves(entries: { id: string; savedAt: number; state: SessionSnapshot }[]) { + if (!autoSaveKey) return; + localStorage.setItem(autoSaveKey, JSON.stringify(entries)); + } + + async function handleDownloadState() { + setAutoSaveStatus(null); + const snapshot = await fetchGameState(); + if (!snapshot) { + setAutoSaveStatus(t("banker.stateDownloadError")); + return; + } + const blob = new Blob([JSON.stringify(snapshot, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `negopoly-${snapshot.code}-${new Date().toISOString()}.json`; + anchor.click(); + URL.revokeObjectURL(url); + setAutoSaveStatus(t("banker.stateDownloaded")); + } + + async function handleLoadState(snapshot: SessionSnapshot) { + if (!manager.sessionId || !manager.me) return; + setLoadStatus(null); + const response = await fetch(`/api/session/${manager.sessionId}/state`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bankerId: manager.me.id, state: snapshot }), + }); + if (!response.ok) { + setLoadStatus(t("banker.stateLoadError")); + return; + } + setLoadStatus(t("banker.stateLoaded")); + } + + async function handleAutoSaveNow() { + if (!autoSaveKey) return; + const snapshot = await fetchGameState(); + if (!snapshot) { + setAutoSaveStatus(t("banker.autosaveFailed")); + return; + } + const maxEntries = Math.max(1, Number(autoSaveLimit) || 5); + const id = + typeof crypto !== "undefined" && "randomUUID" in crypto + ? crypto.randomUUID() + : `${Date.now()}-${Math.random().toString(16).slice(2)}`; + const nextEntry = { id, savedAt: Date.now(), state: snapshot }; + setAutoSaveEntries((prev) => { + const nextEntries = [nextEntry, ...prev].slice(0, maxEntries); + persistAutoSaves(nextEntries); + return nextEntries; + }); + setAutoSaveStatus(t("banker.autosaveSaved")); + } + + useEffect(() => { + if (!autoSaveEnabled || !autoSaveKey) return; + const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3); + const intervalMs = intervalMinutes * 60 * 1000; + const timer = setInterval(() => { + handleAutoSaveNow(); + }, intervalMs); + return () => clearInterval(timer); + }, [autoSaveEnabled, autoSaveInterval, autoSaveKey, autoSaveLimit]); + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("banker.consoleTitle")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+
+ ); + } + + if (!manager.isBanker) { + return ; + } + + const unreadThreadIds = getUnreadThreadIds( + manager.session, + manager.me.id, + manager.isBanker, + manager.chatReadState, + ); + const hasUnread = unreadThreadIds.size > 0; + + return ( +
+ {showBlackout && ( +
+
+

{t("blackout.title")}

+ + {t("blackout.active")} ·{" "} + {manager.session.blackoutReason || t("blackout.defaultReason")} + +
+
+ )} + +
+
+

{t("banker.consoleTitle")}

+ {t("common.sessionLive", { code: manager.session.code })} +
+
+ {manager.me.name} + {t("common.banker")} +
+
+ +
+ +
+ {(() => { + const validTabs = ["dashboard", "tools"]; + const currentTab = tab ?? "dashboard"; + if (!validTabs.includes(currentTab)) { + return ; + } + + if (currentTab === "tools") { + return ( +
+
+ {[ + { id: "players", label: t("banker.tools.playersTab") }, + { id: "admin", label: t("banker.tools.adminTab") }, + ].map((item) => ( + + ))} +
+ + {toolsTab === "players" ? ( +
+
+

{t("banker.playersTitle")}

+ {eligiblePlayers.length === 0 ? ( + {t("banker.noPlayers")} + ) : ( +
+ {eligiblePlayers.map((player) => { + const active = player.id === selectedPlayerId; + return ( + + ); + })} +
+ )} +
+ +
+

{t("banker.playerOverview")}

+ {selectedPlayer ? ( + <> +
+
+ {selectedPlayer.name} + + {selectedPlayer.isDummy ? t("common.dummy") : t("common.player")} ·{" "} + {selectedPlayer.connected ? t("common.online") : t("common.offline")} + +
+
{formatMoney(selectedPlayer.balance)}
+
+
+ {playerTransactions.length === 0 && ( + {t("common.noActivity")} + )} + {playerTransactions.map((transaction) => { + const display = getTransactionDisplay( + transaction, + selectedPlayerId, + manager.session?.players ?? [], + t, + ); + return ( +
+
+ {display.label} + {display.subtitle} +
+
{display.amount}
+
+ ); + })} +
+ + ) : ( + {t("banker.noPlayers")} + )} +
+ +
+

{t("banker.controlsTitle")}

+
+
+ + + setAdjustAmount(event.target.value)} + placeholder={t("banker.adjustAmountPlaceholder")} + /> + setAdjustNote(event.target.value)} + placeholder={t("common.reason")} + /> + +
+ +
+ + + + setForceAmount(event.target.value)} + placeholder={t("common.amount")} + /> + setForceNote(event.target.value)} + placeholder={t("common.note")} + /> + +
+
+
+ +
+

{t("banker.createDummy")}

+
+ setDummyName(event.target.value)} + placeholder={t("banker.dummyName")} + /> + setDummyBalance(event.target.value)} + placeholder={t("common.startingBalance")} + /> + +
+
+
+ ) : ( +
+
+

{t("banker.adminControls")}

+
+ + + setBlackoutReason(event.target.value)} + placeholder={t("banker.blackoutReason")} + /> + +
+ +
+ +
+

{t("banker.stateTitle")}

+

{t("banker.stateSubtitle")}

+
+
+ + + {autoSaveStatus ? {autoSaveStatus} : null} +
+ +
+ + { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result)); + handleLoadState(parsed as SessionSnapshot); + } catch { + setLoadStatus(t("banker.stateLoadInvalid")); + } + }; + reader.readAsText(file); + }} + /> + {loadStatus ? {loadStatus} : null} +
+ +
+ +
+ {autoSaveEntries.length === 0 ? ( + {t("banker.noAutosaves")} + ) : ( + autoSaveEntries.map((entry) => ( +
+ + {t("banker.savedAt", { + time: new Date(entry.savedAt).toLocaleString(), + })} + + +
+ )) + )} +
+
+
+
+ +
+

{t("banker.autosaveTitle")}

+

{t("banker.autosaveSubtitle")}

+
+
+ + +
+
+ + setAutoSaveInterval(event.target.value)} + placeholder={t("banker.autosaveMinutes")} + /> +
+
+ + setAutoSaveLimit(event.target.value)} + placeholder={t("banker.autosaveCount")} + /> +
+
+ + +
+
+
+ + {showPending && ( +
+

{t("banker.takeoverApprovals")}

+
+ {(pendingRequests ?? []).map((request) => { + const requester = manager.session?.players.find( + (player) => player.id === request.requesterId, + ); + const dummy = manager.session?.players.find( + (player) => player.id === request.dummyId, + ); + return ( +
+
+ {requester?.name ?? t("common.player")} + + {t("banker.wants", { name: dummy?.name ?? t("common.dummy") })} + +
+ +
+ ); + })} +
+
+ )} +
+ )} +
+ ); + } + + return ( +
+
+
+

{t("common.transactions")}

+
+ {manager.session.transactions.length === 0 && ( + {t("common.noActivity")} + )} + {manager.session.transactions.slice(0, 8).map((transaction) => { + const display = getTransactionDisplay( + transaction, + null, + manager.session?.players ?? [], + t, + ); + return ( +
+
+ {display.label} + {display.subtitle} +
+
{display.amount}
+
+ ); + })} +
+
+
+
+ ); + })()} +
+
+ + {manager.error && ( +
+ {t("common.notice")} {manager.error} +
+ )} +
+ ); +} + +function PlayerPage({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const params = useParams(); + const { t } = useI18n(); + const tab = params.tab ?? "home"; + const [transferTarget, setTransferTarget] = useState(""); + const [transferAmount, setTransferAmount] = useState(""); + const [transferNote, setTransferNote] = useState(""); + + useEffect(() => { + if (!manager.sessionId) { + navigate("/play"); + return; + } + if (!manager.playerId) { + navigate(`/play/${manager.sessionId}/lobby`); + return; + } + if (manager.session && manager.session.status !== "active") { + navigate(`/play/${manager.sessionId}/lobby`); + } + }, [manager.session, manager.sessionId, manager.playerId, navigate]); + + const players = manager.players; + const activePlayers = players.filter( + (player) => !player.isDummy && player.role !== "banker", + ); + const visibleTransactions = manager.session?.transactions.filter( + (transaction) => transaction.fromId === manager.me?.id || transaction.toId === manager.me?.id, + ); + const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick); + const showBlackout = blackoutActive && !manager.isBanker; + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("player.deskTitle")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+
+ ); + } + + if (manager.isBanker) { + return ; + } + + const unreadThreadIds = getUnreadThreadIds( + manager.session, + manager.me.id, + manager.isBanker, + manager.chatReadState, + ); + const hasUnread = unreadThreadIds.size > 0; + + return ( +
+ {showBlackout && ( +
+
+

{t("blackout.title")}

+ + {t("blackout.active")} ·{" "} + {manager.session.blackoutReason || t("blackout.defaultReason")} + +
+
+ )} + +
+
+

{t("player.deskTitle")}

+ {t("common.sessionLive", { code: manager.session.code })} +
+
+ {manager.me.name} + {t("common.player")} +
+
+ +
+ +
+ {(() => { + const validTabs = ["home", "transfers"]; + const currentTab = tab ?? "home"; + if (!validTabs.includes(currentTab)) { + return ; + } + + if (currentTab === "transfers") { + return ( +
+
+
+

{t("player.quickTransfer")}

+
+ + + + setTransferAmount(event.target.value)} + placeholder={t("common.amount")} + /> + + setTransferNote(event.target.value)} + placeholder={t("player.notePlaceholder")} + /> + +
+
+
+
+ ); + } + + return ( +
+
+
+

{t("home.balance")}

+
+ {formatMoney(manager.me.balance)} + {t("player.lastUpdated", { time: formatTime(manager.me.lastActiveAt) })} +
+
+
+

{t("common.transactions")}

+
+ {(visibleTransactions ?? []).length === 0 && ( + {t("common.noActivity")} + )} + {(visibleTransactions ?? []).slice(0, 6).map((transaction) => { + const display = getTransactionDisplay( + transaction, + manager.me?.id, + manager.session?.players ?? [], + t, + ); + return ( +
+
+ {display.label} + {display.subtitle} +
+
{display.amount}
+
+ ); + })} +
+
+
+
+ ); + })()} +
+
+ + {manager.error && ( +
+ {t("common.notice")} {manager.error} +
+ )} +
+ ); +} + +function ChatListRoute({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + const { t } = useI18n(); + + useEffect(() => { + if (!manager.sessionId) { + navigate("/play"); + return; + } + if (!manager.playerId) { + navigate(`/play/${manager.sessionId}/lobby`); + return; + } + if (manager.session && manager.session.status !== "active" && !manager.isBanker) { + navigate(`/play/${manager.sessionId}/lobby`); + } + }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]); + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("chat.title")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+
+ ); + } + + const backHref = manager.isBanker + ? `/play/${manager.sessionId}/banker/dashboard` + : `/play/${manager.sessionId}/player/home`; + const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick); + const showBlackout = blackoutActive && !manager.isBanker; + const unreadThreadIds = getUnreadThreadIds( + manager.session, + manager.me.id, + manager.isBanker, + manager.chatReadState, + ); + const hasUnread = unreadThreadIds.size > 0; + + return ( + <> + {showBlackout && ( +
+
+

{t("blackout.title")}

+ + {t("blackout.active")} ·{" "} + {manager.session.blackoutReason || t("blackout.defaultReason")} + +
+
+ )} +
+
+ +
+ +
+
+
+ + ); +} + +function ChatThreadRoute({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + const chatId = params.chatId ? decodeURIComponent(params.chatId) : ""; + const { t } = useI18n(); + + useEffect(() => { + if (!manager.sessionId) { + navigate("/play"); + return; + } + if (!manager.playerId) { + navigate(`/play/${manager.sessionId}/lobby`); + return; + } + if (manager.session && manager.session.status !== "active" && !manager.isBanker) { + navigate(`/play/${manager.sessionId}/lobby`); + } + }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]); + + useEffect(() => { + if (!manager.session || !manager.me || !chatId) return; + const latest = getLatestThreadTimestamp(manager.session, chatId); + manager.markChatRead(chatId, latest ?? Date.now()); + }, [manager.session, manager.me, chatId]); + + if (!chatId) { + return ; + } + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("tabs.chat")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+
+ ); + } + const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick); + const showBlackout = blackoutActive && !manager.isBanker; + const unreadThreadIds = getUnreadThreadIds( + manager.session, + manager.me.id, + manager.isBanker, + manager.chatReadState, + ); + const hasUnread = unreadThreadIds.size > 0; + + return ( + <> + {showBlackout && ( +
+
+

{t("blackout.title")}

+ + {t("blackout.active")} ·{" "} + {manager.session.blackoutReason || t("blackout.defaultReason")} + +
+
+ )} +
+
+ +
+ + manager.sendMessage({ + type: "chat_send", + sessionId: manager.sessionId, + playerId: manager.me.id, + body, + groupId, + }) + } + /> +
+
+
+ + ); +} + +function ChatNewRoute({ manager }: { manager: ReturnType }) { + useRouteSessionSync(manager); + const navigate = useNavigate(); + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + const { t } = useI18n(); + + useEffect(() => { + if (!manager.sessionId) { + navigate("/play"); + return; + } + if (!manager.playerId) { + navigate(`/play/${manager.sessionId}/lobby`); + return; + } + if (manager.session && manager.session.status !== "active" && !manager.isBanker) { + navigate(`/play/${manager.sessionId}/lobby`); + } + }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]); + + if (!manager.session || !manager.me) { + return ( +
+
+
+

{t("chat.newTitle")}

+ {t("common.connecting", { id: manager.sessionId })} +
+
+
+ ); + } + const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick); + const showBlackout = blackoutActive && !manager.isBanker; + const unreadThreadIds = getUnreadThreadIds( + manager.session, + manager.me.id, + manager.isBanker, + manager.chatReadState, + ); + const hasUnread = unreadThreadIds.size > 0; + + return ( + <> + {showBlackout && ( +
+
+

{t("blackout.title")}

+ + {t("blackout.active")} ·{" "} + {manager.session.blackoutReason || t("blackout.defaultReason")} + +
+
+ )} +
+
+ +
+ { + manager.sendMessage({ + type: "group_create", + sessionId: manager.sessionId, + playerId: manager.me.id, + name, + memberIds, + }); + navigate(`/play/${manager.sessionId}/chat`); + }} + /> +
+
+
+ + ); +} + +function PlayRouter() { + const manager = useSessionManager(); + + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} + +function RedirectToPlayerDefault() { + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + if (!sessionId) { + return ; + } + return ; +} + +function RedirectToBankerDefault() { + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + if (!sessionId) { + return ; + } + return ; +} + +function RedirectToLobby() { + const params = useParams(); + const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : ""; + if (!sessionId) { + return ; + } + return ; +} + +export default PlayRouter; diff --git a/front/play/chat/ChatList.tsx b/front/play/chat/ChatList.tsx new file mode 100644 index 0000000..eb3333b --- /dev/null +++ b/front/play/chat/ChatList.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import type { ChatThread } from "./types"; +import { useI18n } from "../i18n"; + +function formatTime(value?: number | null) { + if (!value) return ""; + return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +export default function ChatList({ + threads, + basePath, + unreadIds, +}: { + threads: ChatThread[]; + basePath: string; + unreadIds?: Set; +}) { + const { t } = useI18n(); + return ( +
+ {threads.map((thread) => { + const last = thread.lastMessage; + const preview = last?.body ?? t("chat.noMessages"); + const path = `${basePath}/${thread.id}`; + const isUnread = unreadIds?.has(thread.id); + return ( + +
+ {thread.kind === "global" ? "#" : thread.name.charAt(0).toUpperCase()} +
+
+
+ {thread.name} +
+ {formatTime(last?.createdAt)} + {isUnread &&
+
+
+

{preview}

+ + {thread.kind === "global" + ? t("chat.global") + : thread.kind === "direct" + ? t("chat.direct") + : t("chat.group")} + +
+
+ + ); + })} +
+ ); +} diff --git a/front/play/chat/ChatListScreen.tsx b/front/play/chat/ChatListScreen.tsx new file mode 100644 index 0000000..502172f --- /dev/null +++ b/front/play/chat/ChatListScreen.tsx @@ -0,0 +1,68 @@ +import React, { useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import type { SessionSnapshot } from "../../../shared/types"; +import ChatList from "./ChatList"; +import { buildThreads } from "./utils"; +import { useI18n } from "../i18n"; + +export default function ChatListScreen({ + session, + meId, + isBanker, + sessionId, + backHref, + unreadIds, +}: { + session: SessionSnapshot; + meId: string; + isBanker: boolean; + sessionId: string; + backHref: string; + unreadIds: Set; +}) { + const { t } = useI18n(); + const [query, setQuery] = useState(""); + const threads = useMemo(() => buildThreads(session, meId, isBanker), [session, meId, isBanker]); + const filtered = threads.filter((thread) => + thread.name.toLowerCase().includes(query.trim().toLowerCase()), + ); + const list = query.trim() ? filtered : threads; + const conversationLabel = + threads.length === 1 + ? t("chat.conversationCountOne") + : t("chat.conversationCount", { count: threads.length }); + + return ( +
+
+
+ + ← {t("chat.back")} + +
+

{t("chat.title")}

+ {conversationLabel} +
+
+ +
+ setQuery(event.target.value)} + placeholder={t("chat.searchPlaceholder")} + /> +
+ + + + + + + +
+
+ ); +} diff --git a/front/play/chat/ChatNewScreen.tsx b/front/play/chat/ChatNewScreen.tsx new file mode 100644 index 0000000..e39ec35 --- /dev/null +++ b/front/play/chat/ChatNewScreen.tsx @@ -0,0 +1,152 @@ +import React, { useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import type { SessionSnapshot } from "../../../shared/types"; +import { useI18n } from "../i18n"; + +export default function ChatNewScreen({ + session, + meId, + sessionId, + onCreate, +}: { + session: SessionSnapshot; + meId: string; + sessionId: string; + onCreate: (name: string, memberIds: string[]) => void; +}) { + const { t } = useI18n(); + const [mode, setMode] = useState<"direct" | "group">("direct"); + const [groupName, setGroupName] = useState(""); + const [selected, setSelected] = useState([]); + const [error, setError] = useState(null); + + const options = useMemo( + () => session.players.filter((player) => player.id !== meId), + [session.players, meId], + ); + + function resetSelection(nextMode: "direct" | "group") { + setMode(nextMode); + setSelected([]); + setError(null); + if (nextMode === "direct") { + setGroupName(""); + } + } + + function toggleMember(id: string) { + if (mode === "direct") { + setSelected([id]); + return; + } + setSelected((prev) => + prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], + ); + } + + function handleCreate() { + if (mode === "direct") { + if (selected.length !== 1) { + setError(t("chat.error.direct")); + return; + } + onCreate(t("chat.direct"), selected); + return; + } + if (!groupName.trim()) { + setError(t("chat.error.groupName")); + return; + } + if (selected.length === 0) { + setError(t("chat.error.member")); + return; + } + onCreate(groupName.trim(), selected); + } + + return ( +
+
+
+ + ← {t("chat.backChats")} + +
+

{t("chat.newTitle")}

+ {t("chat.newSubtitle")} +
+
+ +
+
+ + +
+ + {mode === "group" && ( + + )} + +
+

{t("chat.choosePlayers")}

+ {options.length === 0 ? ( +

{t("chat.noPlayers")}

+ ) : ( +
+ {options.map((player) => { + const selectedNow = selected.includes(player.id); + return ( + + ); + })} +
+ )} +
+ + {error &&

{error}

} + + +
+
+
+ ); +} diff --git a/front/play/chat/ChatThread.tsx b/front/play/chat/ChatThread.tsx new file mode 100644 index 0000000..77dd203 --- /dev/null +++ b/front/play/chat/ChatThread.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useRef } from "react"; +import type { ChatMessage } from "../../../shared/types"; +import type { ChatThread } from "./types"; +import { useI18n } from "../i18n"; + +function formatTime(value: number) { + return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +export default function ChatThread({ + thread, + messages, + meId, + onSend, + readOnly, + nameById, +}: { + thread: ChatThread; + messages: ChatMessage[]; + meId: string | null; + onSend?: (body: string) => void; + readOnly?: boolean; + nameById: Record; +}) { + const { t } = useI18n(); + const listRef = useRef(null); + + useEffect(() => { + if (listRef.current) { + listRef.current.scrollTop = listRef.current.scrollHeight; + } + }, [messages]); + + const showSender = thread.kind === "group" || thread.kind === "global"; + + return ( +
+
+ {messages.length === 0 && ( +
+

{t("chat.noMessages")}

+ {t("chat.startConversation")} +
+ )} + {messages.map((message) => { + const isMe = message.fromId === meId; + return ( +
+
+ {showSender && !isMe && ( + + {nameById[message.fromId] ?? t("common.player")} + + )} +

{message.body}

+ {formatTime(message.createdAt)} +
+
+ ); + })} +
+ + {!readOnly && ( + { + if (!body.trim()) return; + onSend?.(body); + }} + /> + )} +
+ ); +} + +function ChatComposer({ + onSend, + placeholder, + sendLabel, +}: { + onSend: (body: string) => void; + placeholder: string; + sendLabel: string; +}) { + const [value, setValue] = React.useState(""); + + return ( +
+ setValue(event.target.value)} + placeholder={placeholder} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (!value.trim()) return; + onSend(value); + setValue(""); + } + }} + /> + +
+ ); +} diff --git a/front/play/chat/ChatThreadScreen.tsx b/front/play/chat/ChatThreadScreen.tsx new file mode 100644 index 0000000..a2c10ef --- /dev/null +++ b/front/play/chat/ChatThreadScreen.tsx @@ -0,0 +1,82 @@ +import React, { useMemo } from "react"; +import { Link, Navigate } from "react-router-dom"; +import type { SessionSnapshot } from "../../../shared/types"; +import ChatThread from "./ChatThread"; +import { buildThreads, getThreadMessages } from "./utils"; +import { useI18n } from "../i18n"; + +export default function ChatThreadScreen({ + session, + meId, + isBanker, + sessionId, + chatId, + onSend, +}: { + session: SessionSnapshot; + meId: string; + isBanker: boolean; + sessionId: string; + chatId: string; + onSend: (body: string, groupId: string | null) => void; +}) { + const { t } = useI18n(); + const threads = useMemo(() => buildThreads(session, meId, isBanker), [session, meId, isBanker]); + const thread = threads.find((item) => item.id === chatId) ?? null; + + if (!thread) { + return ; + } + + const messages = getThreadMessages(session, thread.id); + const memberCount = thread.members.length; + const headerSub = + thread.kind === "global" + ? t("chat.everyone") + : thread.kind === "direct" + ? t("chat.directMessage") + : memberCount === 1 + ? t("chat.memberCountOne") + : t("chat.memberCount", { count: memberCount }); + + const nameById = useMemo(() => { + const lookup: Record = {}; + session.players.forEach((player) => { + lookup[player.id] = player.name; + }); + return lookup; + }, [session.players]); + + return ( +
+
+
+
+ + ← {t("chat.backChats")} + +
+

{thread.name}

+ {headerSub} +
+
+
+ {thread.kind === "global" + ? t("chat.global") + : thread.kind === "direct" + ? t("chat.direct") + : t("chat.group")} +
+
+ + onSend(body, thread.id === "global" ? null : thread.id)} + /> +
+
+ ); +} diff --git a/front/play/chat/types.ts b/front/play/chat/types.ts new file mode 100644 index 0000000..72cfc33 --- /dev/null +++ b/front/play/chat/types.ts @@ -0,0 +1,9 @@ +import type { ChatMessage } from "../../../shared/types"; + +export type ChatThread = { + id: string; + name: string; + kind: "global" | "group" | "direct"; + members: string[]; + lastMessage: ChatMessage | null; +}; diff --git a/front/play/chat/utils.ts b/front/play/chat/utils.ts new file mode 100644 index 0000000..14a6272 --- /dev/null +++ b/front/play/chat/utils.ts @@ -0,0 +1,110 @@ +import type { ChatMessage, SessionSnapshot } from "../../../shared/types"; +import type { ChatThread } from "./types"; +import { tStatic } from "../i18n"; + +function getLastMessage(messages: ChatMessage[]): ChatMessage | null { + if (messages.length === 0) return null; + return messages.reduce((latest, current) => + current.createdAt > latest.createdAt ? current : latest, + ); +} + +export function buildThreads( + session: SessionSnapshot, + meId: string | null, + isBanker: boolean, +): ChatThread[] { + const threads: ChatThread[] = []; + const globalMessages = session.chats.filter((message) => message.groupId === null); + threads.push({ + id: "global", + name: tStatic("chat.global"), + kind: "global", + members: [], + lastMessage: getLastMessage(globalMessages), + }); + + session.groups.forEach((group) => { + if (!isBanker && meId && !group.memberIds.includes(meId)) { + return; + } + const groupMessages = session.chats.filter((message) => message.groupId === group.id); + let name = group.name; + let kind: ChatThread["kind"] = "group"; + if (group.memberIds.length === 2) { + const [first, second] = group.memberIds; + if (isBanker || !meId) { + const firstName = + session.players.find((player) => player.id === first)?.name ?? + tStatic("common.player"); + const secondName = + session.players.find((player) => player.id === second)?.name ?? + tStatic("common.player"); + name = `${firstName} & ${secondName}`; + kind = "direct"; + } else { + const otherId = first === meId ? second : first; + const otherName = + session.players.find((player) => player.id === otherId)?.name ?? + tStatic("common.player"); + name = otherName; + kind = "direct"; + } + } + threads.push({ + id: group.id, + name, + kind, + members: group.memberIds, + lastMessage: getLastMessage(groupMessages), + }); + }); + + return threads.sort((a, b) => { + const aTime = a.lastMessage?.createdAt ?? 0; + const bTime = b.lastMessage?.createdAt ?? 0; + return bTime - aTime; + }); +} + +export function getThreadMessages(session: SessionSnapshot, threadId: string): ChatMessage[] { + if (threadId === "global") { + return session.chats.filter((message) => message.groupId === null).slice().reverse(); + } + return session.chats.filter((message) => message.groupId === threadId).slice().reverse(); +} + +export function getLatestThreadTimestamp(session: SessionSnapshot, threadId: string): number | null { + const messages = + threadId === "global" + ? session.chats.filter((message) => message.groupId === null) + : session.chats.filter((message) => message.groupId === threadId); + if (messages.length === 0) return null; + return messages.reduce( + (latest, message) => (message.createdAt > latest ? message.createdAt : latest), + 0, + ); +} + +export function getUnreadThreadIds( + session: SessionSnapshot, + meId: string | null, + isBanker: boolean, + readState: Record, +): Set { + const unread = new Set(); + if (!meId) return unread; + const allowed = new Set( + buildThreads(session, meId, isBanker).map((thread) => thread.id), + ); + session.chats.forEach((message) => { + if (message.fromId === meId) return; + const threadId = message.groupId ?? "global"; + if (!allowed.has(threadId)) return; + const lastRead = readState[threadId] ?? 0; + if (message.createdAt > lastRead) { + unread.add(threadId); + } + }); + return unread; +} diff --git a/front/play/i18n.ts b/front/play/i18n.ts new file mode 100644 index 0000000..65bbb9b --- /dev/null +++ b/front/play/i18n.ts @@ -0,0 +1,421 @@ +import { useCallback, useMemo } from "react"; + +type Locale = "en" | "fr"; + +const translations = { + en: { + "app.name": "Negopoly Bank", + "common.loading": "Loading...", + "common.notice": "Notice:", + "common.reset": "Reset", + "common.guest": "Guest", + "common.dummy": "Dummy", + "common.player": "Player", + "common.banker": "Banker", + "common.bank": "Bank", + "common.online": "online", + "common.offline": "Offline", + "common.name": "Name", + "common.startingBalance": "Starting balance", + "common.selectPlayer": "Select player", + "common.reason": "Reason", + "common.amount": "Amount", + "common.note": "Note", + "common.noReason": "No reason provided", + "common.from": "From", + "common.to": "To", + "common.apply": "Apply", + "common.force": "Force", + "common.trigger": "Trigger", + "common.send": "Send", + "common.download": "Download", + "common.load": "Load", + "common.save": "Save", + "common.transactions": "Transactions", + "common.noActivity": "No activity yet.", + "common.connecting": "Connecting to session {id}...", + "common.sessionLive": "Session {code} · live", + "common.continue": "Continue", + "tabs.dashboard": "Dashboard", + "tabs.tools": "Tools", + "tabs.home": "Home", + "tabs.transfers": "Transfers", + "tabs.chat": "Chat", + "entry.tagline": "Open a lobby or join the city.", + "entry.liveSessions": "Live sessions", + "entry.bankerControlled": "Banker controlled", + "entry.createTitle": "Create a session", + "entry.createSubtitle": "Become the banker and control the flow of money.", + "entry.bankerName": "Banker name", + "entry.openVault": "Open the vault", + "entry.joinTitle": "Join a session", + "entry.joinSubtitle": "Enter a session code to continue.", + "entry.sessionCode": "Session code", + "entry.codePlaceholder": "Enter code", + "entry.newPlayerLabel": "Create a new player", + "entry.playerName": "Player name", + "entry.joinAsNew": "Join as new player", + "entry.takeoverTitle": "Take over a dummy", + "entry.alreadyConnected": "You are already connected to this session.", + "entry.selectDummy": "Select dummy", + "entry.yourNameOptional": "Your name (optional)", + "entry.requestTakeover": "Request takeover", + "entry.noDummies": "No dummies available to take over yet.", + "entry.changeCode": "Change code", + "entry.alert.enterCode": "Enter a session code", + "entry.alert.sessionNotFound": "Session not found", + "entry.alert.selectDummy": "Select a dummy player", + "lobby.title": "Negopoly Lobby", + "lobby.sessionLabel": "Session {id}", + "lobby.joinTitle": "Join this lobby", + "lobby.loadingInfo": "Loading session info...", + "lobby.waitingState": "Waiting for the lobby state.", + "lobby.header": "Lobby · Session {code}", + "lobby.statusLine": "Status: {status} · {count} players", + "lobby.roster": "Lobby roster", + "lobby.startGame": "Start the game", + "lobby.waitingBanker": "Waiting for the banker to start the game.", + "lobby.sessionClosed": "Session closed.", + "lobby.inviteQr": "Invite QR", + "lobby.scanToJoin": "Scan to join this lobby instantly.", + "lobby.addDummyTitle": "Add dummy player", + "lobby.addDummySubtitle": + "Create a player for someone without the app. Dummies can be taken over later.", + "lobby.enterDummyName": "Enter a dummy name", + "lobby.addDummyButton": "Add dummy", + "lobby.errorLoadInfo": "Unable to load session info", + "banker.consoleTitle": "Banker Console", + "banker.controlsTitle": "Banker controls", + "banker.tools.playersTab": "Players", + "banker.tools.adminTab": "Admin", + "banker.playersTitle": "Players", + "banker.playerOverview": "Player overview", + "banker.noPlayers": "No players yet.", + "banker.adminControls": "Session controls", + "banker.adjustBalance": "Adjust balance", + "banker.adjustAmountPlaceholder": "+/- amount", + "banker.forceTransfer": "Force transfer", + "banker.createDummy": "Create dummy", + "banker.dummyName": "Dummy name", + "banker.addDummy": "Add dummy", + "banker.blackout": "EMP", + "banker.blackoutToggle": "Toggle EMP", + "banker.blackoutEnable": "Enable EMP", + "banker.blackoutDisable": "Disable EMP", + "banker.blackoutReason": "EMP reason", + "banker.endSession": "End session", + "banker.takeoverApprovals": "Takeover approvals", + "banker.wants": "Wants {name}", + "banker.approve": "Approve", + "banker.stateTitle": "GameState", + "banker.stateSubtitle": "Export, import, or resume a session from a saved snapshot.", + "banker.downloadState": "Download current GameState", + "banker.loadFromFile": "Load GameState from file", + "banker.loadFromStorage": "Load from browser storage", + "banker.stateDownloaded": "GameState downloaded.", + "banker.stateDownloadError": "Unable to download GameState.", + "banker.stateLoaded": "GameState loaded.", + "banker.stateLoadError": "Unable to load GameState.", + "banker.stateLoadInvalid": "Invalid GameState file.", + "banker.autosaveTitle": "AutoSave", + "banker.autosaveSubtitle": "Keep rolling backups in this browser.", + "banker.autosaveToggle": "Enable AutoSave", + "banker.autosaveEnabled": "AutoSave is enabled", + "banker.autosaveInterval": "Minutes between saves", + "banker.autosaveMinutes": "e.g. 3", + "banker.autosaveKeep": "Snapshots to keep", + "banker.autosaveCount": "e.g. 5", + "banker.autosaveNow": "Save now", + "banker.autosaveSaved": "AutoSave captured.", + "banker.autosaveFailed": "AutoSave failed.", + "banker.noAutosaves": "No autosaves yet.", + "banker.savedAt": "Saved {time}", + "player.deskTitle": "Player Desk", + "player.quickTransfer": "Quick transfer", + "player.sendTo": "Send to", + "player.noteOptional": "Note (optional)", + "player.notePlaceholder": "For what?", + "player.sendFunds": "Send funds", + "transfers.error": "Choose a player and a valid amount.", + "player.lastUpdated": "Last updated {time}", + "home.balance": "Balance", + "blackout.title": "EMP", + "blackout.defaultReason": "EMP in effect", + "blackout.active": "EMP active", + "chat.title": "Chats", + "chat.global": "Global chat", + "chat.conversationCount": "{count} conversations", + "chat.conversationCountOne": "1 conversation", + "chat.searchPlaceholder": "Search chats", + "chat.newTitle": "New chat", + "chat.newSubtitle": "Start a direct or group conversation", + "chat.direct": "Direct", + "chat.group": "Group", + "chat.groupName": "Group name", + "chat.groupPlaceholder": "e.g. Negotiators", + "chat.choosePlayers": "Choose players", + "chat.noPlayers": "No other players available yet.", + "chat.back": "Back", + "chat.backChats": "Chats", + "chat.noMessages": "No messages yet.", + "chat.startConversation": "Start the conversation.", + "chat.messagePlaceholder": "Message", + "chat.startChat": "Start chat", + "chat.everyone": "Everyone in the session", + "chat.directMessage": "Direct message", + "chat.memberCount": "{count} members", + "chat.memberCountOne": "1 member", + "chat.error.direct": "Choose one person to start a direct chat.", + "chat.error.groupName": "Give the group a name.", + "chat.error.member": "Select at least one member.", + "transaction.transfer": "Transfer", + "transaction.banker_adjust": "Banker adjustment", + "transaction.banker_force_transfer": "Forced transfer", + "status.lobby": "Lobby", + "status.active": "Active", + "status.ended": "Ended", + "connection.idle": "idle", + "connection.connecting": "connecting", + "connection.open": "connected", + "connection.error": "error", + "error.parseResponse": "Unable to parse server response", + "error.createSession": "Unable to create session", + "error.joinSession": "Unable to join session", + "error.connectionNotReady": "Connection not ready", + }, + fr: { + "app.name": "Banque Negopoly", + "common.loading": "Chargement...", + "common.notice": "Info :", + "common.reset": "Réinitialiser", + "common.guest": "Invité", + "common.dummy": "Dummy", + "common.player": "Joueur", + "common.banker": "Banquier", + "common.bank": "Banque", + "common.online": "en ligne", + "common.offline": "Hors ligne", + "common.name": "Nom", + "common.startingBalance": "Solde de départ", + "common.selectPlayer": "Choisir un joueur", + "common.reason": "Raison", + "common.amount": "Montant", + "common.note": "Note", + "common.noReason": "Aucune raison fournie", + "common.from": "De", + "common.to": "À", + "common.apply": "Appliquer", + "common.force": "Forcer", + "common.trigger": "Déclencher", + "common.send": "Envoyer", + "common.download": "Télécharger", + "common.load": "Charger", + "common.save": "Enregistrer", + "common.transactions": "Transactions", + "common.noActivity": "Aucune activité.", + "common.connecting": "Connexion à la session {id}...", + "common.sessionLive": "Session {code} · en direct", + "common.continue": "Continuer", + "tabs.dashboard": "Tableau", + "tabs.tools": "Outils", + "tabs.home": "Accueil", + "tabs.transfers": "Transferts", + "tabs.chat": "Chat", + "entry.tagline": "Ouvrez un lobby ou rejoignez la ville.", + "entry.liveSessions": "Sessions en direct", + "entry.bankerControlled": "Contrôlé par le banquier", + "entry.createTitle": "Créer une session", + "entry.createSubtitle": "Devenez banquier et contrôlez le flux d'argent.", + "entry.bankerName": "Nom du banquier", + "entry.openVault": "Ouvrir le coffre", + "entry.joinTitle": "Rejoindre une session", + "entry.joinSubtitle": "Entrez un code pour continuer.", + "entry.sessionCode": "Code de session", + "entry.codePlaceholder": "Entrez le code", + "entry.newPlayerLabel": "Créer un nouveau joueur", + "entry.playerName": "Nom du joueur", + "entry.joinAsNew": "Rejoindre comme nouveau joueur", + "entry.takeoverTitle": "Reprendre un dummy", + "entry.alreadyConnected": "Vous êtes déjà connecté à cette session.", + "entry.selectDummy": "Choisir un dummy", + "entry.yourNameOptional": "Votre nom (optionnel)", + "entry.requestTakeover": "Demander la reprise", + "entry.noDummies": "Aucun dummy disponible pour le moment.", + "entry.changeCode": "Changer de code", + "entry.alert.enterCode": "Entrez un code de session", + "entry.alert.sessionNotFound": "Session introuvable", + "entry.alert.selectDummy": "Sélectionnez un dummy", + "lobby.title": "Lobby Negopoly", + "lobby.sessionLabel": "Session {id}", + "lobby.joinTitle": "Rejoindre ce lobby", + "lobby.loadingInfo": "Chargement des infos de session...", + "lobby.waitingState": "En attente de l'état du lobby.", + "lobby.header": "Lobby · Session {code}", + "lobby.statusLine": "Statut : {status} · {count} joueurs", + "lobby.roster": "Liste des joueurs", + "lobby.startGame": "Démarrer la partie", + "lobby.waitingBanker": "En attente du banquier pour démarrer.", + "lobby.sessionClosed": "Session terminée.", + "lobby.inviteQr": "QR d'invitation", + "lobby.scanToJoin": "Scannez pour rejoindre instantanément.", + "lobby.addDummyTitle": "Ajouter un dummy", + "lobby.addDummySubtitle": + "Créez un joueur pour quelqu'un sans l'application. Les dummies peuvent être repris.", + "lobby.enterDummyName": "Entrez un nom de dummy", + "lobby.addDummyButton": "Ajouter un dummy", + "lobby.errorLoadInfo": "Impossible de charger les infos de session", + "banker.consoleTitle": "Console banquier", + "banker.controlsTitle": "Contrôles banquier", + "banker.tools.playersTab": "Joueurs", + "banker.tools.adminTab": "Admin", + "banker.playersTitle": "Joueurs", + "banker.playerOverview": "Vue joueur", + "banker.noPlayers": "Pas encore de joueurs.", + "banker.adminControls": "Contrôles de session", + "banker.adjustBalance": "Ajuster le solde", + "banker.adjustAmountPlaceholder": "Montant +/-", + "banker.forceTransfer": "Forcer un transfert", + "banker.createDummy": "Créer un dummy", + "banker.dummyName": "Nom du dummy", + "banker.addDummy": "Ajouter un dummy", + "banker.blackout": "EMP", + "banker.blackoutToggle": "Basculer l'EMP", + "banker.blackoutEnable": "Activer l'EMP", + "banker.blackoutDisable": "Désactiver l'EMP", + "banker.blackoutReason": "Raison de l'EMP", + "banker.endSession": "Terminer la session", + "banker.takeoverApprovals": "Approbations de reprise", + "banker.wants": "Veut {name}", + "banker.approve": "Approuver", + "banker.stateTitle": "État de partie", + "banker.stateSubtitle": "Exportez, importez ou reprenez une partie depuis une sauvegarde.", + "banker.downloadState": "Télécharger l'état actuel", + "banker.loadFromFile": "Charger un état depuis un fichier", + "banker.loadFromStorage": "Charger depuis le navigateur", + "banker.stateDownloaded": "État téléchargé.", + "banker.stateDownloadError": "Impossible de télécharger l'état.", + "banker.stateLoaded": "État chargé.", + "banker.stateLoadError": "Impossible de charger l'état.", + "banker.stateLoadInvalid": "Fichier d'état invalide.", + "banker.autosaveTitle": "AutoSave", + "banker.autosaveSubtitle": "Conservez des sauvegardes dans ce navigateur.", + "banker.autosaveToggle": "Activer AutoSave", + "banker.autosaveEnabled": "AutoSave activé", + "banker.autosaveInterval": "Minutes entre sauvegardes", + "banker.autosaveMinutes": "ex. 3", + "banker.autosaveKeep": "Sauvegardes à conserver", + "banker.autosaveCount": "ex. 5", + "banker.autosaveNow": "Sauvegarder maintenant", + "banker.autosaveSaved": "Sauvegarde effectuée.", + "banker.autosaveFailed": "Échec de la sauvegarde.", + "banker.noAutosaves": "Aucune sauvegarde.", + "banker.savedAt": "Sauvé {time}", + "player.deskTitle": "Bureau joueur", + "player.quickTransfer": "Transfert rapide", + "player.sendTo": "Envoyer à", + "player.noteOptional": "Note (optionnel)", + "player.notePlaceholder": "Pour quoi ?", + "player.sendFunds": "Envoyer les fonds", + "transfers.error": "Choisissez un joueur et un montant valide.", + "player.lastUpdated": "Mis à jour {time}", + "home.balance": "Solde", + "blackout.title": "EMP", + "blackout.defaultReason": "EMP en cours", + "blackout.active": "EMP actif", + "chat.title": "Chats", + "chat.global": "Chat global", + "chat.conversationCount": "{count} conversations", + "chat.conversationCountOne": "1 conversation", + "chat.searchPlaceholder": "Rechercher un chat", + "chat.newTitle": "Nouveau chat", + "chat.newSubtitle": "Démarrer une conversation directe ou de groupe", + "chat.direct": "Direct", + "chat.group": "Groupe", + "chat.groupName": "Nom du groupe", + "chat.groupPlaceholder": "ex. Négociateurs", + "chat.choosePlayers": "Choisir des joueurs", + "chat.noPlayers": "Aucun autre joueur disponible.", + "chat.back": "Retour", + "chat.backChats": "Chats", + "chat.noMessages": "Aucun message.", + "chat.startConversation": "Démarrez la conversation.", + "chat.messagePlaceholder": "Message", + "chat.startChat": "Démarrer le chat", + "chat.everyone": "Tout le monde dans la session", + "chat.directMessage": "Message direct", + "chat.memberCount": "{count} membres", + "chat.memberCountOne": "1 membre", + "chat.error.direct": "Choisissez une personne pour un chat direct.", + "chat.error.groupName": "Donnez un nom au groupe.", + "chat.error.member": "Sélectionnez au moins un membre.", + "transaction.transfer": "Transfert", + "transaction.banker_adjust": "Ajustement banquier", + "transaction.banker_force_transfer": "Transfert forcé", + "status.lobby": "Lobby", + "status.active": "Active", + "status.ended": "Terminée", + "connection.idle": "inactif", + "connection.connecting": "connexion", + "connection.open": "connecté", + "connection.error": "erreur", + "error.parseResponse": "Impossible de lire la réponse du serveur", + "error.createSession": "Impossible de créer la session", + "error.joinSession": "Impossible de rejoindre la session", + "error.connectionNotReady": "Connexion non prête", + }, +} as const; + +type I18nKey = keyof typeof translations.en; + +export function getLocale(): Locale { + if (typeof navigator !== "undefined") { + const raw = (navigator.languages?.[0] ?? navigator.language ?? "en").toLowerCase(); + return raw.startsWith("fr") ? "fr" : "en"; + } + return "en"; +} + +function translate(locale: Locale, key: I18nKey, vars?: Record) { + const table = translations[locale] ?? translations.en; + let template = table[key] ?? translations.en[key] ?? key; + if (vars) { + Object.entries(vars).forEach(([name, value]) => { + template = template.replace(new RegExp(`\\{${name}\\}`, "g"), String(value)); + }); + } + return template; +} + +export function useI18n() { + const locale = useMemo(getLocale, []); + const t = useCallback( + (key: I18nKey, vars?: Record) => translate(locale, key, vars), + [locale], + ); + return { t, locale }; +} + +export function tStatic(key: I18nKey, vars?: Record) { + return translate(getLocale(), key, vars); +} + +export function formatTransactionKind( + kind: "transfer" | "banker_adjust" | "banker_force_transfer", + t: (key: I18nKey) => string, +) { + return t(`transaction.${kind}` as I18nKey); +} + +export function formatStatus( + status: "lobby" | "active" | "ended", + t: (key: I18nKey) => string, +) { + return t(`status.${status}` as I18nKey); +} + +export function formatConnectionState( + state: "idle" | "connecting" | "open" | "error", + t: (key: I18nKey) => string, +) { + return t(`connection.${state}` as I18nKey); +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..85bcce2 --- /dev/null +++ b/index.ts @@ -0,0 +1,70 @@ +import home from "./front/index.html"; +import play from "./front/play.html"; +import { apiRoutes } from "./server/api"; +import { handleSocketMessage, registerSocket, unregisterSocket } from "./server/websocket"; + +const appleAppSiteAssociation = Bun.file("./.well-known/apple-app-site-association"); +const assetLinks = Bun.file("./.well-known/assetlinks.json"); + +const server = Bun.serve({ + routes: { + "/.well-known/apple-app-site-association": new Response(appleAppSiteAssociation, { + headers: { "Content-Type": "application/json" }, + }), + "/.well-known/assetlinks.json": new Response(assetLinks, { + headers: { "Content-Type": "application/json" }, + }), + "/": home, + "/play": play, + "/play/:sessionId": play, + "/play/:sessionId/lobby": play, + "/play/:sessionId/banker": play, + "/play/:sessionId/banker/:tab": play, + "/play/:sessionId/player": play, + "/play/:sessionId/player/:tab": play, + "/play/:sessionId/chat": play, + "/play/:sessionId/chat/new": play, + "/play/:sessionId/chat/:chatId": play, + ...apiRoutes, + }, + fetch(req, server) { + const url = new URL(req.url); + if (url.pathname === "/ws") { + const sessionId = url.searchParams.get("sessionId"); + const playerId = url.searchParams.get("playerId"); + if (!sessionId || !playerId) { + return new Response("Missing sessionId or playerId", { status: 400 }); + } + const upgraded = server.upgrade(req, { + data: { sessionId, playerId }, + }); + if (upgraded) { + return; + } + return new Response("Unable to upgrade", { status: 400 }); + } + + return new Response("Not found", { status: 404 }); + }, + websocket: { + open(ws) { + const { sessionId, playerId } = ws.data as { + sessionId: string; + playerId: string; + }; + registerSocket(ws, sessionId, playerId); + }, + message(ws, message) { + handleSocketMessage(ws, message); + }, + close(ws) { + unregisterSocket(ws); + }, + }, + development: { + hmr: true, + console: true, + }, +}); + +console.log(`Listening on ${server.url}`); diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/mobile/App.tsx b/mobile/App.tsx new file mode 100644 index 0000000..14b4de5 --- /dev/null +++ b/mobile/App.tsx @@ -0,0 +1 @@ +export { default } from "./src/App"; diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..3f9fecb --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,17 @@ +# Negopoly Mobile (React Native) + +## Development server +Set the dev API base URL to your machine IP so the app can reach the Bun server. + +Create a `.env` file in `mobile/`: + +``` +EXPO_PUBLIC_DEV_API_BASE_URL=http://:3000 +``` + +The app will use `https://negopoly.fr` automatically in production builds. + +## Run +``` +npm run dev +``` diff --git a/mobile/app.json b/mobile/app.json new file mode 100644 index 0000000..1c21f29 --- /dev/null +++ b/mobile/app.json @@ -0,0 +1,46 @@ +{ + "expo": { + "name": "Negopoly Companion", + "slug": "negopoly-companion", + "version": "1.0.0", + "platforms": ["ios", "android"], + "orientation": "portrait", + "icon": "./assets/icon.png", + "scheme": "negopoly", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "bundleIdentifier": "fr.negopoly.app", + "supportsTablet": true, + "appleTeamId": "VD9WQ6BYX2", + "associatedDomains": ["applinks:negopoly.fr"] + }, + "android": { + "package": "fr.negopoly.app", + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false, + "intentFilters": [ + { + "action": "VIEW", + "data": [ + { + "scheme": "https", + "host": "negopoly.fr", + "pathPrefix": "/play" + } + ], + "category": ["BROWSABLE", "DEFAULT"] + } + ] + } + } +} diff --git a/mobile/assets/adaptive-icon.png b/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000..a7743a5 Binary files /dev/null and b/mobile/assets/adaptive-icon.png differ diff --git a/mobile/assets/favicon.png b/mobile/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/mobile/assets/favicon.png differ diff --git a/mobile/assets/icon.png b/mobile/assets/icon.png new file mode 100644 index 0000000..a7743a5 Binary files /dev/null and b/mobile/assets/icon.png differ diff --git a/mobile/assets/splash-icon.png b/mobile/assets/splash-icon.png new file mode 100644 index 0000000..a7743a5 Binary files /dev/null and b/mobile/assets/splash-icon.png differ diff --git a/mobile/index.ts b/mobile/index.ts new file mode 100644 index 0000000..64ebbbc --- /dev/null +++ b/mobile/index.ts @@ -0,0 +1,9 @@ +import "react-native-gesture-handler"; +import { registerRootComponent } from "expo"; + +import App from "./App"; + +// registerRootComponent calls AppRegistry.registerComponent('main', () => App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/mobile/package-lock.json b/mobile/package-lock.json new file mode 100644 index 0000000..1f41816 --- /dev/null +++ b/mobile/package-lock.json @@ -0,0 +1,8341 @@ +{ + "name": "mobile", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mobile", + "version": "1.0.0", + "dependencies": { + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/bottom-tabs": "^7.12.0", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.12.0", + "expo": "~54.0.33", + "expo-constants": "~18.0.13", + "expo-dev-client": "^6.0.20", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "typescript": "~5.9.2" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", + "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", + "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@expo/cli": { + "version": "54.0.23", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz", + "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.8", + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.0.8", + "@expo/image-utils": "^0.8.8", + "@expo/json-file": "^10.0.8", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~54.0.14", + "@expo/osascript": "^2.3.8", + "@expo/package-manager": "^1.9.10", + "@expo/plist": "^0.4.8", + "@expo/prebuild-config": "^54.0.8", + "@expo/schema-utils": "^0.1.8", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "0.81.5", + "@urql/core": "^5.0.6", + "@urql/exchange-retry": "^1.3.0", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "env-editor": "^0.4.1", + "expo-server": "^1.0.5", + "freeport-async": "^2.0.0", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.1.6", + "minimatch": "^9.0.0", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^3.0.1", + "pretty-bytes": "^5.6.0", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", + "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.3", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "tar": "^7.5.2", + "terminal-link": "^2.1.1", + "undici": "^6.18.2", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1" + }, + "bin": { + "expo-internal": "build/bin/cli" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", + "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", + "license": "MIT", + "dependencies": { + "node-forge": "^1.3.3" + } + }, + "node_modules/@expo/config": { + "version": "12.0.13", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", + "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "@expo/config-plugins": "~54.0.4", + "@expo/config-types": "^54.0.10", + "@expo/json-file": "^10.0.8", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4", + "sucrase": "~3.35.1" + } + }, + "node_modules/@expo/config-plugins": { + "version": "54.0.4", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", + "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", + "license": "MIT", + "dependencies": { + "@expo/config-types": "^54.0.10", + "@expo/json-file": "~10.0.8", + "@expo/plist": "^0.4.8", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slash": "^3.0.0", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-types": { + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", + "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", + "license": "MIT" + }, + "node_modules/@expo/devcert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", + "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", + "license": "MIT", + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/devtools": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz", + "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/env": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz", + "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, + "node_modules/@expo/fingerprint": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz", + "integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "ignore": "^5.3.1", + "minimatch": "^9.0.0", + "p-limit": "^3.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.8.tgz", + "integrity": "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0", + "semver": "^7.6.0", + "temp-dir": "~2.0.0", + "unique-string": "~2.0.0" + } + }, + "node_modules/@expo/json-file": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz", + "integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/metro": { + "version": "54.2.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", + "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", + "license": "MIT", + "dependencies": { + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3" + } + }, + "node_modules/@expo/metro-config": { + "version": "54.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz", + "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~12.0.13", + "@expo/env": "~2.0.8", + "@expo/json-file": "~10.0.8", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.29.1", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "minimatch": "^9.0.0", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@expo/osascript": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz", + "integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "exec-async": "^2.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz", + "integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==", + "license": "MIT", + "dependencies": { + "@expo/json-file": "^10.0.8", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/plist": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", + "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@expo/prebuild-config": { + "version": "54.0.8", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", + "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/config-types": "^54.0.10", + "@expo/image-utils": "^0.8.8", + "@expo/json-file": "^10.0.8", + "@react-native/normalize-colors": "0.81.5", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/@expo/schema-utils": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz", + "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==", + "license": "MIT" + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", + "license": "MIT" + }, + "node_modules/@expo/spawn-async": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", + "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", + "license": "MIT" + }, + "node_modules/@expo/vector-icons": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.3.tgz", + "integrity": "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==", + "license": "MIT", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", + "license": "MIT" + }, + "node_modules/@expo/xcpretty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz", + "integrity": "sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "chalk": "^4.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@expo/xcpretty/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", + "integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz", + "integrity": "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.81.5" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.81.5.tgz", + "integrity": "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.81.5", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.5.tgz", + "integrity": "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.29.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/codegen/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz", + "integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.81.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.83.1", + "metro-config": "^0.83.1", + "metro-core": "^0.83.1", + "semver": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz", + "integrity": "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz", + "integrity": "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.81.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz", + "integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz", + "integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", + "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", + "integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.12.0.tgz", + "integrity": "sha512-/GtOfVWRligHG0mvX39I1FGdUWeWl0GVF2okEziQSQj0bOTrLIt7y44C3r/aCLkEpTVltCPGM3swqGTH3UfRCw==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.5", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.14.0.tgz", + "integrity": "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.3", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.5.tgz", + "integrity": "sha512-iHZU8rRN1014Upz73AqNVXDvSMZDh5/ktQ1CMe21rdgnOY79RWtHHBp9qOS3VtqlUVYGkuX5GEw5mDt4tKdl0g==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.28", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz", + "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.14.0", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.12.0.tgz", + "integrity": "sha512-XmNJsPshjkNsahgbxNgGWQUq4s1l6HqH/Fei4QsjBNn/0mTvVrRVZwJ1XrY9YhWYvyiYkAN6/OmarWQaQJ0otQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.5", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", + "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", + "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@urql/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", + "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.13", + "wonka": "^6.3.2" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", + "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", + "license": "MIT", + "dependencies": { + "@urql/core": "^5.1.2", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "@urql/core": "^5.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", + "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz", + "integrity": "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.29.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-expo": { + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz", + "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.81.5", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + } + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/exec-async": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", + "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==", + "license": "MIT" + }, + "node_modules/expo": { + "version": "54.0.33", + "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", + "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "54.0.23", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/devtools": "0.1.8", + "@expo/fingerprint": "0.15.4", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "54.0.14", + "@expo/vector-icons": "^15.0.3", + "@ungap/structured-clone": "^1.3.0", + "babel-preset-expo": "~54.0.10", + "expo-asset": "~12.0.12", + "expo-constants": "~18.0.13", + "expo-file-system": "~19.0.21", + "expo-font": "~14.0.11", + "expo-keep-awake": "~15.0.8", + "expo-modules-autolinking": "3.0.24", + "expo-modules-core": "3.0.29", + "pretty-format": "^29.7.0", + "react-refresh": "^0.14.2", + "whatwg-url-without-unicode": "8.0.0-3" + }, + "bin": { + "expo": "bin/cli", + "expo-modules-autolinking": "bin/autolinking", + "fingerprint": "bin/fingerprint" + }, + "peerDependencies": { + "@expo/dom-webview": "*", + "@expo/metro-runtime": "*", + "react": "*", + "react-native": "*", + "react-native-webview": "*" + }, + "peerDependenciesMeta": { + "@expo/dom-webview": { + "optional": true + }, + "@expo/metro-runtime": { + "optional": true + }, + "react-native-webview": { + "optional": true + } + } + }, + "node_modules/expo-asset": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz", + "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.8.8", + "expo-constants": "~18.0.12" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-constants": { + "version": "18.0.13", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", + "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.13", + "@expo/env": "~2.0.8" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-dev-client": { + "version": "6.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz", + "integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "6.0.20", + "expo-dev-menu": "7.0.18", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.10", + "expo-updates-interface": "~2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "6.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz", + "integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "expo-dev-menu": "7.0.18", + "expo-manifests": "~1.0.10" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz", + "integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-file-system": { + "version": "19.0.21", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", + "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-font": { + "version": "14.0.11", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", + "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, + "node_modules/expo-keep-awake": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", + "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-manifests": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz", + "integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.11", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz", + "integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-core": { + "version": "3.0.29", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.29.tgz", + "integrity": "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-server": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", + "integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==", + "license": "MIT", + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/expo-status-bar": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz", + "integrity": "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.2.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-updates-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", + "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "license": "BSD-2-Clause" + }, + "node_modules/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/getenv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", + "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", + "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lan-network": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz", + "integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==", + "license": "MIT", + "bin": { + "lan-network": "dist/lan-network-cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/metro": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", + "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.32.0", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", + "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.32.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/metro-cache": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", + "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache-key": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", + "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-config": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", + "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.83.3", + "metro-cache": "0.83.3", + "metro-core": "0.83.3", + "metro-runtime": "0.83.3", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-core": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", + "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-file-map": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", + "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", + "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-resolver": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", + "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-runtime": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", + "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-source-map": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", + "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.83.3", + "nullthrows": "^1.1.1", + "ob1": "0.83.3", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", + "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.83.3", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", + "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", + "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-source-map": "0.83.3", + "metro-transform-plugins": "0.83.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", + "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ora/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", + "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", + "license": "MIT", + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", + "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.81.5", + "@react-native/codegen": "0.81.5", + "@react-native/community-cli-plugin": "0.81.5", + "@react-native/gradle-plugin": "0.81.5", + "@react-native/js-polyfills": "0.81.5", + "@react-native/normalize-colors": "0.81.5", + "@react-native/virtualized-lists": "0.81.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.83.1", + "metro-source-map": "^0.83.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-gesture-handler": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", + "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", + "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", + "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-native/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "license": "MIT", + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-workspace-root": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", + "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==", + "license": "MIT" + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "license": "MIT", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wonka": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", + "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", + "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/mobile/package.json b/mobile/package.json new file mode 100644 index 0000000..0e3fe03 --- /dev/null +++ b/mobile/package.json @@ -0,0 +1,31 @@ +{ + "name": "mobile", + "version": "1.0.0", + "main": "index.ts", + "scripts": { + "start": "expo start", + "dev": "sh ./scripts/start-dev.sh", + "android": "expo run:android", + "ios": "expo run:ios" + }, + "dependencies": { + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/bottom-tabs": "^7.12.0", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.12.0", + "expo": "~54.0.33", + "expo-constants": "~18.0.13", + "expo-dev-client": "^6.0.20", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "typescript": "~5.9.2" + }, + "private": true +} diff --git a/mobile/scripts/start-dev.sh b/mobile/scripts/start-dev.sh new file mode 100644 index 0000000..e9de1ae --- /dev/null +++ b/mobile/scripts/start-dev.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +IP="$(ipconfig getifaddr en0 2>/dev/null || true)" +if [ -z "$IP" ]; then + IP="$(ipconfig getifaddr en1 2>/dev/null || true)" +fi + +if [ -z "$IP" ]; then + echo "Could not determine LAN IP (tried en0/en1)." + exit 1 +fi + +export EXPO_PUBLIC_DEV_API_BASE_URL="http://$IP:3000" +echo "Using EXPO_PUBLIC_DEV_API_BASE_URL=$EXPO_PUBLIC_DEV_API_BASE_URL" +exec npx expo start \ No newline at end of file diff --git a/mobile/src/App.tsx b/mobile/src/App.tsx new file mode 100644 index 0000000..abc906b --- /dev/null +++ b/mobile/src/App.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Linking } from "react-native"; +import { + NavigationContainer, + type LinkingOptions, + useNavigationContainerRef, +} from "@react-navigation/native"; +import { SafeAreaProvider } from "react-native-safe-area-context"; +import { StatusBar } from "expo-status-bar"; +import AppNavigator from "./navigation/AppNavigator"; +import type { RootStackParamList } from "./navigation/types"; +import { SessionProvider, useSession } from "./state/session-context"; +import { getNavigationTheme, useTheme } from "./theme"; + +function extractGameId(url: string): string | null { + try { + const parsed = new URL(url); + const path = parsed.pathname || ""; + if (parsed.protocol === "https:" || parsed.protocol === "http:") { + const match = path.match(/^\/play\/?([^/]+)?/); + const id = match?.[1]?.trim(); + return id ? id : null; + } + if (parsed.host === "play") { + const id = path.replace(/^\//, "").trim(); + return id ? id : null; + } + const fallbackMatch = path.match(/^\/play\/?([^/]+)?/); + const fallbackId = fallbackMatch?.[1]?.trim(); + return fallbackId ? fallbackId : null; + } catch { + return null; + } +} + +function logDeepLink(url: string) { + if (!__DEV__) return; + const gameId = extractGameId(url); + console.log(`[deep-link] url=${url} gameId=${gameId ?? "invalid"}`); +} + +function RootNavigationGate() { + const manager = useSession(); + const navigationRef = useNavigationContainerRef(); + const [navReady, setNavReady] = useState(false); + const lastTargetRef = useRef(null); + const lastLinkRef = useRef(null); + const theme = useTheme(); + const navigationTheme = getNavigationTheme(theme); + const linking = useMemo>( + () => ({ + prefixes: ["negopoly://", "https://negopoly.fr"], + config: { + screens: { + Entry: "play/:gameId", + }, + }, + getInitialURL: async () => { + const url = await Linking.getInitialURL(); + if (url) { + lastLinkRef.current = url; + logDeepLink(url); + } + return url; + }, + subscribe: (listener) => { + const onReceiveURL = ({ url }: { url: string }) => { + if (!url) return; + if (lastLinkRef.current === url) return; + lastLinkRef.current = url; + logDeepLink(url); + listener(url); + }; + const subscription = Linking.addEventListener("url", onReceiveURL); + return () => subscription.remove(); + }, + }), + [], + ); + + useEffect(() => { + if (!navReady || !navigationRef.isReady()) return; + + let target: keyof RootStackParamList; + if (!manager.sessionId) { + target = "Entry"; + } else if (!manager.session) { + target = manager.connectionState === "error" ? "Entry" : "Lobby"; + } else if (manager.session.status === "lobby") { + target = "Lobby"; + } else if (manager.isBanker) { + target = "BankerTabs"; + } else { + target = "PlayerTabs"; + } + + const currentRoute = navigationRef.getCurrentRoute(); + if (currentRoute?.name === target || lastTargetRef.current === target) { + return; + } + + navigationRef.reset({ + index: 0, + routes: [{ name: target }], + }); + lastTargetRef.current = target; + }, [ + manager.sessionId, + manager.session, + manager.isBanker, + manager.connectionState, + navReady, + navigationRef, + ]); + + return ( + setNavReady(true)} + linking={linking} + theme={navigationTheme} + > + + + ); +} + +export default function App() { + const theme = useTheme(); + return ( + + + + + + + ); +} diff --git a/mobile/src/components/EmpOverlay.tsx b/mobile/src/components/EmpOverlay.tsx new file mode 100644 index 0000000..80e8e2c --- /dev/null +++ b/mobile/src/components/EmpOverlay.tsx @@ -0,0 +1,51 @@ +import React, { useMemo } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import { useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; + +type EmpOverlayProps = { + visible: boolean; + reason?: string | null; +}; + +export default function EmpOverlay({ visible, reason }: EmpOverlayProps) { + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + + if (!visible) return null; + + const reasonText = reason?.trim() || t("blackout.defaultReason"); + + return ( + + {t("blackout.title")} + + {t("blackout.active")} · {reasonText} + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(12, 24, 36, 0.88)", + alignItems: "center", + justifyContent: "center", + padding: 24, + zIndex: 10, + }, + title: { + fontSize: 24, + fontWeight: "700", + color: "#ffffff", + marginBottom: 8, + }, + subtitle: { + color: "rgba(255, 255, 255, 0.8)", + textAlign: "center", + }, + }); diff --git a/mobile/src/components/ExitGameButton.tsx b/mobile/src/components/ExitGameButton.tsx new file mode 100644 index 0000000..0b8e99b --- /dev/null +++ b/mobile/src/components/ExitGameButton.tsx @@ -0,0 +1,79 @@ +import React, { useCallback, useMemo } from "react"; +import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { useI18n } from "../i18n"; +import { useSession } from "../state/session-context"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; + +type ExitGameButtonProps = { + mode?: "header" | "full"; +}; + +export default function ExitGameButton({ mode = "header" }: ExitGameButtonProps) { + const { t } = useI18n(); + const manager = useSession(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + + const handlePress = useCallback(() => { + Alert.alert(t("session.exitPrompt"), t("session.exitMessage"), [ + { text: t("common.cancel"), style: "cancel" }, + { + text: t("common.exit"), + style: "destructive", + onPress: () => manager.leaveSession(), + }, + ]); + }, [manager, t]); + + if (!manager.sessionId) return null; + + if (mode === "full") { + return ( + + {t("session.exit")} + + ); + } + + return ( + + + {t("session.exit")} + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + headerButton: { + paddingHorizontal: 8, + paddingVertical: 6, + }, + headerPill: { + borderRadius: 999, + borderWidth: 1, + borderColor: theme.colors.danger, + paddingHorizontal: 10, + paddingVertical: 4, + }, + headerText: { + color: theme.colors.danger, + fontSize: 12, + fontWeight: "600", + }, + fullButton: { + borderRadius: 999, + borderWidth: 1, + borderColor: theme.colors.danger, + paddingVertical: 12, + alignItems: "center", + backgroundColor: theme.colors.surface, + marginTop: 8, + }, + fullButtonText: { + color: theme.colors.danger, + fontWeight: "600", + }, + }); diff --git a/mobile/src/config/api.ts b/mobile/src/config/api.ts new file mode 100644 index 0000000..d3d1b77 --- /dev/null +++ b/mobile/src/config/api.ts @@ -0,0 +1,21 @@ +import Constants from "expo-constants"; + +const PROD_BASE_URL = "https://negopoly.fr"; +const DEV_BASE_URL = + process.env.EXPO_PUBLIC_DEV_API_BASE_URL || + (Constants.expoConfig?.extra as { devApiBaseUrl?: string } | undefined)?.devApiBaseUrl || + "http://:3000"; + +function normalizeBaseUrl(value: string) { + return value.replace(/\/$/, ""); +} + +export function getApiBaseUrl() { + return normalizeBaseUrl(__DEV__ ? DEV_BASE_URL : PROD_BASE_URL); +} + +export function getWsUrl(sessionId: string, playerId: string) { + const base = getApiBaseUrl().replace(/^http/, "ws"); + const params = new URLSearchParams({ sessionId, playerId }); + return `${base}/ws?${params.toString()}`; +} diff --git a/mobile/src/i18n.ts b/mobile/src/i18n.ts new file mode 100644 index 0000000..cf0ca6c --- /dev/null +++ b/mobile/src/i18n.ts @@ -0,0 +1,346 @@ +import { useCallback, useMemo } from "react"; + +type Locale = "en" | "fr"; + +const translations = { + en: { + "app.name": "Negopoly Companion", + "common.loading": "Loading...", + "common.loadingChats": "Loading chats...", + "common.loadingChat": "Loading chat...", + "common.loadingLobby": "Joining lobby...", + "common.notice": "Notice:", + "common.online": "online", + "common.offline": "offline", + "common.dummy": "Dummy", + "common.player": "Player", + "common.banker": "Banker", + "common.bank": "Bank", + "common.from": "From", + "common.to": "To", + "common.guest": "Guest", + "common.you": "You", + "common.join": "Join", + "common.continue": "Continue", + "common.send": "Send", + "common.reset": "Reset", + "common.cancel": "Cancel", + "common.done": "Done", + "common.exit": "Exit", + "common.save": "Save", + "common.load": "Load", + "common.noReason": "No reason provided", + "entry.subtitle": "Create or join a session.", + "entry.joinTitle": "Join a session", + "entry.sessionCode": "Session code", + "entry.newPlayer": "New player", + "entry.playerName": "Player name", + "entry.takeoverTitle": "Take over dummy", + "entry.alreadyConnected": "You are already connected.", + "entry.dummyId": "Dummy ID (select later)", + "entry.selectDummy": "Select a dummy", + "entry.yourNameOptional": "Your name (optional)", + "entry.requestTakeover": "Request takeover", + "entry.noDummies": "No dummies available yet.", + "entry.createTitle": "Create a session", + "entry.bankerName": "Banker name", + "entry.openVault": "Open the vault", + "entry.alert.enterCode": "Enter a session code", + "entry.alert.sessionNotFound": "Session not found", + "entry.alert.selectDummy": "Select a dummy player", + "lobby.title": "Lobby", + "lobby.code": "Code: {code}", + "lobby.startGame": "Start game", + "lobby.addDummyTitle": "Add dummy player", + "lobby.addDummySubtitle": "Create a player for someone without the app.", + "lobby.enterDummyName": "Enter a dummy name", + "lobby.addDummyButton": "Add dummy", + "session.exit": "Exit game", + "session.exitPrompt": "Leave this game?", + "session.exitMessage": "You can rejoin later with the session code.", + "transfers.title": "Make a transfer", + "transfers.subtitle": "Move funds instantly between players.", + "transfers.from": "From", + "transfers.to": "To", + "transfers.availableBalance": "Available balance", + "transfers.noPlayers": "No other players available yet.", + "transfers.dummy": "Dummy player", + "transfers.player": "Player", + "transfers.amount": "Amount", + "transfers.note": "Note", + "transfers.notePlaceholder": "What is this for?", + "transfers.sending": "Sending", + "transfers.summary": "₦{amount} to {name}", + "transfers.selectPlayer": "Select a player", + "transfers.send": "Send transfer", + "transfers.error": "Choose a player and a valid amount.", + "home.balance": "Balance", + "home.recent": "Recent activity", + "home.noActivity": "No activity yet.", + "blackout.title": "EMP", + "blackout.defaultReason": "EMP in effect", + "blackout.active": "EMP active", + "banker.dashboard.title": "Session activity", + "banker.tools.title": "Banker tools", + "banker.tools.playersTab": "Players", + "banker.tools.adminTab": "Admin", + "banker.tools.playerOverview": "Player overview", + "banker.tools.noPlayers": "No players yet.", + "banker.tools.adjust": "Adjust balance", + "banker.tools.apply": "Apply", + "banker.tools.forceTransfer": "Force transfer", + "banker.tools.force": "Force", + "banker.tools.createDummy": "Create dummy", + "banker.tools.addDummy": "Add dummy", + "banker.tools.blackout": "EMP", + "banker.tools.blackoutActive": "EMP active", + "banker.tools.blackoutReason": "EMP reason", + "banker.tools.blackoutEnable": "Enable EMP", + "banker.tools.blackoutDisable": "Disable EMP", + "banker.tools.trigger": "Trigger", + "banker.tools.endSession": "End session", + "banker.tools.playerId": "Player ID", + "banker.tools.amountAdjust": "Amount (+/-)", + "banker.tools.reason": "Reason", + "banker.tools.fromPlayer": "From player ID", + "banker.tools.toPlayer": "To player ID", + "banker.tools.amount": "Amount", + "banker.tools.note": "Note", + "banker.tools.dummyName": "Dummy name", + "banker.tools.startingBalance": "Starting balance", + "banker.stateTitle": "GameState", + "banker.stateSubtitle": "Export or restore the current session.", + "banker.downloadState": "Export GameState", + "banker.loadFromFile": "Load GameState", + "banker.importPlaceholder": "Paste GameState JSON here", + "banker.loadFromStorage": "Load from saved snapshots", + "banker.stateDownloaded": "GameState exported.", + "banker.stateDownloadError": "Unable to export GameState.", + "banker.stateLoaded": "GameState loaded.", + "banker.stateLoadError": "Unable to load GameState.", + "banker.stateLoadInvalid": "Invalid GameState JSON.", + "banker.autosaveTitle": "AutoSave", + "banker.autosaveSubtitle": "Save rolling snapshots on this device.", + "banker.autosaveEnabled": "Enable AutoSave", + "banker.autosaveInterval": "Minutes between saves", + "banker.autosaveKeep": "Snapshots to keep", + "banker.autosaveNow": "Save now", + "banker.autosaveSaved": "AutoSave captured.", + "banker.autosaveFailed": "AutoSave failed.", + "banker.noAutosaves": "No autosaves yet.", + "banker.savedAt": "Saved {time}", + "chat.title": "Chats", + "chat.noMessages": "No messages yet", + "chat.global": "Global chat", + "chat.newTitle": "New chat", + "chat.direct": "Direct", + "chat.group": "Group", + "chat.groupName": "Group name", + "chat.choosePlayers": "Choose players", + "chat.startChat": "Start chat", + "chat.notFound": "Chat not found.", + "chat.messagePlaceholder": "Message", + "tabs.home": "Home", + "tabs.transfers": "Transfers", + "tabs.chat": "Chat", + "tabs.dashboard": "Dashboard", + "tabs.tools": "Tools", + "transaction.transfer": "Transfer", + "transaction.banker_adjust": "Banker adjustment", + "transaction.banker_force_transfer": "Forced transfer", + "error.parseResponse": "Unable to parse server response", + "error.createSession": "Unable to create session", + "error.joinSession": "Unable to join session", + "error.loadSessionInfo": "Unable to load session info", + "error.connectionNotReady": "Connection not ready", + }, + fr: { + "app.name": "Negopoly Companion", + "common.loading": "Chargement...", + "common.loadingChats": "Chargement des chats...", + "common.loadingChat": "Chargement du chat...", + "common.loadingLobby": "Connexion au lobby...", + "common.notice": "Info :", + "common.online": "en ligne", + "common.offline": "hors ligne", + "common.dummy": "Dummy", + "common.player": "Joueur", + "common.banker": "Banquier", + "common.bank": "Banque", + "common.from": "De", + "common.to": "Vers", + "common.guest": "Invité", + "common.you": "Vous", + "common.join": "Rejoindre", + "common.continue": "Continuer", + "common.send": "Envoyer", + "common.reset": "Réinitialiser", + "common.cancel": "Annuler", + "common.done": "OK", + "common.exit": "Quitter", + "common.save": "Enregistrer", + "common.load": "Charger", + "common.noReason": "Aucune raison fournie", + "entry.subtitle": "Créez ou rejoignez une session.", + "entry.joinTitle": "Rejoindre une session", + "entry.sessionCode": "Code de session", + "entry.newPlayer": "Nouveau joueur", + "entry.playerName": "Nom du joueur", + "entry.takeoverTitle": "Reprendre un dummy", + "entry.alreadyConnected": "Vous êtes déjà connecté.", + "entry.dummyId": "ID du dummy (plus tard)", + "entry.selectDummy": "Sélectionnez un dummy", + "entry.yourNameOptional": "Votre nom (optionnel)", + "entry.requestTakeover": "Demander la reprise", + "entry.noDummies": "Aucun dummy disponible pour le moment.", + "entry.createTitle": "Créer une session", + "entry.bankerName": "Nom du banquier", + "entry.openVault": "Ouvrir le coffre", + "entry.alert.enterCode": "Entrez un code de session", + "entry.alert.sessionNotFound": "Session introuvable", + "entry.alert.selectDummy": "Sélectionnez un dummy", + "lobby.title": "Lobby", + "lobby.code": "Code : {code}", + "lobby.startGame": "Démarrer la partie", + "lobby.addDummyTitle": "Ajouter un dummy", + "lobby.addDummySubtitle": "Créez un joueur pour quelqu'un sans l'application.", + "lobby.enterDummyName": "Entrez un nom de dummy", + "lobby.addDummyButton": "Ajouter un dummy", + "session.exit": "Quitter la partie", + "session.exitPrompt": "Quitter cette session ?", + "session.exitMessage": "Vous pourrez rejoindre plus tard avec le code.", + "transfers.title": "Faire un transfert", + "transfers.subtitle": "Transférez des fonds instantanément entre joueurs.", + "transfers.from": "De", + "transfers.to": "Vers", + "transfers.availableBalance": "Solde disponible", + "transfers.noPlayers": "Aucun autre joueur disponible.", + "transfers.dummy": "Dummy", + "transfers.player": "Joueur", + "transfers.amount": "Montant", + "transfers.note": "Note", + "transfers.notePlaceholder": "Pour quoi ?", + "transfers.sending": "Envoi", + "transfers.summary": "₦{amount} à {name}", + "transfers.selectPlayer": "Choisissez un joueur", + "transfers.send": "Envoyer le transfert", + "transfers.error": "Choisissez un joueur et un montant valide.", + "home.balance": "Solde", + "home.recent": "Activité récente", + "home.noActivity": "Aucune activité.", + "blackout.title": "EMP", + "blackout.defaultReason": "EMP en cours", + "blackout.active": "EMP actif", + "banker.dashboard.title": "Activité de la session", + "banker.tools.title": "Outils banquier", + "banker.tools.playersTab": "Joueurs", + "banker.tools.adminTab": "Admin", + "banker.tools.playerOverview": "Vue joueur", + "banker.tools.noPlayers": "Pas encore de joueurs.", + "banker.tools.adjust": "Ajuster le solde", + "banker.tools.apply": "Appliquer", + "banker.tools.forceTransfer": "Forcer un transfert", + "banker.tools.force": "Forcer", + "banker.tools.createDummy": "Créer un dummy", + "banker.tools.addDummy": "Ajouter un dummy", + "banker.tools.blackout": "EMP", + "banker.tools.blackoutActive": "EMP actif", + "banker.tools.blackoutReason": "Raison de l'EMP", + "banker.tools.blackoutEnable": "Activer l'EMP", + "banker.tools.blackoutDisable": "Désactiver l'EMP", + "banker.tools.trigger": "Déclencher", + "banker.tools.endSession": "Terminer la session", + "banker.tools.playerId": "ID joueur", + "banker.tools.amountAdjust": "Montant (+/-)", + "banker.tools.reason": "Raison", + "banker.tools.fromPlayer": "ID joueur source", + "banker.tools.toPlayer": "ID joueur cible", + "banker.tools.amount": "Montant", + "banker.tools.note": "Note", + "banker.tools.dummyName": "Nom du dummy", + "banker.tools.startingBalance": "Solde de départ", + "banker.stateTitle": "État de partie", + "banker.stateSubtitle": "Exportez ou restaurez la session.", + "banker.downloadState": "Exporter l'état", + "banker.loadFromFile": "Charger l'état", + "banker.importPlaceholder": "Collez le JSON d'état ici", + "banker.loadFromStorage": "Charger depuis les sauvegardes", + "banker.stateDownloaded": "État exporté.", + "banker.stateDownloadError": "Impossible d'exporter l'état.", + "banker.stateLoaded": "État chargé.", + "banker.stateLoadError": "Impossible de charger l'état.", + "banker.stateLoadInvalid": "JSON d'état invalide.", + "banker.autosaveTitle": "AutoSave", + "banker.autosaveSubtitle": "Enregistrez des sauvegardes sur l'appareil.", + "banker.autosaveEnabled": "Activer AutoSave", + "banker.autosaveInterval": "Minutes entre sauvegardes", + "banker.autosaveKeep": "Sauvegardes à conserver", + "banker.autosaveNow": "Sauvegarder maintenant", + "banker.autosaveSaved": "Sauvegarde effectuée.", + "banker.autosaveFailed": "Échec de la sauvegarde.", + "banker.noAutosaves": "Aucune sauvegarde.", + "banker.savedAt": "Sauvé {time}", + "chat.title": "Chats", + "chat.noMessages": "Aucun message", + "chat.global": "Chat global", + "chat.newTitle": "Nouveau chat", + "chat.direct": "Direct", + "chat.group": "Groupe", + "chat.groupName": "Nom du groupe", + "chat.choosePlayers": "Choisir des joueurs", + "chat.startChat": "Démarrer le chat", + "chat.notFound": "Chat introuvable.", + "chat.messagePlaceholder": "Message", + "tabs.home": "Accueil", + "tabs.transfers": "Transferts", + "tabs.chat": "Chat", + "tabs.dashboard": "Tableau", + "tabs.tools": "Outils", + "transaction.transfer": "Transfert", + "transaction.banker_adjust": "Ajustement banquier", + "transaction.banker_force_transfer": "Transfert forcé", + "error.parseResponse": "Impossible de lire la réponse du serveur", + "error.createSession": "Impossible de créer la session", + "error.joinSession": "Impossible de rejoindre la session", + "error.loadSessionInfo": "Impossible de charger les infos de session", + "error.connectionNotReady": "Connexion non prête", + }, +} as const; + +type I18nKey = keyof typeof translations.en; + +export function getLocale(): Locale { + const raw = Intl.DateTimeFormat().resolvedOptions().locale?.toLowerCase() ?? "en"; + return raw.startsWith("fr") ? "fr" : "en"; +} + +function translate(locale: Locale, key: I18nKey, vars?: Record) { + const table = translations[locale] ?? translations.en; + let template = table[key] ?? translations.en[key] ?? key; + if (vars) { + Object.entries(vars).forEach(([name, value]) => { + template = template.replace(new RegExp(`\\{${name}\\}`, "g"), String(value)); + }); + } + return template; +} + +export function useI18n() { + const locale = useMemo(getLocale, []); + const t = useCallback( + (key: I18nKey, vars?: Record) => translate(locale, key, vars), + [locale], + ); + return { t, locale }; +} + +export function tStatic(key: I18nKey, vars?: Record) { + return translate(getLocale(), key, vars); +} + +export function formatTransactionKind( + kind: "transfer" | "banker_adjust" | "banker_force_transfer", + t: (key: I18nKey) => string, +) { + return t(`transaction.${kind}` as I18nKey); +} diff --git a/mobile/src/navigation/AppNavigator.tsx b/mobile/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..12c1a2a --- /dev/null +++ b/mobile/src/navigation/AppNavigator.tsx @@ -0,0 +1,199 @@ +import React from "react"; +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { Ionicons } from "@expo/vector-icons"; +import EntryScreen from "../screens/EntryScreen"; +import LobbyScreen from "../screens/LobbyScreen"; +import PlayerHomeScreen from "../screens/PlayerHomeScreen"; +import PlayerTransfersScreen from "../screens/PlayerTransfersScreen"; +import BankerDashboardScreen from "../screens/BankerDashboardScreen"; +import BankerToolsScreen from "../screens/BankerToolsScreen"; +import ChatListScreen from "../screens/chat/ChatListScreen"; +import ChatThreadScreen from "../screens/chat/ChatThreadScreen"; +import ChatNewScreen from "../screens/chat/ChatNewScreen"; +import { useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import ExitGameButton from "../components/ExitGameButton"; +import type { + BankerTabsParamList, + ChatStackParamList, + PlayerTabsParamList, + RootStackParamList, +} from "./types"; + +const RootStack = createNativeStackNavigator(); +const PlayerTabs = createBottomTabNavigator(); +const BankerTabs = createBottomTabNavigator(); +const ChatStack = createNativeStackNavigator(); + +function ChatStackNavigator() { + const { t } = useI18n(); + const theme = useTheme(); + return ( + , + }} + > + + + + + ); +} + +export function PlayerTabsNavigator() { + const { t } = useI18n(); + const theme = useTheme(); + return ( + , + }} + > + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} + +export function BankerTabsNavigator() { + const { t } = useI18n(); + const theme = useTheme(); + return ( + , + }} + > + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} + +export default function AppNavigator() { + const { t } = useI18n(); + const theme = useTheme(); + return ( + + + + + + + ); +} diff --git a/mobile/src/navigation/types.ts b/mobile/src/navigation/types.ts new file mode 100644 index 0000000..524a2de --- /dev/null +++ b/mobile/src/navigation/types.ts @@ -0,0 +1,24 @@ +export type RootStackParamList = { + Entry: { gameId?: string } | undefined; + Lobby: undefined; + PlayerTabs: undefined; + BankerTabs: undefined; +}; + +export type ChatStackParamList = { + ChatList: undefined; + ChatThread: { chatId: string }; + ChatNew: undefined; +}; + +export type PlayerTabsParamList = { + PlayerHome: undefined; + PlayerTransfers: undefined; + PlayerChat: undefined; +}; + +export type BankerTabsParamList = { + BankerDashboard: undefined; + BankerTools: undefined; + BankerChat: undefined; +}; diff --git a/mobile/src/screens/BankerDashboardScreen.tsx b/mobile/src/screens/BankerDashboardScreen.tsx new file mode 100644 index 0000000..29c320f --- /dev/null +++ b/mobile/src/screens/BankerDashboardScreen.tsx @@ -0,0 +1,181 @@ +import React, { useMemo } from "react"; +import { FlatList, StyleSheet, Text, View } from "react-native"; +import { useSession } from "../state/session-context"; +import { formatTransactionKind, getLocale, useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; +import type { Player, Transaction } from "../shared/types"; + +function formatMoney(amount: number) { + const value = new Intl.NumberFormat(getLocale(), { + maximumFractionDigits: 0, + }).format(amount); + return `₦${value}`; +} + +function formatTransactionTimestamp(value: number) { + const date = new Date(value); + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + const time = date.toLocaleTimeString(getLocale(), { hour: "2-digit", minute: "2-digit" }); + if (sameDay) return time; + const day = date.toLocaleDateString(getLocale(), { month: "short", day: "numeric" }); + return `${day} ${time}`; +} + +function getTransactionLabel( + kind: string, + note: string | null | undefined, + t: ReturnType["t"], +) { + if (kind === "banker_adjust" || kind === "banker_force_transfer") { + const trimmed = note?.trim(); + return trimmed || t("common.noReason"); + } + return formatTransactionKind(kind, t); +} + +function getTransactionDisplay( + transaction: Transaction, + viewerId: string | null | undefined, + players: Player[], + t: ReturnType["t"], +) { + const absAmount = Math.abs(transaction.amount); + const label = getTransactionLabel(transaction.kind, transaction.note, t); + const findPlayer = (id: string | null) => players.find((player) => player.id === id); + const from = findPlayer(transaction.fromId); + const to = findPlayer(transaction.toId); + let outgoing = false; + let counterparty = t("common.bank"); + const timeLabel = formatTransactionTimestamp(transaction.createdAt); + + if (transaction.kind === "banker_adjust") { + outgoing = transaction.amount < 0; + counterparty = t("common.bank"); + } else if (transaction.kind === "transfer" || transaction.kind === "banker_force_transfer") { + if (viewerId && transaction.fromId === viewerId) { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } else if (viewerId && transaction.toId === viewerId) { + outgoing = false; + counterparty = from?.name ?? t("common.player"); + } else { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } + } + + return { + label, + subtitle: viewerId + ? `${outgoing ? t("common.to") : t("common.from")} ${counterparty} · ${timeLabel}` + : transaction.kind === "banker_adjust" + ? outgoing + ? `${to?.name ?? t("common.player")} → ${t("common.bank")} · ${timeLabel}` + : `${t("common.bank")} → ${to?.name ?? t("common.player")} · ${timeLabel}` + : `${from?.name ?? t("common.player")} → ${to?.name ?? t("common.player")} · ${timeLabel}`, + amount: `${outgoing ? "-" : ""}${formatMoney(absAmount)}`, + outgoing, + }; +} + +export default function BankerDashboardScreen() { + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + + if (!manager.session) { + return ( + + {t("common.loading")} + + ); + } + + return ( + + {t("banker.dashboard.title")} + item.id} + contentContainerStyle={styles.list} + renderItem={({ item }) => { + const display = getTransactionDisplay( + item, + null, + manager.session?.players ?? [], + t, + ); + return ( + + + {display.label} + {display.subtitle} + + + {display.amount} + + + ); + }} + ListEmptyComponent={{t("home.noActivity")}} + /> + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: theme.colors.background, + }, + title: { + fontSize: 20, + fontWeight: "700", + marginBottom: 16, + color: theme.colors.text, + }, + list: { + gap: 10, + }, + listItem: { + backgroundColor: theme.colors.surface, + borderRadius: 12, + padding: 12, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + listContent: { + flex: 1, + paddingRight: 12, + }, + listTitle: { + fontWeight: "600", + color: theme.colors.text, + }, + listSubtitle: { + marginTop: 2, + fontSize: 12, + color: theme.colors.textMuted, + }, + listAmount: { + fontWeight: "600", + color: theme.colors.primary, + }, + amountNegative: { + color: theme.colors.danger, + }, + helper: { + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/BankerToolsScreen.tsx b/mobile/src/screens/BankerToolsScreen.tsx new file mode 100644 index 0000000..11a55a9 --- /dev/null +++ b/mobile/src/screens/BankerToolsScreen.tsx @@ -0,0 +1,1057 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + Alert, + Pressable, + ScrollView, + Share, + StyleSheet, + Switch, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { useSession } from "../state/session-context"; +import { formatTransactionKind, getLocale, useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; +import type { Player, SessionSnapshot, Transaction } from "../shared/types"; +import { getApiBaseUrl } from "../config/api"; + +function initials(value: string) { + return value + .split(" ") + .filter(Boolean) + .map((part) => part[0]) + .join("") + .slice(0, 2) + .toUpperCase(); +} + +function formatMoney(amount: number) { + return `₦${amount.toLocaleString()}`; +} + +function formatTransactionTimestamp(value: number) { + const date = new Date(value); + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + const time = date.toLocaleTimeString(getLocale(), { hour: "2-digit", minute: "2-digit" }); + if (sameDay) return time; + const day = date.toLocaleDateString(getLocale(), { month: "short", day: "numeric" }); + return `${day} ${time}`; +} + +function getTransactionLabel( + kind: string, + note: string | null | undefined, + t: ReturnType["t"], +) { + if (kind === "banker_adjust" || kind === "banker_force_transfer") { + const trimmed = note?.trim(); + return trimmed || t("common.noReason"); + } + return formatTransactionKind(kind, t); +} + +function getTransactionDisplay( + transaction: Transaction, + viewerId: string | null | undefined, + players: Player[], + t: ReturnType["t"], +) { + const absAmount = Math.abs(transaction.amount); + const label = getTransactionLabel(transaction.kind, transaction.note, t); + const findPlayer = (id: string | null) => players.find((player) => player.id === id); + const from = findPlayer(transaction.fromId); + const to = findPlayer(transaction.toId); + let outgoing = false; + let counterparty = t("common.bank"); + const timeLabel = formatTransactionTimestamp(transaction.createdAt); + + if (transaction.kind === "banker_adjust") { + outgoing = transaction.amount < 0; + counterparty = t("common.bank"); + } else if (transaction.kind === "transfer" || transaction.kind === "banker_force_transfer") { + if (viewerId && transaction.fromId === viewerId) { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } else if (viewerId && transaction.toId === viewerId) { + outgoing = false; + counterparty = from?.name ?? t("common.player"); + } else { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } + } + + return { + label, + subtitle: viewerId + ? `${outgoing ? t("common.to") : t("common.from")} ${counterparty} · ${timeLabel}` + : transaction.kind === "banker_adjust" + ? outgoing + ? `${to?.name ?? t("common.player")} → ${t("common.bank")} · ${timeLabel}` + : `${t("common.bank")} → ${to?.name ?? t("common.player")} · ${timeLabel}` + : `${from?.name ?? t("common.player")} → ${to?.name ?? t("common.player")} · ${timeLabel}`, + amount: `${outgoing ? "-" : ""}${formatMoney(absAmount)}`, + outgoing, + }; +} + +export default function BankerToolsScreen() { + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + const [tab, setTab] = useState<"players" | "admin">("players"); + const [selectedPlayerId, setSelectedPlayerId] = useState(""); + const [adjustAmount, setAdjustAmount] = useState(""); + const [adjustNote, setAdjustNote] = useState(""); + const [forceTo, setForceTo] = useState(""); + const [forceAmount, setForceAmount] = useState(""); + const [forceNote, setForceNote] = useState(""); + const [dummyName, setDummyName] = useState(""); + const [dummyBalance, setDummyBalance] = useState("1500"); + const [blackoutReason, setBlackoutReason] = useState(""); + const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); + const [autoSaveInterval, setAutoSaveInterval] = useState("3"); + const [autoSaveLimit, setAutoSaveLimit] = useState("5"); + const [autoSaveEntries, setAutoSaveEntries] = useState< + { id: string; savedAt: number; state: SessionSnapshot }[] + >([]); + const [importJson, setImportJson] = useState(""); + const [autoSaveStatus, setAutoSaveStatus] = useState(null); + + const autoSaveKey = `negopoly:autosave:${manager.sessionId}`; + const autoSaveSettingsKey = `${autoSaveKey}:settings`; + + useEffect(() => { + if (!manager.sessionId) return; + let mounted = true; + async function loadSettings() { + try { + const raw = await AsyncStorage.getItem(autoSaveSettingsKey); + if (!raw || !mounted) return; + const settings = JSON.parse(raw) as { + enabled?: boolean; + intervalMinutes?: number; + maxEntries?: number; + }; + setAutoSaveEnabled(Boolean(settings.enabled)); + if (settings.intervalMinutes) { + setAutoSaveInterval(String(settings.intervalMinutes)); + } + if (settings.maxEntries) { + setAutoSaveLimit(String(settings.maxEntries)); + } + } catch { + // ignore invalid settings + } + } + + async function loadEntries() { + try { + const raw = await AsyncStorage.getItem(autoSaveKey); + if (!raw || !mounted) return; + const entries = JSON.parse(raw) as { + id: string; + savedAt: number; + state: SessionSnapshot; + }[]; + setAutoSaveEntries(entries); + } catch { + setAutoSaveEntries([]); + } + } + + loadSettings(); + loadEntries(); + + return () => { + mounted = false; + }; + }, [autoSaveKey, autoSaveSettingsKey, manager.sessionId]); + + useEffect(() => { + if (!manager.sessionId) return; + async function persistSettings() { + const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3); + const maxEntries = Math.max(1, Number(autoSaveLimit) || 5); + await AsyncStorage.setItem( + autoSaveSettingsKey, + JSON.stringify({ + enabled: autoSaveEnabled, + intervalMinutes, + maxEntries, + }), + ); + } + persistSettings(); + }, [autoSaveEnabled, autoSaveInterval, autoSaveLimit, autoSaveSettingsKey, manager.sessionId]); + + async function fetchGameState(): Promise { + if (!manager.sessionId || !manager.me?.id) return null; + const response = await fetch( + `${getApiBaseUrl()}/api/session/${manager.sessionId}/state?bankerId=${manager.me?.id}`, + ); + if (!response.ok) return null; + return (await response.json()) as SessionSnapshot; + } + + async function persistAutoSaves( + entries: { id: string; savedAt: number; state: SessionSnapshot }[], + ) { + if (!manager.sessionId) return; + await AsyncStorage.setItem(autoSaveKey, JSON.stringify(entries)); + } + + async function handleAutoSaveNow() { + const snapshot = await fetchGameState(); + if (!snapshot) { + setAutoSaveStatus(t("banker.autosaveFailed")); + return; + } + const maxEntries = Math.max(1, Number(autoSaveLimit) || 5); + const entry = { + id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, + savedAt: Date.now(), + state: snapshot, + }; + setAutoSaveEntries((prev) => { + const next = [entry, ...prev].slice(0, maxEntries); + persistAutoSaves(next); + return next; + }); + setAutoSaveStatus(t("banker.autosaveSaved")); + } + + useEffect(() => { + if (!autoSaveEnabled || !manager.sessionId) return; + const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3); + const intervalMs = intervalMinutes * 60 * 1000; + const timer = setInterval(() => { + handleAutoSaveNow(); + }, intervalMs); + return () => clearInterval(timer); + }, [autoSaveEnabled, autoSaveInterval, autoSaveLimit, manager.sessionId]); + + async function handleExport() { + const snapshot = await fetchGameState(); + if (!snapshot) { + Alert.alert(t("banker.stateDownloadError")); + return; + } + await Share.share({ + message: JSON.stringify(snapshot, null, 2), + title: t("banker.downloadState"), + }); + setAutoSaveStatus(t("banker.stateDownloaded")); + } + + async function handleLoadState(snapshot: SessionSnapshot) { + if (!manager.sessionId || !manager.me?.id) return; + const response = await fetch(`${getApiBaseUrl()}/api/session/${manager.sessionId}/state`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bankerId: manager.me?.id, state: snapshot }), + }); + if (!response.ok) { + Alert.alert(t("banker.stateLoadError")); + return; + } + Alert.alert(t("banker.stateLoaded")); + } + + function handleImportJson() { + if (!importJson.trim()) return; + try { + const parsed = JSON.parse(importJson) as SessionSnapshot; + handleLoadState(parsed); + } catch { + Alert.alert(t("banker.stateLoadInvalid")); + } + } + + const eligiblePlayers = useMemo( + () => manager.session?.players.filter((player) => player.role !== "banker") ?? [], + [manager.session], + ); + const selectedPlayer = + eligiblePlayers.find((player) => player.id === selectedPlayerId) ?? null; + const otherPlayers = eligiblePlayers.filter((player) => player.id !== selectedPlayerId); + const playerTransactions = + manager.session?.transactions.filter( + (transaction) => + transaction.fromId === selectedPlayerId || transaction.toId === selectedPlayerId, + ) ?? []; + + useEffect(() => { + if (eligiblePlayers.length === 0) { + if (selectedPlayerId) { + setSelectedPlayerId(""); + } + return; + } + if (!selectedPlayerId || !eligiblePlayers.some((player) => player.id === selectedPlayerId)) { + setSelectedPlayerId(eligiblePlayers[0].id); + } + }, [eligiblePlayers, selectedPlayerId]); + + useEffect(() => { + if (otherPlayers.length === 0) { + if (forceTo) setForceTo(""); + return; + } + if (!forceTo || forceTo === selectedPlayerId) { + setForceTo(otherPlayers[0].id); + } + }, [otherPlayers, forceTo, selectedPlayerId]); + + if (!manager.session || !manager.me) { + return ( + + {t("common.loading")} + + ); + } + + const normalizedAdjustAmount = adjustAmount.replace(",", "."); + const adjustValue = Number(normalizedAdjustAmount); + const canAdjust = + Boolean(selectedPlayerId) && Number.isFinite(adjustValue) && adjustValue !== 0; + + const normalizedForceAmount = forceAmount.replace(",", "."); + const forceValue = Number(normalizedForceAmount); + const canForce = + Boolean(selectedPlayerId) && + Boolean(forceTo) && + Number.isFinite(forceValue) && + forceValue > 0; + + return ( + + {t("banker.tools.title")} + + + {[ + { id: "players", label: t("banker.tools.playersTab") }, + { id: "admin", label: t("banker.tools.adminTab") }, + ].map((item) => ( + setTab(item.id as "players" | "admin")} + > + + {item.label} + + + ))} + + + {tab === "players" ? ( + <> + + {t("banker.tools.playersTab")} + {eligiblePlayers.length === 0 ? ( + {t("banker.tools.noPlayers")} + ) : ( + + {eligiblePlayers.map((player) => { + const active = player.id === selectedPlayerId; + return ( + setSelectedPlayerId(player.id)} + style={({ pressed }) => [ + styles.playerCard, + active ? styles.playerCardActive : null, + pressed ? styles.playerCardPressed : null, + ]} + > + + + {initials(player.name)} + + + {player.name} + + {player.isDummy ? t("common.dummy") : t("common.player")} ·{" "} + {player.connected ? t("common.online") : t("common.offline")} + + + + {formatMoney(player.balance)} + + + + ); + })} + + )} + + + + {t("banker.tools.playerOverview")} + {selectedPlayer ? ( + <> + + + {selectedPlayer.name} + + {selectedPlayer.isDummy ? t("common.dummy") : t("common.player")} ·{" "} + {selectedPlayer.connected ? t("common.online") : t("common.offline")} + + + {formatMoney(selectedPlayer.balance)} + + + {t("common.transactions")} + {playerTransactions.length === 0 ? ( + {t("home.noActivity")} + ) : ( + + {playerTransactions.map((transaction) => { + const display = getTransactionDisplay( + transaction, + selectedPlayerId, + manager.session?.players ?? [], + t, + ); + return ( + + + {display.label} + {display.subtitle} + + + {display.amount} + + + ); + })} + + )} + + ) : ( + {t("banker.tools.noPlayers")} + )} + + + + {t("banker.tools.adjust")} + + {selectedPlayer?.name ?? t("transfers.selectPlayer")} + + + + { + if (!selectedPlayerId || !Number.isFinite(adjustValue)) { + Alert.alert(t("transfers.error")); + return; + } + manager.sendMessage({ + type: "banker_adjust", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + targetId: selectedPlayerId, + amount: adjustValue, + note: adjustNote, + }); + setAdjustAmount(""); + setAdjustNote(""); + }} + > + {t("banker.tools.apply")} + + + + + {t("banker.tools.forceTransfer")} + + {t("transfers.from")} {selectedPlayer?.name ?? t("transfers.selectPlayer")} + + {otherPlayers.length === 0 ? ( + {t("transfers.noPlayers")} + ) : ( + + {otherPlayers.map((player) => { + const active = player.id === forceTo; + return ( + setForceTo(player.id)} + style={({ pressed }) => [ + styles.recipientCard, + active ? styles.recipientCardActive : null, + pressed ? styles.recipientCardPressed : null, + ]} + > + + + {initials(player.name)} + + + {player.name} + + {player.isDummy ? t("common.dummy") : t("common.player")} + + + + + + ); + })} + + )} + + + { + if (!selectedPlayerId || !forceTo || !Number.isFinite(forceValue)) { + Alert.alert(t("transfers.error")); + return; + } + manager.sendMessage({ + type: "banker_force_transfer", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + fromId: selectedPlayerId, + toId: forceTo, + amount: forceValue, + note: forceNote, + }); + setForceAmount(""); + setForceNote(""); + }} + > + {t("banker.tools.force")} + + + + + {t("banker.tools.createDummy")} + + + { + manager.sendMessage({ + type: "banker_create_dummy", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + name: dummyName, + balance: Number(dummyBalance) || undefined, + }); + setDummyName(""); + setDummyBalance("1500"); + }} + > + {t("banker.tools.addDummy")} + + + + ) : ( + <> + + {t("banker.tools.blackout")} + + {t("banker.tools.blackoutActive")} + + manager.sendMessage({ + type: "banker_blackout", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + active: value, + reason: value ? blackoutReason : null, + }) + } + /> + + + + manager.sendMessage({ + type: "banker_blackout", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + active: !manager.session.blackoutActive, + reason: !manager.session.blackoutActive ? blackoutReason : null, + }) + } + > + + {manager.session.blackoutActive + ? t("banker.tools.blackoutDisable") + : t("banker.tools.blackoutEnable")} + + + + manager.sendMessage({ + type: "banker_end", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + }) + } + > + {t("banker.tools.endSession")} + + + + + {t("banker.stateTitle")} + {t("banker.stateSubtitle")} + + {t("banker.downloadState")} + + + + + {t("banker.loadFromFile")} + + + {autoSaveEntries.length > 0 ? ( + + {autoSaveEntries.map((entry) => ( + + + {t("banker.savedAt", { time: new Date(entry.savedAt).toLocaleString() })} + + handleLoadState(entry.state)} + > + {t("common.load")} + + + ))} + + ) : ( + {t("banker.noAutosaves")} + )} + + {autoSaveStatus ? {autoSaveStatus} : null} + + + + {t("banker.autosaveTitle")} + {t("banker.autosaveSubtitle")} + + + {t("banker.autosaveEnabled")} + + + + + + + {t("banker.autosaveNow")} + + + + )} + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + scroll: { + flex: 1, + backgroundColor: theme.colors.background, + }, + container: { + padding: 20, + gap: 16, + }, + title: { + fontSize: 20, + fontWeight: "700", + color: theme.colors.text, + }, + tabRow: { + flexDirection: "row", + gap: 8, + padding: 4, + borderRadius: 999, + backgroundColor: theme.colors.surface, + borderWidth: 1, + borderColor: theme.colors.border, + }, + tabButton: { + flex: 1, + paddingVertical: 8, + borderRadius: 999, + alignItems: "center", + }, + tabButtonActive: { + backgroundColor: theme.colors.primary, + }, + tabText: { + color: theme.colors.textMuted, + fontWeight: "600", + }, + tabTextActive: { + color: theme.colors.primaryText, + }, + card: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 16, + gap: 10, + }, + cardTitle: { + fontWeight: "600", + color: theme.colors.text, + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.inputText, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + }, + buttonDisabled: { + opacity: 0.6, + }, + button: { + backgroundColor: theme.colors.primary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + buttonSecondary: { + backgroundColor: theme.colors.secondary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonSecondaryText: { + color: theme.colors.secondaryText, + fontWeight: "600", + }, + buttonDanger: { + backgroundColor: theme.colors.danger, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonDangerText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + textarea: { + minHeight: 120, + }, + playerList: { + gap: 10, + }, + playerCard: { + borderRadius: 14, + padding: 12, + backgroundColor: theme.colors.surfaceAlt, + borderWidth: 1, + borderColor: "transparent", + }, + playerCardActive: { + borderColor: theme.colors.accent, + backgroundColor: theme.colors.accentSurface, + }, + playerCardPressed: { + opacity: 0.9, + }, + playerRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + avatarBadge: { + width: 40, + height: 40, + borderRadius: 14, + backgroundColor: theme.colors.avatarSurface, + alignItems: "center", + justifyContent: "center", + }, + avatarText: { + color: theme.colors.avatarText, + fontWeight: "700", + }, + playerMeta: { + flex: 1, + gap: 2, + }, + playerName: { + fontWeight: "600", + color: theme.colors.text, + }, + playerSub: { + fontSize: 12, + color: theme.colors.textMuted, + }, + balancePill: { + backgroundColor: theme.colors.chipBackground, + borderRadius: 999, + paddingHorizontal: 10, + paddingVertical: 4, + borderWidth: 1, + borderColor: theme.colors.chipBorder, + }, + balanceText: { + fontSize: 12, + fontWeight: "700", + color: theme.colors.chipText, + }, + summaryRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: 12, + borderRadius: 14, + backgroundColor: theme.colors.brandSurface, + }, + summaryName: { + fontSize: 16, + fontWeight: "700", + color: theme.colors.brandText, + }, + summarySub: { + color: theme.colors.brandTextMuted, + fontSize: 12, + }, + summaryBalance: { + fontSize: 16, + fontWeight: "700", + color: theme.colors.brandText, + }, + sectionLabel: { + fontSize: 12, + fontWeight: "700", + textTransform: "uppercase", + letterSpacing: 1, + color: theme.colors.textMuted, + }, + transactionList: { + gap: 8, + }, + transactionScroll: { + maxHeight: 240, + }, + transactionItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: theme.colors.borderMuted, + }, + transactionInfo: { + flex: 1, + paddingRight: 12, + }, + transactionTitle: { + fontWeight: "600", + color: theme.colors.text, + }, + transactionSub: { + fontSize: 12, + color: theme.colors.textMuted, + }, + transactionAmount: { + fontWeight: "600", + color: theme.colors.primary, + }, + amountNegative: { + color: theme.colors.danger, + }, + recipientList: { + gap: 10, + }, + recipientCard: { + borderRadius: 14, + padding: 12, + backgroundColor: theme.colors.surfaceAlt, + borderWidth: 1, + borderColor: "transparent", + }, + recipientCardActive: { + borderColor: theme.colors.accent, + backgroundColor: theme.colors.accentSurface, + }, + recipientCardPressed: { + opacity: 0.9, + }, + recipientRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + recipientAvatar: { + width: 38, + height: 38, + borderRadius: 12, + backgroundColor: theme.colors.listAvatarBackground, + alignItems: "center", + justifyContent: "center", + }, + recipientInitials: { + fontWeight: "700", + color: theme.colors.listAvatarText, + }, + recipientMeta: { + flex: 1, + gap: 2, + }, + recipientName: { + fontWeight: "600", + color: theme.colors.text, + }, + recipientSub: { + fontSize: 12, + color: theme.colors.textMuted, + }, + radio: { + width: 12, + height: 12, + borderRadius: 6, + borderWidth: 2, + borderColor: theme.colors.radioBorder, + }, + radioActive: { + borderColor: theme.colors.accent, + backgroundColor: theme.colors.accent, + }, + autosaveList: { + gap: 8, + }, + autosaveItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 6, + borderBottomWidth: 1, + borderBottomColor: theme.colors.borderMuted, + }, + buttonSmall: { + backgroundColor: theme.colors.secondary, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + }, + buttonSmallText: { + color: theme.colors.secondaryText, + fontWeight: "600", + fontSize: 12, + }, + switchRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + helper: { + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/EntryScreen.tsx b/mobile/src/screens/EntryScreen.tsx new file mode 100644 index 0000000..9a41eb9 --- /dev/null +++ b/mobile/src/screens/EntryScreen.tsx @@ -0,0 +1,391 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { + Alert, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useNavigation, useRoute } from "@react-navigation/native"; +import type { RouteProp } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "../navigation/types"; +import { useSession } from "../state/session-context"; +import type { SessionPreview } from "../shared/types"; +import { useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; + +export default function EntryScreen() { + const navigation = useNavigation>(); + const route = useRoute>(); + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + const handledLinkRef = useRef(null); + const insets = useSafeAreaInsets(); + const contentStyle = useMemo( + () => [ + styles.container, + { + paddingTop: insets.top + 20, + paddingBottom: insets.bottom + 20, + paddingLeft: insets.left + 20, + paddingRight: insets.right + 20, + }, + ], + [styles.container, insets.top, insets.bottom, insets.left, insets.right], + ); + const [createName, setCreateName] = useState(""); + const [joinCode, setJoinCode] = useState(""); + const [joinStep, setJoinStep] = useState<"code" | "choice">("code"); + const [joinPreview, setJoinPreview] = useState(null); + const [joinName, setJoinName] = useState(""); + const [takeoverName, setTakeoverName] = useState(""); + const [takeoverDummyId, setTakeoverDummyId] = useState(""); + const [showDummyOptions, setShowDummyOptions] = useState(false); + + const dummyOptions = useMemo( + () => joinPreview?.players.filter((player) => player.isDummy) ?? [], + [joinPreview], + ); + const storedPlayer = joinPreview?.players.find((player) => player.id === manager.playerId); + const takeoverDisabled = storedPlayer?.connected === true; + + async function handleCreate() { + const data = await manager.createSession(createName.trim()); + if (data) { + navigation.replace("Lobby"); + } + } + + async function handleJoinPreview() { + if (!joinCode.trim()) { + Alert.alert(t("entry.alert.enterCode")); + return; + } + const preview = await manager.fetchSessionPreview(joinCode.trim().toUpperCase()); + if (!preview) { + Alert.alert(t("entry.alert.sessionNotFound")); + return; + } + setJoinPreview(preview); + setJoinStep("choice"); + } + + useEffect(() => { + const raw = route.params?.gameId; + if (typeof raw !== "string") return; + const normalized = raw.trim(); + if (!normalized) { + if (__DEV__) { + console.log("[deep-link] invalid gameId"); + } + return; + } + const code = normalized.toUpperCase(); + if (handledLinkRef.current === code) return; + handledLinkRef.current = code; + if (__DEV__) { + console.log(`[deep-link] navigating to session ${code}`); + } + setJoinCode(code); + setJoinStep("code"); + setJoinPreview(null); + setJoinName(""); + setTakeoverName(""); + setTakeoverDummyId(""); + manager.fetchSessionPreview(code).then((preview) => { + if (!preview) { + Alert.alert(t("entry.alert.sessionNotFound")); + return; + } + setJoinPreview(preview); + setJoinStep("choice"); + }); + }, [route.params?.gameId, manager, t]); + + async function handleJoinNew() { + if (!joinPreview) return; + const data = await manager.joinSession(joinPreview.code, joinName.trim()); + if (data) { + navigation.replace("Lobby"); + } + } + + async function handleTakeover() { + if (!joinPreview) return; + if (!takeoverDummyId) { + Alert.alert(t("entry.alert.selectDummy")); + return; + } + const data = await manager.joinSession( + joinPreview.code, + takeoverName.trim() || t("common.guest"), + ); + if (data) { + manager.setPendingTakeoverId(takeoverDummyId); + navigation.replace("Lobby"); + } + } + + useEffect(() => { + if (joinStep === "code" || !joinPreview) { + setShowDummyOptions(false); + } + }, [joinStep, joinPreview]); + + return ( + + {t("app.name")} + {t("entry.subtitle")} + + + {t("entry.joinTitle")} + { + setJoinCode(value.toUpperCase()); + if (joinStep === "choice") { + setJoinStep("code"); + setJoinPreview(null); + setJoinName(""); + setTakeoverName(""); + setTakeoverDummyId(""); + setShowDummyOptions(false); + } + }} + /> + + {joinStep === "code" ? ( + + {t("common.continue")} + + ) : null} + + {joinStep === "choice" && joinPreview ? ( + + + {t("entry.newPlayer")} + + + {t("common.join")} + + + + + {t("entry.takeoverTitle")} + {takeoverDisabled ? ( + {t("entry.alreadyConnected")} + ) : ( + <> + + { + if (dummyOptions.length === 0) return; + setShowDummyOptions((prev) => !prev); + }} + > + + {dummyOptions.find((player) => player.id === takeoverDummyId)?.name + ? `${dummyOptions.find((player) => player.id === takeoverDummyId)?.name} · ${takeoverDummyId}` + : t("entry.selectDummy")} + + + {showDummyOptions && dummyOptions.length > 0 ? ( + + {dummyOptions.map((player) => ( + { + setTakeoverDummyId(player.id); + setShowDummyOptions(false); + }} + > + {player.name} + {player.id} + + ))} + + ) : null} + + + + {t("entry.requestTakeover")} + + + )} + {!takeoverDisabled && dummyOptions.length === 0 ? ( + {t("entry.noDummies")} + ) : null} + + + ) : null} + + + + {t("entry.createTitle")} + + + {t("entry.openVault")} + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + scroll: { + flex: 1, + backgroundColor: theme.colors.background, + }, + container: { + padding: 0, + gap: 16, + }, + title: { + fontSize: 28, + fontWeight: "700", + color: theme.colors.text, + }, + subtitle: { + fontSize: 16, + color: theme.colors.textMuted, + }, + card: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 16, + shadowColor: "#000", + shadowOpacity: theme.dark ? 0.2 : 0.08, + shadowRadius: 12, + shadowOffset: { width: 0, height: 6 }, + }, + cardTitle: { + fontSize: 18, + fontWeight: "600", + marginBottom: 12, + color: theme.colors.text, + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.inputText, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + marginBottom: 10, + }, + dropdown: { + gap: 6, + marginBottom: 10, + }, + dropdownButton: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + }, + dropdownText: { + color: theme.colors.inputText, + fontWeight: "600", + }, + dropdownList: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 12, + backgroundColor: theme.colors.surface, + overflow: "hidden", + }, + dropdownItem: { + paddingHorizontal: 12, + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: theme.colors.borderMuted, + }, + dropdownItemActive: { + backgroundColor: theme.colors.accentSurface, + }, + dropdownItemText: { + fontWeight: "600", + color: theme.colors.text, + }, + dropdownItemMeta: { + color: theme.colors.textMuted, + fontSize: 12, + }, + button: { + backgroundColor: theme.colors.primary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + buttonSecondary: { + backgroundColor: theme.colors.secondary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonSecondaryText: { + color: theme.colors.secondaryText, + fontWeight: "600", + }, + choiceGrid: { + marginTop: 12, + gap: 12, + }, + choiceCard: { + backgroundColor: theme.colors.surfaceAlt, + borderRadius: 12, + padding: 12, + }, + choiceTitle: { + fontWeight: "600", + marginBottom: 8, + color: theme.colors.text, + }, + helper: { + fontSize: 12, + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/LobbyScreen.tsx b/mobile/src/screens/LobbyScreen.tsx new file mode 100644 index 0000000..552b608 --- /dev/null +++ b/mobile/src/screens/LobbyScreen.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + FlatList, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { RootStackParamList } from "../navigation/types"; +import { useSession } from "../state/session-context"; +import { useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; +import ExitGameButton from "../components/ExitGameButton"; + +export default function LobbyScreen() { + const navigation = useNavigation>(); + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + const [dummyName, setDummyName] = useState(""); + const [dummyBalance, setDummyBalance] = useState("1500"); + const insets = useSafeAreaInsets(); + const topInset = insets.top || (Platform.OS === "ios" ? 44 : 0); + const containerStyle = useMemo( + () => [ + styles.container, + { + paddingTop: topInset + 20, + paddingBottom: insets.bottom + 20, + paddingLeft: insets.left + 20, + paddingRight: insets.right + 20, + }, + ], + [ + styles.container, + topInset, + insets.bottom, + insets.left, + insets.right, + ], + ); + + useEffect(() => { + if (!manager.session || !manager.me) return; + if (manager.session.status === "active") { + navigation.replace(manager.isBanker ? "BankerTabs" : "PlayerTabs"); + } + }, [manager.session, manager.me, manager.isBanker, navigation]); + + if (!manager.session || !manager.me) { + return ( + + {t("common.loadingLobby")} + {manager.error ? {manager.error} : null} + + + ); + } + + const canStart = manager.isBanker && manager.session.status === "lobby"; + + return ( + + {t("lobby.title")} + {t("lobby.code", { code: manager.session.code })} + + item.id} + contentContainerStyle={styles.list} + renderItem={({ item }) => ( + + + {item.name} + + {item.role === "banker" ? t("common.banker") : t("common.player")}{" "} + {item.isDummy ? `- ${t("common.dummy")}` : ""} + + + + {item.connected ? t("common.online") : t("common.offline")} + + + )} + /> + + {manager.isBanker && manager.session.status === "lobby" && ( + + {t("lobby.addDummyTitle")} + {t("lobby.addDummySubtitle")} + + + { + manager.sendMessage({ + type: "banker_create_dummy", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + name: dummyName, + balance: Number(dummyBalance) || undefined, + }); + setDummyName(""); + setDummyBalance("1500"); + }} + > + {t("lobby.addDummyButton")} + + + )} + + {canStart && ( + + manager.sendMessage({ + type: "banker_start", + sessionId: manager.sessionId, + bankerId: manager.me?.id, + }) + } + > + {t("lobby.startGame")} + + )} + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 0, + paddingBottom: 0, + gap: 12, + backgroundColor: theme.colors.background, + }, + title: { + fontSize: 24, + fontWeight: "700", + color: theme.colors.text, + }, + subtitle: { + color: theme.colors.textMuted, + }, + list: { + gap: 10, + paddingBottom: 20, + }, + listItem: { + backgroundColor: theme.colors.surface, + borderRadius: 12, + padding: 12, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + playerName: { + fontWeight: "600", + color: theme.colors.text, + }, + playerMeta: { + fontSize: 12, + color: theme.colors.textMuted, + }, + card: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 16, + gap: 10, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + }, + cardTitle: { + fontWeight: "600", + color: theme.colors.text, + }, + helper: { + color: theme.colors.textMuted, + fontSize: 12, + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.inputText, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + }, + button: { + backgroundColor: theme.colors.primary, + paddingVertical: 14, + borderRadius: 999, + alignItems: "center", + }, + buttonText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + buttonSecondary: { + backgroundColor: theme.colors.secondary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + }, + buttonSecondaryText: { + color: theme.colors.secondaryText, + fontWeight: "600", + }, + }); diff --git a/mobile/src/screens/PlayerHomeScreen.tsx b/mobile/src/screens/PlayerHomeScreen.tsx new file mode 100644 index 0000000..dd96f8f --- /dev/null +++ b/mobile/src/screens/PlayerHomeScreen.tsx @@ -0,0 +1,222 @@ +import React, { useMemo, useState } from "react"; +import { FlatList, StyleSheet, Text, View } from "react-native"; +import { useSession } from "../state/session-context"; +import EmpOverlay from "../components/EmpOverlay"; +import { formatTransactionKind, getLocale, useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; +import type { Player, Transaction } from "../shared/types"; + +function formatMoney(amount: number) { + const value = new Intl.NumberFormat(getLocale(), { + maximumFractionDigits: 0, + }).format(amount); + return `₦${value}`; +} + +function formatTransactionTimestamp(value: number) { + const date = new Date(value); + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + const time = date.toLocaleTimeString(getLocale(), { hour: "2-digit", minute: "2-digit" }); + if (sameDay) return time; + const day = date.toLocaleDateString(getLocale(), { month: "short", day: "numeric" }); + return `${day} ${time}`; +} + +function getTransactionLabel( + kind: string, + note: string | null | undefined, + t: ReturnType["t"], +) { + if (kind === "banker_adjust" || kind === "banker_force_transfer") { + const trimmed = note?.trim(); + return trimmed || t("common.noReason"); + } + return formatTransactionKind(kind, t); +} + +function getTransactionDisplay( + transaction: Transaction, + viewerId: string | null | undefined, + players: Player[], + t: ReturnType["t"], +) { + const absAmount = Math.abs(transaction.amount); + const label = getTransactionLabel(transaction.kind, transaction.note, t); + const findPlayer = (id: string | null) => players.find((player) => player.id === id); + const from = findPlayer(transaction.fromId); + const to = findPlayer(transaction.toId); + let outgoing = false; + let counterparty = t("common.bank"); + const timeLabel = formatTransactionTimestamp(transaction.createdAt); + + if (transaction.kind === "banker_adjust") { + outgoing = transaction.amount < 0; + counterparty = t("common.bank"); + } else if (transaction.kind === "transfer" || transaction.kind === "banker_force_transfer") { + if (viewerId && transaction.fromId === viewerId) { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } else if (viewerId && transaction.toId === viewerId) { + outgoing = false; + counterparty = from?.name ?? t("common.player"); + } else { + outgoing = true; + counterparty = to?.name ?? t("common.player"); + } + } + + return { + label, + subtitle: viewerId + ? `${outgoing ? t("common.to") : t("common.from")} ${counterparty} · ${timeLabel}` + : transaction.kind === "banker_adjust" + ? outgoing + ? `${to?.name ?? t("common.player")} → ${t("common.bank")} · ${timeLabel}` + : `${t("common.bank")} → ${to?.name ?? t("common.player")} · ${timeLabel}` + : `${from?.name ?? t("common.player")} → ${to?.name ?? t("common.player")} · ${timeLabel}`, + amount: `${outgoing ? "-" : ""}${formatMoney(absAmount)}`, + outgoing, + }; +} + +export default function PlayerHomeScreen() { + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const [visibleCount, setVisibleCount] = useState(20); + + if (!manager.session || !manager.me) { + return ( + + {t("common.loading")} + + ); + } + + const myTransactions = manager.session.transactions.filter( + (tx) => tx.fromId === manager.me?.id || tx.toId === manager.me?.id, + ); + const visibleTransactions = myTransactions.slice(0, visibleCount); + + const showEmp = manager.session.blackoutActive && !manager.isBanker; + + return ( + + + {t("home.balance")} + {formatMoney(manager.me.balance)} + + + {t("home.recent")} + item.id} + contentContainerStyle={styles.list} + initialNumToRender={20} + windowSize={7} + onEndReachedThreshold={0.4} + onEndReached={() => { + if (visibleCount >= myTransactions.length) return; + setVisibleCount((count) => Math.min(count + 20, myTransactions.length)); + }} + renderItem={({ item }) => { + const display = getTransactionDisplay( + item, + manager.me?.id, + manager.session?.players ?? [], + t, + ); + return ( + + + {display.label} + {display.subtitle} + + + {display.amount} + + + ); + }} + ListEmptyComponent={{t("home.noActivity")}} + /> + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: theme.colors.background, + position: "relative", + }, + balanceCard: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 18, + marginBottom: 20, + }, + balanceLabel: { + fontSize: 14, + color: theme.colors.textMuted, + }, + balanceValue: { + fontSize: 28, + fontWeight: "700", + color: theme.colors.text, + }, + sectionTitle: { + fontSize: 16, + fontWeight: "600", + marginBottom: 12, + color: theme.colors.text, + }, + list: { + gap: 10, + }, + listItem: { + backgroundColor: theme.colors.surface, + borderRadius: 12, + padding: 12, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + listContent: { + flex: 1, + paddingRight: 12, + }, + listTitle: { + fontWeight: "600", + color: theme.colors.text, + }, + listSubtitle: { + marginTop: 2, + fontSize: 12, + color: theme.colors.textMuted, + }, + listAmount: { + fontWeight: "600", + color: theme.colors.primary, + }, + amountNegative: { + color: theme.colors.danger, + }, + helper: { + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/PlayerTransfersScreen.tsx b/mobile/src/screens/PlayerTransfersScreen.tsx new file mode 100644 index 0000000..79065b1 --- /dev/null +++ b/mobile/src/screens/PlayerTransfersScreen.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + Keyboard, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useSession } from "../state/session-context"; +import { useI18n } from "../i18n"; +import { useTheme } from "../theme"; +import type { AppTheme } from "../theme"; +import EmpOverlay from "../components/EmpOverlay"; + +function initials(value: string) { + return value + .split(" ") + .filter(Boolean) + .map((part) => part[0]) + .join("") + .slice(0, 2) + .toUpperCase(); +} + +export default function PlayerTransfersScreen() { + const manager = useSession(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + const [targetId, setTargetId] = useState(""); + const [amount, setAmount] = useState(""); + const [note, setNote] = useState(""); + const [errorText, setErrorText] = useState(""); + + const eligible = useMemo( + () => + manager.session?.players.filter( + (player) => player.role !== "banker" && player.id !== manager.me?.id, + ) ?? [], + [manager.session, manager.me?.id], + ); + + useEffect(() => { + if (!targetId && eligible.length > 0) { + setTargetId(eligible[0].id); + } + }, [eligible, targetId]); + + const selectedPlayer = eligible.find((player) => player.id === targetId); + const quickAmounts = [10, 25, 50, 100]; + const normalizedAmount = amount.replace(",", "."); + const amountValue = Number(normalizedAmount); + const canSend = Boolean(targetId) && Number.isFinite(amountValue) && amountValue > 0; + const showEmp = Boolean(manager.session?.blackoutActive) && !manager.isBanker; + + if (!manager.session || !manager.me) { + return ( + + {t("common.loading")} + + ); + } + + return ( + + + + {t("transfers.title")} + {t("transfers.subtitle")} + + + + {t("transfers.from")} + + + + {initials(manager.me.name || t("common.you"))} + + + + {manager.me.name || t("common.you")} + {t("transfers.availableBalance")} + + + ₦{manager.me.balance ?? 0} + + + + + + {t("transfers.to")} + {eligible.length === 0 ? ( + {t("transfers.noPlayers")} + ) : ( + + {eligible.map((player) => { + const active = player.id === targetId; + return ( + setTargetId(player.id)} + style={({ pressed }) => [ + styles.recipientCard, + active ? styles.recipientCardActive : null, + pressed ? styles.recipientCardPressed : null, + ]} + > + + + {initials(player.name)} + + + {player.name} + + {player.isDummy ? t("transfers.dummy") : t("transfers.player")} + + + + + + ); + })} + + )} + + + + {t("transfers.amount")} + + + setAmount(value.replace(/[^0-9.,]/g, ""))} + /> + Keyboard.dismiss()} + accessibilityRole="button" + > + {t("common.done")} + + + + {quickAmounts.map((value) => { + const active = Number.isFinite(amountValue) && amountValue === value; + return ( + setAmount(String(value))} + style={[styles.chip, active ? styles.chipActive : null]} + > + + ₦{value} + + + ); + })} + + + + + {t("transfers.note")} + + + + {errorText ? {errorText} : null} + + + {t("transfers.sending")} + + {canSend + ? t("transfers.summary", { + amount: amountValue, + name: selectedPlayer?.name ?? t("common.player"), + }) + : t("transfers.selectPlayer")} + + + + { + if (!targetId || !Number.isFinite(amountValue) || amountValue <= 0) { + setErrorText(t("transfers.error")); + return; + } + manager.sendMessage({ + type: "transfer", + sessionId: manager.sessionId, + playerId: manager.me?.id, + toPlayerId: targetId, + amount: amountValue, + note, + }); + setErrorText(""); + setAmount(""); + setNote(""); + }} + > + {t("transfers.send")} + + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + wrapper: { + flex: 1, + position: "relative", + }, + scroll: { + flex: 1, + backgroundColor: theme.colors.background, + }, + container: { + padding: 20, + gap: 18, + backgroundColor: theme.colors.background, + }, + hero: { + gap: 4, + }, + title: { + fontSize: 26, + fontWeight: "700", + color: theme.colors.text, + }, + subtitle: { + color: theme.colors.textMuted, + }, + section: { + gap: 10, + }, + sectionTitle: { + fontSize: 12, + fontWeight: "700", + letterSpacing: 1, + textTransform: "uppercase", + color: theme.colors.textMuted, + }, + profileCard: { + backgroundColor: theme.colors.brandSurface, + borderRadius: 18, + padding: 16, + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + avatarBadge: { + width: 44, + height: 44, + borderRadius: 16, + backgroundColor: theme.colors.brandSurfaceAlt, + alignItems: "center", + justifyContent: "center", + }, + avatarText: { + color: theme.colors.brandTextMuted, + fontWeight: "700", + }, + profileMeta: { + flex: 1, + gap: 4, + }, + profileName: { + color: theme.colors.brandText, + fontSize: 16, + fontWeight: "600", + }, + profileSub: { + color: theme.colors.brandTextMuted, + fontSize: 12, + }, + balancePill: { + backgroundColor: theme.colors.brandAccent, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + }, + balanceText: { + color: theme.colors.brandAccentText, + fontWeight: "700", + fontSize: 12, + }, + recipientList: { + gap: 10, + }, + recipientCard: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 12, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + }, + recipientCardActive: { + borderColor: theme.colors.accent, + backgroundColor: theme.colors.accentSurface, + }, + recipientCardPressed: { + opacity: 0.85, + }, + recipientRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + recipientAvatar: { + width: 40, + height: 40, + borderRadius: 14, + backgroundColor: theme.colors.avatarSurface, + alignItems: "center", + justifyContent: "center", + }, + recipientInitials: { + color: theme.colors.avatarText, + fontWeight: "700", + }, + recipientMeta: { + flex: 1, + }, + recipientName: { + fontSize: 15, + fontWeight: "600", + color: theme.colors.text, + }, + recipientSub: { + fontSize: 12, + color: theme.colors.textMuted, + marginTop: 2, + }, + radio: { + width: 16, + height: 16, + borderRadius: 8, + borderWidth: 2, + borderColor: theme.colors.radioBorder, + }, + radioActive: { + borderColor: theme.colors.accent, + backgroundColor: theme.colors.accent, + }, + helper: { + fontSize: 12, + color: theme.colors.textMuted, + }, + amountRow: { + flexDirection: "row", + alignItems: "center", + backgroundColor: theme.colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + paddingHorizontal: 12, + paddingVertical: 8, + gap: 8, + }, + currency: { + fontSize: 12, + fontWeight: "700", + color: theme.colors.text, + }, + amountInput: { + flex: 1, + fontSize: 18, + fontWeight: "600", + color: theme.colors.inputText, + }, + dismissButton: { + backgroundColor: theme.colors.surfaceAlt, + paddingVertical: 8, + paddingHorizontal: 10, + borderRadius: 10, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + }, + dismissButtonText: { + fontSize: 12, + fontWeight: "600", + color: theme.colors.text, + }, + chipRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + chip: { + backgroundColor: theme.colors.chipBackground, + borderRadius: 999, + borderWidth: 1, + borderColor: theme.colors.chipBorder, + paddingHorizontal: 12, + paddingVertical: 6, + }, + chipActive: { + backgroundColor: theme.colors.chipActiveBackground, + borderColor: theme.colors.chipActiveBackground, + }, + chipText: { + fontSize: 12, + color: theme.colors.chipText, + fontWeight: "600", + }, + chipTextActive: { + color: theme.colors.chipActiveText, + }, + noteInput: { + backgroundColor: theme.colors.inputBackground, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + color: theme.colors.inputText, + paddingHorizontal: 12, + paddingVertical: 10, + }, + summary: { + backgroundColor: theme.colors.warningSurface, + borderRadius: 16, + padding: 14, + borderWidth: 1, + borderColor: theme.colors.warningBorder, + gap: 6, + }, + summaryLabel: { + fontSize: 12, + textTransform: "uppercase", + letterSpacing: 1, + color: theme.colors.warningText, + fontWeight: "700", + }, + summaryValue: { + fontSize: 14, + fontWeight: "600", + color: theme.colors.warningTextStrong, + }, + error: { + color: theme.colors.danger, + fontWeight: "600", + }, + button: { + backgroundColor: theme.colors.action, + paddingVertical: 14, + borderRadius: 999, + alignItems: "center", + marginBottom: 16, + }, + buttonText: { + color: theme.colors.actionText, + fontWeight: "700", + letterSpacing: 0.4, + }, + buttonDisabled: { + backgroundColor: theme.colors.tabInactive, + }, + loading: { + color: theme.colors.text, + }, + }); diff --git a/mobile/src/screens/chat/ChatListScreen.tsx b/mobile/src/screens/chat/ChatListScreen.tsx new file mode 100644 index 0000000..df73814 --- /dev/null +++ b/mobile/src/screens/chat/ChatListScreen.tsx @@ -0,0 +1,164 @@ +import React, { useMemo } from "react"; +import { + FlatList, + Pressable, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { ChatStackParamList } from "../../navigation/types"; +import { useSession } from "../../state/session-context"; +import { buildThreads } from "./chat-utils"; +import { useI18n } from "../../i18n"; +import { useTheme } from "../../theme"; +import type { AppTheme } from "../../theme"; +import EmpOverlay from "../../components/EmpOverlay"; + +function formatTime(value?: number | null) { + if (!value) return ""; + return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +export default function ChatListScreen() { + const manager = useSession(); + const navigation = useNavigation>(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + + const threads = useMemo(() => { + if (!manager.session) return []; + return buildThreads(manager.session, manager.me?.id ?? null, manager.isBanker); + }, [manager.session, manager.me, manager.isBanker]); + + if (!manager.session) { + return ( + + {t("common.loadingChats")} + + ); + } + const showEmp = manager.session.blackoutActive && !manager.isBanker; + + return ( + + {t("chat.title")} + item.id} + contentContainerStyle={styles.list} + renderItem={({ item }) => ( + navigation.navigate("ChatThread", { chatId: item.id })} + > + + + {item.kind === "global" ? "#" : item.name.charAt(0).toUpperCase()} + + + + + {item.name} + {formatTime(item.lastMessage?.createdAt)} + + + {item.lastMessage?.body ?? t("chat.noMessages")} + + + + )} + /> + + navigation.navigate("ChatNew")} + > + + + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: theme.colors.background, + position: "relative", + }, + title: { + fontSize: 20, + fontWeight: "700", + marginBottom: 16, + color: theme.colors.text, + }, + list: { + gap: 12, + paddingBottom: 80, + }, + listItem: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + padding: 12, + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + avatar: { + width: 44, + height: 44, + borderRadius: 16, + backgroundColor: theme.colors.listAvatarBackground, + justifyContent: "center", + alignItems: "center", + }, + avatarText: { + fontWeight: "700", + color: theme.colors.listAvatarText, + }, + meta: { + flex: 1, + }, + metaRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + name: { + fontWeight: "600", + color: theme.colors.text, + }, + time: { + fontSize: 12, + color: theme.colors.textMuted, + }, + preview: { + color: theme.colors.textMuted, + marginTop: 4, + }, + helper: { + color: theme.colors.textMuted, + }, + fab: { + position: "absolute", + right: 20, + bottom: 24, + width: 52, + height: 52, + borderRadius: 26, + backgroundColor: theme.colors.primary, + alignItems: "center", + justifyContent: "center", + }, + fabText: { + color: theme.colors.primaryText, + fontSize: 26, + fontWeight: "700", + }, + }); diff --git a/mobile/src/screens/chat/ChatNewScreen.tsx b/mobile/src/screens/chat/ChatNewScreen.tsx new file mode 100644 index 0000000..c21478c --- /dev/null +++ b/mobile/src/screens/chat/ChatNewScreen.tsx @@ -0,0 +1,230 @@ +import React, { useMemo, useState } from "react"; +import { + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import type { ChatStackParamList } from "../../navigation/types"; +import { useSession } from "../../state/session-context"; +import { useI18n } from "../../i18n"; +import { useTheme } from "../../theme"; +import type { AppTheme } from "../../theme"; +import EmpOverlay from "../../components/EmpOverlay"; + +export default function ChatNewScreen() { + const manager = useSession(); + const navigation = useNavigation>(); + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + const [mode, setMode] = useState<"direct" | "group">("direct"); + const [groupName, setGroupName] = useState(""); + const [selected, setSelected] = useState([]); + + const options = useMemo( + () => manager.session?.players.filter((player) => player.id !== manager.me?.id) ?? [], + [manager.session, manager.me], + ); + + function toggleMember(id: string) { + if (mode === "direct") { + setSelected([id]); + return; + } + setSelected((prev) => + prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], + ); + } + + if (!manager.session || !manager.me) { + return ( + + {t("common.loading")} + + ); + } + const showEmp = manager.session.blackoutActive && !manager.isBanker; + + return ( + + + {t("chat.newTitle")} + + + { + setMode("direct"); + setSelected([]); + }} + > + + {t("chat.direct")} + + + { + setMode("group"); + setSelected([]); + }} + > + + {t("chat.group")} + + + + + {mode === "group" && ( + + )} + + {t("chat.choosePlayers")} + {options.map((player) => { + const active = selected.includes(player.id); + return ( + toggleMember(player.id)} + > + {player.name} + + {player.role === "banker" + ? t("common.banker") + : player.isDummy + ? t("common.dummy") + : t("common.player")} + + + ); + })} + + { + if (showEmp) return; + if (mode === "direct" && selected.length !== 1) return; + if (mode === "group" && (!groupName.trim() || selected.length === 0)) return; + manager.sendMessage({ + type: "group_create", + sessionId: manager.sessionId, + playerId: manager.me?.id, + name: mode === "direct" ? t("chat.direct") : groupName.trim(), + memberIds: selected, + }); + navigation.goBack(); + }} + > + {t("chat.startChat")} + + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + wrapper: { + flex: 1, + position: "relative", + }, + scroll: { + flex: 1, + backgroundColor: theme.colors.background, + }, + container: { + padding: 20, + gap: 12, + }, + title: { + fontSize: 20, + fontWeight: "700", + color: theme.colors.text, + }, + toggleRow: { + flexDirection: "row", + gap: 8, + }, + toggleButton: { + flex: 1, + paddingVertical: 10, + borderRadius: 999, + borderWidth: 1, + borderColor: theme.colors.border, + alignItems: "center", + backgroundColor: theme.colors.surface, + }, + toggleActive: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + }, + toggleText: { + color: theme.colors.text, + fontWeight: "600", + }, + toggleTextActive: { + color: theme.colors.primaryText, + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.inputText, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + }, + sectionTitle: { + fontWeight: "600", + marginTop: 8, + color: theme.colors.text, + }, + memberRow: { + backgroundColor: theme.colors.surface, + padding: 12, + borderRadius: 12, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + memberActive: { + borderWidth: 1, + borderColor: theme.colors.primary, + }, + memberName: { + fontWeight: "600", + color: theme.colors.text, + }, + memberMeta: { + fontSize: 12, + color: theme.colors.textMuted, + }, + button: { + backgroundColor: theme.colors.primary, + paddingVertical: 12, + borderRadius: 999, + alignItems: "center", + marginTop: 12, + }, + buttonText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + helper: { + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/chat/ChatThreadScreen.tsx b/mobile/src/screens/chat/ChatThreadScreen.tsx new file mode 100644 index 0000000..f778d6e --- /dev/null +++ b/mobile/src/screens/chat/ChatThreadScreen.tsx @@ -0,0 +1,225 @@ +import React, { useMemo, useRef, useState } from "react"; +import { + FlatList, + KeyboardAvoidingView, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useRoute } from "@react-navigation/native"; +import type { RouteProp } from "@react-navigation/native"; +import { useHeaderHeight } from "@react-navigation/elements"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import type { ChatStackParamList } from "../../navigation/types"; +import { useSession } from "../../state/session-context"; +import { buildThreads, getThreadMessages } from "./chat-utils"; +import { useI18n } from "../../i18n"; +import { useTheme } from "../../theme"; +import type { AppTheme } from "../../theme"; +import EmpOverlay from "../../components/EmpOverlay"; + +function formatTime(value: number) { + return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +export default function ChatThreadScreen() { + const manager = useSession(); + const route = useRoute>(); + const [message, setMessage] = useState(""); + const listRef = useRef(null); + const headerHeight = useHeaderHeight(); + const insets = useSafeAreaInsets(); + const keyboardOffset = Platform.OS === "ios" ? headerHeight : 0; + const { t } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const placeholderColor = theme.colors.placeholder; + + if (!manager.session || !manager.me) { + return ( + + {t("common.loadingChat")} + + ); + } + + const threads = buildThreads(manager.session, manager.me.id, manager.isBanker); + const thread = threads.find((item) => item.id === route.params.chatId); + const messages = thread ? getThreadMessages(manager.session, thread.id) : []; + const threadKindLabel = thread + ? thread.kind === "global" + ? t("chat.global") + : thread.kind === "direct" + ? t("chat.direct") + : t("chat.group") + : ""; + + if (!thread) { + return ( + + {t("chat.notFound")} + + ); + } + const showEmp = manager.session.blackoutActive && !manager.isBanker; + + function handleSend() { + if (showEmp) return; + if (!message.trim()) return; + manager.sendMessage({ + type: "chat_send", + sessionId: manager.sessionId, + playerId: manager.me?.id, + body: message.trim(), + groupId: thread.id === "global" ? null : thread.id, + }); + setMessage(""); + } + + return ( + + + + {thread.name} + {threadKindLabel} + + + item.id} + contentContainerStyle={styles.list} + keyboardShouldPersistTaps="handled" + onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: true })} + renderItem={({ item }) => { + const isMe = item.fromId === manager.me?.id; + return ( + + + {item.body} + {formatTime(item.createdAt)} + + + ); + }} + /> + + + + + {t("common.send")} + + + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + wrapper: { + flex: 1, + position: "relative", + }, + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + header: { + padding: 16, + borderBottomWidth: 1, + borderBottomColor: theme.colors.borderMuted, + backgroundColor: theme.colors.surface, + }, + headerTitle: { + fontSize: 18, + fontWeight: "700", + color: theme.colors.text, + }, + headerSubtitle: { + color: theme.colors.textMuted, + fontSize: 12, + marginTop: 4, + }, + list: { + padding: 16, + gap: 10, + }, + bubbleRow: { + flexDirection: "row", + justifyContent: "flex-start", + }, + bubbleRowMe: { + justifyContent: "flex-end", + }, + bubble: { + maxWidth: "75%", + backgroundColor: theme.colors.surface, + padding: 10, + borderRadius: 16, + }, + bubbleMe: { + backgroundColor: theme.colors.bubbleMe, + }, + bubbleText: { + fontSize: 14, + color: theme.colors.text, + }, + bubbleTime: { + fontSize: 10, + color: theme.colors.textMuted, + marginTop: 6, + textAlign: "right", + }, + composer: { + flexDirection: "row", + padding: 12, + gap: 10, + borderTopWidth: 1, + borderTopColor: theme.colors.borderMuted, + backgroundColor: theme.colors.surface, + }, + input: { + flex: 1, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.inputText, + borderRadius: 999, + paddingHorizontal: 12, + paddingVertical: 8, + }, + sendButton: { + backgroundColor: theme.colors.primary, + borderRadius: 999, + paddingHorizontal: 16, + justifyContent: "center", + }, + sendText: { + color: theme.colors.primaryText, + fontWeight: "600", + }, + helper: { + color: theme.colors.textMuted, + }, + }); diff --git a/mobile/src/screens/chat/chat-utils.ts b/mobile/src/screens/chat/chat-utils.ts new file mode 100644 index 0000000..6dca011 --- /dev/null +++ b/mobile/src/screens/chat/chat-utils.ts @@ -0,0 +1,80 @@ +import type { ChatMessage, SessionSnapshot } from "../../shared/types"; +import { tStatic } from "../../i18n"; + +export type ChatThread = { + id: string; + name: string; + kind: "global" | "group" | "direct"; + members: string[]; + lastMessage: ChatMessage | null; +}; + +function getLastMessage(messages: ChatMessage[]): ChatMessage | null { + if (messages.length === 0) return null; + return messages.reduce((latest, current) => + current.createdAt > latest.createdAt ? current : latest, + ); +} + +export function buildThreads( + session: SessionSnapshot, + meId: string | null, + isBanker: boolean, +): ChatThread[] { + const threads: ChatThread[] = []; + const globalMessages = session.chats.filter((message) => message.groupId === null); + threads.push({ + id: "global", + name: tStatic("chat.global"), + kind: "global", + members: [], + lastMessage: getLastMessage(globalMessages), + }); + + session.groups.forEach((group) => { + if (!isBanker && meId && !group.memberIds.includes(meId)) { + return; + } + const groupMessages = session.chats.filter((message) => message.groupId === group.id); + let name = group.name; + let kind: ChatThread["kind"] = "group"; + if (group.memberIds.length === 2) { + const [first, second] = group.memberIds; + if (isBanker || !meId) { + const firstName = + session.players.find((player) => player.id === first)?.name ?? tStatic("common.player"); + const secondName = + session.players.find((player) => player.id === second)?.name ?? tStatic("common.player"); + name = `${firstName} & ${secondName}`; + kind = "direct"; + } else { + const otherId = first === meId ? second : first; + const otherName = + session.players.find((player) => player.id === otherId)?.name ?? + tStatic("common.player"); + name = otherName; + kind = "direct"; + } + } + threads.push({ + id: group.id, + name, + kind, + members: group.memberIds, + lastMessage: getLastMessage(groupMessages), + }); + }); + + return threads.sort((a, b) => { + const aTime = a.lastMessage?.createdAt ?? 0; + const bTime = b.lastMessage?.createdAt ?? 0; + return bTime - aTime; + }); +} + +export function getThreadMessages(session: SessionSnapshot, threadId: string): ChatMessage[] { + if (threadId === "global") { + return session.chats.filter((message) => message.groupId === null).slice().reverse(); + } + return session.chats.filter((message) => message.groupId === threadId).slice().reverse(); +} diff --git a/mobile/src/shared/types.ts b/mobile/src/shared/types.ts new file mode 100644 index 0000000..26f4a1d --- /dev/null +++ b/mobile/src/shared/types.ts @@ -0,0 +1,91 @@ +export type SessionStatus = "lobby" | "active" | "ended"; +export type PlayerRole = "banker" | "player"; + +export type Player = { + id: string; + name: string; + role: PlayerRole; + balance: number; + connected: boolean; + isDummy: boolean; + joinedAt: number; + lastActiveAt: number; +}; + +export type TransactionKind = + | "transfer" + | "banker_adjust" + | "banker_force_transfer"; + +export type Transaction = { + id: string; + kind: TransactionKind; + fromId: string | null; + toId: string | null; + amount: number; + note: string | null; + createdAt: number; + initiatedBy: PlayerRole; +}; + +export type ChatGroup = { + id: string; + name: string; + memberIds: string[]; + createdAt: number; + createdBy: string; +}; + +export type ChatMessage = { + id: string; + fromId: string; + body: string; + createdAt: number; + groupId: string | null; +}; + +export type TakeoverRequest = { + id: string; + dummyId: string; + requesterId: string; + createdAt: number; + status: "pending" | "approved" | "rejected"; +}; + +export type SessionSnapshot = { + id: string; + code: string; + status: SessionStatus; + createdAt: number; + bankerId: string; + blackoutActive: boolean; + blackoutReason: string | null; + players: Player[]; + transactions: Transaction[]; + chats: ChatMessage[]; + groups: ChatGroup[]; + takeoverRequests: TakeoverRequest[]; +}; + +export type SessionPreviewPlayer = { + id: string; + name: string; + role: PlayerRole; + isDummy: boolean; + connected: boolean; +}; + +export type SessionPreview = { + sessionId: string; + code: string; + status: SessionStatus; + players: SessionPreviewPlayer[]; +}; + +export type JoinResponse = { + sessionId: string; + sessionCode: string; + playerId: string; + role: PlayerRole; + status: SessionStatus; +}; diff --git a/mobile/src/state/session-context.tsx b/mobile/src/state/session-context.tsx new file mode 100644 index 0000000..8cdbfdd --- /dev/null +++ b/mobile/src/state/session-context.tsx @@ -0,0 +1,17 @@ +import React, { createContext, useContext } from "react"; +import { useSessionManager } from "./session"; + +const SessionContext = createContext | null>(null); + +export function SessionProvider({ children }: { children: React.ReactNode }) { + const manager = useSessionManager(); + return {children}; +} + +export function useSession() { + const context = useContext(SessionContext); + if (!context) { + throw new Error("useSession must be used within SessionProvider"); + } + return context; +} diff --git a/mobile/src/state/session.ts b/mobile/src/state/session.ts new file mode 100644 index 0000000..e1a06d9 --- /dev/null +++ b/mobile/src/state/session.ts @@ -0,0 +1,267 @@ +import { useEffect, useRef, useState } from "react"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import type { JoinResponse, SessionPreview, SessionSnapshot } from "../shared/types"; +import { getApiBaseUrl, getWsUrl } from "../config/api"; +import { tStatic } from "../i18n"; + +const STORAGE_KEY = "negopoly:session"; + +type StoredSession = { + sessionId: string; + sessionCode: string; + playerId: string; +}; + +async function readStoredSession(): Promise { + try { + const raw = await AsyncStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as StoredSession; + } catch { + return null; + } +} + +async function writeStoredSession(session: StoredSession) { + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(session)); +} + +async function clearStoredSession() { + await AsyncStorage.removeItem(STORAGE_KEY); +} + +export function useSessionManager() { + const [sessionId, setSessionId] = useState(""); + const [sessionCode, setSessionCode] = useState(""); + const [playerId, setPlayerId] = useState(""); + const [session, setSession] = useState(null); + const [error, setError] = useState(null); + const [pendingTakeoverId, setPendingTakeoverId] = useState(null); + const [connectionState, setConnectionState] = useState< + "idle" | "connecting" | "open" | "error" + >("idle"); + const [tick, setTick] = useState(0); + + const wsRef = useRef(null); + + useEffect(() => { + let mounted = true; + readStoredSession().then((stored) => { + if (!mounted || !stored) return; + setSessionId(stored.sessionId); + setSessionCode(stored.sessionCode); + setPlayerId(stored.playerId); + }); + return () => { + mounted = false; + }; + }, []); + + useEffect(() => { + const timer = setInterval(() => setTick((value) => value + 1), 1000); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + if (!sessionId || !playerId) { + setConnectionState("idle"); + setSession(null); + return; + } + + setConnectionState("connecting"); + const ws = new WebSocket(getWsUrl(sessionId, playerId)); + wsRef.current = ws; + + ws.onopen = () => setConnectionState("open"); + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + if (message.type === "state") { + setSession(message.session as SessionSnapshot); + } + if (message.type === "error") { + setError(message.message); + } + if (message.type === "takeover_approved") { + const assignedId = message.assignedPlayerId as string; + setPlayerId(assignedId); + if (sessionId && sessionCode) { + writeStoredSession({ + sessionId, + sessionCode, + playerId: assignedId, + }); + } + } + } catch { + setError(tStatic("error.parseResponse")); + } + }; + + ws.onerror = () => setConnectionState("error"); + ws.onclose = (event) => { + setConnectionState("error"); + const reason = typeof event?.reason === "string" ? event.reason : ""; + if (event?.code === 1008 && /session not found|player not found/i.test(reason)) { + resetSession(); + } + }; + + const pingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping", sessionId, playerId })); + } + }, 15000); + + return () => { + clearInterval(pingTimer); + ws.close(); + }; + }, [sessionId, playerId]); + + useEffect(() => { + if (!pendingTakeoverId) return; + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + if (!sessionId || !playerId) return; + wsRef.current.send( + JSON.stringify({ + type: "takeover_request", + sessionId, + playerId, + dummyId: pendingTakeoverId, + }), + ); + setPendingTakeoverId(null); + }, [pendingTakeoverId, sessionId, playerId]); + + async function createSession(bankerName: string) { + setError(null); + setSession(null); + const response = await fetch(`${getApiBaseUrl()}/api/session`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bankerName }), + }); + if (!response.ok) { + setError(tStatic("error.createSession")); + return null; + } + const data = (await response.json()) as JoinResponse; + setSessionId(data.sessionId); + setSessionCode(data.sessionCode); + setPlayerId(data.playerId); + await writeStoredSession({ + sessionId: data.sessionId, + sessionCode: data.sessionCode, + playerId: data.playerId, + }); + return data; + } + + async function joinSession(code: string, name: string) { + setError(null); + setSession(null); + if (!code) { + setError(tStatic("entry.alert.enterCode")); + return null; + } + const storedNow = await readStoredSession(); + const reusePlayerId = + storedNow && (storedNow.sessionCode === code || storedNow.sessionId === code) + ? storedNow.playerId + : undefined; + + const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/join`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, playerId: reusePlayerId }), + }); + if (!response.ok) { + setError(tStatic("error.joinSession")); + return null; + } + const data = (await response.json()) as JoinResponse; + setSessionId(data.sessionId); + setSessionCode(data.sessionCode); + setPlayerId(data.playerId); + await writeStoredSession({ + sessionId: data.sessionId, + sessionCode: data.sessionCode, + playerId: data.playerId, + }); + return data; + } + + async function fetchSessionPreview(code: string): Promise { + if (!code) return null; + const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/info`); + if (!response.ok) { + setError(tStatic("error.loadSessionInfo")); + return null; + } + return (await response.json()) as SessionPreview; + } + + function sendMessage(payload: Record) { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + setError(tStatic("error.connectionNotReady")); + return; + } + wsRef.current.send(JSON.stringify(payload)); + } + + async function resetSession() { + try { + await clearStoredSession(); + } catch { + // Ignore storage errors on reset. + } + setSessionId(""); + setSessionCode(""); + setPlayerId(""); + setSession(null); + setError(null); + setPendingTakeoverId(null); + setConnectionState("idle"); + } + + async function leaveSession() { + const ws = wsRef.current; + wsRef.current = null; + if (ws && ws.readyState !== WebSocket.CLOSED) { + try { + ws.close(); + } catch { + // Ignore failures while closing the socket. + } + } + await resetSession(); + } + + const me = session?.players.find((player) => player.id === playerId) ?? null; + const isBanker = me?.role === "banker"; + + return { + sessionId, + sessionCode, + playerId, + session, + me, + isBanker, + tick, + error, + connectionState, + setError, + createSession, + joinSession, + fetchSessionPreview, + sendMessage, + resetSession, + leaveSession, + setSessionId, + setPlayerId, + setSession, + setPendingTakeoverId, + }; +} diff --git a/mobile/src/theme.ts b/mobile/src/theme.ts new file mode 100644 index 0000000..ed4e824 --- /dev/null +++ b/mobile/src/theme.ts @@ -0,0 +1,181 @@ +import { useColorScheme } from "react-native"; +import { + DarkTheme, + DefaultTheme, + type Theme as NavigationTheme, +} from "@react-navigation/native"; + +export type AppTheme = { + dark: boolean; + colors: { + background: string; + surface: string; + surfaceAlt: string; + text: string; + textMuted: string; + border: string; + borderMuted: string; + primary: string; + primaryText: string; + secondary: string; + secondaryText: string; + accent: string; + accentText: string; + accentSurface: string; + danger: string; + warningSurface: string; + warningBorder: string; + warningText: string; + warningTextStrong: string; + brandSurface: string; + brandSurfaceAlt: string; + brandText: string; + brandTextMuted: string; + brandAccent: string; + brandAccentText: string; + avatarSurface: string; + avatarText: string; + chipBackground: string; + chipBorder: string; + chipText: string; + chipActiveBackground: string; + chipActiveText: string; + listAvatarBackground: string; + listAvatarText: string; + bubbleMe: string; + inputBackground: string; + inputText: string; + placeholder: string; + tabActive: string; + tabInactive: string; + headerBackground: string; + headerText: string; + action: string; + actionText: string; + radioBorder: string; + }; +}; + +const lightTheme: AppTheme = { + dark: false, + colors: { + background: "#f7f7f9", + surface: "#ffffff", + surfaceAlt: "#f6f8fa", + text: "#0b1a2b", + textMuted: "#6b7280", + border: "#d8dee5", + borderMuted: "#e2e8f0", + primary: "#1b8b75", + primaryText: "#ffffff", + secondary: "#e7ecef", + secondaryText: "#0c1824", + accent: "#14b8a6", + accentText: "#042f2e", + accentSurface: "#ecfdf9", + danger: "#b91c1c", + warningSurface: "#fff6e5", + warningBorder: "#fde7c1", + warningText: "#b45309", + warningTextStrong: "#7c2d12", + brandSurface: "#0b1a2b", + brandSurfaceAlt: "#1f334d", + brandText: "#f8fafc", + brandTextMuted: "#9fb3c8", + brandAccent: "#14b8a6", + brandAccentText: "#042f2e", + avatarSurface: "#0f172a", + avatarText: "#e2e8f0", + chipBackground: "#ffffff", + chipBorder: "#e2e8f0", + chipText: "#0f172a", + chipActiveBackground: "#0f172a", + chipActiveText: "#f8fafc", + listAvatarBackground: "#e6f6f2", + listAvatarText: "#1b8b75", + bubbleMe: "#dff7ef", + inputBackground: "#ffffff", + inputText: "#0b1a2b", + placeholder: "#9aa6b2", + tabActive: "#0f172a", + tabInactive: "#94a3b8", + headerBackground: "#ffffff", + headerText: "#0b1a2b", + action: "#0f172a", + actionText: "#f8fafc", + radioBorder: "#cbd5f5", + }, +}; + +const darkTheme: AppTheme = { + dark: true, + colors: { + background: "#0b0f14", + surface: "#111922", + surfaceAlt: "#0f1620", + text: "#f8fafc", + textMuted: "#a7b4c5", + border: "#1f2a37", + borderMuted: "#243244", + primary: "#1fbf98", + primaryText: "#ffffff", + secondary: "#1f2a37", + secondaryText: "#e2e8f0", + accent: "#2dd4bf", + accentText: "#04221b", + accentSurface: "#0f2a24", + danger: "#f87171", + warningSurface: "#2a1f0b", + warningBorder: "#5f3b11", + warningText: "#f59e0b", + warningTextStrong: "#fbbf24", + brandSurface: "#101a27", + brandSurfaceAlt: "#1b2b3f", + brandText: "#f8fafc", + brandTextMuted: "#9fb3c8", + brandAccent: "#2dd4bf", + brandAccentText: "#04221b", + avatarSurface: "#1e293b", + avatarText: "#e2e8f0", + chipBackground: "#111922", + chipBorder: "#273244", + chipText: "#e2e8f0", + chipActiveBackground: "#2dd4bf", + chipActiveText: "#04221b", + listAvatarBackground: "#0f2a24", + listAvatarText: "#5eead4", + bubbleMe: "#103128", + inputBackground: "#0f1620", + inputText: "#f8fafc", + placeholder: "#7f90a6", + tabActive: "#e2e8f0", + tabInactive: "#64748b", + headerBackground: "#111922", + headerText: "#f8fafc", + action: "#e2e8f0", + actionText: "#0b1a2b", + radioBorder: "#334155", + }, +}; + +export function useTheme(): AppTheme { + const scheme = useColorScheme(); + return scheme === "dark" ? darkTheme : lightTheme; +} + +export function getNavigationTheme(theme: AppTheme): NavigationTheme { + const baseTheme = theme.dark ? DarkTheme : DefaultTheme; + return { + ...baseTheme, + dark: theme.dark, + colors: { + ...baseTheme.colors, + primary: theme.colors.primary, + background: theme.colors.background, + card: theme.colors.headerBackground, + text: theme.colors.headerText, + border: theme.colors.border, + notification: theme.colors.accent, + }, + }; +} diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json new file mode 100644 index 0000000..b9567f6 --- /dev/null +++ b/mobile/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7bb5dfe --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "créditmabligopapp", + "module": "index.ts", + "type": "module", + "private": true, + "dependencies": { + "qrcode": "^1.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.5" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/qrcode": "^1.5.5" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/react-native-plan.md b/react-native-plan.md new file mode 100644 index 0000000..8bcb6ad --- /dev/null +++ b/react-native-plan.md @@ -0,0 +1,73 @@ +# React Native Version Plan + +Goal: build a React Native companion app that connects to the same Bun server used by the web app. +Production base URL: `https://negopoly.fr` +Development base URL: `http://` + +## 1) Project setup +- Choose framework: React Native + Expo (fast iteration) or bare RN (native modules). +- Initialize inside this repo at `mobile/`. +- Configure TypeScript and linting to match existing code style. +- Decide navigation: React Navigation for stack + tab layouts. + +## 2) Shared types + networking +- Extract/share types from `shared/types.ts` into a package or keep a synced copy for RN. +- Create a `config` module to switch base URL by build type: + - Dev: `http://` (read from env or app config). + - Prod: `https://negopoly.fr`. +- Create an API client wrapper for REST and WebSocket calls. + +## 3) Auth/session flow +- Implement session creation/joining screens matching web UX: + - Mobile-first flow: Join first, then Create. + - Join flow: room code → choose name or dummy takeover. +- Persist session + player IDs with `AsyncStorage`. +- Sync with server via WebSocket (mirrors web `useSessionManager`). + +## 4) Navigation structure +- Root stack: + - Home + - Play (entry) + - Lobby + - Player tabs + - Banker tabs + - Chat list / thread / new +- Player tab layout: + - Home / Transfers / Chat (bottom tabs) +- Banker tab layout: + - Dashboard / Tools / Chat (bottom tabs) + +## 5) Real-time state handling +- Implement WebSocket client (auto-reconnect + heartbeats). +- Mirror server messages: state updates, errors, takeover approvals, chat events. +- Handle blackout states with full-screen overlay. + +## 6) Chat UX parity +- Chat list with unread indicators. +- Thread view like WhatsApp (bubbles + timestamps + composer). +- New chat screen (direct/group). +- Unread tracking (client-side initially; server-side later). + +## 7) Banking features +- Player view: balance, transaction history, transfer form. +- Banker view: force transfers, balance adjustments, dummy creation, approvals, blackout, end session. +- Ensure banker has no balance view. + +## 8) UX + performance +- Ensure mobile-safe typography (no auto zoom). +- Use safe-area insets for bottom tabs. +- Optimize list rendering (`FlatList` for chats/transactions). + +## 9) Testing +- Add unit tests for API helpers + state reducers (if extracted). +- Smoke test flows for create/join, lobby, player, banker, chat. + +## 10) Build + release +- Configure dev builds with a local IP target. +- Configure production builds to use `https://negopoly.fr`. +- Set up CI for builds and OTA updates (if using Expo). + +## Open questions +- Expo or bare React Native? +- Any custom native features planned (QR scanner, push notifications)? +- Preferred directory layout for the mobile app inside this repo? diff --git a/server/api.ts b/server/api.ts new file mode 100644 index 0000000..4d5f787 --- /dev/null +++ b/server/api.ts @@ -0,0 +1,187 @@ +import type { SessionSnapshot } from "../shared/types"; +import type { BunRequest } from "bun"; +import { applySnapshot, joinSession, snapshotSession } from "./domain"; +import { + createSession, + createTestPreview, + createTestSession, + getSession, + getSessionByCode, + isTestSessionCode, +} from "./store"; +import { broadcastSessionState, startTestSimulation } from "./websocket"; + +function jsonResponse(data: unknown, status = 200): Response { + return Response.json(data, { status }); +} + +async function readJson(req: Request): Promise { + return (await req.json()) as T; +} + +function previewSession(session: SessionSnapshot) { + return { + sessionId: session.id, + code: session.code, + status: session.status, + players: session.players.map((player) => ({ + id: player.id, + name: player.name, + role: player.role, + isDummy: player.isDummy, + connected: player.connected, + })), + }; +} + +function readBankerId(req: Request): string | null { + const url = new URL(req.url); + return url.searchParams.get("bankerId"); +} + +function isSnapshotCandidate(value: unknown): value is SessionSnapshot { + if (!value || typeof value !== "object") return false; + const snapshot = value as SessionSnapshot; + return ( + typeof snapshot.id === "string" && + typeof snapshot.code === "string" && + typeof snapshot.status === "string" && + typeof snapshot.createdAt === "number" && + typeof snapshot.bankerId === "string" && + typeof snapshot.blackoutActive === "boolean" && + Array.isArray(snapshot.players) && + Array.isArray(snapshot.transactions) && + Array.isArray(snapshot.chats) && + Array.isArray(snapshot.groups) && + Array.isArray(snapshot.takeoverRequests) + ); +} + +export const apiRoutes = { + "/api/health": { + GET() { + return jsonResponse({ ok: true }); + }, + }, + "/api/session": { + async POST(req: BunRequest) { + let body: { bankerName?: string }; + try { + body = await readJson<{ bankerName?: string }>(req); + } catch { + return jsonResponse({ message: "Invalid request body" }, 400); + } + const bankerName = body.bankerName?.trim() || "Banker"; + const { session, banker } = createSession(bankerName); + return jsonResponse({ + sessionId: session.id, + sessionCode: session.code, + playerId: banker.id, + role: banker.role, + status: session.status, + }); + }, + }, + "/api/session/:code/join": { + async POST(req: BunRequest) { + const code = req.params.code; + let body: { name?: string; playerId?: string }; + try { + body = await readJson<{ name?: string; playerId?: string }>(req); + } catch { + return jsonResponse({ message: "Invalid request body" }, 400); + } + if (isTestSessionCode(code)) { + const session = createTestSession(); + const player = joinSession(session, body.name ?? "Player", body.playerId); + startTestSimulation(session.id); + return jsonResponse({ + sessionId: session.id, + sessionCode: session.code, + playerId: player.id, + role: player.role, + status: session.status, + }); + } + const session = getSessionByCode(code) ?? getSession(code); + if (!session) { + return jsonResponse({ message: "Session not found" }, 404); + } + if (session.status === "ended") { + return jsonResponse({ message: "Session has ended" }, 400); + } + const player = joinSession(session, body.name ?? "Player", body.playerId); + return jsonResponse({ + sessionId: session.id, + sessionCode: session.code, + playerId: player.id, + role: player.role, + status: session.status, + }); + }, + }, + "/api/session/:id": { + GET(req: BunRequest) { + const session = getSession(req.params.id); + if (!session) { + return jsonResponse({ message: "Session not found" }, 404); + } + const snapshot: SessionSnapshot = snapshotSession(session); + return jsonResponse(snapshot); + }, + }, + "/api/session/:id/state": { + GET(req: BunRequest) { + const session = getSession(req.params.id) ?? getSessionByCode(req.params.id); + if (!session) { + return jsonResponse({ message: "Session not found" }, 404); + } + const bankerId = readBankerId(req); + if (!bankerId || bankerId !== session.bankerId) { + return jsonResponse({ message: "Not authorized" }, 403); + } + const snapshot: SessionSnapshot = snapshotSession(session); + return jsonResponse(snapshot); + }, + async POST(req: BunRequest) { + const session = getSession(req.params.id) ?? getSessionByCode(req.params.id); + if (!session) { + return jsonResponse({ message: "Session not found" }, 404); + } + let body: Record; + try { + body = await readJson>(req); + } catch { + return jsonResponse({ message: "Invalid request body" }, 400); + } + const bankerId = (typeof body.bankerId === "string" ? body.bankerId : null) ?? readBankerId(req); + if (!bankerId || bankerId !== session.bankerId) { + return jsonResponse({ message: "Not authorized" }, 403); + } + + const candidate = (body.state ?? body) as unknown; + if (!isSnapshotCandidate(candidate)) { + return jsonResponse({ message: "Invalid session snapshot" }, 400); + } + + applySnapshot(session, candidate); + broadcastSessionState(session.id); + return jsonResponse({ ok: true }); + }, + }, + "/api/session/:code/info": { + GET(req: BunRequest) { + const code = req.params.code; + if (isTestSessionCode(code)) { + const snapshot = createTestPreview(); + return jsonResponse(previewSession(snapshot)); + } + const session = getSessionByCode(code) ?? getSession(code); + if (!session) { + return jsonResponse({ message: "Session not found" }, 404); + } + const snapshot: SessionSnapshot = snapshotSession(session); + return jsonResponse(previewSession(snapshot)); + }, + }, +}; diff --git a/server/domain.test.ts b/server/domain.test.ts new file mode 100644 index 0000000..e914b0a --- /dev/null +++ b/server/domain.test.ts @@ -0,0 +1,89 @@ +import { expect, test } from "bun:test"; +import { + approveTakeover, + bankerAdjust, + disconnectPlayer, + joinSession, + requestTakeover, + setBlackout, + startSession, + transfer, +} from "./domain"; +import { createSession, removeSession } from "./store"; + +function setupSession() { + const { session, banker } = createSession("Avery"); + return { session, banker }; +} + +test("createSession seeds banker with lobby status", () => { + const { session, banker } = setupSession(); + expect(session.status).toBe("lobby"); + expect(banker.role).toBe("banker"); + expect(session.players.get(banker.id)).toBeDefined(); + expect(session.code.length).toBe(5); + removeSession(session.id); +}); + +test("joinSession reuses player when id is provided", () => { + const { session, banker } = setupSession(); + const player = joinSession(session, "Rosa"); + const rejoin = joinSession(session, "Rosa II", player.id); + expect(rejoin.id).toBe(player.id); + expect(rejoin.name).toBe("Rosa II"); + expect(session.players.size).toBe(2); + removeSession(session.id); +}); + +test("transfer moves balances between players", () => { + const { session, banker } = setupSession(); + startSession(session, banker.id); + const player = joinSession(session, "Sam"); + const other = joinSession(session, "Riley"); + const before = player.balance; + transfer(session, player.id, other.id, 200, "Rent"); + expect(player.balance).toBe(before - 200); + removeSession(session.id); +}); + +test("blackout blocks money movement", () => { + const { session, banker } = setupSession(); + startSession(session, banker.id); + const player = joinSession(session, "Kai"); + setBlackout(session, banker.id, true, "City outage"); + const other = joinSession(session, "Pax"); + expect(() => transfer(session, player.id, other.id, 50, "Loan")).toThrow(); + removeSession(session.id); +}); + +test("blackout does not block banker actions", () => { + const { session, banker } = setupSession(); + startSession(session, banker.id); + const player = joinSession(session, "Zuri"); + setBlackout(session, banker.id, true, "City outage"); + expect(() => bankerAdjust(session, banker.id, player.id, 75, "Override")).not.toThrow(); + removeSession(session.id); +}); + +test("banker can adjust balances", () => { + const { session, banker } = setupSession(); + startSession(session, banker.id); + const player = joinSession(session, "Luna"); + bankerAdjust(session, banker.id, player.id, 120, "Bonus"); + expect(player.balance).toBeGreaterThan(1500); + removeSession(session.id); +}); + +test("takeover approval assigns dummy to requester", () => { + const { session, banker } = setupSession(); + startSession(session, banker.id); + const original = joinSession(session, "Ghost"); + disconnectPlayer(session, original.id); + const requester = joinSession(session, "Jules"); + requestTakeover(session, requester.id, original.id); + const assignedId = approveTakeover(session, banker.id, original.id, requester.id); + expect(assignedId).toBe(original.id); + expect(session.players.get(original.id)?.isDummy).toBe(false); + expect(session.players.get(requester.id)).toBeUndefined(); + removeSession(session.id); +}); diff --git a/server/domain.ts b/server/domain.ts new file mode 100644 index 0000000..faf0554 --- /dev/null +++ b/server/domain.ts @@ -0,0 +1,528 @@ +import type { + ChatGroup, + ChatMessage, + Player, + SessionSnapshot, + TakeoverRequest, + Transaction, +} from "../shared/types"; +import type { Session } from "./types"; +import { DomainError } from "./errors"; +import { + clampBalance, + createId, + now, + parseAmount, + DEFAULT_START_BALANCE, +} from "./util"; + +const MAX_CHAT_LENGTH = 280; + +export function snapshotSession(session: Session): SessionSnapshot { + return { + id: session.id, + code: session.code, + status: session.status, + createdAt: session.createdAt, + bankerId: session.bankerId, + blackoutActive: session.blackoutActive, + blackoutReason: session.blackoutReason, + players: Array.from(session.players.values()), + transactions: session.transactions, + chats: session.chats, + groups: session.groups, + takeoverRequests: session.takeoverRequests, + }; +} + +function remapPlayerId(value: string, fromId: string, toId: string): string { + return value === fromId ? toId : value; +} + +function remapSnapshotBanker(snapshot: SessionSnapshot, banker: Player): SessionSnapshot { + const fromId = snapshot.bankerId; + const toId = banker.id; + if (fromId === toId) { + return snapshot; + } + + const players = snapshot.players + .filter((player) => player.id !== toId) + .map((player) => { + if (player.id !== fromId) return player; + return { + ...player, + id: toId, + name: banker.name, + role: "banker" as const, + connected: true, + isDummy: false, + lastActiveAt: now(), + }; + }); + + if (!players.some((player) => player.id === toId)) { + players.push({ + ...banker, + role: "banker", + connected: true, + isDummy: false, + lastActiveAt: now(), + }); + } + + const transactions = snapshot.transactions.map((tx) => ({ + ...tx, + fromId: tx.fromId ? remapPlayerId(tx.fromId, fromId, toId) : null, + toId: tx.toId ? remapPlayerId(tx.toId, fromId, toId) : null, + })); + + const chats = snapshot.chats.map((message) => ({ + ...message, + fromId: remapPlayerId(message.fromId, fromId, toId), + })); + + const groups = snapshot.groups.map((group) => ({ + ...group, + createdBy: remapPlayerId(group.createdBy, fromId, toId), + memberIds: group.memberIds.map((id) => remapPlayerId(id, fromId, toId)), + })); + + const takeoverRequests = snapshot.takeoverRequests.map((request) => ({ + ...request, + dummyId: remapPlayerId(request.dummyId, fromId, toId), + requesterId: remapPlayerId(request.requesterId, fromId, toId), + })); + + return { + ...snapshot, + bankerId: toId, + players, + transactions, + chats, + groups, + takeoverRequests, + }; +} + +export function applySnapshot(session: Session, snapshot: SessionSnapshot): void { + const currentBanker = session.players.get(session.bankerId) ?? null; + const normalized = currentBanker ? remapSnapshotBanker(snapshot, currentBanker) : snapshot; + + session.status = normalized.status; + session.createdAt = normalized.createdAt; + session.bankerId = currentBanker ? currentBanker.id : normalized.bankerId; + session.blackoutActive = normalized.blackoutActive; + session.blackoutReason = normalized.blackoutReason; + + const players = new Map(); + normalized.players.forEach((player) => { + players.set(player.id, { ...player }); + }); + + const banker = players.get(session.bankerId); + if (banker) { + banker.role = "banker"; + banker.connected = true; + banker.isDummy = false; + banker.lastActiveAt = now(); + } + + players.forEach((player) => { + if (player.id !== session.bankerId && player.role === "banker") { + player.role = "player"; + } + }); + + session.players = players; + session.transactions = normalized.transactions.map((tx) => ({ ...tx })); + session.chats = normalized.chats.map((chat) => ({ ...chat })); + session.groups = normalized.groups.map((group) => ({ ...group })); + session.takeoverRequests = normalized.takeoverRequests.map((request) => ({ ...request })); +} + +export function isBlackoutActive(session: Session): boolean { + return session.blackoutActive; +} + +export function joinSession( + session: Session, + name: string, + playerId?: string | null, +): Player { + const trimmedName = name.trim() || "Player"; + if (playerId) { + const existing = session.players.get(playerId); + if (existing) { + existing.connected = true; + existing.isDummy = false; + existing.name = trimmedName; + existing.lastActiveAt = now(); + return existing; + } + } + + const id = createId(); + const joinedAt = now(); + const player: Player = { + id, + name: trimmedName, + role: "player", + balance: DEFAULT_START_BALANCE, + connected: true, + isDummy: false, + joinedAt, + lastActiveAt: joinedAt, + }; + + session.players.set(id, player); + return player; +} + +export function startSession(session: Session, bankerId: string): void { + ensureBanker(session, bankerId); + if (session.status === "ended") { + throw new DomainError("Session has already ended"); + } + session.status = "active"; +} + +export function endSession(session: Session, bankerId: string): void { + ensureBanker(session, bankerId); + session.status = "ended"; +} + +export function disconnectPlayer(session: Session, playerId: string): void { + const player = getPlayer(session, playerId); + player.connected = false; + player.lastActiveAt = now(); + if (player.role !== "banker") { + player.isDummy = true; + } +} + +export function setBlackout( + session: Session, + bankerId: string, + active: boolean, + reason?: string | null, +): void { + ensureOpenSession(session); + ensureBanker(session, bankerId); + session.blackoutActive = Boolean(active); + session.blackoutReason = active ? reason?.trim() || "EMP in effect" : null; +} + +export function transfer( + session: Session, + fromId: string, + toId: string, + amountValue: unknown, + note?: string | null, +): Transaction { + ensureActiveSession(session); + ensureNotBlackout(session, fromId); + const amount = parseAmount(amountValue); + if (amount === null || amount <= 0) { + throw new DomainError("Transfer amount must be positive"); + } + const from = getPlayer(session, fromId); + const to = getPlayer(session, toId); + if (from.role === "banker" || to.role === "banker") { + throw new DomainError("Banker does not hold a balance"); + } + if (from.id === to.id) { + throw new DomainError("Cannot transfer to yourself"); + } + if (from.balance < amount) { + throw new DomainError("Insufficient funds"); + } + from.balance = clampBalance(from.balance - amount); + to.balance = clampBalance(to.balance + amount); + const transaction = createTransaction("transfer", from.id, to.id, amount, note, "player"); + session.transactions.unshift(transaction); + return transaction; +} + +export function bankerAdjust( + session: Session, + bankerId: string, + targetId: string, + amountValue: unknown, + note?: string | null, +): Transaction { + ensureActiveSession(session); + ensureNotBlackout(session, bankerId); + ensureBanker(session, bankerId); + const amount = parseAmount(amountValue); + if (amount === null || amount === 0) { + throw new DomainError("Adjustment amount must be non-zero"); + } + const target = getPlayer(session, targetId); + if (target.role === "banker") { + throw new DomainError("Banker does not hold a balance"); + } + target.balance = clampBalance(target.balance + amount); + const transaction = createTransaction( + "banker_adjust", + null, + target.id, + amount, + note, + "banker", + ); + session.transactions.unshift(transaction); + return transaction; +} + +export function bankerForceTransfer( + session: Session, + bankerId: string, + fromId: string, + toId: string, + amountValue: unknown, + note?: string | null, +): Transaction { + ensureActiveSession(session); + ensureNotBlackout(session, bankerId); + ensureBanker(session, bankerId); + const amount = parseAmount(amountValue); + if (amount === null || amount <= 0) { + throw new DomainError("Transfer amount must be positive"); + } + const from = getPlayer(session, fromId); + const to = getPlayer(session, toId); + if (from.role === "banker" || to.role === "banker") { + throw new DomainError("Banker does not hold a balance"); + } + if (from.id === to.id) { + throw new DomainError("Cannot transfer to the same player"); + } + if (from.balance < amount) { + throw new DomainError("Source player lacks funds"); + } + from.balance = clampBalance(from.balance - amount); + to.balance = clampBalance(to.balance + amount); + const transaction = createTransaction( + "banker_force_transfer", + from.id, + to.id, + amount, + note, + "banker", + ); + session.transactions.unshift(transaction); + return transaction; +} + +export function createDummyPlayer( + session: Session, + bankerId: string, + name: string, + balanceValue?: unknown, +): Player { + ensureOpenSession(session); + ensureBanker(session, bankerId); + const joinedAt = now(); + const balance = parseAmount(balanceValue ?? DEFAULT_START_BALANCE) ?? DEFAULT_START_BALANCE; + const player: Player = { + id: createId(), + name: name.trim() || "Dummy", + role: "player", + balance: clampBalance(balance), + connected: false, + isDummy: true, + joinedAt, + lastActiveAt: joinedAt, + }; + session.players.set(player.id, player); + return player; +} + +export function addChatMessage( + session: Session, + playerId: string, + body: string, + groupId?: string | null, +): ChatMessage { + ensureOpenSession(session); + ensureNotBlackout(session, playerId); + const trimmed = body.trim(); + if (!trimmed) { + throw new DomainError("Message cannot be empty"); + } + if (trimmed.length > MAX_CHAT_LENGTH) { + throw new DomainError("Message is too long"); + } + const sender = getPlayer(session, playerId); + const message: ChatMessage = { + id: createId(), + fromId: sender.id, + body: trimmed, + createdAt: now(), + groupId: groupId ?? null, + }; + session.chats.unshift(message); + return message; +} + +export function createChatGroup( + session: Session, + playerId: string, + name: string, + memberIds: string[], +): ChatGroup { + ensureOpenSession(session); + ensureNotBlackout(session, playerId); + getPlayer(session, playerId); + const trimmed = name.trim(); + if (!trimmed) { + throw new DomainError("Group name is required"); + } + const deduped = Array.from(new Set([playerId, ...memberIds])); + deduped.forEach((memberId) => getPlayer(session, memberId)); + const group: ChatGroup = { + id: createId(), + name: trimmed, + memberIds: deduped, + createdAt: now(), + createdBy: playerId, + }; + session.groups.unshift(group); + return group; +} + +export function requestTakeover( + session: Session, + requesterId: string, + dummyId: string, +): TakeoverRequest { + ensureOpenSession(session); + const requester = getPlayer(session, requesterId); + const dummy = getPlayer(session, dummyId); + if (!dummy.isDummy) { + throw new DomainError("Selected player is not available for takeover"); + } + if (requester.role === "banker") { + throw new DomainError("Banker cannot request takeover"); + } + const existing = session.takeoverRequests.find( + (request) => + request.dummyId === dummyId && + request.requesterId === requesterId && + request.status === "pending", + ); + if (existing) { + return existing; + } + const request: TakeoverRequest = { + id: createId(), + dummyId, + requesterId, + createdAt: now(), + status: "pending", + }; + session.takeoverRequests.unshift(request); + return request; +} + +export function approveTakeover( + session: Session, + bankerId: string, + dummyId: string, + requesterId: string, +): string { + ensureOpenSession(session); + ensureBanker(session, bankerId); + const dummy = getPlayer(session, dummyId); + const requester = getPlayer(session, requesterId); + if (!dummy.isDummy) { + throw new DomainError("Selected player is not a dummy"); + } + const targetRequest = session.takeoverRequests.find( + (request) => + request.dummyId === dummyId && + request.requesterId === requesterId && + request.status === "pending", + ); + if (!targetRequest) { + throw new DomainError("No takeover request found"); + } + targetRequest.status = "approved"; + // Reject any other pending requests for the same dummy. + session.takeoverRequests.forEach((request) => { + if ( + request.dummyId === dummyId && + request.status === "pending" && + request.requesterId !== requesterId + ) { + request.status = "rejected"; + } + }); + + dummy.isDummy = false; + dummy.connected = requester.connected; + dummy.name = requester.name; + dummy.lastActiveAt = now(); + session.players.delete(requesterId); + + return dummy.id; +} + +function ensureActiveSession(session: Session): void { + ensureOpenSession(session); +} + +function ensureNotBlackout(session: Session, actorId?: string): void { + if (!isBlackoutActive(session)) { + return; + } + if (actorId) { + const actor = session.players.get(actorId); + if (actor?.role === "banker") { + return; + } + } + throw new DomainError("EMP in effect"); +} + +function ensureOpenSession(session: Session): void { + if (session.status === "ended") { + throw new DomainError("Session has ended"); + } +} + +function ensureBanker(session: Session, bankerId: string): void { + const banker = getPlayer(session, bankerId); + if (banker.role !== "banker") { + throw new DomainError("Only the banker can perform this action"); + } +} + +function getPlayer(session: Session, playerId: string): Player { + const player = session.players.get(playerId); + if (!player) { + throw new DomainError("Player not found"); + } + return player; +} + +function createTransaction( + kind: Transaction["kind"], + fromId: string | null, + toId: string | null, + amount: number, + note: string | null | undefined, + initiatedBy: Transaction["initiatedBy"], +): Transaction { + return { + id: createId(), + kind, + fromId, + toId, + amount: clampBalance(amount), + note: note?.trim() || null, + createdAt: now(), + initiatedBy, + }; +} diff --git a/server/errors.ts b/server/errors.ts new file mode 100644 index 0000000..9bfce67 --- /dev/null +++ b/server/errors.ts @@ -0,0 +1,8 @@ +export class DomainError extends Error { + code: string; + + constructor(message: string, code = "domain_error") { + super(message); + this.code = code; + } +} diff --git a/server/protocol.ts b/server/protocol.ts new file mode 100644 index 0000000..c2a1aae --- /dev/null +++ b/server/protocol.ts @@ -0,0 +1,98 @@ +import type { SessionSnapshot } from "../shared/types"; + +export type ClientMessage = + | { + type: "chat_send"; + sessionId: string; + playerId: string; + body: string; + groupId?: string | null; + } + | { + type: "transfer"; + sessionId: string; + playerId: string; + toPlayerId: string; + amount: number; + note?: string | null; + } + | { + type: "banker_adjust"; + sessionId: string; + bankerId: string; + targetId: string; + amount: number; + note?: string | null; + } + | { + type: "banker_force_transfer"; + sessionId: string; + bankerId: string; + fromId: string; + toId: string; + amount: number; + note?: string | null; + } + | { + type: "banker_blackout"; + sessionId: string; + bankerId: string; + active: boolean; + reason?: string | null; + } + | { + type: "banker_start"; + sessionId: string; + bankerId: string; + } + | { + type: "banker_end"; + sessionId: string; + bankerId: string; + } + | { + type: "banker_create_dummy"; + sessionId: string; + bankerId: string; + name: string; + balance?: number; + } + | { + type: "group_create"; + sessionId: string; + playerId: string; + name: string; + memberIds: string[]; + } + | { + type: "takeover_request"; + sessionId: string; + playerId: string; + dummyId: string; + } + | { + type: "banker_takeover_approve"; + sessionId: string; + bankerId: string; + dummyId: string; + requesterId: string; + } + | { + type: "ping"; + sessionId: string; + playerId: string; + }; + +export type ServerMessage = + | { + type: "state"; + session: SessionSnapshot; + } + | { + type: "error"; + message: string; + } + | { + type: "takeover_approved"; + assignedPlayerId: string; + }; diff --git a/server/store.ts b/server/store.ts new file mode 100644 index 0000000..3e631b0 --- /dev/null +++ b/server/store.ts @@ -0,0 +1,177 @@ +import type { Session } from "./types"; +import type { Player, SessionSnapshot } from "../shared/types"; +import { createId, createSessionCode, now, DEFAULT_START_BALANCE } from "./util"; + +const sessions = new Map(); +const sessionsByCode = new Map(); + +export const TEST_SESSION_CODE = "TEST"; +const TEST_BANKER_NAME = "Alan Wa Chahabab"; +const TEST_PLAYER_NAMES = ["Nesta", "Niko Stinko", "Gaspard l'Batard"]; + +export type CreateSessionResult = { + session: Session; + banker: Player; +}; + +export function createSession(bankerName: string): CreateSessionResult { + const id = createId(); + const code = createSessionCode(new Set(sessionsByCode.keys())); + const createdAt = now(); + const bankerId = createId(); + + const banker: Player = { + id: bankerId, + name: bankerName.trim() || "Banker", + role: "banker", + balance: 0, + connected: true, + isDummy: false, + joinedAt: createdAt, + lastActiveAt: createdAt, + }; + + const session: Session = { + id, + code, + status: "lobby", + createdAt, + bankerId, + blackoutActive: false, + blackoutReason: null, + players: new Map([[bankerId, banker]]), + transactions: [], + chats: [], + groups: [], + takeoverRequests: [], + }; + + sessions.set(id, session); + sessionsByCode.set(code, id); + + return { session, banker }; +} + +export function isTestSessionCode(code: string): boolean { + return code.trim().toUpperCase() === TEST_SESSION_CODE; +} + +export function createTestSession(): Session { + const id = createId(); + const createdAt = now(); + const bankerId = createId(); + + const banker: Player = { + id: bankerId, + name: TEST_BANKER_NAME, + role: "banker", + balance: 0, + connected: true, + isDummy: false, + joinedAt: createdAt, + lastActiveAt: createdAt, + }; + + const players = new Map(); + players.set(bankerId, banker); + TEST_PLAYER_NAMES.forEach((name) => { + const playerId = createId(); + players.set(playerId, { + id: playerId, + name, + role: "player", + balance: DEFAULT_START_BALANCE, + connected: true, + isDummy: false, + joinedAt: createdAt, + lastActiveAt: createdAt, + }); + }); + + const session: Session = { + id, + code: TEST_SESSION_CODE, + status: "active", + createdAt, + bankerId, + blackoutActive: false, + blackoutReason: null, + players, + transactions: [], + chats: [], + groups: [], + takeoverRequests: [], + isTest: true, + }; + + sessions.set(id, session); + + return session; +} + +export function createTestPreview(): SessionSnapshot { + const createdAt = now(); + const bankerId = createId(); + const players: Player[] = [ + { + id: bankerId, + name: TEST_BANKER_NAME, + role: "banker", + balance: 0, + connected: true, + isDummy: false, + joinedAt: createdAt, + lastActiveAt: createdAt, + }, + ...TEST_PLAYER_NAMES.map((name) => ({ + id: createId(), + name, + role: "player" as const, + balance: DEFAULT_START_BALANCE, + connected: true, + isDummy: false, + joinedAt: createdAt, + lastActiveAt: createdAt, + })), + ]; + + return { + id: TEST_SESSION_CODE, + code: TEST_SESSION_CODE, + status: "active", + createdAt, + bankerId, + blackoutActive: false, + blackoutReason: null, + players, + transactions: [], + chats: [], + groups: [], + takeoverRequests: [], + }; +} + +export function getSession(id: string): Session | null { + return sessions.get(id) ?? null; +} + +export function getSessionByCode(code: string): Session | null { + const id = sessionsByCode.get(code.toUpperCase()); + if (!id) { + return null; + } + return sessions.get(id) ?? null; +} + +export function removeSession(id: string): void { + const session = sessions.get(id); + if (!session) { + return; + } + sessions.delete(id); + sessionsByCode.delete(session.code); +} + +export function listSessions(): Session[] { + return Array.from(sessions.values()); +} diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..1a0706d --- /dev/null +++ b/server/types.ts @@ -0,0 +1,27 @@ +import type { + ChatGroup, + ChatMessage, + Player, + SessionSnapshot, + SessionStatus, + TakeoverRequest, + Transaction, +} from "../shared/types"; + +export type Session = { + id: string; + code: string; + status: SessionStatus; + createdAt: number; + bankerId: string; + blackoutActive: boolean; + blackoutReason: string | null; + players: Map; + transactions: Transaction[]; + chats: ChatMessage[]; + groups: ChatGroup[]; + takeoverRequests: TakeoverRequest[]; + isTest?: boolean; +}; + +export type SessionSnapshotPayload = SessionSnapshot; diff --git a/server/util.ts b/server/util.ts new file mode 100644 index 0000000..6fd14ce --- /dev/null +++ b/server/util.ts @@ -0,0 +1,41 @@ +export const DEFAULT_START_BALANCE = 1500; + +const CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +export function createId(): string { + return crypto.randomUUID(); +} + +export function createSessionCode(existing: Set): string { + for (let i = 0; i < 1000; i += 1) { + let code = ""; + for (let j = 0; j < 5; j += 1) { + code += CODE_CHARS[Math.floor(Math.random() * CODE_CHARS.length)]; + } + if (!existing.has(code)) { + return code; + } + } + throw new Error("Unable to allocate session code"); +} + +export function now(): number { + return Date.now(); +} + +export function parseAmount(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string") { + const parsed = Number.parseFloat(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + return null; +} + +export function clampBalance(value: number): number { + return Math.round(value * 100) / 100; +} diff --git a/server/websocket.ts b/server/websocket.ts new file mode 100644 index 0000000..dfdeb27 --- /dev/null +++ b/server/websocket.ts @@ -0,0 +1,284 @@ +import type { ServerMessage, ClientMessage } from "./protocol"; +import type { Session } from "./types"; +import { DomainError } from "./errors"; +import { + addChatMessage, + approveTakeover, + bankerAdjust, + bankerForceTransfer, + createChatGroup, + createDummyPlayer, + disconnectPlayer, + requestTakeover, + setBlackout, + snapshotSession, + startSession, + endSession, + transfer, +} from "./domain"; +import { getSession, removeSession } from "./store"; +import { now } from "./util"; + +const socketsBySession = new Map>(); +const metaBySocket = new WeakMap(); +const testTimers = new Map>(); + +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function scheduleTestTransfer(sessionId: string): void { + const delay = randomInt(3000, 7000); + const timer = setTimeout(() => { + testTimers.delete(sessionId); + runTestTransfer(sessionId); + }, delay); + testTimers.set(sessionId, timer); +} + +function runTestTransfer(sessionId: string): void { + const session = getSession(sessionId); + if (!session || !session.isTest) { + return; + } + if (session.status !== "active") { + return; + } + const players = Array.from(session.players.values()).filter( + (player) => player.role === "player", + ); + if (players.length < 2) { + scheduleTestTransfer(sessionId); + return; + } + let attempts = 0; + while (attempts < 5) { + const from = players[randomInt(0, players.length - 1)]; + let to = players[randomInt(0, players.length - 1)]; + if (to.id === from.id) { + attempts += 1; + continue; + } + const amount = randomInt(10, 250); + if (from.balance < amount) { + attempts += 1; + continue; + } + try { + transfer(session, from.id, to.id, amount, null); + sendStateToSession(session); + break; + } catch { + attempts += 1; + } + } + scheduleTestTransfer(sessionId); +} + +export function startTestSimulation(sessionId: string): void { + if (testTimers.has(sessionId)) return; + scheduleTestTransfer(sessionId); +} + +export function stopTestSimulation(sessionId: string): void { + const timer = testTimers.get(sessionId); + if (timer) { + clearTimeout(timer); + } + testTimers.delete(sessionId); +} + +function getSessionSockets(sessionId: string): Set { + let set = socketsBySession.get(sessionId); + if (!set) { + set = new Set(); + socketsBySession.set(sessionId, set); + } + return set; +} + +export function registerSocket(ws: WebSocket, sessionId: string, playerId: string): void { + const session = getSession(sessionId); + if (!session) { + ws.close(1008, "Session not found"); + return; + } + const player = session.players.get(playerId); + if (!player) { + ws.close(1008, "Player not found"); + return; + } + player.connected = true; + player.isDummy = false; + player.lastActiveAt = now(); + + metaBySocket.set(ws, { sessionId, playerId }); + getSessionSockets(sessionId).add(ws); + + sendStateToSession(session); +} + +export function unregisterSocket(ws: WebSocket): void { + const meta = metaBySocket.get(ws); + if (!meta) { + return; + } + const { sessionId, playerId } = meta; + const session = getSession(sessionId); + if (session) { + disconnectPlayer(session, playerId); + sendStateToSession(session); + } + const set = socketsBySession.get(sessionId); + if (set) { + set.delete(ws); + if (set.size === 0) { + socketsBySession.delete(sessionId); + if (session?.isTest) { + stopTestSimulation(sessionId); + removeSession(sessionId); + } + } + } + metaBySocket.delete(ws); +} + +export function handleSocketMessage(ws: WebSocket, raw: string | ArrayBuffer): void { + const messageText = typeof raw === "string" ? raw : new TextDecoder().decode(raw); + let parsed: ClientMessage; + try { + parsed = JSON.parse(messageText) as ClientMessage; + } catch (error) { + send(ws, { type: "error", message: "Invalid message" }); + return; + } + + const session = getSession(parsed.sessionId); + if (!session) { + send(ws, { type: "error", message: "Session not found" }); + return; + } + + try { + handleMessage(session, parsed); + } catch (error) { + const message = + error instanceof DomainError + ? error.message + : "Unexpected error while processing request"; + send(ws, { type: "error", message }); + return; + } + + sendStateToSession(session); +} + +function handleMessage(session: Session, message: ClientMessage): void { + switch (message.type) { + case "chat_send": + addChatMessage(session, message.playerId, message.body, message.groupId); + return; + case "transfer": + transfer(session, message.playerId, message.toPlayerId, message.amount, message.note); + return; + case "banker_adjust": + bankerAdjust(session, message.bankerId, message.targetId, message.amount, message.note); + return; + case "banker_force_transfer": + bankerForceTransfer( + session, + message.bankerId, + message.fromId, + message.toId, + message.amount, + message.note, + ); + return; + case "banker_blackout": + setBlackout(session, message.bankerId, message.active, message.reason); + return; + case "banker_start": + startSession(session, message.bankerId); + return; + case "banker_end": + endSession(session, message.bankerId); + return; + case "banker_create_dummy": + createDummyPlayer(session, message.bankerId, message.name, message.balance); + return; + case "group_create": + createChatGroup(session, message.playerId, message.name, message.memberIds); + return; + case "takeover_request": + requestTakeover(session, message.playerId, message.dummyId); + return; + case "banker_takeover_approve": { + const assignedId = approveTakeover( + session, + message.bankerId, + message.dummyId, + message.requesterId, + ); + notifyTakeoverApproval(session.id, message.requesterId, assignedId); + return; + } + case "ping": + touchPlayer(session, message.playerId); + return; + default: + return; + } +} + +function touchPlayer(session: Session, playerId: string): void { + const player = session.players.get(playerId); + if (player) { + player.lastActiveAt = now(); + } +} + +function sendStateToSession(session: Session): void { + const sockets = socketsBySession.get(session.id); + if (!sockets) { + return; + } + const stateMessage: ServerMessage = { + type: "state", + session: snapshotSession(session), + }; + sockets.forEach((socket) => send(socket, stateMessage)); +} + +export function broadcastSessionState(sessionId: string): void { + const session = getSession(sessionId); + if (!session) return; + sendStateToSession(session); +} + +function notifyTakeoverApproval( + sessionId: string, + requesterId: string, + assignedId: string, +): void { + const sockets = socketsBySession.get(sessionId); + if (!sockets) { + return; + } + sockets.forEach((socket) => { + const meta = metaBySocket.get(socket); + if (!meta) { + return; + } + if (meta.playerId === requesterId) { + send(socket, { type: "takeover_approved", assignedPlayerId: assignedId }); + meta.playerId = assignedId; + } + }); +} + +function send(ws: WebSocket, message: ServerMessage): void { + if (ws.readyState !== WebSocket.OPEN) { + return; + } + ws.send(JSON.stringify(message)); +} diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..bd8ee57 --- /dev/null +++ b/shared/types.ts @@ -0,0 +1,68 @@ +export type SessionStatus = "lobby" | "active" | "ended"; +export type PlayerRole = "banker" | "player"; + +export type Player = { + id: string; + name: string; + role: PlayerRole; + balance: number; + connected: boolean; + isDummy: boolean; + joinedAt: number; + lastActiveAt: number; +}; + +export type TransactionKind = + | "transfer" + | "banker_adjust" + | "banker_force_transfer"; + +export type Transaction = { + id: string; + kind: TransactionKind; + fromId: string | null; + toId: string | null; + amount: number; + note: string | null; + createdAt: number; + initiatedBy: PlayerRole; +}; + +export type ChatGroup = { + id: string; + name: string; + memberIds: string[]; + createdAt: number; + createdBy: string; +}; + +export type ChatMessage = { + id: string; + fromId: string; + body: string; + createdAt: number; + groupId: string | null; +}; + +export type TakeoverRequest = { + id: string; + dummyId: string; + requesterId: string; + createdAt: number; + status: "pending" | "approved" | "rejected"; +}; + +export type SessionSnapshot = { + id: string; + code: string; + status: SessionStatus; + createdAt: number; + bankerId: string; + blackoutActive: boolean; + blackoutReason: string | null; + players: Player[]; + transactions: Transaction[]; + chats: ChatMessage[]; + groups: ChatGroup[]; + takeoverRequests: TakeoverRequest[]; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}