From f430c2b757d01719cbc01720523b911839d946ef Mon Sep 17 00:00:00 2001 From: AG Date: Sat, 20 Dec 2025 16:32:05 +0200 Subject: [PATCH] Initial commit --- .context/Autosave Adjustment.md | 75 + .context/Local Storage Autosave.md | 40 + .context/Manual Session State Save.md | 82 + .context/task.md | 76 + .context/task2.md | 103 + .gitignore | 24 + README.md | 22 + defaultState.js | 24 + docker-compose.yml | 23 + ecosystem.config.js | 11 + package-lock.json | 2520 +++++++++++++++++++++++++ package.json | 27 + public/App.tsx | 101 + public/components/Modal.tsx | 37 + public/components/Sequencer.tsx | 350 ++++ public/components/icons.tsx | 70 + public/constants.ts | 39 + public/hooks/useCursors.ts | 96 + public/hooks/useDrumMachine.ts | 616 ++++++ public/hooks/useSession.ts | 24 + public/hooks/useWebSocket.ts | 66 + public/index.css | 12 + public/index.html | 15 + public/index.tsx | 17 + public/metadata.json | 6 + public/tsconfig.json | 30 + public/types.ts | 16 + public/utils.ts | 21 + server.dev.js | 104 + server.js | 119 ++ vite.config.ts | 31 + 31 files changed, 4797 insertions(+) create mode 100644 .context/Autosave Adjustment.md create mode 100644 .context/Local Storage Autosave.md create mode 100644 .context/Manual Session State Save.md create mode 100644 .context/task.md create mode 100644 .context/task2.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 defaultState.js create mode 100644 docker-compose.yml create mode 100644 ecosystem.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/App.tsx create mode 100644 public/components/Modal.tsx create mode 100644 public/components/Sequencer.tsx create mode 100644 public/components/icons.tsx create mode 100644 public/constants.ts create mode 100644 public/hooks/useCursors.ts create mode 100644 public/hooks/useDrumMachine.ts create mode 100644 public/hooks/useSession.ts create mode 100644 public/hooks/useWebSocket.ts create mode 100644 public/index.css create mode 100644 public/index.html create mode 100644 public/index.tsx create mode 100644 public/metadata.json create mode 100644 public/tsconfig.json create mode 100644 public/types.ts create mode 100644 public/utils.ts create mode 100644 server.dev.js create mode 100644 server.js create mode 100644 vite.config.ts diff --git a/.context/Autosave Adjustment.md b/.context/Autosave Adjustment.md new file mode 100644 index 0000000..37bcae2 --- /dev/null +++ b/.context/Autosave Adjustment.md @@ -0,0 +1,75 @@ +🔍 Problem Summary + +You implemented **Local Storage Autosave** (item #2), but **on page reload, the session does not restore the saved state**. The page loads with the default state instead. + +--- + +## 🔎 Likely Root Cause + +### ❌ You're saving the state to Local Storage, but **not loading it on startup**. + +In `useDrumMachine.ts`, the state is probably initialized like this (or similar): + +```ts +const [state, setState] = useState(defaultState); +``` + +This means the state **always starts from `defaultState`**, and no logic attempts to retrieve from `localStorage`. + +--- + +## ✅ Solution + +### 🛠 Modify the Initial State to Load from Local Storage + +Update your `useDrumMachine` hook to **attempt to load the autosaved session from localStorage first**, before falling back to the default state. + +#### ✅ Example Patch: + +```ts +const loadInitialState = (): StateType => { + try { + const saved = localStorage.getItem('Autosaved Session'); + if (saved) { + const parsed = JSON.parse(saved); + return parsed.data; + } + } catch (e) { + console.error('Failed to load autosaved session:', e); + } + return defaultState; +}; + +const [state, setState] = useState(loadInitialState); +``` + +--- + +## 📌 Notes + +- Make sure this runs **only for new sessions**, not when joining an existing session. + +- If your app distinguishes between "new session" and "joined session" using `useSession`, you can use that to conditionally load from local storage. + + +#### Example: + +```ts +const sessionId = useSession(); +const isNewSession = sessionId.startsWith('new-'); // or whatever logic you use + +const [state, setState] = useState(() => { + if (isNewSession) return loadInitialState(); + return defaultState; +}); +``` + +--- + +## ✅ Recap + +|Problem|Fix| +|---|---| +|State always loads from `defaultState`|Load from `localStorage` on first mount| +|Only autosave was implemented|Add **autosave + restore** for full loop| +|Make it conditional on session type|Don't overwrite a joined session with autosaved local one| diff --git a/.context/Local Storage Autosave.md b/.context/Local Storage Autosave.md new file mode 100644 index 0000000..47666b6 --- /dev/null +++ b/.context/Local Storage Autosave.md @@ -0,0 +1,40 @@ +### Local Storage Autosave + +#### ✅ Behavior + +- All local state changes (either **user-made** or **received from others**) are autosaved into local storage under the key `Autosaved Session`. + +- Throttle or debounce autosaving to reduce performance overhead. + - **Delay** of `750–1500ms` is typically ideal — tweak based on UX feedback. + - Wrap autosave logic in a `useEffect` (for React) that depends on your state. + - **Cancel debounce** on unmount to avoid memory leaks: + ``` +```ts +useEffect(() => { + return () => debouncedAutosave.cancel(); +}, []); + +``` + +#### 🔁 Autosave Trigger Events + +- State change from user interaction + +- State update from WebSocket messages + +- After loading a saved session (explained further below) + + +#### 🧠 Decision Logic on Load + +1. **New session**: + + - Start with default state + + - Autosave begins immediately + +2. **Join existing session**: + + - Request full state from server + + - Begin autosaving once session state is received and applied \ No newline at end of file diff --git a/.context/Manual Session State Save.md b/.context/Manual Session State Save.md new file mode 100644 index 0000000..78f7753 --- /dev/null +++ b/.context/Manual Session State Save.md @@ -0,0 +1,82 @@ +### 1. Manual Save + +#### ✅ "Save Session" Button + +- Button icon: 🖫 (diskette), with tooltip: "Save session" + +- Opens modal: + + - Input: name of the session + + - Option: overwrite existing session with same name (if applicable) + +- Stores: + + - Name + + - Timestamp + + - State (`data`) + + - `isAutosave: false` + + +--- + +### 2. Saved Sessions Panel + +#### ✅ Access + +- Button/icon to open modal or dropdown with saved sessions + + +#### 🔁 Display Rules + +- Top item: "Autosaved Session" (non-deletable, clearly labeled) + +- Followed by manually saved sessions, **sorted alphabetically by name** + + +#### ✅ Item UI Elements + +- Session name + +- Save timestamp + +- Load button (clickable name) + +- Delete button (only for manual saves) + + - Delete must open confirmation modal: + + > “Delete this saved session? This action cannot be undone.” + + +#### ✅ Loading a Saved Session + +- Replaces current app state with the saved one + +- If unsaved changes exist, confirm before loading: + + > “Load saved session? This will replace your current progress.” + + +--- + +### 🧠 Logic Extension: Loading Saved Sessions & Autosave + +> ✅ **When the user loads a saved session, autosave continues in the background.** + +- The **loaded state becomes the new working session**. + +- All further changes are autosaved into the `Autosaved Session`. + +- The user can still press "Save Session" again: + + - If using the **same name**, prompt: + + > “Overwrite the session ‘ChillBeat’? This will replace the saved version.” + + - Or offer a new name input + +- User can also choose to **delete the older version** after saving. \ No newline at end of file diff --git a/.context/task.md b/.context/task.md new file mode 100644 index 0000000..b89b414 --- /dev/null +++ b/.context/task.md @@ -0,0 +1,76 @@ +Here is a clear and concise explanation of the task for an AI agent to fix the described problem. + +--- + +### **Objective: Fix Session State Synchronization for New Clients** + +Your task is to correct a bug in the application's real-time collaboration feature. Currently, when a new client joins an existing session, they see the application's default state, not the session's current, modified state. This creates a state desynchronization issue. + +### **Problem Analysis** + +The client-side application is already architected to handle an initial state dump upon connection. + +1. **`useWebSocket.ts`:** When a message with `type: 'welcome'` is received, the hook is designed to take the `message.payload.state` object and pass it to the application's main state management hook. + +2. **`useDrumMachine.ts`:** This hook has a `useEffect` that listens for incoming messages. It contains a specific `case 'state':` that correctly processes a full state object, updating the grid, bassline, tempo, steps, and other parameters. + + +The problem is not on the client side. **The root cause is on the server-side WebSocket implementation.** + +The server is likely doing one of the following incorrect things: + +- It sends a `'welcome'` message that does **not** contain the complete, current state of the session. + +- It sends a `'welcome'` message with an empty or default `state` object. + +- There is a race condition where the server sends the `'welcome'` message _before_ it has finished retrieving the current session state from its data store. + + +### **Required Implementation** + +You must modify the **server-side** WebSocket logic to ensure state is synchronized correctly. + +1. **On a new WebSocket connection:** When a client connects and provides a `sessionId`, your server-side code must immediately perform the following steps. + +2. **Retrieve the complete current state** for that `sessionId`. This state must include: + + - The drum `grid` + + - The `bassLine` + + - The current `tempo` + + - The number of `steps` + + - The `mutes` array + + - The `drumVolume` + + - The `bassVolume` + +3. **Construct a `'welcome'` message.** This message must contain the full state object you just retrieved. The JSON payload sent to the newly connected client must follow this exact structure: + + JSON + + ``` + { + "type": "welcome", + "payload": { + "clientId": "UNIQUE_CLIENT_ID_FOR_NEW_CLIENT", + "state": { + "grid": [/* current grid state */], + "bassLine": [/* current bassline state */], + "tempo": 135, // The current tempo + "steps": 32, // The current number of steps + "mutes": [false, true, ...], // The current mute states + "drumVolume": 0.8, + "bassVolume": 0.5 + } + } + } + ``` + +4. **Send this `'welcome'` message** to the newly connected client _immediately_ upon their connection. This must be the very first message they receive, ensuring the UI is rendered with the correct state from the start. + + +This change will ensure that any client joining an in-progress session will see the exact same state as all other participants, resolving the synchronization bug. \ No newline at end of file diff --git a/.context/task2.md b/.context/task2.md new file mode 100644 index 0000000..34194e3 --- /dev/null +++ b/.context/task2.md @@ -0,0 +1,103 @@ +To solve the **desynchronization problem** where **Client 2 sees default state instead of current state**, the AI agent needs to implement **state synchronization on join**. Here's a detailed plan for what needs to be done, which you can feed to an AI agent or developer: + +--- + +## ✅ Problem Summary + +* **Client 1** changes app state; changes are broadcast via WebSocket. +* **Client 2** joins a session but does **not receive the current state** of all elements—only the default. +* When Client 1 makes further changes, Client 2 sees them, but this causes layout inconsistency since Client 2’s other state is outdated. +* If Client 2 makes changes, it **overwrites** the correct session state. + +--- + +## 🧠 What the AI Agent Must Do + +### 1. **Server: Maintain the Current State per Session** + +* In `server.js`, ensure there is a per-session object (e.g., `{ sessionId: stateObject }`) that stores **all element states**. +* When **Client 1** sends a change message (`{ type: 'update', payload: {...} }`), update that stored session state accordingly. + +#### ✅ Example in `server.js` + +```js +const sessions = {}; // { [sessionId]: { state: {...}, clients: [] } } + +function handleMessage(message, ws, sessionId) { + const data = JSON.parse(message); + if (data.type === 'update') { + // Update session state + if (!sessions[sessionId].state) sessions[sessionId].state = {}; + sessions[sessionId].state[data.payload.id] = data.payload; + + // Broadcast to others + broadcastExcept(ws, sessionId, message); + } +} +``` + +--- + +### 2. **Client 2: Request the Current Session State on Join** + +* When Client 2 connects via WebSocket, it must send a message like: + +```json +{ "type": "get_state" } +``` + +--- + +### 3. **Server: Respond to `get_state` with Full State Snapshot** + +* On receiving `{ type: 'get_state' }`, send: + +```json +{ "type": "session_state", "payload": { ...allCurrentState } } +``` + +--- + +### 4. **Client 2: Apply the Session State on First Load** + +In `useDrumMachine` (or wherever state is applied), handle a new message type: + +```ts +if (message.type === 'session_state') { + applyFullState(message.payload); // update all controls +} +``` + +--- + +### 5. **(Optional) Mark Synchronization Complete** + +* Use a flag like `isSynchronized` on the client to **ignore all inputs** until the full session state is received and applied. + +--- + +## 🧩 Where This Fits in Your Code + +* In `App.tsx`, you’re already using a `useWebSocket` hook — you’ll need to modify it so that: + + * On `open`, send `get_state`. + * On receiving `session_state`, trigger state application. + +* The actual application of state will happen inside `useDrumMachine`. + +--- + +## ✅ Summary Checklist + +| Step | Task | +| ---- | --------------------------------------------------------- | +| ✅ | Store session state on server per `sessionId` | +| ✅ | Update session state on every change | +| ✅ | On new client connect, send `get_state` | +| ✅ | Server responds with `session_state` | +| ✅ | Client applies full session state before handling updates | +| ⛔ | Don't allow local edits before sync is complete | + +--- + +Would you like help modifying the specific `useWebSocket` or `useDrumMachine` code to support this logic? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cd978a --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` + + +## New features + +1. Updated volume sliders styles for Firefox. +2. Headings and page Title update. +3. Space toggles Play/ Stop. +4. While playback is idle, pressing on a cell triggers instrument sound. \ No newline at end of file diff --git a/defaultState.js b/defaultState.js new file mode 100644 index 0000000..8ddbb82 --- /dev/null +++ b/defaultState.js @@ -0,0 +1,24 @@ +const INITIAL_STEPS = 16; +const INITIAL_TEMPO = 76; +const INSTRUMENTS_LENGTH = 5; + +const createEmptyGrid = (steps) => { + return Array.from({ length: INSTRUMENTS_LENGTH }, () => Array(steps).fill(false)); +}; + +const createEmptyBassLine = (steps) => { + return Array.from({ length: steps }, () => []); +} + +const defaultState = { + grid: createEmptyGrid(INITIAL_STEPS), + bassLine: createEmptyBassLine(INITIAL_STEPS), + tempo: INITIAL_TEMPO, + steps: INITIAL_STEPS, + mutes: Array(INSTRUMENTS_LENGTH).fill(false), + drumVolume: 1, + bassVolume: 0.4, + isPlaying: false +}; + +module.exports = { defaultState }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cef8509 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + nodejs-apps: + image: node:lts-slim + + container_name: node-apps + ports: + - "3030:3000" + - "3031:3001" + - "3032:3002" + + # Set the working directory inside the container + working_dir: /usr/src/app + volumes: + - ./nodejs_data:/usr/src/app + + # The command to execute when the container starts. + # It performs the following sequence: + # 1. Installs PM2 globally. + # 2. Runs the main application dependencies installation (assuming you have a package.json). + # 3. Executes pm2-runtime, using the config file to launch all three apps + # (ag-home, ag-beats, ag-ball) and keeping the container running. + command: /bin/sh -c "npm install -g pm2 && cd ag-beats && npm install && cd ../ball-shooting && npm install && cd .. && pm2 start server.js --name ag-home && cd ag-beats && pm2 start ecosystem.config.js && cd ../ball-shooting && pm2 start npm --name ag-ball -- start && pm2 logs --raw" + restart: unless-stopped diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..5a26291 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,11 @@ +module.exports = { + apps : [{ + name : "ag-beats", + script : "./server.js", + interpreter: "node", + args: "", + node_args: "", + wait_ready: true, + listen_timeout: 5000 + }] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0698153 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2520 @@ +{ + "name": "ag-beats", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ag-beats", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "express": "^4.19.2", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "uuid": "^9.0.1", + "vite": "^5.3.3", + "ws": "^8.18.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.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-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", + "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", + "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", + "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", + "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", + "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", + "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", + "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", + "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", + "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", + "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", + "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", + "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", + "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", + "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", + "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", + "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", + "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", + "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", + "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", + "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/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/body-parser/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/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.182", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", + "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", + "dev": true, + "license": "ISC" + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "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/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/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/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/express/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/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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.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/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/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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", + "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.0", + "@rollup/rollup-android-arm64": "4.45.0", + "@rollup/rollup-darwin-arm64": "4.45.0", + "@rollup/rollup-darwin-x64": "4.45.0", + "@rollup/rollup-freebsd-arm64": "4.45.0", + "@rollup/rollup-freebsd-x64": "4.45.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", + "@rollup/rollup-linux-arm-musleabihf": "4.45.0", + "@rollup/rollup-linux-arm64-gnu": "4.45.0", + "@rollup/rollup-linux-arm64-musl": "4.45.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-musl": "4.45.0", + "@rollup/rollup-linux-s390x-gnu": "4.45.0", + "@rollup/rollup-linux-x64-gnu": "4.45.0", + "@rollup/rollup-linux-x64-musl": "4.45.0", + "@rollup/rollup-win32-arm64-msvc": "4.45.0", + "@rollup/rollup-win32-ia32-msvc": "4.45.0", + "@rollup/rollup-win32-x64-msvc": "4.45.0", + "fsevents": "~2.3.2" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "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": "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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6dff420 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "ag-beats", + "version": "1.0.0", + "description": "A web-based drum machine.", + "main": "server.js", + "type": "commonjs", + "scripts": { + "start": "node server.js", + "build": "vite build", + "postinstall": "npm run build", + "dev": "node server.dev.js" + }, + "dependencies": { + "express": "^4.19.2", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "ws": "^8.18.0", + "uuid": "^9.0.1" + } +} \ No newline at end of file diff --git a/public/App.tsx b/public/App.tsx new file mode 100644 index 0000000..b8c4803 --- /dev/null +++ b/public/App.tsx @@ -0,0 +1,101 @@ + +import React, { useEffect, useCallback, useState, useRef } from 'react'; +import { Sequencer } from './components/Sequencer'; +import { useDrumMachine } from './hooks/useDrumMachine'; +import { useSession } from './hooks/useSession'; +import { useWebSocket } from './hooks/useWebSocket'; +import { useCursors } from './hooks/useCursors'; +import { ShareIcon, CursorIcon } from './components/icons'; + +function App(): React.ReactNode { + const sessionId = useSession(); + const { lastMessage, sendMessage, isConnected, isSynchronized, clientId } = useWebSocket(sessionId); + const drumMachine = useDrumMachine(lastMessage, sendMessage); + const mainRef = useRef(null); + const cursors = useCursors(sendMessage, lastMessage, clientId, mainRef); + const { isPlaying, startPlayback, stopPlayback } = drumMachine; + + const [showCopyMessage, setShowCopyMessage] = useState(false); + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (event.code === 'Space' && isSynchronized) { + event.preventDefault(); + if (isPlaying) { + stopPlayback(); + } else { + startPlayback(); + } + } + }, [isPlaying, startPlayback, stopPlayback, isSynchronized]); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + const handleShareSession = () => { + navigator.clipboard.writeText(window.location.href).then(() => { + setShowCopyMessage(true); + setTimeout(() => setShowCopyMessage(false), 3000); + }); + }; + + return ( +
+
+
+

AG Beats

+

Craft your beats and bass lines with this interactive step sequencer.

+
+
+ {isConnected ? '● Connected' : '● Disconnected'} +
+
+ + {showCopyMessage && ( +
+ Session link copied. Send it to your friends! +
+ )} +
+
+
+
+ {Object.values(cursors).map((cursor: any) => ( +
+ +
+ ))} + {isSynchronized ? ( + + ) : ( +
+

Synchronizing session state...

+
+ )} +
+
+

Built with React, TypeScript, and Tailwind CSS. Powered by the Web Audio API and WebSockets.

+
+
+
+ ); +} + +export default App; diff --git a/public/components/Modal.tsx b/public/components/Modal.tsx new file mode 100644 index 0000000..ac43982 --- /dev/null +++ b/public/components/Modal.tsx @@ -0,0 +1,37 @@ + +import React from 'react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + children: React.ReactNode; +} + +export const Modal: React.FC = ({ isOpen, onClose, onConfirm, title, children }) => { + if (!isOpen) return null; + + return ( +
+
+

