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
index 75dc756..065d82d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,54 @@
-# Zoom Accel - A configurable
+# ZoomAccel
-## Features
-- Customizable "EQ style" zoom scaling
+A tool for studying zoom acceleration curves in pointing tasks. Built with React + TypeScript + Vite.
+
+## Overview
+
+ZoomAccel presents users with a series of target acquisition tasks in a zoomable space. Users can enable/disable zoom acceleration and customize the acceleration curve to study its effects on pointing performance.
+
+### Features
+
+- Target acquisition tasks in a large virtual space
+- Configurable zoom acceleration curves
+- Real-time performance stats
+- Trial history with detailed metrics
+- CSV export for data analysis
+- State persistence between sessions
+
+## Getting Started
+
+```bash
+# Install dependencies
+pnpm install
+
+# Start development server
+pnpm dev
+
+# Build for production
+pnpm build
+```
+
+## Usage
+
+1. Click the green target circles to complete trials
+2. Use Ctrl/Cmd + Scroll to zoom in/out
+3. Click and drag to pan the view
+4. Adjust acceleration curve in Settings ⚙️
+5. View performance history in History 📊
+6. Export data as CSV for analysis 📥
+
+## Data Collection
+
+Each trial records:
+- Time taken (ms)
+- Number of misclicks
+- Acceleration status
+- Current acceleration curve values
+- Timestamp
+
+## Development
+
+Built with:
+- React
+- TypeScript
+- Vite
diff --git a/background.js b/background.js
deleted file mode 100644
index abbc448..0000000
--- a/background.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// Dynamic Icon generator
-function createIcon(text, size = 16) {
- const canvas = new OffscreenCanvas(size, size);
- const ctx = canvas.getContext("2d");
-
- // Wipe canvas
- ctx.clearRect(0, 0, size, size);
-
- // Background
- ctx.fillStyle = "#4285F4"; // Google Blue
- ctx.fillRect(0, 0, size, size);
-
- // Text
- ctx.fillStyle = "white";
- ctx.font = `bold ${Math.floor(size * 0.5625)}px Arial`;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(text, size / 2, size / 2);
-
- return ctx.getImageData(0, 0, size, size);
-}
-
-// Update the extension icon
-function updateExtensionIcon(zoomLevel, accelerationMultiplier) {
- const zoomText = zoomLevel.toFixed(2);
- const accelText = accelerationMultiplier.toFixed(2);
-
- // Create icons of different sizes
- const icon16 = createIcon(zoomText, 16);
- const icon32 = createIcon(zoomText, 32);
- const icon48 = createIcon(zoomText, 48);
- const icon128 = createIcon(zoomText, 128);
-
- // Set the icon
- chrome.action.setIcon({
- imageData: {
- 16: icon16,
- 32: icon32,
- 48: icon48,
- 128: icon128,
- },
- });
-
- // Update badge with multiplier
- chrome.action.setBadgeText({ text: accelText + "x" });
- chrome.action.setBadgeBackgroundColor({ color: "#34A853" }); // Google Green
-}
-
-// Listen for messages
-chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
- if (request.action === "updateZoomInfo") {
- updateExtensionIcon(request.zoomLevel, request.accelerationMultiplier);
- }
-});
-
-// Set icon
-updateExtensionIcon(1, 1);
diff --git a/content.js b/content.js
deleted file mode 100644
index 9ddf677..0000000
--- a/content.js
+++ /dev/null
@@ -1,93 +0,0 @@
-let zoomLevel = 1;
-let lastGestureTime = 0;
-let lastGestureScale = 1;
-const minZoom = 0.1;
-const maxZoom = 5;
-let accelerationCurve = [1, 1, 1, 1, 1];
-
-function interpolateAcceleration(speed) {
- const index = Math.min(
- Math.floor((speed * (accelerationCurve.length - 1)) / 2),
- accelerationCurve.length - 2,
- );
- const t = ((speed * (accelerationCurve.length - 1)) / 2) % 1;
- return accelerationCurve[index] * (1 - t) + accelerationCurve[index + 1] * t;
-}
-
-function applyZoom(delta, speed) {
- const accelerationMultiplier = interpolateAcceleration(speed);
- const zoomChange = delta * accelerationMultiplier;
- zoomLevel *= 1 + zoomChange;
- zoomLevel = Math.min(Math.max(zoomLevel, minZoom), maxZoom);
-
- document.body.style.transform = `scale(${zoomLevel})`;
- document.body.style.transformOrigin = "center top";
-
- // Update Icon via Messages
- chrome.runtime.sendMessage({
- action: "updateZoomInfo",
- zoomLevel: zoomLevel,
- accelerationMultiplier: accelerationMultiplier,
- });
-}
-
-window.addEventListener(
- "wheel",
- (event) => {
- if (event.ctrlKey || event.metaKey) {
- event.preventDefault();
- const currentTime = performance.now();
- const timeDiff = (currentTime - lastGestureTime) / 1000;
- const delta = event.deltaY * -0.001;
- const speed = Math.abs(delta / timeDiff);
- applyZoom(delta, speed);
- lastGestureTime = currentTime;
- }
- },
- { passive: false },
-);
-
-window.addEventListener(
- "gesturestart",
- (event) => {
- event.preventDefault();
- lastGestureScale = 1;
- lastGestureTime = performance.now();
- },
- { passive: false },
-);
-
-window.addEventListener(
- "gesturechange",
- (event) => {
- event.preventDefault();
- const currentTime = performance.now();
- const timeDiff = (currentTime - lastGestureTime) / 1000;
- const delta = event.scale - lastGestureScale;
- const speed = Math.abs(delta / timeDiff);
- applyZoom(delta, speed);
- lastGestureScale = event.scale;
- lastGestureTime = currentTime;
- },
- { passive: false },
-);
-
-window.addEventListener(
- "gestureend",
- (event) => {
- event.preventDefault();
- },
- { passive: false },
-);
-
-chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
- if (request.action === "updateSettings") {
- accelerationCurve = request.settings;
- }
-});
-
-chrome.storage.sync.get("accelerationCurve", (data) => {
- if (data.accelerationCurve) {
- accelerationCurve = data.accelerationCurve;
- }
-});
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..092408a
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,28 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ },
+)
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6810d54
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ✨ ZoomAccel ✨
+
+
+
+
+
+
diff --git a/manifest.json b/manifest.json
deleted file mode 100644
index 41ea2e0..0000000
--- a/manifest.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "manifest_version": 3,
- "name": "Zoom Accelerator",
- "version": "1.0",
- "description": "Adds customizable smooth zoom acceleration to Chrome",
- "permissions": ["activeTab", "storage"],
- "host_permissions": [""],
- "action": {
- "default_popup": "popup.html"
- },
- "background": {
- "service_worker": "background.js"
- },
- "content_scripts": [
- {
- "matches": [""],
- "js": ["content.js"]
- }
- ]
-}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e3e47ea
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "zoomaccell",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.19.0",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.19.0",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.18",
+ "globals": "^15.14.0",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.22.0",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..8b4cc8b
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,2004 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ react:
+ specifier: ^19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.0.0(react@19.0.0)
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.19.0
+ version: 9.20.0
+ '@types/react':
+ specifier: ^19.0.8
+ version: 19.0.8
+ '@types/react-dom':
+ specifier: ^19.0.3
+ version: 19.0.3(@types/react@19.0.8)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.3.4(vite@6.1.0)
+ eslint:
+ specifier: ^9.19.0
+ version: 9.20.1
+ eslint-plugin-react-hooks:
+ specifier: ^5.0.0
+ version: 5.1.0(eslint@9.20.1)
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.18
+ version: 0.4.19(eslint@9.20.1)
+ globals:
+ specifier: ^15.14.0
+ version: 15.15.0
+ typescript:
+ specifier: ~5.7.2
+ version: 5.7.3
+ typescript-eslint:
+ specifier: ^8.22.0
+ version: 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ vite:
+ specifier: ^6.1.0
+ version: 6.1.0
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@babel/code-frame@7.26.2':
+ resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.26.8':
+ resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.26.8':
+ resolution: {integrity: sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.26.8':
+ resolution: {integrity: sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.26.5':
+ resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.25.9':
+ resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.26.0':
+ resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.26.5':
+ resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.25.9':
+ resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.25.9':
+ resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.25.9':
+ resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.26.7':
+ resolution: {integrity: sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.26.8':
+ resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.25.9':
+ resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.25.9':
+ resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.26.8':
+ resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.26.8':
+ resolution: {integrity: sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.26.8':
+ resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.24.2':
+ resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.24.2':
+ resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.24.2':
+ resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.24.2':
+ resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.24.2':
+ resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.24.2':
+ resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.24.2':
+ resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.24.2':
+ resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.24.2':
+ resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.24.2':
+ resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.24.2':
+ resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.24.2':
+ resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.24.2':
+ resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.24.2':
+ resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.24.2':
+ resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.24.2':
+ resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.24.2':
+ resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.24.2':
+ resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.24.2':
+ resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.24.2':
+ resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.24.2':
+ resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.24.2':
+ resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.24.2':
+ resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.24.2':
+ resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.24.2':
+ resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.4.1':
+ resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.19.2':
+ resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.10.0':
+ resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.11.0':
+ resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.2.0':
+ resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.20.0':
+ resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.2.5':
+ resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.1':
+ resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.8':
+ resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/set-array@1.2.1':
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.0':
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@rollup/rollup-android-arm-eabi@4.34.6':
+ resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.34.6':
+ resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.34.6':
+ resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.34.6':
+ resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.34.6':
+ resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.34.6':
+ resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.34.6':
+ resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.34.6':
+ resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.34.6':
+ resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.34.6':
+ resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.34.6':
+ resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
+ resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.34.6':
+ resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.34.6':
+ resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.34.6':
+ resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.34.6':
+ resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.34.6':
+ resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.34.6':
+ resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.34.6':
+ resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==}
+ cpu: [x64]
+ os: [win32]
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.6.8':
+ resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.20.6':
+ resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
+
+ '@types/estree@1.0.6':
+ resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+
+ '@types/gensync@1.0.4':
+ resolution: {integrity: sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/react-dom@19.0.3':
+ resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.0.8':
+ resolution: {integrity: sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==}
+
+ '@typescript-eslint/eslint-plugin@8.24.0':
+ resolution: {integrity: sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.8.0'
+
+ '@typescript-eslint/parser@8.24.0':
+ resolution: {integrity: sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.8.0'
+
+ '@typescript-eslint/scope-manager@8.24.0':
+ resolution: {integrity: sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@8.24.0':
+ resolution: {integrity: sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.8.0'
+
+ '@typescript-eslint/types@8.24.0':
+ resolution: {integrity: sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.24.0':
+ resolution: {integrity: sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.8.0'
+
+ '@typescript-eslint/utils@8.24.0':
+ resolution: {integrity: sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.8.0'
+
+ '@typescript-eslint/visitor-keys@8.24.0':
+ resolution: {integrity: sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@4.3.4':
+ resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.14.0:
+ resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+ brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.24.4:
+ resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001699:
+ resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ debug@4.4.0:
+ resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ electron-to-chromium@1.5.99:
+ resolution: {integrity: sha512-77c/+fCyL2U+aOyqfIFi89wYLBeSTCs55xCZL0oFH0KjqsvSvyh6AdQ+UIl1vgpnQQE6g+/KK8hOIupH6VwPtg==}
+
+ esbuild@0.24.2:
+ resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.1.0:
+ resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.19:
+ resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.2.0:
+ resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.0:
+ resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.20.1:
+ resolution: {integrity: sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.3.0:
+ resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.19.0:
+ resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.2:
+ resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
+ graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.8:
+ resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ postcss@8.5.2:
+ resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@19.0.0:
+ resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
+ peerDependencies:
+ react: ^19.0.0
+
+ react-refresh@0.14.2:
+ resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
+ engines: {node: '>=0.10.0'}
+
+ react@19.0.0:
+ resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.34.6:
+ resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ scheduler@0.25.0:
+ resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ ts-api-utils@2.0.1:
+ resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.24.0:
+ resolution: {integrity: sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.8.0'
+
+ typescript@5.7.3:
+ resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ update-browserslist-db@1.1.2:
+ resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@6.1.0:
+ resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@babel/code-frame@7.26.2':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.25.9
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.26.8': {}
+
+ '@babel/core@7.26.8':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.8
+ '@babel/helper-compilation-targets': 7.26.5
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.8)
+ '@babel/helpers': 7.26.7
+ '@babel/parser': 7.26.8
+ '@babel/template': 7.26.8
+ '@babel/traverse': 7.26.8
+ '@babel/types': 7.26.8
+ '@types/gensync': 1.0.4
+ convert-source-map: 2.0.0
+ debug: 4.4.0
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.26.8':
+ dependencies:
+ '@babel/parser': 7.26.8
+ '@babel/types': 7.26.8
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.26.5':
+ dependencies:
+ '@babel/compat-data': 7.26.8
+ '@babel/helper-validator-option': 7.25.9
+ browserslist: 4.24.4
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-module-imports@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.26.8
+ '@babel/types': 7.26.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.8)':
+ dependencies:
+ '@babel/core': 7.26.8
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+ '@babel/traverse': 7.26.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.26.5': {}
+
+ '@babel/helper-string-parser@7.25.9': {}
+
+ '@babel/helper-validator-identifier@7.25.9': {}
+
+ '@babel/helper-validator-option@7.25.9': {}
+
+ '@babel/helpers@7.26.7':
+ dependencies:
+ '@babel/template': 7.26.8
+ '@babel/types': 7.26.8
+
+ '@babel/parser@7.26.8':
+ dependencies:
+ '@babel/types': 7.26.8
+
+ '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.8)':
+ dependencies:
+ '@babel/core': 7.26.8
+ '@babel/helper-plugin-utils': 7.26.5
+
+ '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.8)':
+ dependencies:
+ '@babel/core': 7.26.8
+ '@babel/helper-plugin-utils': 7.26.5
+
+ '@babel/template@7.26.8':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/parser': 7.26.8
+ '@babel/types': 7.26.8
+
+ '@babel/traverse@7.26.8':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.8
+ '@babel/parser': 7.26.8
+ '@babel/template': 7.26.8
+ '@babel/types': 7.26.8
+ debug: 4.4.0
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.26.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+
+ '@esbuild/aix-ppc64@0.24.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/android-arm@0.24.2':
+ optional: true
+
+ '@esbuild/android-x64@0.24.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.24.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.24.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.24.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.24.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.24.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.24.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.24.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.24.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.24.2':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)':
+ dependencies:
+ eslint: 9.20.1
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.19.2':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.0
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/core@0.10.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/core@0.11.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.2.0':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.0
+ espree: 10.3.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.20.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.2.5':
+ dependencies:
+ '@eslint/core': 0.10.0
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.1': {}
+
+ '@jridgewell/gen-mapping@0.3.8':
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/set-array@1.2.1': {}
+
+ '@jridgewell/sourcemap-codec@1.5.0': {}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.0
+
+ '@rollup/rollup-android-arm-eabi@4.34.6':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.34.6':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.34.6':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.34.6':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.34.6':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.34.6':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.34.6':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.34.6':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.34.6':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.34.6':
+ optional: true
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.26.8
+ '@babel/types': 7.26.8
+ '@types/babel__generator': 7.6.8
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.20.6
+
+ '@types/babel__generator@7.6.8':
+ dependencies:
+ '@babel/types': 7.26.8
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.26.8
+ '@babel/types': 7.26.8
+
+ '@types/babel__traverse@7.20.6':
+ dependencies:
+ '@babel/types': 7.26.8
+
+ '@types/estree@1.0.6': {}
+
+ '@types/gensync@1.0.4': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.0.3(@types/react@19.0.8)':
+ dependencies:
+ '@types/react': 19.0.8
+
+ '@types/react@19.0.8':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/scope-manager': 8.24.0
+ '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.24.0
+ eslint: 9.20.1
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ natural-compare: 1.4.0
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.24.0
+ '@typescript-eslint/types': 8.24.0
+ '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.24.0
+ debug: 4.4.0
+ eslint: 9.20.1
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.24.0':
+ dependencies:
+ '@typescript-eslint/types': 8.24.0
+ '@typescript-eslint/visitor-keys': 8.24.0
+
+ '@typescript-eslint/type-utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ debug: 4.4.0
+ eslint: 9.20.1
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.24.0': {}
+
+ '@typescript-eslint/typescript-estree@8.24.0(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.24.0
+ '@typescript-eslint/visitor-keys': 8.24.0
+ debug: 4.4.0
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.1
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1)
+ '@typescript-eslint/scope-manager': 8.24.0
+ '@typescript-eslint/types': 8.24.0
+ '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
+ eslint: 9.20.1
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.24.0':
+ dependencies:
+ '@typescript-eslint/types': 8.24.0
+ eslint-visitor-keys: 4.2.0
+
+ '@vitejs/plugin-react@4.3.4(vite@6.1.0)':
+ dependencies:
+ '@babel/core': 7.26.8
+ '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.8)
+ '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.8)
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.14.2
+ vite: 6.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ acorn-jsx@5.3.2(acorn@8.14.0):
+ dependencies:
+ acorn: 8.14.0
+
+ acorn@8.14.0: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.11:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.1:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.24.4:
+ dependencies:
+ caniuse-lite: 1.0.30001699
+ electron-to-chromium: 1.5.99
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.2(browserslist@4.24.4)
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001699: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ concat-map@0.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.1.3: {}
+
+ debug@4.4.0:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ electron-to-chromium@1.5.99: {}
+
+ esbuild@0.24.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.24.2
+ '@esbuild/android-arm': 0.24.2
+ '@esbuild/android-arm64': 0.24.2
+ '@esbuild/android-x64': 0.24.2
+ '@esbuild/darwin-arm64': 0.24.2
+ '@esbuild/darwin-x64': 0.24.2
+ '@esbuild/freebsd-arm64': 0.24.2
+ '@esbuild/freebsd-x64': 0.24.2
+ '@esbuild/linux-arm': 0.24.2
+ '@esbuild/linux-arm64': 0.24.2
+ '@esbuild/linux-ia32': 0.24.2
+ '@esbuild/linux-loong64': 0.24.2
+ '@esbuild/linux-mips64el': 0.24.2
+ '@esbuild/linux-ppc64': 0.24.2
+ '@esbuild/linux-riscv64': 0.24.2
+ '@esbuild/linux-s390x': 0.24.2
+ '@esbuild/linux-x64': 0.24.2
+ '@esbuild/netbsd-arm64': 0.24.2
+ '@esbuild/netbsd-x64': 0.24.2
+ '@esbuild/openbsd-arm64': 0.24.2
+ '@esbuild/openbsd-x64': 0.24.2
+ '@esbuild/sunos-x64': 0.24.2
+ '@esbuild/win32-arm64': 0.24.2
+ '@esbuild/win32-ia32': 0.24.2
+ '@esbuild/win32-x64': 0.24.2
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.1.0(eslint@9.20.1):
+ dependencies:
+ eslint: 9.20.1
+
+ eslint-plugin-react-refresh@0.4.19(eslint@9.20.1):
+ dependencies:
+ eslint: 9.20.1
+
+ eslint-scope@8.2.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.0: {}
+
+ eslint@9.20.1:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1)
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.19.2
+ '@eslint/core': 0.11.0
+ '@eslint/eslintrc': 3.2.0
+ '@eslint/js': 9.20.0
+ '@eslint/plugin-kit': 0.2.5
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.1
+ '@types/estree': 1.0.6
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.2.0
+ eslint-visitor-keys: 4.2.0
+ espree: 10.3.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.3.0:
+ dependencies:
+ acorn: 8.14.0
+ acorn-jsx: 5.3.2(acorn@8.14.0)
+ eslint-visitor-keys: 4.2.0
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.0:
+ dependencies:
+ reusify: 1.0.4
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.2
+ keyv: 4.5.4
+
+ flatted@3.3.2: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@11.12.0: {}
+
+ globals@14.0.0: {}
+
+ globals@15.15.0: {}
+
+ graphemer@1.4.0: {}
+
+ has-flag@4.0.0: {}
+
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ isexe@2.0.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.11
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.8: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.19: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ postcss@8.5.2:
+ dependencies:
+ nanoid: 3.3.8
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@19.0.0(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ scheduler: 0.25.0
+
+ react-refresh@0.14.2: {}
+
+ react@19.0.0: {}
+
+ resolve-from@4.0.0: {}
+
+ reusify@1.0.4: {}
+
+ rollup@4.34.6:
+ dependencies:
+ '@types/estree': 1.0.6
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.34.6
+ '@rollup/rollup-android-arm64': 4.34.6
+ '@rollup/rollup-darwin-arm64': 4.34.6
+ '@rollup/rollup-darwin-x64': 4.34.6
+ '@rollup/rollup-freebsd-arm64': 4.34.6
+ '@rollup/rollup-freebsd-x64': 4.34.6
+ '@rollup/rollup-linux-arm-gnueabihf': 4.34.6
+ '@rollup/rollup-linux-arm-musleabihf': 4.34.6
+ '@rollup/rollup-linux-arm64-gnu': 4.34.6
+ '@rollup/rollup-linux-arm64-musl': 4.34.6
+ '@rollup/rollup-linux-loongarch64-gnu': 4.34.6
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6
+ '@rollup/rollup-linux-riscv64-gnu': 4.34.6
+ '@rollup/rollup-linux-s390x-gnu': 4.34.6
+ '@rollup/rollup-linux-x64-gnu': 4.34.6
+ '@rollup/rollup-linux-x64-musl': 4.34.6
+ '@rollup/rollup-win32-arm64-msvc': 4.34.6
+ '@rollup/rollup-win32-ia32-msvc': 4.34.6
+ '@rollup/rollup-win32-x64-msvc': 4.34.6
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ scheduler@0.25.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ ts-api-utils@2.0.1(typescript@5.7.3):
+ dependencies:
+ typescript: 5.7.3
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.24.0(eslint@9.20.1)(typescript@5.7.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ eslint: 9.20.1
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.7.3: {}
+
+ update-browserslist-db@1.1.2(browserslist@4.24.4):
+ dependencies:
+ browserslist: 4.24.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@6.1.0:
+ dependencies:
+ esbuild: 0.24.2
+ postcss: 8.5.2
+ rollup: 4.34.6
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
diff --git a/popup.html b/popup.html
deleted file mode 100644
index 4c77a18..0000000
--- a/popup.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
- Zoom Acceleration Curve
-
-
-
-
-
diff --git a/popup.js b/popup.js
deleted file mode 100644
index 3ec3d43..0000000
--- a/popup.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const numSliders = 5;
-const defaultMultiplier = 1;
-
-function createSliders() {
- const slidersContainer = document.getElementById("sliders");
- for (let i = 0; i < numSliders; i++) {
- const sliderContainer = document.createElement("div");
- sliderContainer.className = "slider-container";
-
- const label = document.createElement("label");
- label.textContent = `${(i / (numSliders - 1) * 2).toFixed(2)} zoom units/s`;
-
- const slider = document.createElement("input");
- slider.type = "range";
- slider.min = "1";
- slider.max = "10";
- slider.step = "0.1";
- slider.value = defaultMultiplier;
- slider.className = "slider";
-
- const value = document.createElement("span");
- value.className = "value";
- value.textContent = `${defaultMultiplier}x`;
-
- sliderContainer.appendChild(label);
- sliderContainer.appendChild(slider);
- sliderContainer.appendChild(value);
- slidersContainer.appendChild(sliderContainer);
-
- slider.addEventListener("input", () => {
- value.textContent = `${parseFloat(slider.value).toFixed(1)}x`;
- saveSettings();
- });
- }
-}
-
-function saveSettings() {
- const sliders = document.querySelectorAll(".slider");
- const settings = Array.from(sliders).map((slider) => parseFloat(slider.value));
- chrome.storage.sync.set({ accelerationCurve: settings }, () => {
- sendSettingsToActiveTab(settings);
- });
-}
-
-function sendSettingsToActiveTab(settings) {
- chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
- if (tabs[0]) {
- chrome.tabs.sendMessage(
- tabs[0].id,
- { action: "updateSettings", settings },
- (response) => {
- if (chrome.runtime.lastError) {
- console.log("Could not send settings to content script.");
- }
- }
- );
- }
- });
-}
-
-function loadSettings() {
- chrome.storage.sync.get("accelerationCurve", (data) => {
- if (data.accelerationCurve) {
- const sliders = document.querySelectorAll(".slider");
- data.accelerationCurve.forEach((value, index) => {
- sliders[index].value = value;
- sliders[index].nextElementSibling.textContent = `${value.toFixed(1)}x`;
- });
- }
- });
-}
-
-createSliders();
-loadSettings();
-
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..791ad69
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,129 @@
+.app {
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+ background-color: white;
+ position: relative;
+}
+
+.stats {
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ transform: none;
+ color: #213547;
+ background-color: white;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-family: Inter, system-ui, Arial, sans-serif;
+ font-size: 14px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ border: 1px solid #ddd;
+ z-index: 100;
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.stats span {
+ white-space: nowrap;
+}
+
+.container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ cursor: crosshair;
+ touch-action: none;
+}
+
+.content {
+ position: absolute;
+ width: 10000px;
+ /* Match VIRTUAL_SIZE */
+ height: 10000px;
+ transform-origin: 0 0;
+ background-color: white;
+ background-image: linear-gradient(#ddd 1px, transparent 1px),
+ linear-gradient(90deg, #ddd 1px, transparent 1px);
+ background-size: 100px 100px;
+}
+
+.target {
+ position: absolute;
+ background-color: #888888;
+ border-radius: 50%;
+ transition: background-color 0.2s ease;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+ transform: translate(-50%, -50%);
+}
+
+.target.active {
+ background-color: #44aa44;
+ box-shadow: 0 0 20px rgba(68, 170, 68, 0.4);
+}
+
+html,
+body,
+#root {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background-color: white;
+}
+
+.controls {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ gap: 10px;
+ z-index: 1001;
+}
+
+.settings-toggle,
+.rounds-toggle,
+.export-button,
+.reset-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: white;
+ border: 1px solid #ddd;
+ padding: 8px 16px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: all 0.2s ease;
+}
+
+.settings-toggle:hover,
+.rounds-toggle:hover,
+.export-button:hover,
+.reset-button:hover {
+ background: #f5f5f5;
+ border-color: #646cff;
+}
+
+.export-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.export-button:disabled:hover {
+ background: white;
+ border-color: #ddd;
+}
+
+.reset-button {
+ border-color: #ffcdd2;
+}
+
+.reset-button:hover {
+ background: #fff5f5;
+ border-color: #ef5350;
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..377e55b
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,391 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Settings } from './components/Settings';
+import { RoundsHistory } from './components/RoundsHistory';
+import { saveState, loadState, exportToCSV } from './utils';
+import {
+ Target,
+ Stats,
+ ViewportOffset,
+ AccelerationSettings,
+ Round,
+ CONSTANTS,
+} from './types';
+import './App.css';
+
+function App() {
+ // Core state
+ const [targets, setTargets] = useState([]);
+ const [currentTarget, setCurrentTarget] = useState(null);
+ const [zoom, setZoom] = useState(CONSTANTS.ZOOM_LEVELS.default);
+ const [stats, setStats] = useState({
+ startTime: Date.now(),
+ misclicks: 0,
+ targetsHit: 0,
+ times: [],
+ });
+
+ // Load settings from localStorage or use defaults
+ const [settings, setSettings] = useState(() => {
+ const savedState = loadState();
+ return savedState?.settings ?? {
+ enabled: true,
+ curve: Array(CONSTANTS.NUM_CURVE_POINTS).fill(1),
+ };
+ });
+
+ const [rounds, setRounds] = useState(() => {
+ const savedState = loadState();
+ return savedState?.rounds ?? [];
+ });
+
+ // UI state
+ const [showSettings, setShowSettings] = useState(false);
+ const [showRounds, setShowRounds] = useState(false);
+ const [viewportOffset, setViewportOffset] = useState({
+ x: 0,
+ y: 0,
+ });
+
+ // Refs for handling zoom behavior
+ const containerRef = useRef(null);
+ const wheelDeltaSamples = useRef([]);
+ const touchStartDistance = useRef(0);
+
+ useEffect(() => {
+ const savedState = loadState();
+ if (savedState?.stats) {
+ setStats(savedState.stats);
+ }
+ }, []);
+
+ useEffect(() => {
+ saveState({
+ rounds,
+ stats,
+ settings,
+ });
+ }, [rounds, stats, settings]);
+
+ useEffect(() => {
+ generateTargets();
+ }, []);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const handleWheelEvent = (e: WheelEvent) => {
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault();
+
+ const rect = container.getBoundingClientRect();
+ const cursorX = e.clientX - rect.left;
+ const cursorY = e.clientY - rect.top;
+
+ const virtualX = viewportOffset.x + cursorX / zoom;
+ const virtualY = viewportOffset.y + cursorY / zoom;
+
+ // Prevent crazy fast scrolling
+ const deltaY = Math.max(-100, Math.min(100, -e.deltaY));
+
+ if (isFinite(deltaY)) {
+ wheelDeltaSamples.current.push(Math.abs(deltaY));
+ if (wheelDeltaSamples.current.length > CONSTANTS.WHEEL_SAMPLES) {
+ wheelDeltaSamples.current.shift();
+ }
+ }
+
+ let zoomDelta = deltaY * 0.001;
+
+ // Apply acceleration curve if enabled
+ if (settings.enabled && wheelDeltaSamples.current.length > 0) {
+ const averageSpeed = wheelDeltaSamples.current.reduce((a, b) => a + b, 0) /
+ wheelDeltaSamples.current.length;
+
+ const normalizedSpeed = Math.max(0, Math.min(2, averageSpeed / 100));
+ const curveIndex = normalizedSpeed * (settings.curve.length - 1);
+ const lowerIndex = Math.floor(curveIndex);
+ const upperIndex = Math.min(settings.curve.length - 1, Math.ceil(curveIndex));
+ const t = curveIndex - lowerIndex;
+
+ const accelerationFactor =
+ settings.curve[lowerIndex] * (1 - t) +
+ settings.curve[upperIndex] * t;
+
+ if (isFinite(accelerationFactor)) {
+ zoomDelta *= accelerationFactor;
+ }
+ }
+
+ zoomDelta = Math.max(-0.5, Math.min(0.5, zoomDelta));
+
+ const newZoom = Math.max(
+ CONSTANTS.ZOOM_LEVELS.min,
+ Math.min(CONSTANTS.ZOOM_LEVELS.max, zoom * (1 + zoomDelta))
+ );
+
+ // Sanity check before updating state
+ if (isFinite(newZoom) && newZoom > 0) {
+ const newOffsetX = virtualX - cursorX / newZoom;
+ const newOffsetY = virtualY - cursorY / newZoom;
+
+ if (isFinite(newOffsetX) && isFinite(newOffsetY)) {
+ setZoom(newZoom);
+ setViewportOffset({ x: newOffsetX, y: newOffsetY });
+ }
+ }
+ }
+ };
+
+ container.addEventListener('wheel', handleWheelEvent, { passive: false });
+
+ return () => {
+ container.removeEventListener('wheel', handleWheelEvent);
+ };
+ }, [zoom, viewportOffset, settings]);
+
+ const generateTargets = () => {
+ const newTargets: Target[] = [];
+ const PADDING = CONSTANTS.MAX_RADIUS * 2;
+
+ const usableArea = {
+ start: PADDING,
+ end: CONSTANTS.VIRTUAL_SIZE - PADDING
+ };
+
+ for (let i = 0; i < CONSTANTS.NUM_TARGETS; i++) {
+ let validPosition = false;
+ let newTarget: Target;
+
+ while (!validPosition) {
+ const radius = CONSTANTS.MIN_RADIUS +
+ Math.random() * (CONSTANTS.MAX_RADIUS - CONSTANTS.MIN_RADIUS);
+ const x = usableArea.start + Math.random() * (usableArea.end - usableArea.start);
+ const y = usableArea.start + Math.random() * (usableArea.end - usableArea.start);
+
+ newTarget = { x, y, radius };
+ validPosition = true;
+
+ for (const existing of newTargets) {
+ const distance = Math.hypot(existing.x - x, existing.y - y);
+ if (distance < (existing.radius + radius) * 2) {
+ validPosition = false;
+ break;
+ }
+ }
+ }
+ newTargets.push(newTarget!);
+ }
+
+ setTargets(newTargets);
+ setCurrentTarget(Math.floor(Math.random() * CONSTANTS.NUM_TARGETS));
+
+ const initialZoom = 0.2;
+ setZoom(initialZoom);
+
+ setViewportOffset({
+ x: (CONSTANTS.VIRTUAL_SIZE - window.innerWidth / initialZoom) / 2,
+ y: (CONSTANTS.VIRTUAL_SIZE - window.innerHeight / initialZoom) / 2,
+ });
+
+ setStats((prev) => ({ ...prev, startTime: Date.now() }));
+ };
+
+ const handleTouchStart = (e: React.TouchEvent) => {
+ if (e.touches.length === 2) {
+ const touch1 = e.touches[0];
+ const touch2 = e.touches[1];
+ touchStartDistance.current = Math.hypot(
+ touch2.clientX - touch1.clientX,
+ touch2.clientY - touch1.clientY
+ );
+ }
+ };
+
+ const handleTouchMove = (e: React.TouchEvent) => {
+ if (e.touches.length === 2) {
+ e.preventDefault();
+ const touch1 = e.touches[0];
+ const touch2 = e.touches[1];
+ const currentDistance = Math.hypot(
+ touch2.clientX - touch1.clientX,
+ touch2.clientY - touch1.clientY
+ );
+
+ const zoomFactor = currentDistance / touchStartDistance.current;
+ setZoom((prev) =>
+ Math.max(
+ CONSTANTS.ZOOM_LEVELS.min,
+ Math.min(CONSTANTS.ZOOM_LEVELS.max, prev * zoomFactor)
+ )
+ );
+ }
+ };
+
+ const handleTouchEnd = () => {
+ touchStartDistance.current = 0;
+ };
+
+ const handleClick = (e: React.MouseEvent) => {
+ if (!containerRef.current || currentTarget === null) return;
+
+ const rect = containerRef.current.getBoundingClientRect();
+ const x = (e.clientX - rect.left) / zoom + viewportOffset.x;
+ const y = (e.clientY - rect.top) / zoom + viewportOffset.y;
+
+ const target = targets[currentTarget];
+ if (!target) return;
+
+ const distance = Math.hypot(target.x - x, target.y - y);
+
+ if (distance <= target.radius) {
+ const time = Date.now() - stats.startTime;
+
+ // Log the round with acceleration curve if enabled
+ setRounds(prev => [...prev, {
+ timeSpent: time,
+ misclicks: stats.misclicks,
+ accelerationEnabled: settings.enabled,
+ accelerationCurve: settings.enabled ? [...settings.curve] : undefined,
+ timestamp: Date.now(),
+ }]);
+
+ // Reset stats for next round
+ setStats({
+ startTime: Date.now(),
+ misclicks: 0,
+ targetsHit: stats.targetsHit + 1,
+ times: [...stats.times, time],
+ });
+
+ // Select new target
+ let newTarget: number;
+ do {
+ newTarget = Math.floor(Math.random() * CONSTANTS.NUM_TARGETS);
+ } while (newTarget === currentTarget);
+ setCurrentTarget(newTarget);
+ } else {
+ setStats((prev) => ({
+ ...prev,
+ misclicks: prev.misclicks + 1,
+ }));
+ }
+ };
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ if (e.buttons === 1) {
+ setViewportOffset((prev) => ({
+ x: prev.x - e.movementX / zoom,
+ y: prev.y - e.movementY / zoom,
+ }));
+ }
+ };
+
+ const handleReset = () => {
+ if (window.confirm('Are you sure you want to reset all trials and history? This cannot be undone.')) {
+ setRounds([]);
+ setStats({
+ startTime: Date.now(),
+ misclicks: 0,
+ targetsHit: 0,
+ times: [],
+ });
+ generateTargets();
+ }
+ };
+
+ const handleExport = () => {
+ exportToCSV(rounds);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
setShowSettings(false)}
+ />
+
+ setShowRounds(false)}
+ />
+
+
+ Targets: {stats.targetsHit} •
+ Misclicks: {stats.misclicks} •
+ Avg Time: {stats.times.length
+ ? Math.round(stats.times.reduce((a, b) => a + b) / stats.times.length)
+ : 0} ms •
+ Zoom: {zoom.toFixed(2)}x
+
+
+
+
+ {targets.map((target, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/RoundsHistory.css b/src/components/RoundsHistory.css
new file mode 100644
index 0000000..c644b0a
--- /dev/null
+++ b/src/components/RoundsHistory.css
@@ -0,0 +1,82 @@
+.rounds-panel {
+ position: fixed;
+ top: 80px;
+ left: 20px;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ width: 300px;
+ max-height: calc(100vh - 120px);
+ overflow-y: auto;
+ color: #213547;
+}
+
+.rounds-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ position: sticky;
+ top: 0;
+ background: white;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+}
+
+.rounds-header h2 {
+ margin: 0;
+ font-size: 1.2em;
+ font-weight: 600;
+ color: #213547;
+}
+
+.rounds-header button {
+ background: none;
+ border: none;
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0 8px;
+ box-shadow: none;
+}
+
+.rounds-header button:hover {
+ background: none;
+ color: #646cff;
+}
+
+.rounds-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.round-item {
+ padding: 12px;
+ border: 1px solid #eee;
+ border-radius: 6px;
+ background: #f9f9f9;
+ transition: all 0.2s ease;
+}
+
+.round-item:hover {
+ border-color: #ddd;
+ background: white;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.round-number {
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: #213547;
+}
+
+.round-stats {
+ font-size: 0.9em;
+ color: #666;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
\ No newline at end of file
diff --git a/src/components/RoundsHistory.tsx b/src/components/RoundsHistory.tsx
new file mode 100644
index 0000000..8f058c9
--- /dev/null
+++ b/src/components/RoundsHistory.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Round } from '../types';
+import './RoundsHistory.css';
+
+interface RoundsHistoryProps {
+ rounds: Round[];
+ visible: boolean;
+ onClose: () => void;
+}
+
+export const RoundsHistory: React.FC = ({
+ rounds,
+ visible,
+ onClose,
+}) => {
+ if (!visible) return null;
+
+ return (
+
+
+
Rounds History
+
+
+
+ {rounds.slice().reverse().map((round, index) => (
+
+
Round {rounds.length - index}
+
+
Time: {round.timeSpent}ms
+
Misclicks: {round.misclicks}
+
Acceleration: {round.accelerationEnabled ? 'On' : 'Off'}
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/Settings.css b/src/components/Settings.css
new file mode 100644
index 0000000..35ab6cc
--- /dev/null
+++ b/src/components/Settings.css
@@ -0,0 +1,85 @@
+.settings-panel {
+ position: fixed;
+ top: 80px;
+ right: 20px;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ width: 300px;
+ color: #213547;
+}
+
+.settings-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.settings-header h2 {
+ margin: 0;
+ font-size: 1.2em;
+ font-weight: 600;
+ color: #213547;
+}
+
+.settings-header button {
+ background: none;
+ border: none;
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0 8px;
+ box-shadow: none;
+}
+
+.settings-header button:hover {
+ background: none;
+ color: #646cff;
+}
+
+.slider-row {
+ display: flex;
+ align-items: center;
+ margin: 10px 0;
+ gap: 10px;
+}
+
+.slider-row label {
+ width: 120px;
+ font-size: 14px;
+ color: #213547;
+}
+
+.slider-row input[type="range"] {
+ flex: 1;
+}
+
+.slider-row span {
+ width: 40px;
+ text-align: right;
+ font-size: 14px;
+ color: #666;
+}
+
+.acceleration-toggle {
+ margin-bottom: 20px;
+}
+
+.acceleration-toggle label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ color: #213547;
+}
+
+.sliders-container h3 {
+ margin: 0 0 15px 0;
+ font-size: 1em;
+ font-weight: 600;
+ color: #213547;
+}
\ No newline at end of file
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
new file mode 100644
index 0000000..a5f1d75
--- /dev/null
+++ b/src/components/Settings.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { AccelerationSettings } from '../types';
+import './Settings.css';
+
+interface SettingsProps {
+ settings: AccelerationSettings;
+ onSettingsChange: (settings: AccelerationSettings) => void;
+ visible: boolean;
+ onClose: () => void;
+}
+
+export const Settings: React.FC = ({
+ settings,
+ onSettingsChange,
+ visible,
+ onClose,
+}) => {
+ const numSliders = 5;
+
+ const handleSliderChange = (index: number, value: number) => {
+ const newCurve = [...settings.curve];
+ newCurve[index] = value;
+ onSettingsChange({ ...settings, curve: newCurve });
+ };
+
+ if (!visible) return null;
+
+ return (
+
+
+
Settings
+
+
+
+
+
+
+
+
+
Acceleration Curve
+ {Array.from({ length: numSliders }).map((_, i) => (
+
+
+ handleSliderChange(i, parseFloat(e.target.value))}
+ disabled={!settings.enabled}
+ />
+ {settings.curve[i].toFixed(1)}x
+
+ ))}
+
+
+ );
+};
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..5170904
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,74 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #213547;
+ background-color: #ffffff;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: white;
+ cursor: pointer;
+ transition: all 0.25s;
+ color: #213547;
+}
+
+button:hover {
+ border-color: #646cff;
+ background-color: #f5f5f5;
+}
+
+button:focus,
+button:focus-visible {
+ outline: 2px solid #646cff;
+ outline-offset: 2px;
+}
+
+#root {
+ width: 100%;
+ height: 100%;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..0c08923
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,45 @@
+export interface Target {
+ x: number;
+ y: number;
+ radius: number;
+}
+
+export interface Stats {
+ startTime: number;
+ misclicks: number;
+ targetsHit: number;
+ times: number[];
+}
+
+export interface ViewportOffset {
+ x: number;
+ y: number;
+}
+
+export interface AccelerationSettings {
+ enabled: boolean;
+ curve: number[];
+}
+
+export interface Round {
+ timeSpent: number;
+ misclicks: number;
+ accelerationEnabled: boolean;
+ accelerationCurve?: number[]; // Only present when acceleration is enabled
+ timestamp: number;
+}
+
+// Game constants
+export const CONSTANTS = {
+ NUM_TARGETS: 50,
+ MIN_RADIUS: 20,
+ MAX_RADIUS: 60,
+ VIRTUAL_SIZE: 10000, // Size of the virtual canvas
+ ZOOM_LEVELS: {
+ min: 0.1,
+ max: 10,
+ default: 1,
+ },
+ WHEEL_SAMPLES: 5, // Number of samples to average for smooth zooming
+ NUM_CURVE_POINTS: 5, // Points in the acceleration curve
+} as const;
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..d3f1a8f
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,82 @@
+import { Round, Stats, AccelerationSettings } from './types';
+
+interface AppState {
+ rounds: Round[];
+ stats: Stats;
+ settings: AccelerationSettings;
+}
+
+// Persist app state to localStorage
+export const saveState = (state: AppState) => {
+ try {
+ localStorage.setItem('zoomaccell_state', JSON.stringify(state));
+ } catch (error) {
+ console.error('Failed to save state:', error);
+ }
+};
+
+export const loadState = (): AppState | null => {
+ try {
+ const savedState = localStorage.getItem('zoomaccell_state');
+ if (savedState) {
+ return JSON.parse(savedState);
+ }
+ } catch (error) {
+ console.error('Failed to load state:', error);
+ }
+ return null;
+};
+
+export const exportToCSV = (rounds: Round[]) => {
+ const baseHeaders = ['Round Number', 'Time (ms)', 'Misclicks', 'Acceleration Enabled', 'Timestamp'];
+
+ // Figure out how many acceleration points we need
+ const maxCurvePoints = rounds.reduce((max, round) => {
+ return Math.max(max, round.accelerationCurve?.length || 0);
+ }, 0);
+
+ const curveHeaders = maxCurvePoints > 0
+ ? Array.from({ length: maxCurvePoints }, (_, i) => `Accel ${i + 1}`)
+ : [];
+
+ const headers = [...baseHeaders, ...curveHeaders];
+
+ const rows = rounds.map((round, index) => {
+ const baseData = [
+ index + 1,
+ round.timeSpent,
+ round.misclicks,
+ round.accelerationEnabled ? 'true' : 'false',
+ new Date(round.timestamp).toISOString()
+ ];
+
+ const curveData = maxCurvePoints > 0
+ ? Array.from({ length: maxCurvePoints }, (_, i) =>
+ round.accelerationCurve?.[i]?.toFixed(2) || '')
+ : [];
+
+ return [...baseData, ...curveData];
+ });
+
+ const csvContent = [
+ headers.join(','),
+ ...rows.map(row => row.join(','))
+ ].join('\n');
+
+ // Download the file
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+
+ const now = new Date();
+ const dateStr = now.toISOString().split('T')[0];
+ const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');
+
+ link.setAttribute('href', url);
+ link.setAttribute('download', `zoom_trials_${dateStr}_${timeStr}.csv`);
+ link.style.visibility = 'hidden';
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+};
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..358ca9b
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..db0becc
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})