{title}

+
{children}
+
+ + +
+
+
+ ); +}; diff --git a/public/components/Sequencer.tsx b/public/components/Sequencer.tsx new file mode 100644 index 0000000..9de3123 --- /dev/null +++ b/public/components/Sequencer.tsx @@ -0,0 +1,350 @@ + +import React, { useRef, useState, useEffect } from 'react'; +import { INSTRUMENTS, BASS_NOTES, MIN_TEMPO, MAX_TEMPO, MIN_STEPS, MAX_STEPS } from '../constants'; +import { PlayIcon, StopIcon, ClearIcon, UploadIcon, DownloadIcon, PlusIcon, MinusIcon, MuteIcon, UnmuteIcon } from './icons'; +import { Grid, BassLineGrid } from '../types'; +import { Modal } from './Modal'; + +interface SequencerProps { + steps: number; + grid: Grid; + bassLine: BassLineGrid; + isPlaying: boolean; + tempo: number; + currentStep: number | null; + mutes: boolean[]; + drumVolume: number; + bassVolume: number; + setStep: (instrumentIndex: number, stepIndex: number, isActive: boolean) => void; + setBassNote: (stepIndex: number, note: string) => void; + clearPattern: () => void; + setTempo: React.Dispatch>; + handleStepsChange: (newSteps: number) => void; + startPlayback: () => Promise; + stopPlayback: () => void; + exportBeat: () => void; + importBeat: (event: React.ChangeEvent) => void; + toggleMute: (instrumentIndex: number) => void; + handleDrumVolumeChange: (newVolume: number) => void; + handleBassVolumeChange: (newVolume: number) => void; + triggerSound: (instrumentName: string) => void; + triggerBassNote: (note: string) => void; +} + +const SequencerButton: React.FC<{ onClick?: () => void; children: React.ReactNode; className?: string; disabled?: boolean }> = ({ onClick, children, className, disabled }) => ( + +); + +export const Sequencer: React.FC = ({ + steps, grid, isPlaying, tempo, currentStep, mutes, bassLine, + drumVolume, bassVolume, + clearPattern, setTempo, handleStepsChange, setStep, setBassNote, + startPlayback, stopPlayback, exportBeat, importBeat, toggleMute, + handleDrumVolumeChange, handleBassVolumeChange, triggerSound, triggerBassNote +}) => { + + const fileInputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [dragActivationState, setDragActivationState] = useState(false); + const [dragStartRow, setDragStartRow] = useState(null); + + const [isBassDragging, setIsBassDragging] = useState(false); + const [bassDragActivationState, setBassDragActivationState] = useState(false); + const [isClearModalOpen, setIsClearModalOpen] = useState(false); + + useEffect(() => { + const handleMouseUp = () => { + setIsDragging(false); + setDragStartRow(null); + setIsBassDragging(false); + }; + window.addEventListener('mouseup', handleMouseUp); + return () => window.removeEventListener('mouseup', handleMouseUp); + }, []); + + + const handleImportClick = () => { + fileInputRef.current?.click(); + }; + + const handleCellMouseDown = (instIndex: number, stepIndex: number) => { + setIsDragging(true); + setDragStartRow(instIndex); + const newState = !grid[instIndex]?.[stepIndex]; + setDragActivationState(newState); + setStep(instIndex, stepIndex, newState); + if (newState && !isPlaying) { + triggerSound(INSTRUMENTS[instIndex].name); + } + }; + + const handleCellMouseEnter = (instIndex: number, stepIndex: number) => { + if (isDragging && instIndex === dragStartRow) { + setStep(instIndex, stepIndex, dragActivationState); + } + }; + + const handleBassCellMouseDown = (noteName: string, stepIndex: number) => { + const isCurrentlyActive = bassLine[stepIndex]?.includes(noteName); + const newState = !isCurrentlyActive; + setBassNote(stepIndex, noteName); + setIsBassDragging(true); + setBassDragActivationState(newState); + if (newState && !isPlaying) { + triggerBassNote(noteName); + } + }; + + const handleBassCellMouseEnter = (noteName: string, stepIndex: number) => { + if (isBassDragging) { + const isCurrentlyActive = bassLine[stepIndex]?.includes(noteName); + if (bassDragActivationState !== isCurrentlyActive) { + setBassNote(stepIndex, noteName); + } + } + }; + + const incrementTempo = () => setTempo(t => Math.min(MAX_TEMPO, t + 1)); + const decrementTempo = () => setTempo(t => Math.max(MIN_TEMPO, t - 1)); + + const handleTempoScroll = (e: React.WheelEvent) => { + e.preventDefault(); + if (e.deltaY < 0) { + decrementTempo(); + } else { + incrementTempo(); + } + }; + + const incrementSteps = () => { + const newSteps = steps + 4; + if (newSteps <= MAX_STEPS) { + handleStepsChange(newSteps); + } + }; + + const decrementSteps = () => { + const newSteps = steps - 4; + if (newSteps >= MIN_STEPS) { + handleStepsChange(newSteps); + } + }; + + const handleClearConfirm = () => { + clearPattern(); + setIsClearModalOpen(false); + }; + + return ( + <> + setIsClearModalOpen(false)} + onConfirm={handleClearConfirm} + title="Clear Session" + > +

Are you sure you want to clear the session? This will erase all current progress.

+
+
+
+
+ {!isPlaying ? ( + + Play + + ) : ( + + Stop + + )} + setIsClearModalOpen(true)} title="Clear"> + Clear + +
+ +
+
+ +
+ + + {tempo} BPM + + +
+
+
+ +
+ + + {steps} + + +
+
+
+ +
+ + Import + + + + Export + +
+
+ +
+
+ handleDrumVolumeChange(Number(e.target.value))} + className="w-4 h-48 appearance-none cursor-pointer bg-slate-200 rounded-lg [writing-mode:vertical-lr] [direction:rtl] accent-orange-500" + aria-orientation="vertical" + /> + +
+
+
+ {/* Header */} +
+ {Array.from({ length: steps }, (_, i) => ( +
+ {(i % 4 === 0) ? (i/4 + 1) : ''} +
+ ))} + {/* Grid Rows */} + {INSTRUMENTS.map((instrument, instIndex) => ( +
+
+ + {instrument.name} +
+ {grid[instIndex]?.map((isActive, stepIndex) => { + const isCurrent = currentStep === stepIndex; + const isFourth = stepIndex % 4 === 0; + return ( +
handleCellMouseDown(instIndex, stepIndex)} + onMouseEnter={() => handleCellMouseEnter(instIndex, stepIndex)} + onDragStart={(e) => e.preventDefault()} + /> + ) + })} +
+ ))} +
+
+
+ {/* Bass Sequencer */} +
+
+ handleBassVolumeChange(Number(e.target.value))} + className="w-4 h-48 appearance-none cursor-pointer bg-slate-200 rounded-lg [writing-mode:vertical-lr] [direction:rtl] accent-purple-500" + aria-orientation="vertical" + /> + +
+
+
+ {/* Header */} +
+ {Array.from({ length: steps }, (_, i) => ( +
+ {(i % 4 === 0) ? {(i/4 + 1)} : ''} +
+ ))} + {/* Bass Grid Rows (reversed to have low notes at the bottom) */} + {[...BASS_NOTES].reverse().map((note) => ( +
+
+ {note.name} +
+ {Array.from({ length: steps }).map((_, stepIndex) => { + const isSelected = bassLine[stepIndex]?.includes(note.name); + const isCurrent = currentStep === stepIndex; + return ( +
handleBassCellMouseDown(note.name, stepIndex)} + onMouseEnter={() => handleBassCellMouseEnter(note.name, stepIndex)} + onDragStart={(e) => e.preventDefault()} + > + {isCurrent &&
} + {stepIndex > 0 && stepIndex % 4 === 0 &&
} +
+ ) + })} +
+ ))} +
+
+
+
+ + ); +}; \ No newline at end of file diff --git a/public/components/icons.tsx b/public/components/icons.tsx new file mode 100644 index 0000000..8dcd98f --- /dev/null +++ b/public/components/icons.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +interface IconProps extends React.SVGProps {} + +export const PlayIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const StopIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const ClearIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const UploadIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const DownloadIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const PlusIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const MinusIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const MuteIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + + +); + +export const UnmuteIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); + +export const CursorIcon: React.FC = (props) => ( + + + +); + +export const ShareIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + +); \ No newline at end of file diff --git a/public/constants.ts b/public/constants.ts new file mode 100644 index 0000000..35578e0 --- /dev/null +++ b/public/constants.ts @@ -0,0 +1,39 @@ + +import { Instrument } from './types'; + +export const INSTRUMENTS: Instrument[] = [ + { name: 'Kick' }, + { name: 'Snare' }, + { name: 'Hi-Hat' }, + { name: 'Open Hat' }, + { name: 'Ride' }, +]; + +export const INITIAL_TEMPO = 76; +export const INITIAL_STEPS = 16; +export const MIN_TEMPO = 40; +export const MAX_TEMPO = 240; +export const MIN_STEPS = 4; +export const MAX_STEPS = 64; + +// --- BASS CONSTANTS --- + +const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + +const getFrequency = (midiNote: number): number => { + // A4 (MIDI 69) is 440 Hz + return 440 * Math.pow(2, (midiNote - 69) / 12); +}; + +export const BASS_NOTES: { name: string; isSharp: boolean }[] = []; +export const NOTE_FREQ_MAP: { [key: string]: number } = {}; + +// Generate notes for 2 octaves, starting from C2 (MIDI 36) up to B3 +for (let octave = 2; octave < 4; octave++) { + for (let i = 0; i < 12; i++) { + const noteName = `${NOTE_NAMES[i]}${octave}`; + const midiNote = 36 + (octave - 2) * 12 + i; + BASS_NOTES.push({ name: noteName, isSharp: NOTE_NAMES[i].includes('#') }); + NOTE_FREQ_MAP[noteName] = getFrequency(midiNote); + } +} diff --git a/public/hooks/useCursors.ts b/public/hooks/useCursors.ts new file mode 100644 index 0000000..52a1120 --- /dev/null +++ b/public/hooks/useCursors.ts @@ -0,0 +1,96 @@ +import { useState, useEffect, useCallback, RefObject } from 'react'; + +const COLORS = [ + '#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', + '#43AA8B', '#4D908E', '#577590', '#277DA1', '#F94144' +]; + +function getCursorColor(id: string) { + let hash = 0; + for (let i = 0; i < id.length; i++) { + hash = id.charCodeAt(i) + ((hash << 5) - hash); + } + return COLORS[Math.abs(hash) % COLORS.length]; +} + +export function useCursors(sendMessage: (message: any) => void, lastMessage: any, clientId: string | null, mainRef: RefObject) { + const [normalizedCursors, setNormalizedCursors] = useState({}); + const [cursors, setCursors] = useState({}); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!mainRef.current) return; + + const mainRect = mainRef.current.getBoundingClientRect(); + const x = e.clientX - mainRect.left; + const y = e.clientY - mainRect.top; + + const normalizedX = x / mainRect.width; + const normalizedY = y / mainRect.height; + + sendMessage({ + type: 'cursor-move', + payload: { x: normalizedX, y: normalizedY } + }); + }, [sendMessage, mainRef]); + + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + }; + }, [handleMouseMove]); + + const updateCursorPositions = useCallback(() => { + if (!mainRef.current) return; + const mainRect = mainRef.current.getBoundingClientRect(); + const newCursors = {}; + for (const id in normalizedCursors) { + const nc = normalizedCursors[id]; + newCursors[id] = { + ...nc, + x: nc.x * mainRect.width, + y: nc.y * mainRect.height + }; + } + setCursors(newCursors); + }, [normalizedCursors, mainRef]); + + + useEffect(() => { + if (!lastMessage) return; + + const { type, payload, senderId } = lastMessage; + + if (type === 'user-update') { + const remoteUsers = payload.users.filter((user: any) => user.id !== clientId); + const newCursors = {}; + remoteUsers.forEach((user: any) => { + const existing = normalizedCursors[user.id]; + newCursors[user.id] = { + id: user.id, + color: getCursorColor(user.id), + x: existing?.x || 0, + y: existing?.y || 0 + }; + }); + setNormalizedCursors(newCursors); + } else if (type === 'cursor-move' && senderId !== clientId) { + setNormalizedCursors(prev => ({ + ...prev, + [senderId]: { ...prev[senderId], ...payload } + })); + } + + }, [lastMessage, clientId]); + + useEffect(() => { + updateCursorPositions(); + window.addEventListener('resize', updateCursorPositions); + return () => { + window.removeEventListener('resize', updateCursorPositions); + }; + }, [updateCursorPositions]); + + + return cursors; +} diff --git a/public/hooks/useDrumMachine.ts b/public/hooks/useDrumMachine.ts new file mode 100644 index 0000000..cfabb06 --- /dev/null +++ b/public/hooks/useDrumMachine.ts @@ -0,0 +1,616 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { INSTRUMENTS, INITIAL_TEMPO, INITIAL_STEPS, MIN_STEPS, MAX_STEPS, NOTE_FREQ_MAP } from '../constants'; +import { Grid, BeatData, BassLineGrid } from '../types'; + +const createEmptyGrid = (steps: number): Grid => { + return Array.from({ length: INSTRUMENTS.length }, () => Array(steps).fill(false)); +}; + +const createEmptyBassLine = (steps: number): BassLineGrid => { + return Array.from({ length: steps }, () => []); +} + +const playKick = (audioContext: AudioContext, time: number, destination: AudioNode) => { + const osc = audioContext.createOscillator(); + const subOsc = audioContext.createOscillator(); + const gain = audioContext.createGain(); + const subGain = audioContext.createGain(); + const shaper = audioContext.createWaveShaper(); + + shaper.curve = new Float32Array(65536).map((_, i) => { + const x = (i / 32768) - 1; + return Math.tanh(3 * x); + }); + shaper.oversample = '4x'; + + osc.connect(gain); + subOsc.connect(subGain); + gain.connect(shaper); + subGain.connect(shaper); + shaper.connect(destination); + + const duration = 0.6; + const finalStopTime = time + duration; + + osc.type = 'sine'; + osc.frequency.setValueAtTime(180, time); + osc.frequency.exponentialRampToValueAtTime(30, time + 0.1); + gain.gain.setValueAtTime(1.8, time); + gain.gain.setTargetAtTime(0, time, 0.1); + + subOsc.type = 'sine'; + subOsc.frequency.setValueAtTime(50, time); + subGain.gain.setValueAtTime(0.35, time); + subGain.gain.exponentialRampToValueAtTime(0.001, time + 0.2); + + osc.start(time); + subOsc.start(time); + osc.stop(finalStopTime); + subOsc.stop(finalStopTime); +}; + +const playSnare = (audioContext: AudioContext, time: number, destination: AudioNode) => { + const noiseGain = audioContext.createGain(); + const noiseFilter = audioContext.createBiquadFilter(); + const osc = audioContext.createOscillator(); + const oscGain = audioContext.createGain(); + + const noiseBuffer = audioContext.createBuffer(1, audioContext.sampleRate * 0.5, audioContext.sampleRate); + const output = noiseBuffer.getChannelData(0); + for (let i = 0; i < output.length; i++) { + output[i] = Math.random() * 2 - 1; + } + const noiseSource = audioContext.createBufferSource(); + noiseSource.buffer = noiseBuffer; + + noiseFilter.type = 'highpass'; + noiseFilter.frequency.value = 1000; + noiseSource.connect(noiseFilter); + noiseFilter.connect(noiseGain); + noiseGain.connect(destination); + + osc.type = 'triangle'; + osc.frequency.setValueAtTime(200, time); + osc.connect(oscGain); + oscGain.connect(destination); + + const duration = 0.2; + noiseGain.gain.setValueAtTime(1, time); + noiseGain.gain.exponentialRampToValueAtTime(0.01, time + duration); + oscGain.gain.setValueAtTime(0.7, time); + oscGain.gain.exponentialRampToValueAtTime(0.01, time + duration / 2); + + noiseSource.start(time); + osc.start(time); + noiseSource.stop(time + duration); + osc.stop(time + duration); +}; + +const createHiHatSound = (audioContext: AudioContext, time: number, destination: AudioNode, duration: number) => { + const fundamental = 40; + const ratios = [2, 3, 4.16, 5.43, 6.79, 8.21]; + const gain = audioContext.createGain(); + const bandpass = audioContext.createBiquadFilter(); + const highpass = audioContext.createBiquadFilter(); + + bandpass.type = 'bandpass'; + bandpass.frequency.value = 10000; + bandpass.Q.value = 0.5; + + highpass.type = 'highpass'; + highpass.frequency.value = 7000; + + gain.connect(bandpass); + bandpass.connect(highpass); + highpass.connect(destination); + + ratios.forEach(ratio => { + const osc = audioContext.createOscillator(); + osc.type = 'square'; + osc.frequency.value = (fundamental * ratio) + (Math.random() * fundamental * 0.1); + osc.connect(gain); + osc.start(time); + osc.stop(time + duration); + }); + + gain.gain.setValueAtTime(0.00001, time); + gain.gain.exponentialRampToValueAtTime(0.4, time + 0.02); + gain.gain.exponentialRampToValueAtTime(0.00001, time + duration); +}; + +const playHiHat = (audioContext: AudioContext, time: number, destination: AudioNode) => { + createHiHatSound(audioContext, time, destination, 0.08); +}; + +const playOpenHat = (audioContext: AudioContext, time: number, destination: AudioNode) => { + createHiHatSound(audioContext, time, destination, 0.8); +}; + +const playRide = (audioContext: AudioContext, time: number, destination: AudioNode) => { + const masterGain = audioContext.createGain(); + const highpass = audioContext.createBiquadFilter(); + + highpass.type = 'highpass'; + highpass.frequency.setValueAtTime(800, time); + highpass.Q.value = 0.8; + + masterGain.connect(highpass); + highpass.connect(destination); + + const tickOsc = audioContext.createOscillator(); + const tickGain = audioContext.createGain(); + tickOsc.type = 'square'; + tickOsc.frequency.setValueAtTime(1200, time); + + tickGain.gain.setValueAtTime(0.5, time); + tickGain.gain.exponentialRampToValueAtTime(0.0001, time + 0.02); + + tickOsc.connect(tickGain); + tickGain.connect(masterGain); + + const fundamental = 120; + const ratios = [1.00, 1.41, 2.23, 2.77, 3.14, 4.01]; + ratios.forEach(ratio => { + const osc = audioContext.createOscillator(); + osc.type = 'square'; + osc.frequency.value = fundamental * ratio + (Math.random() - 0.5) * 5; + osc.connect(masterGain); + osc.start(time); + osc.stop(time + 1.2); + }); + + masterGain.gain.setValueAtTime(0.0001, time); + masterGain.gain.exponentialRampToValueAtTime(0.6, time + 0.005); + masterGain.gain.exponentialRampToValueAtTime(0.2, time + 0.1); + masterGain.gain.exponentialRampToValueAtTime(0.0001, time + 1.2); + + tickOsc.start(time); + tickOsc.stop(time + 0.03); +}; + + +export const useDrumMachine = (lastMessage: any, sendMessage: (message: any) => void) => { + const [steps, setSteps] = useState(INITIAL_STEPS); + const [grid, setGrid] = useState(createEmptyGrid(INITIAL_STEPS)); + const [bassLine, setBassLine] = useState(() => createEmptyBassLine(INITIAL_STEPS)); + const [isPlaying, setIsPlaying] = useState(false); + const [tempo, setTempo] = useState(INITIAL_TEMPO); + const [currentStep, setCurrentStep] = useState(null); + const [mutes, setMutes] = useState(() => Array(INSTRUMENTS.length).fill(false)); + const [drumVolume, setDrumVolume] = useState(1); + const [bassVolume, setBassVolume] = useState(0.4); + + const audioContextRef = useRef(null); + const audioBuffersRef = useRef>(new Map()); + const timerRef = useRef(null); + const lookahead = 25.0; + const scheduleAheadTime = 0.1; + const nextNoteTimeRef = useRef(0.0); + const sequenceStepRef = useRef(0); + + const activeOscillatorsRef = useRef>(new Map()); + const drumMasterGainRef = useRef(null); + const bassMasterGainRef = useRef(null); + + const loadSamples = useCallback(async (context: AudioContext) => { + const newBuffers = new Map(); + for (const instrument of INSTRUMENTS) { + if (!instrument.sampleUrl) continue; + try { + const response = await fetch(instrument.sampleUrl); + const arrayBuffer = await response.arrayBuffer(); + const decodedData = await context.decodeAudioData(arrayBuffer); + newBuffers.set(instrument.name, decodedData); + } catch (error) { + console.error(`Error loading sample for ${instrument.name}:`, error); + } + } + audioBuffersRef.current = newBuffers; + }, []); + + const initAudio = useCallback(async () => { + if (!audioContextRef.current) { + audioContextRef.current = new window.AudioContext(); + await loadSamples(audioContextRef.current); + + drumMasterGainRef.current = audioContextRef.current.createGain(); + drumMasterGainRef.current.gain.value = drumVolume; + drumMasterGainRef.current.connect(audioContextRef.current.destination); + + bassMasterGainRef.current = audioContextRef.current.createGain(); + bassMasterGainRef.current.gain.value = bassVolume; + bassMasterGainRef.current.connect(audioContextRef.current.destination); + } + }, [loadSamples, drumVolume, bassVolume]); + + const stopAllBassNotes = useCallback(() => { + if (!audioContextRef.current || !bassMasterGainRef.current) return; + const now = audioContextRef.current.currentTime; + + activeOscillatorsRef.current.forEach(({ osc, gain }) => { + try { + gain.gain.cancelScheduledValues(now); + gain.gain.setTargetAtTime(0, now, 0.01); + osc.stop(now + 0.1); + } catch (e) { /* Ignore errors */ } + }); + activeOscillatorsRef.current.clear(); + }, []); + + const setStep = (instrumentIndex: number, stepIndex: number, isActive: boolean) => { + const newGrid = grid.map(row => [...row]); + if(newGrid[instrumentIndex]){ + newGrid[instrumentIndex][stepIndex] = isActive; + } + setGrid(newGrid); + sendMessage({ type: 'grid', payload: { grid: newGrid } }); + }; + + const setBassNote = (stepIndex: number, note: string) => { + const newBassLine = bassLine.map(stepNotes => [...stepNotes]); + const stepNotes = newBassLine[stepIndex]; + const noteIndex = stepNotes.indexOf(note); + + if (noteIndex > -1) { + stepNotes.splice(noteIndex, 1); + } else { + stepNotes.push(note); + } + setBassLine(newBassLine); + sendMessage({ type: 'bassLine', payload: { bassLine: newBassLine } }); + }; + + const clearPattern = () => { + if (timerRef.current) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + const newGrid = createEmptyGrid(steps); + const newBassLine = createEmptyBassLine(steps); + setGrid(newGrid); + setBassLine(newBassLine); + stopAllBassNotes(); + sendMessage({ type: 'clear', payload: { steps } }); + }; + + const toggleMute = (instrumentIndex: number) => { + const newMutes = [...mutes]; + newMutes[instrumentIndex] = !newMutes[instrumentIndex]; + setMutes(newMutes); + sendMessage({ type: 'mutes', payload: { mutes: newMutes } }); + }; + + const resizeGrid = useCallback((newSteps: number) => { + setGrid(prevGrid => { + const newGrid = createEmptyGrid(newSteps); + for(let i=0; i { + const newBassLine = createEmptyBassLine(newSteps); + for (let i = 0; i < Math.min(prevBassLine.length, newSteps); i++) { + newBassLine[i] = prevBassLine[i]; + } + return newBassLine; + }); + }, []); + + const handleStepsChange = (newSteps: number) => { + const clampedSteps = Math.max(MIN_STEPS, Math.min(MAX_STEPS, newSteps)); + if (clampedSteps === steps) return; + + setSteps(clampedSteps); + resizeGrid(clampedSteps); + sendMessage({ type: 'steps', payload: { steps: clampedSteps } }); + }; + + const scheduleNote = useCallback((beatNumber: number, time: number) => { + const audioContext = audioContextRef.current; + if (!audioContext || !bassMasterGainRef.current || !drumMasterGainRef.current) return; + + for (let i = 0; i < INSTRUMENTS.length; i++) { + const instrumentName = INSTRUMENTS[i].name; + if (grid[i]?.[beatNumber] && !mutes[i]) { + if (instrumentName === 'Kick') playKick(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Snare') playSnare(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Hi-Hat') playHiHat(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Open Hat') playOpenHat(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Ride') playRide(audioContext, time, drumMasterGainRef.current); + } + } + + const previousBeatNumber = (beatNumber - 1 + steps) % steps; + const currentNotes = bassLine[beatNumber] || []; + const previousNotes = bassLine[previousBeatNumber] || []; + + previousNotes.forEach(note => { + if (!currentNotes.includes(note)) { + const activeNode = activeOscillatorsRef.current.get(note); + if (activeNode) { + const { osc, gain } = activeNode; + gain.gain.cancelScheduledValues(time); + gain.gain.setValueAtTime(gain.gain.value, time); + gain.gain.linearRampToValueAtTime(0, time + 0.02); + osc.stop(time + 0.02); + activeOscillatorsRef.current.delete(note); + } + } + }); + + currentNotes.forEach(note => { + if (!previousNotes.includes(note)) { + const freq = NOTE_FREQ_MAP[note]; + if (freq) { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(freq, time); + + gainNode.connect(bassMasterGainRef.current as GainNode); + + gainNode.gain.setValueAtTime(0, time); + gainNode.gain.linearRampToValueAtTime(0.3, time + 0.01); + + oscillator.connect(gainNode); + oscillator.start(time); + + activeOscillatorsRef.current.set(note, { osc: oscillator, gain: gainNode }); + } + } + }); + }, [grid, mutes, bassLine, steps]); + + const scheduler = useCallback(() => { + const audioContext = audioContextRef.current; + if (!audioContext) return; + + while (nextNoteTimeRef.current < audioContext.currentTime + scheduleAheadTime) { + scheduleNote(sequenceStepRef.current, nextNoteTimeRef.current); + setCurrentStep(sequenceStepRef.current); + + const secondsPerBeat = 60.0 / tempo; + const secondsPerStep = secondsPerBeat / 4; + nextNoteTimeRef.current += secondsPerStep; + sequenceStepRef.current = (sequenceStepRef.current + 1) % steps; + } + + timerRef.current = window.setTimeout(scheduler, lookahead); + }, [tempo, steps, scheduleNote]); + + const startPlayback = async () => { + await initAudio(); + const audioContext = audioContextRef.current; + if (!audioContext || !bassMasterGainRef.current) return; + + if (audioContext.state === 'suspended') await audioContext.resume(); + + const now = audioContext.currentTime; + bassMasterGainRef.current.gain.cancelScheduledValues(now); + bassMasterGainRef.current.gain.setTargetAtTime(bassVolume, now, 0.01); + + sequenceStepRef.current = 0; + setCurrentStep(null); + nextNoteTimeRef.current = now; + + setIsPlaying(true); + sendMessage({ type: 'playback', payload: { isPlaying: true } }); + }; + + const stopPlayback = () => { + setIsPlaying(false); + sendMessage({ type: 'playback', payload: { isPlaying: false } }); + }; + + const exportBeat = () => { + const beatData: BeatData = { tempo, steps, grid, mutes, bassLine }; + const jsonString = JSON.stringify(beatData, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'my-beat.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const importBeat = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const text = e.target?.result; + if (typeof text !== 'string') throw new Error("File is not valid text"); + const data: BeatData = JSON.parse(text); + + if(typeof data.tempo !== 'number' || typeof data.steps !== 'number' || !Array.isArray(data.grid)) { + throw new Error("Invalid beat file format."); + } + + setTempo(data.tempo); + setSteps(data.steps); + setGrid(data.grid); + setMutes(data.mutes || Array(INSTRUMENTS.length).fill(false)); + setBassLine(data.bassLine || createEmptyBassLine(data.steps)); + + sendMessage({ type: 'import', payload: data }); + + alert('Beat imported successfully!'); + } catch (error) { + console.error("Failed to import beat:", error); + alert('Failed to import beat. The file may be invalid.'); + } + }; + reader.readAsText(file); + event.target.value = ''; + }; + + const handleDrumVolumeChange = useCallback((newVolume: number) => { + setDrumVolume(newVolume); + if (drumMasterGainRef.current && audioContextRef.current) { + drumMasterGainRef.current.gain.setTargetAtTime(newVolume, audioContextRef.current.currentTime, 0.01); + } + sendMessage({ type: 'drumVolume', payload: { drumVolume: newVolume } }); + }, [sendMessage]); + + const handleBassVolumeChange = useCallback((newVolume: number) => { + setBassVolume(newVolume); + if (bassMasterGainRef.current && audioContextRef.current) { + bassMasterGainRef.current.gain.setTargetAtTime(newVolume, audioContextRef.current.currentTime, 0.01); + } + sendMessage({ type: 'bassVolume', payload: { bassVolume: newVolume } }); + }, [sendMessage]); + + const triggerSound = useCallback(async (instrumentName: string) => { + await initAudio(); + const audioContext = audioContextRef.current; + if (!audioContext || !drumMasterGainRef.current) return; + const time = audioContext.currentTime; + + if (instrumentName === 'Kick') playKick(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Snare') playSnare(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Hi-Hat') playHiHat(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Open Hat') playOpenHat(audioContext, time, drumMasterGainRef.current); + else if (instrumentName === 'Ride') playRide(audioContext, time, drumMasterGainRef.current); + + }, [initAudio]); + + const triggerBassNote = useCallback(async (note: string) => { + await initAudio(); + const audioContext = audioContextRef.current; + if (!audioContext || !bassMasterGainRef.current) return; + + const time = audioContext.currentTime; + + bassMasterGainRef.current.gain.cancelScheduledValues(time); + bassMasterGainRef.current.gain.setTargetAtTime(bassVolume, time, 0.01); + + const freq = NOTE_FREQ_MAP[note]; + if (freq) { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(freq, time); + + gainNode.connect(bassMasterGainRef.current as GainNode); + + gainNode.gain.setValueAtTime(0, time); + gainNode.gain.linearRampToValueAtTime(0.3, time + 0.01); + gainNode.gain.linearRampToValueAtTime(0, time + 1.0); + + oscillator.connect(gainNode); + oscillator.start(time); + oscillator.stop(time + 1.0); + } + }, [initAudio, bassVolume]); + + const setTempoCallback = useCallback((value: React.SetStateAction) => { + setTempo(prevTempo => { + const newTempo = typeof value === 'function' ? value(prevTempo) : value; + sendMessage({ type: 'tempo', payload: { tempo: newTempo } }); + return newTempo; + }); + }, [sendMessage]); + + useEffect(() => { + if (isPlaying) { + if (timerRef.current) clearTimeout(timerRef.current); + scheduler(); + } else { + if (timerRef.current) clearTimeout(timerRef.current); + stopAllBassNotes(); + setCurrentStep(null); + } + + return () => { + if (timerRef.current) window.clearTimeout(timerRef.current); + }; + }, [isPlaying, scheduler, stopAllBassNotes]); + + useEffect(() => { + if (lastMessage) { + const { type, payload } = lastMessage; + switch (type) { + case 'state': + if (payload.grid) setGrid(payload.grid); + if (payload.bassLine) setBassLine(payload.bassLine); + if (payload.tempo) setTempo(payload.tempo); + if (payload.isPlaying) setIsPlaying(payload.isPlaying); + if (payload.mutes) setMutes(payload.mutes); + if (payload.drumVolume) setDrumVolume(payload.drumVolume); + if (payload.bassVolume) setBassVolume(payload.bassVolume); + if (payload.steps) { + setSteps(payload.steps); + resizeGrid(payload.steps); + } + break; + case 'grid': setGrid(payload.grid); break; + case 'bassLine': setBassLine(payload.bassLine); break; + case 'tempo': setTempo(payload.tempo); break; + case 'playback': setIsPlaying(payload.isPlaying); break; + case 'mutes': setMutes(payload.mutes); break; + case 'drumVolume': setDrumVolume(payload.drumVolume); break; + case 'bassVolume': setBassVolume(payload.bassVolume); break; + case 'steps': + setSteps(payload.steps); + resizeGrid(payload.steps); + break; + case 'clear': + setGrid(createEmptyGrid(payload.steps)); + setBassLine(createEmptyBassLine(payload.steps)); + break; + case 'import': + setTempo(payload.tempo); + setSteps(payload.steps); + setGrid(payload.grid); + setMutes(payload.mutes); + setBassLine(payload.bassLine); + break; + } + } + }, [lastMessage, resizeGrid]); + + useEffect(() => { + return () => { + stopAllBassNotes(); + if (audioContextRef.current?.state !== 'closed') { + audioContextRef.current?.close(); + } + }; + }, [stopAllBassNotes]); + + return { + steps, + grid, + bassLine, + isPlaying, + tempo, + currentStep, + mutes, + drumVolume, + bassVolume, + setStep, + setBassNote, + clearPattern, + setTempo: setTempoCallback, + handleStepsChange, + startPlayback, + stopPlayback, + exportBeat, + importBeat, + toggleMute, + handleDrumVolumeChange, + handleBassVolumeChange, + triggerSound, + triggerBassNote, + }; +}; \ No newline at end of file diff --git a/public/hooks/useSession.ts b/public/hooks/useSession.ts new file mode 100644 index 0000000..d183e3c --- /dev/null +++ b/public/hooks/useSession.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from 'react'; + +function generateSessionId() { + return Math.random().toString(36).substring(2, 15); +} + +export function useSession() { + const [sessionId, setSessionId] = useState(null); + + useEffect(() => { + const url = new URL(window.location.href); + let id = url.searchParams.get('sessionId'); + + if (!id) { + id = generateSessionId(); + url.searchParams.set('sessionId', id); + window.history.replaceState({}, '', url.toString()); + } + + setSessionId(id); + }, []); + + return sessionId; +} diff --git a/public/hooks/useWebSocket.ts b/public/hooks/useWebSocket.ts new file mode 100644 index 0000000..fb9e615 --- /dev/null +++ b/public/hooks/useWebSocket.ts @@ -0,0 +1,66 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; + +export function useWebSocket(sessionId: string | null) { + const [isConnected, setIsConnected] = useState(false); + const [isSynchronized, setIsSynchronized] = useState(false); + const [lastMessage, setLastMessage] = useState(null); + const [clientId, setClientId] = useState(null); + const ws = useRef(null); + + const connect = useCallback(() => { + if (!sessionId || ws.current) return; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ag-beats?sessionId=${sessionId}`; + const socket = new WebSocket(wsUrl); + + socket.onopen = () => { + console.log('WebSocket connected'); + setIsConnected(true); + // Request the full session state upon connecting + socket.send(JSON.stringify({ type: 'get_state' })); + }; + + socket.onmessage = (event) => { + const message = JSON.parse(event.data); + if (message.type === 'welcome') { + setClientId(message.payload.clientId); + } else if (message.type === 'session_state') { + // This is the full state, apply it and mark as synchronized + setLastMessage({ type: 'state', payload: message.payload }); + setIsSynchronized(true); + } else { + setLastMessage(message); + } + }; + + socket.onclose = () => { + console.log('WebSocket disconnected'); + setIsConnected(false); + setIsSynchronized(false); + ws.current = null; + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + socket.close(); + }; + + ws.current = socket; + }, [sessionId]); + + useEffect(() => { + connect(); + return () => { + ws.current?.close(); + }; + }, [connect]); + + const sendMessage = (message: any) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + ws.current.send(JSON.stringify(message)); + } + }; + + return { isConnected, isSynchronized, lastMessage, sendMessage, clientId }; +} diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..3bc9a34 --- /dev/null +++ b/public/index.css @@ -0,0 +1,12 @@ +/* Firefox specific styles for range input thumb */ +input[type=range]::-moz-range-thumb { + background-color: #f97316; /* orange-500 */ + border: none; + border-radius: 9999px; /* full rounded */ + height: 16px; + width: 16px; + cursor: pointer; +} +input[type=range]#bass-volume::-moz-range-thumb { + background-color: #8b5cf6; /* purple-500 */ +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b72d4d2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + Gemini Rhythm Machine + + + +
+ + + diff --git a/public/index.tsx b/public/index.tsx new file mode 100644 index 0000000..1a099f8 --- /dev/null +++ b/public/index.tsx @@ -0,0 +1,17 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/public/metadata.json b/public/metadata.json new file mode 100644 index 0000000..4d24838 --- /dev/null +++ b/public/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "AG Beats", + "description": "A rhythm machine and step sequencer built with React and Tailwind CSS. Create beats, adjust the tempo, change the number of steps, and import/export your creations as JSON files.", + "requestFramePermissions": [], + "prompt": "" +} \ No newline at end of file diff --git a/public/tsconfig.json b/public/tsconfig.json new file mode 100644 index 0000000..4d0fdee --- /dev/null +++ b/public/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "allowJs": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "paths": { + "@/*" : ["./*"] + } + } +} diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 0000000..b1b5a4c --- /dev/null +++ b/public/types.ts @@ -0,0 +1,16 @@ +export interface Instrument { + name: string; + sampleUrl?: string; +} + +export type Grid = boolean[][]; + +export type BassLineGrid = string[][]; + +export interface BeatData { + tempo: number; + steps: number; + grid: Grid; + mutes?: boolean[]; + bassLine?: BassLineGrid; +} \ No newline at end of file diff --git a/public/utils.ts b/public/utils.ts new file mode 100644 index 0000000..31b437f --- /dev/null +++ b/public/utils.ts @@ -0,0 +1,21 @@ +// A simple debounce function +export const debounce = any>(func: F, waitFor: number) => { + let timeout: ReturnType | null = null; + + const debounced = (...args: Parameters) => { + if (timeout !== null) { + clearTimeout(timeout); + timeout = null; + } + timeout = setTimeout(() => func(...args), waitFor); + }; + + const cancel = () => { + if (timeout !== null) { + clearTimeout(timeout); + timeout = null; + } + }; + + return [debounced, cancel] as [(...args: Parameters) => void, () => void]; +}; diff --git a/server.dev.js b/server.dev.js new file mode 100644 index 0000000..ab4d9a2 --- /dev/null +++ b/server.dev.js @@ -0,0 +1,104 @@ +const express = require('express'); +const path = require('path'); +const http = require('http'); +const { WebSocketServer } = require('ws'); +const { v4: uuidv4 } = require('uuid'); + +const app = express(); +const port = process.env.PORT || 3001; +const subfolder = '/ag-beats'; +const distPath = path.join(__dirname, 'dist'); + +// --- Helper function for random colors --- +const COLORS = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B', '#4D908E', '#577590', '#277DA1']; +const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]; + +// --- HTTP and Static Server Setup --- +app.use(subfolder, express.static(distPath)); +app.get(subfolder + '/*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); +}); +app.get('/', (req, res) => { + res.redirect(subfolder); +}); + +const httpServer = http.createServer(app); + +// --- WebSocket Server --- +const wss = new WebSocketServer({ path: '/ag-beats', server: httpServer }); +const sessions = new Map(); + +wss.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const sessionId = url.searchParams.get('sessionId'); + const clientId = uuidv4(); + const clientColor = getRandomColor(); + + if (!sessionId) { + return ws.close(1008, 'Session ID required'); + } + + if (!sessions.has(sessionId)) { + sessions.set(sessionId, { clients: new Map(), state: {} }); + } + const session = sessions.get(sessionId); + session.clients.set(clientId, { ws, color: clientColor }); + + console.log(`Client ${clientId} connected to session ${sessionId}`); + + // Welcome message with client's own ID and the session's current state + ws.send(JSON.stringify({ + type: 'welcome', + payload: { + clientId, + state: session.state + } + })); + + // Inform all clients about the current users + const userList = Array.from(session.clients.entries()).map(([id, { color }]) => ({ id, color })); + const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); + session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); + + + ws.on('message', (messageBuffer) => { + const message = messageBuffer.toString(); + const parsedMessage = JSON.parse(message); + + // Add sender's ID to the message for client-side identification + const messageToSend = JSON.stringify({ ...parsedMessage, senderId: clientId }); + + // Persist state on the server, excluding cursor movements + if (parsedMessage.type !== 'cursor-move') { + session.state = { ...session.state, ...parsedMessage.payload }; + } + + // Broadcast to all clients in the session, including the sender + session.clients.forEach(({ ws: clientWs }) => { + if (clientWs.readyState === clientWs.OPEN) { + clientWs.send(messageToSend); + } + }); + }); + + ws.on('close', () => { + console.log(`Client ${clientId} disconnected from session ${sessionId}`); + session.clients.delete(clientId); + + if (session.clients.size === 0) { + sessions.delete(sessionId); + console.log(`Session ${sessionId} closed.`); + } else { + // Inform remaining clients that a user has left + const userList = Array.from(session.clients.keys()).map(id => ({ id, color: session.clients.get(id).color })); + const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); + session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); + } + }); + + ws.on('error', (error) => console.error(`WebSocket error for client ${clientId}:`, error)); +}); + +httpServer.listen(port, '0.0.0.0', () => { + console.log(`AG Beats development server started on port ${port} with HTTP.`); +}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..cd822dd --- /dev/null +++ b/server.js @@ -0,0 +1,119 @@ +const express = require('express'); +const path = require('path'); +const http = require('http'); +const fs = require('fs'); +const { WebSocketServer } = require('ws'); +const { v4: uuidv4 } = require('uuid'); +const { defaultState } = require('./defaultState'); + +const app = express(); +const port = process.env.PORT || 3001; +const subfolder = '/ag-beats'; +const distPath = path.join(__dirname, 'dist'); + +// --- Helper function for random colors --- +const COLORS = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B', '#4D908E', '#577590', '#277DA1']; +const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]; + +// --- HTTP and Static Server Setup --- +app.use(subfolder, express.static(distPath)); +app.get(subfolder + '/*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); +}); +app.get('/', (req, res) => { + res.redirect(subfolder); +}); + +const httpServer = http.createServer(app); + +// --- WebSocket Server --- +const wss = new WebSocketServer({ path: '/ag-beats', server: httpServer }); +const sessions = new Map(); + +wss.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const sessionId = url.searchParams.get('sessionId'); + const clientId = uuidv4(); + const clientColor = getRandomColor(); + + if (!sessionId) { + return ws.close(1008, 'Session ID required'); + } + + if (!sessions.has(sessionId)) { + // Deep copy the default state to ensure each session has its own mutable state + const initialState = JSON.parse(JSON.stringify(defaultState)); + sessions.set(sessionId, { clients: new Map(), state: initialState }); + } + const session = sessions.get(sessionId); + session.clients.set(clientId, { ws, color: clientColor }); + + console.log(`Client ${clientId} connected to session ${sessionId}`); + + // Welcome message with client's own ID + ws.send(JSON.stringify({ + type: 'welcome', + payload: { + clientId, + } + })); + + // Inform all clients about the current users + const userList = Array.from(session.clients.entries()).map(([id, { color }]) => ({ id, color })); + const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); + session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); + + + ws.on('message', (messageBuffer) => { + const message = messageBuffer.toString(); + const parsedMessage = JSON.parse(message); + + // Handle state requests + if (parsedMessage.type === 'get_state') { + ws.send(JSON.stringify({ + type: 'session_state', + payload: session.state + })); + return; + } + + // Add sender's ID to the message for client-side identification + const messageToSend = JSON.stringify({ ...parsedMessage, senderId: clientId }); + + // Persist state on the server, excluding cursor movements + if (parsedMessage.type !== 'cursor-move') { + session.state = { ...session.state, ...parsedMessage.payload }; + } + + // Broadcast to all clients in the session, including the sender + session.clients.forEach(({ ws: clientWs }) => { + if (clientWs.readyState === clientWs.OPEN) { + clientWs.send(messageToSend); + } + }); + }); + + ws.on('close', () => { + console.log(`Client ${clientId} disconnected from session ${sessionId}`); + session.clients.delete(clientId); + + if (session.clients.size === 0) { + sessions.delete(sessionId); + console.log(`Session ${sessionId} closed.`); + } else { + // Inform remaining clients that a user has left + const userList = Array.from(session.clients.keys()).map(id => ({ id, color: session.clients.get(id).color })); + const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); + session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); + } + }); + + ws.on('error', (error) => console.error(`WebSocket error for client ${clientId}:`, error)); +}); + +httpServer.listen(port, '0.0.0.0', () => { + console.log(`AG Beats server started on port ${port} with HTTP.`); + if (process.send) { + process.send('ready'); + } +}); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..46e28d4 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,31 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + root: 'public', + plugins: [react()], + base: '/ag-beats/', + build: { + outDir: '../dist', + emptyOutDir: true, + assetsDir: '.', + rollupOptions: { + input: { + main: path.resolve(__dirname, 'public/index.html') + } + } + }, + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './public'), + } + } + }; +});